Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weak event handler model for use with lambdas

OK, so this is more of an answer than a question, but after asking this question, and pulling together the various bits from Dustin Campbell, Egor, and also one last tip from the 'IObservable/Rx/Reactive framework', I think I've worked out a workable solution for this particular problem. It may be completely superseded by IObservable/Rx/Reactive framework, but only experience will show that.

I've deliberately created a new question, to give me space to explain how I got to this solution, as it may not be immediately obvious.

There are many related questions, most telling you you can't use inline lambdas if you want to be able to detach them later:

  • Weak events in .Net?
  • Unhooking events with lambdas in C#
  • Can using lambdas as event handlers cause a memory leak?
  • How to unsubscribe from an event which uses a lambda expression?
  • Unsubscribe anonymous method in C#

And it is true that if YOU want to be able to detach them later, you need to keep a reference to your lambda. However, if you just want the event handler to detach itself when your subscriber falls out of scope, this answer is for you.

like image 601
Benjol Avatar asked Nov 17 '09 07:11

Benjol


1 Answers

'The' answer

(Read more below if you want to see how I got to this solution)

Usage, given a control with a vanilla MouseDown event, and a specific EventHandler<ValueEventArgs> ValueEvent event:

// for 'vanilla' events SetAnyHandler<Subscriber, MouseEventHandler, MouseEventArgs>(     h => (o,e) => h(o,e), //don't ask me, but it works*.     h => control.MouseDown += h,     h => control.MouseDown -= h,     subscriber,     (s, e) => s.DoSomething(e));  //**See note below  // for generic events SetAnyHandler<Subscriber, ValueEventArgs>(     h => control.ValueEvent += h,     h => control.ValueEvent -= h,     subscriber,     (s, e) => s.DoSomething(e));  //**See note below 

(*This is a workaround from Rx)

(** it is important to avoid invoking the subscriber object directly here (for instance putting subscriber.DoSomething(e), or invoking DoSomething(e) directly if we are inside the Subscriber class. Doing this effectively creates a reference to subscriber, which completely defeats the object...)

Note: in some circumstances, this CAN leave references to the wrapping classes created for the lambdas in memory, but they only weigh bytes, so I'm not too bothered.

Implementation:

//This overload handles any type of EventHandler public static void SetAnyHandler<S, TDelegate, TArgs>(     Func<EventHandler<TArgs>, TDelegate> converter,      Action<TDelegate> add, Action<TDelegate> remove,     S subscriber, Action<S, TArgs> action)     where TArgs : EventArgs     where TDelegate : class     where S : class {     var subs_weak_ref = new WeakReference(subscriber);     TDelegate handler = null;     handler = converter(new EventHandler<TArgs>(         (s, e) =>         {             var subs_strong_ref = subs_weak_ref.Target as S;             if(subs_strong_ref != null)             {                 action(subs_strong_ref, e);             }             else             {                 remove(handler);                 handler = null;             }         }));     add(handler); }  // this overload is simplified for generic EventHandlers public static void SetAnyHandler<S, TArgs>(     Action<EventHandler<TArgs>> add, Action<EventHandler<TArgs>> remove,     S subscriber, Action<S, TArgs> action)     where TArgs : EventArgs     where S : class {     SetAnyHandler<S, EventHandler<TArgs>, TArgs>(         h => h, add, remove, subscriber, action); } 

The detail

My starting point was Egor's excellent answer (see link for version with comments):

public static void Link(Publisher publisher, Control subscriber) {     var subscriber_weak_ref = new WeakReference(subscriber);     EventHandler<ValueEventArgs<bool>> handler = null;     handler = delegate(object sender, ValueEventArgs<bool> e) {             var subscriber_strong_ref = subscriber_weak_ref.Target as Control;             if (subscriber_strong_ref != null) subscriber_strong_ref.Enabled = e.Value;             else {                     ((Publisher)sender).EnabledChanged -= handler;                     handler = null;              }     };      publisher.EnabledChanged += handler; } 

What bothered me was that the event is hard coded into the method. So that means for each new event, there is a new method to write.

I fiddled around and managed to come up with this generic solution:

private static void SetAnyGenericHandler<S, T>(      Action<EventHandler<T>> add,     //to add event listener to publisher      Action<EventHandler<T>> remove,  //to remove event listener from publisher      S subscriber,                    //ref to subscriber (to pass to action)      Action<S, T> action)             //called when event is raised     where T : EventArgs     where S : class {     var subscriber_weak_ref = new WeakReference(subscriber);     EventHandler<T> handler = null;     handler = delegate(object sender, T e)     {         var subscriber_strong_ref = subscriber_weak_ref.Target as S;         if(subscriber_strong_ref != null)         {             Console.WriteLine("New event received by subscriber");             action(subscriber_strong_ref, e);         }         else         {             remove(handler);             handler = null;         }     };     add(handler); } 

However the problem with that solution is that it is ONLY generic, it can't handle the standard winforms MouseUp, MouseDown, etc...

So I tried to make it even more generic:

private static void SetAnyHandler<T, R>(     Action<T> add,      //to add event listener to publisher     Action<T> remove,   //to remove event listener from publisher     Subscriber subscriber,  //ref to subscriber (to pass to action)     Action<Subscriber, R> action)      where T : class {     var subscriber_weak_ref = new WeakReference(subscriber);     T handler = null;     handler = delegate(object sender, R e) //<-compiler doesn't like this line     {         var subscriber_strong_ref = subscriber_weak_ref.Target as Subscriber;         if(subscriber_strong_ref != null)         {             action(subscriber_strong_ref, e);         }         else         {             remove(handler);             handler = null;         }     };     remove(handler); } 

However, as I hinted here, this won't compile, because there is no way of constraining T to be a delegate.

At that point, I pretty much gave up. There's no point trying to fight with the C# specs.

However, yesterday, I discovered the Observable.FromEvent method from the Reactive framework, I didn't have the implementation, but the usage seemed slightly familiar, and very interesting:

var mousedown = Observable.FromEvent<MouseEventHandler, MouseDownEventArgs>(       h => new MouseEventHandler(h),       h => control.MouseDown += h,       h => control.MouseDown -= h); 

It was the first argument that caught my attention. This is the workaround for the absence of a delegate type constraint. We take of it by passing in the function which will create the delegate.

Putting all this together gives us the solution shown at the top of this answer.

Afterthought

I thoroughly recommended taking the time to learn about the reactive framework (or whatever it ends up being called). It is VERY interesting, and slightly mindblowing. I suspect that it will also render questions like this totally redundant.

So far, the most interesting stuff I've seen has been the videos on Channel9.

like image 135
14 revs, 2 users 99% Avatar answered Oct 14 '22 03:10

14 revs, 2 users 99%