Introduction
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;
public static class WeakEventManager
{
private readonly static List<WeakEvent> registeredEvents = new List<WeakEvent>();
public static void AddHandler<TEventArgs>
(Type sourceType, string eventName, EventHandler<TEventArgs> handler)
{
EventInfo eventInfo = sourceType.GetRuntimeEvent(eventName);
registeredEvents.Add(
new WeakEvent(null, eventInfo, handler));
}
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));
}
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;
}
}
}
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
{
}
}
Naturally, in production, you’d want some argument validation (i.e. not null
s 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;
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 (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).
Filed under: CodeProject