WeakEventManager for WinRT

When registering to an event with an instance method then a reference will be stored from the event source to the event handler. This is normally fine, however, there are situations when the event source may live longer than the event handler. An example I recently ran into was when I wanted to listen to the Clipboard.ContextChanged event, which is static.

In WPF land you’d use the WeakEventManager<TEventSource, TEventArgs> class, however, this isn’t available to Window Store apps, so let’s build it! I’ve changed the API slightly, as I needed overloads to accept a Type due to Clipboard being a static class (which you can’t use for generic type parameters). So, here’s what I ended up with to register the event:

WeakEventManager.AddHandler<object>(
    typeof(Clipboard),
    "ContentChanged",
    this.OnClipboardContentChanged);

Here’s the main class – the actual logic is performed by the nested class:

using System;
using System.Collections.Generic;
using System.Reflection;

/// <summary>
/// Implements a weak event listener that allows the owner to be garbage
/// collected if its only remaining link is an event handler.
/// </summary>
public static class WeakEventManager
{
    private readonly static List<WeakEvent> registeredEvents = new List<WeakEvent>();

    /// <summary>
    /// Adds the specified event handler to the specified event.
    /// </summary>
    /// <typeparam name="TEventArgs">The type that holds the event data.</typeparam>
    /// <param name="sourceType">
    /// The type of the class that contains the specified event.
    /// </param>
    /// <param name="eventName">The name of the event to subscribe to.</param>
    /// <param name="handler">The delegate that handles the event.</param>
    public static void AddHandler<TEventArgs>
        (Type sourceType, string eventName, EventHandler<TEventArgs> handler)
    {
        EventInfo eventInfo = sourceType.GetRuntimeEvent(eventName);
        registeredEvents.Add(
            new WeakEvent(null, eventInfo, handler));
    }

    /// <summary>
    /// Adds the specified event handler to the specified event.
    /// </summary>
    /// <typeparam name="TEventSource">The type that raises the event.</typeparam>
    /// <typeparam name="TEventArgs">The type that holds the event data.</typeparam>
    /// <param name="source">
    /// The source object that raises the specified event.
    /// </param>
    /// <param name="eventName">The name of the event to subscribe to.</param>
    /// <param name="handler">The delegate that handles the event.</param>
    public static void AddHandler<TEventSource, TEventArgs>
        (TEventSource source, string eventName, EventHandler<TEventArgs> handler)
    {
        EventInfo eventInfo = typeof(TEventSource).GetRuntimeEvent(eventName);
        registeredEvents.Add(
            new WeakEvent(source, eventInfo, handler));
    }

    /// <summary>
    /// Removes the specified event handler from the specified event.
    /// </summary>
    /// <typeparam name="TEventArgs">The type that holds the event data.</typeparam>
    /// <param name="sourceType">
    /// The type of the class that contains the specified event.
    /// </param>
    /// <param name="eventName">
    /// The name of the event to remove the handler from.
    /// </param>
    /// <param name="handler">The delegate to remove.</param>
    public static void RemoveHandler<TEventArgs>
        (Type sourceType, string eventName, EventHandler<TEventArgs> handler)
    {
        EventInfo eventInfo = sourceType.GetRuntimeEvent(eventName);
        foreach (WeakEvent weakEvent in registeredEvents)
        {
            if (weakEvent.IsEqualTo(null, eventInfo, handler))
            {
                weakEvent.Detach();
                break;
            }
        }
    }

    /// <summary>
    /// Removes the specified event handler from the specified event.
    /// </summary>
    /// <typeparam name="TEventSource">The type that raises the event.</typeparam>
    /// <typeparam name="TEventArgs">The type that holds the event data.</typeparam>
    /// <param name="source">
    /// The source object that raises the specified event, or null if it's
    /// a static event.
    /// </param>
    /// <param name="eventName">
    /// The name of the event to remove the handler from.
    /// </param>
    /// <param name="handler">The delegate to remove.</param>
    public static void RemoveHandler<TEventSource, TEventArgs>
        (TEventSource source, string eventName, EventHandler<TEventArgs> handler)
    {
        EventInfo eventInfo = typeof(TEventSource).GetRuntimeEvent(eventName);
        foreach (WeakEvent weakEvent in registeredEvents)
        {
            if (weakEvent.IsEqualTo(source, eventInfo, handler))
            {
                weakEvent.Detach();
                break;
            }
        }
    }

    private class WeakEvent
    {
        // Implementation details to follow
    }
}

Naturally, in production you’d want some argument validation (i.e. not nulls and that GetRuntimeEvent returned something), but to keep the code simple I’ve omitted it. The actual logic goes in the WeakEvent nested class. This is needed because you can’t just keep a weak reference to the event handler object, as this most likely will be a temporary object that will soon get garbage collected. You also can’t keep a strong reference to the event handler, as it has a reference to the instance of the target so will keep it alive, defeating the point of the class! Therefore, the WeakEvent class has to wrap the parts of the event handler delegate in a way that allows the target class to be garbage collected. Here’s how I’ve approached it:

private class WeakEvent
{
    private static readonly MethodInfo onEventInfo =
        typeof(WeakEvent).GetTypeInfo().GetDeclaredMethod("OnEvent");

