views:

1621

answers:

3

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:

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.

+10  A: 

'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,
    Subscriber subscriber, Action<S, TArgs> action)
    where TArgs : EventArgs
    where S : class
{
    SetAnyHandler<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.

Benjol
Seems a bit more like a blog, but...
Marc Gravell
Yeah, I know, I really should start a technical blog, but I rarely have anything interesting to say..
Benjol
The area you are exploring is far beyond my level of .NET and C# competency at this time, but it is interesting, and thought provoking. Thanks for taking the time to post in detail ! imho this kind of mutant question-answer "dynamic-duo" deserves to be a special kind of StackOverFlow "object" (a "meta-comment" ?). Perhaps suitable for a CodeProject article ? best,
BillW
Most people don't realize how important what you're talking about here is. Very well done. Thank you
Neil
was looking for something only tangentially related, but... mind blown!
John Gardner
@Benjol, There seems to be a formatting issue with the generic SetAnyHandler method which makes it not compile. I get the feeling its fairly trivial but this stuff is pretty confusing to me already :). Any chance you can fix?
fostandy
@fostandy, done (I think). Your comment just goes to show that no one else has tried this code yet :) Probably wisely...
Benjol
@Benjol, sorry, still no go. The genericized version seemed to be calling the non-genericized with the wrong number of arguments. I've tried deleting `h => h` and `Subscriber` to `S`, but this moves the compile error to the usage.
fostandy
@Benjol and au contraire, my comment probably just shows everybody else who tried this code was smart enough to figure it out on their own :)
fostandy
A: 

Dustin Campbell's approach is already excellent. The only thing left, save a solution integrated into .NET, is a really simple way to create really generic weak event handlers:

http://puremsil.wordpress.com/2010/05/03/generic-weak-event-handlers/

Stefan
A: 

If you head over to CodePlex there's a project called Sharp Observation in which the author has built a good weak delegate provider, implemented in MSIL. Fast, flexible, easy to use: e.g.

Action<int,int> myDelegate = new Action<int,int>( aMethodOnMyClass );
myDelegate.MakeWeak();

As easy as that!

Mark