Thursday, April 7, 2011

True weak delegates (Part 2)

Again a month was over faster than I thought. This time I'll try to explain my solution to true weak delegates, which works in all scenarios. First I'll explain the reference graph for the normal scenario.


The event source is the object which holds the event to which a listener subscribes a delegate as event handler. Here we are interested in an event handler which needs the listener to operate, so C# will capture a reference in the delegates closure object.

Note that from the description in my last post there actually is a reference from the delegate to the closure object which isn't shown in the diagram. However you can't make that reference weak because then the closure object would become garbage right away (only the delegate knows about it!).

From the diagram you can nicely see the reference chain from the event source through the delegate and its closure back to the listener, which will keep the listener alive as long as the event source stays alive. The reference from the listener to the delegate is optional in the sense that you only really need it if you want to unsubscribe from the event source.

The reference from the closure to the listener isn't always direct, it could go through a chain of references, too. This means the only place where we have any hope to replace the reference by a weak one is the part from the event source to the delegate.

Ideally the .NET framework would provide a way to subscribe to an event by keeping the reference from event to delegate weak, but as it doesn't we need to be insert another layer of indirection to have any chance at all. Consider the following modified diagram:


Here we've separated the reference from event source to delegate by inserting another object which I've called weak closure. I could've called it weak delegate, but since it stores more data and doesn't inherit from delegate I figured calling it weak closure might be less confusing.

So, let's analyze the diagram to understand how it works.

The most important thing you notice is that the strong reference chain from event source to listener is broken by a weak reference (the dotted arrow). Once all other references to the listener and delegate go away it means both can be collected (but not the weak closure).

If we can tolerate the fact that the weak closure stays subscribed when the delegate and listener went away then we don't need a link from the weak closure to the event source; but if we provide such a link (either weak or strong) then the weak closure can unsubscribe itself. When the event is raised the next time it will notice the delegate went away and can use its reference back to the event source to unsubscribe itself.

Another thing to notice is that the listener now is required to hold a reference to the delegate. While this may seem strange at first it does make sense, because if the listener doesn't hold a reference to the delegate then the only reference is the weak reference, meaning the delegate will be collected right away. The reference to the weak closure is optional because (like before) you only need it if you want to explicitely unsubscribe.

One minor detail which may be missed is the effect of the backreference from the weak closure to the event source. Like said before it is requied to auto-unsubscribe when the listener/delegate goes away. However, if it is a strong reference this means we now can have a chain from the listener over the weak closure to the event source!

This means we have the inverse scenario from which we started, now the event source can't be collected while the listener stays alive. It is easily fixed by weakening the reference from the weak closure to the event source. However, often it doesn't matter that the event source will be kept alive, and it may be worth to save the overhead of a second weak reference.


If you have been wondering why I unsubscribe from the weak closure instead of using a finalizer, this is intentional. Finalizers run on a separate thread and depending on how the event source is coded it may not be safe to unsubscribe asynchronously, but if you know that it is safe for your event source it would be alright to use a finalizer to unsubscribe when the delegate goes away.

I've setup a mercurial repository at https://code.weltkante.de containing my weak closure implementation and some samples. Direct link to sourcecode. Direct link to download. Note that the API isn't exactly nice, I've written most of it a long time ago, you could probably use some more tricks to make the sytax nicer. For ideas you should look at the article I linked last time, it shows some nice syntax tricks to capture event subscription.

6 comments:

  1. Hallo Herr Käs,

    there seems to be a bug in the WeakClosure::Bind function: line 180:

    mEventSource = semiWeak ? eventSource : new WeakReference(EventSource);

    should be
    mEventSource = semiWeak ? eventSource : new WeakReference(eventSource);

    othwerwise there is not even a weak reference to the source.
    Kind regards
    Jürgen

    ReplyDelete
  2. You are of course correct, thanks for point it out, I've updated the repository. I guess I should've written test cases to cover all code paths ;)

    ReplyDelete
  3. Admittedly, my test for Bind initially missed it. Not sure whether the code looked so "obviously correct" or I just forgot to test the content of the WeakReference. But we found it when writing the test for Match, because there the source is actually used.

    Thanks for this nice piece of code.

    ReplyDelete
  4. Hallo Herr Käs,

    here is a small improvement proposal, because we have problems to run unittests. GetEvent(object...) does not work on events in interfaces that are stubbed by the Moles environment, because the events are not visible on the stub object type, only on the interface.

    private static EventDescriptor GetEvent(Type eventSource, string eventName)
    {
    // We go through TypeDescriptor instead of using direct reflection to allow
    // the System.ComponentModel framework to add/replace additional events
    // which aren't present on the physical class. Designers like to use this.
    return TypeDescriptor.GetEvents(eventSource)[eventName];
    }

    //public static EventListener CreateWeakListener(object eventSource, string eventName, Delegate eventHandler)
    //{
    // return new EventListener(new WeakReflectiveClosure(
    // eventSource, GetEvent(eventSource, eventName), eventHandler, false), eventHandler);
    //}

    public static EventListener CreateWeakListener(T eventSource, string eventName, Delegate eventHandler)
    {
    return new EventListener(new WeakReflectiveClosure(
    eventSource, GetEvent(typeof(T), eventName)??GetEvent(eventSource,eventName), eventHandler, false), eventHandler);
    }



    //public static EventListener CreateSemiWeakListener(object eventSource, string eventName, Delegate eventHandler)
    //{
    // return new EventListener(new WeakReflectiveClosure(
    // eventSource, GetEvent(eventSource, eventName), eventHandler, true), eventHandler);
    //}

    public static EventListener CreateSemiWeakListener(T eventSource, string eventName, Delegate eventHandler)
    {
    return new EventListener(new WeakReflectiveClosure(
    eventSource, GetEvent(typeof(T), eventName) ?? GetEvent(eventSource, eventName), eventHandler, true), eventHandler);
    }

    ReplyDelete
  5. Template arguments lost in the HTML space...
    public static EventListener CreateWeakListener<T>(T eventSource, string eventName, Delegate eventHandler)...
    public static EventListener CreateSemiWeakListener<T>(T eventSource, string eventName, Delegate eventHandler)...

    ReplyDelete
  6. I see how this is a problem, however your proposed solution seems backwards. Would "GetEvent(eventSource, eventName) ?? GetEvent(typeof(T), eventName)" work instead? If there are instance-specific overrides they need to be checked before going to the type.

    On the other hand, if you don't care about System.ComponentModel, just do plain reflection on the type you pass as generic parameter. The original implementation is due to my personal use-case of weak delegates within WinForms, there designers are using the ComponentModel framework to override the type descriptors of their controls with design-time metadata and implementations.

    An alternative solution which allows to keep System.ComponentModel support and still do proper unit tests is to use the TypeDescriptor override features to build a bridge to the generated stub types.

    ReplyDelete