    private EventInfo eventInfo;
    private object eventRegistration;
    private MethodInfo handlerMethod;
    private WeakReference<object> handlerTarget;
    private object source;

    public WeakEvent(object source, EventInfo eventInfo, Delegate handler)
    {
        this.source = source;
        this.eventInfo = eventInfo;

        // We can't store a reference to the handler (as that will keep
        // the target alive) but we also can't store a WeakReference to
        // handler, as that could be GC'd before its target.
        this.handlerMethod = handler.GetMethodInfo();
        this.handlerTarget = new WeakReference<object>(handler.Target);

        object onEventHandler = this.CreateHandler();
        this.eventRegistration = eventInfo.AddMethod.Invoke(
            source,
            new object[] { onEventHandler });

        // If the AddMethod returned null then it was void - to
        // unregister we simply need to pass in the same delegate.
        if (this.eventRegistration == null)
        {
            this.eventRegistration = onEventHandler;
        }
    }

    public void Detach()
    {
        if (this.eventInfo != null)
        {
            WeakEventManager.registeredEvents.Remove(this);

            this.eventInfo.RemoveMethod.Invoke(
                this.source,
                new object[] { eventRegistration });

            this.eventInfo = null;
            this.eventRegistration = null;
        }
    }

    public bool IsEqualTo(object source, EventInfo eventInfo, Delegate handler)
    {
        if ((source == this.source) && (eventInfo == this.eventInfo))
        {
            object target;
            if (this.handlerTarget.TryGetTarget(out target))
            {
                return (handler.Target == target) &&
                        (handler.GetMethodInfo() == this.handlerMethod);
            }
        }

        return false;
    }

    public void OnEvent<T>(object sender, T args)
    {
        object instance;
        if (this.handlerTarget.TryGetTarget(out instance))
        {
            this.handlerMethod.Invoke(instance, new object[] { sender, args });
        }
        else
        {
            this.Detach();
        }
    }

    private object CreateHandler()
    {
        Type eventType = this.eventInfo.EventHandlerType;
        ParameterInfo[] parameters = eventType.GetTypeInfo()
                                                .GetDeclaredMethod("Invoke")
                                                .GetParameters();

        return onEventInfo.MakeGenericMethod(parameters[1].ParameterType)
                            .CreateDelegate(eventType, this);
    }
}

Basically the parts of the delegate have been stored in two parts; the method to invoke (stored as a normal reference) and the target to invoke the method on (stored as a weak reference). The tricky part is registering our handler on the event via reflection, made more difficult because events in the core WinRT classes are slightly different to the .NET events (for example, the add method of the event returns an EventRegistrationToken that must be passed in to the remove method; .NET events return void for the add method and require the delegate passed into the add method for the parameter to the remove method). When we get called back we have a quick check to see if our target is still valid; if it isn’t then we unregister from the event (after all, we’ve got nothing to do when it’s invoked in the future!), which allows the GC to clean up the WeakEvent instance (not that it should be that heavy anyway).

Advertisements

4 thoughts on “WeakEventManager for WinRT

  1. One problem I’m seeing is that if the event never fires, the registered handlers never get cleaned up, so the list of weak events can grow over time.

    Is there a (simple) way to resolve that? Part of the reason for wanting to use weak events is because it isn’t trivial to spot when the listeners aren’t needed any longer and so not easy to unsubscribe from the event.

    Thanks!

  2. I think I figured out how to clean up the registered handlers … call RemoveHandler from the destructor for each class that is calling AddHandler. Since the link is weak, the listening class can now be disposed of by GC, so the destructor can take care of cleaning up.

    When I made this change, though, I started hitting problems in RemoveHandler with an exception getting thrown in the for loop because the collection was getting changed. The simplest solution I found was to modify AddHandler *and* RemoveHandler so that a Mutex is used to ensure that the collection can’t be modified while it is being checked. I don’t know if that is the cleanest/best solution but it worked for me after trying out other ideas (like taking a copy of the collection before the for loop, which failed because of the size or something like that).

    • Not sure about the collection being modified – as soon as we have a match we break out of the loop iterating over it. Maybe it’s related to the finalizer being ran on a different thread, as the posted code isn’t thread safe (using a simple lock object should help, as you suggested).

      Another option, which might be simpler, would to just schedule a task every minute or so and do a scan over all the stored WeakEvents to see if they still point to a valid target. An improvement to this would be to only scan for a cleanup when the garbage collector has finished a collection, however, I can’t find a simple way of finding that out under WinRT.

      • I think you hit the nail on the head about the finalizer being run on a different thread. I don’t think it was multiple instances of the RemoveHandler being called that was the problem – I think it was AddHandler being called on one thread while RemoveHandler was being called on another thread. I came to this conclusion because putting a mutex just on RemoveHandler wasn’t enough – I had to put it on both Remove and Add. I think that it was the act of *adding* a handler that was causing the collection to change while RemoveHandler was iterating through the collection.

        Still, it isn’t too hard to call mutex.WaitOne() and mutex.ReleaseMutex() to solve the problem, and it doesn’t seem to be significantly detrimental to performance.

        Thanks for writing the weak event manager in the first place! Sorely needed for WinRT/UWP apps!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s