[Journal - Open Instance Delegates and Weak References]

Open Instance Delegates and Weak References

Sunday, November 19, 2006

Let's run this code:

internal static void TestWeakEventHandlers()
{
    SampleSender sender = new SampleSender();
    SampleListener listener = new SampleListener();

    sender.Click += listener.HandleSenderClick1;
    sender.Click += listener.HandleSenderClick2;
    sender.Click += SampleListener.StaticHandleSenderClick;

    sender.FireClick();

    sender.Click -= listener.HandleSenderClick2;
    Console.WriteLine("Removed listener 2");

    sender.FireClick();

    listener = null;
    GC.Collect();
    Console.WriteLine("Collected garbage.");

    sender.FireClick();
}

This is the output we get:

SampleListener.HandleSenderClick1(object, ClickEventArgs)
SampleListener.HandleSenderClick2(object, ClickEventArgs)
SampleListener.StaticHandleSenderClick(object, ClickEventArgs)
Removed listener 2
SampleListener.HandleSenderClick1(object, ClickEventArgs)
SampleListener.StaticHandleSenderClick(object, ClickEventArgs)
Collected garbage.
SampleListener.StaticHandleSenderClick(object, ClickEventArgs)

At first sight, it doesn't seem so strange; at least if we forget for a momemt how events and delegates work: After we've set our only reference to the listener object to null, and invoked the GC, the object is no longer there, hence its instance method event handlers are no longer invoked - right?

But of course, there's more to it. When an instance method is registered as an event handler, whoever fires the event ends up holding a reference to the listener instance. In our case, "sender" references "listener", however indirectly. Therefore, the listener object cannot be garbage collected.

Still, I assure you, that if you run this code from the TestCore2 project (which is part of the Gregor.Core solution), you'll get this output. And I'm not saying the GC is buggy.

What's really happening here is that the sender's Click event is implemented in a way that the listener object is not strongly referenced. And the beauty of it is that it all happens behind the scenes: subscribing to the Click event works like subscribing to any regular event.

So let's have a look at the implementation. I'm skipping the ClickEventArgs class, and SampleListener isn't out of the ordinary, either. So here's how the SampleSender class (sans the FireClickEvent method) has implemented the Click event:

public class SampleSender
{
    private IList<IWeakEventHandler<ClickEventArgs>> m_ClickHandlers;

    public event EventHandler<ClickEventArgs> Click{
        add{
            WeakDelegateUtil.AddWeakEventHandler(ref m_ClickHandlers, value);
        }
        remove{
            WeakDelegateUtil.RemoveWeakEventHandler(m_ClickHandlers, value);
        }
    }
}

A bunch of library calls! People never change.

What's behind the IWeakEventHandler(Of EventArgsType As System.EventArgs) interface? Well, there's a class that implements that generic type, and introduces yet another generic type parameter:

public class CWeakEventHandler<TargetType, EventArgsType>
    : IWeakEventHandler<EventArgsType>
    where TargetType : class
    where EventArgsType : System.EventArgs
{
    // ...
}

The additional generic type parameter is needed because of the way delegates are used to ultimately invoke the handler methods. An instance method that handles an event looks like - drumroll, please - this:

void HandleSenderClick(object sender, ClickEventArgs e);

But because we're a little complicated, we know that the method really looks like this:

void HandleSenderClick(SampleListener this, object sender, ClickEventArgs e);

Why is this important? Recall that a delegate pointing to an instance method really points to a couple of things - the object and the function. And as for referencing the object, we don't want to do that too strongly. So both pointers need to be separated; the pointer to the instance is wrapped in a WeakReference, and the function pointer goes into a new delegate, one that reflects the handler method's true nature.

So we simply mangle the delegate's signature. OK, it isn't all that simple, and it's only possible from .NET 2.0 onwards.

Creating a delegate that points to an instance method but not the instance - requiring an additional, zeroth, argument to be supplied at call time - requires reflection. System.Delegate.CreateDelegate does it, but make sure to use those overloads that have a parameter called "object firstArgument".

The open instance delegate looks like this:

delegate void OpenInstanceEventHandler
    <TargetType, EventArgsType>
    (TargetType target, object sender, EventArgsType e)
    where EventArgsType : System.EventArgs;

The first parameter must be of the type (or derived from the type) that defines the handler method. The actual listener instance may also be of a derived class, of course.

So, to make a long story short, this is the reason that the CWeakEventHandler<TargetType, EventArgsType> class has an additional generic type parameter called TargetType.

Again, binding to the target method only works if the delegate uses exactly the method's declaring type or a type derived from that as the type of its first parameter. So this is another case where parameter contravariance, ie., having a delegate type whose parameters are more restrictive than the those of the actual handler method, is available. I haven't tested if - using a derived type for the "this" parameter in the delegate signature - binding to the target method works if the handler method is private, though.

You can see the details of how the open instance delegate is instantiated in CWeakEventHandler's constructor. Here's the essence of it:

// create open instance delegate
Type tpDel = typeof(OpenInstanceEventHandler<TargetType, EventArgsType>);
MethodInfo mi = originalHandler.Method;
Delegate del = Delegate.CreateDelegate(tpDel, null, mi, true);

// save delegate, and weakly reference the target
m_Handler = (OpenInstanceEventHandler<TargetType, EventArgsType>) del;
m_WeakTarget = new WeakReference(originalHandler.Target);

However, there is one open question, still: if it's the sender's job to manage that weak delegate business, and statically knowing the listener's type is required, how can events still be anonymous? Well, here reflection comes into play, a second time (or the first time, in execution order).

What we do is construct a closed generic type at runtime, deriving one type argument (the one for TargetType) from a regular delegate (supplied by the listener), and using one explicitly supplied by the caller (the sender). The regular (instance method, with "this" pointer) delegate that the listener (or the event-hooking code, in our example) created is then passed to the constructor, so it can do its work shown above. Here's the code:

public static IWeakEventHandler<EventArgsType> CreateWeakEventHandler<EventArgsType>(EventHandler<EventArgsType> originalHandler)
    where EventArgsType : System.EventArgs
{
    // gather the type arguments
    Type[] typeArgs = {originalHandler.Method.DeclaringType, typeof(EventArgsType)};

    // reflectivly bind the type
    Type tpWeakHandler = typeof(CWeakEventHandler<,>).MakeGenericType(typeArgs);

    // instantiate it
    ConstructorInfo ctor = tpWeakHandler.GetConstructor(
        BindingFlags.Instance | BindingFlags.NonPublic,
        null,
        new Type[]{typeof(EventHandler<EventArgsType>)},
        null);
    object[] args = {originalHandler};
    object ret = ctor.Invoke(args);

    // We're done! Now what?
    return (IWeakEventHandler<EventArgsType>) ret;
}

WeakDelegateUtil also has a helper function for firing the event, so it's convenient for the sender. Note that CWeakEventHandler supports static handler methods as well, so no functionality is lost, here. You can study the code in all its glory in the Gregor.Core project.

Concluding, this is another use for open instance delegates (besides their use for invoking callbacks in a flexible multi-level sorting, changing comparee [sic] instances on the fly).