Skip to content

Instantly share code, notes, and snippets.

@waltdestler
Created July 19, 2017 21:50
Show Gist options
  • Save waltdestler/2a339bdd0d7d647501eb4690772e3b50 to your computer and use it in GitHub Desktop.
Save waltdestler/2a339bdd0d7d647501eb4690772e3b50 to your computer and use it in GitHub Desktop.
/// <summary>
/// An event handler that weakly-references its target object and automatically unregisters
/// itself once its target has been garbage-collected.
/// </summary>
public static class WeakEventHandler
{
#region Fields
private static readonly ConditionalWeakTable<object, Dictionary<EventInfo, object>> s_sourceObjects = new ConditionalWeakTable<object, Dictionary<EventInfo, object>>();
private static readonly Dictionary<EventInfo, object> s_staticEvents = new Dictionary<EventInfo, object>();
private static readonly Dictionary<(Type Type, string EventName), EventInfo> s_staticEventInfoCache = new Dictionary<(Type Type, string EventName), EventInfo>();
#endregion
#region Public Static Methods
/// <summary>
/// Subscribes the specified event handler to an event on the specified source.
/// This allows the target of the event and its EventHandler delegate to be garbage-collected.
/// </summary>
/// <param name="source">The object providing the event to be subscribed to.</param>
/// <param name="eventName">The name of the event to be subscribed to.</param>
/// <param name="eventHandler">The EventHandler that will be invoked when the source's event is fired.</param>
public static void Subscribe<TSource, TEventArgs>(TSource source, string eventName, EventHandler<TEventArgs> eventHandler)
where TSource : class
{
if(source == null)
throw new ArgumentNullException(nameof(source));
if(eventName == null)
throw new ArgumentNullException(nameof(eventName));
if(eventHandler == null)
throw new ArgumentNullException(nameof(eventHandler));
EventInfo eventInfo = EventInfoCache<TSource>.Get(eventName);
if(eventInfo.EventHandlerType != typeof(EventHandler<TEventArgs>))
throw new InvalidOperationException($"Expected to subscribe to event '{eventName}' of type {typeof(EventHandler<TEventArgs>)} but found event of type {eventInfo.EventHandlerType}.");
Subscriber<TEventArgs> subscriber = new Subscriber<TEventArgs>(eventHandler);
Dictionary<EventInfo, object> events;
lock(s_sourceObjects) // ConditionalWeakTable is supposed to be thread-safe, but apparently not.
events = s_sourceObjects.GetOrCreateValue(source);
SubscribeHelper(source, events, eventInfo, subscriber);
}
/// <summary>
/// Subscribes the specified event handler to a static event on the specified source type.
/// This allows the target of the event and its EventHandler delegate to be garbage-collected.
/// </summary>
/// <param name="sourceType">The type containing the static event to subscribe to.</param>
/// <param name="eventName">The name of the event to be subscribed to.</param>
/// <param name="eventHandler">The EventHandler that will be invoked when the source type's event is fired.</param>
public static void SubscribeStatic<TEventArgs>(Type sourceType, string eventName, EventHandler<TEventArgs> eventHandler)
{
if(sourceType == null)
throw new ArgumentNullException(nameof(sourceType));
if(eventName == null)
throw new ArgumentNullException(nameof(eventName));
if(eventHandler == null)
throw new ArgumentNullException(nameof(eventHandler));
EventInfo eventInfo = GetStaticEventInfo(sourceType, eventName);
if(eventInfo.EventHandlerType != typeof(EventHandler<TEventArgs>))
throw new InvalidOperationException($"Expected to subscribe to static event '{eventName}' of type {typeof(EventHandler<TEventArgs>)} but found event of type {eventInfo.EventHandlerType}.");
Subscriber<TEventArgs> subscriber = new Subscriber<TEventArgs>(eventHandler);
SubscribeHelper<object, TEventArgs>(null, s_staticEvents, eventInfo, subscriber);
}
/// <summary>
/// Unsubscribes the specified event handler from an event on the specified source.
/// The event handler should previously have been subscribed using WeakEventHandler.Subscribe().
/// </summary>
/// <param name="source">The object providing the event to be unsubscribed from.</param>
/// <param name="eventName">The name of the event to be unsubscribed from.</param>
/// <param name="eventHandler">The EventHandler that should no longer be invoked when the source's event is fired.</param>
public static void Unsubscribe<TSource, TEventArgs>(TSource source, string eventName, EventHandler<TEventArgs> eventHandler)
where TSource : class
{
if(source == null)
throw new ArgumentNullException(nameof(source));
if(eventName == null)
throw new ArgumentNullException(nameof(eventName));
if(eventHandler == null)
throw new ArgumentNullException(nameof(eventHandler));
EventInfo eventInfo = EventInfoCache<TSource>.Get(eventName);
if(eventInfo.EventHandlerType != typeof(EventHandler<TEventArgs>))
throw new InvalidOperationException($"Expected to unsubscribe from event '{eventName}' of type {typeof(EventHandler<TEventArgs>)} but found event of type {eventInfo.EventHandlerType}.");
bool success;
Dictionary<EventInfo, object> events;
lock(s_sourceObjects)
success = s_sourceObjects.TryGetValue(source, out events);
if(success && UnsubscribeHelper(eventName, eventHandler, events, eventInfo))
return;
throw new InvalidOperationException($"The specified event handler was not subscribed to event {eventName} on object of type {typeof(TSource)}.");
}
/// <summary>
/// Unsubscribes the specified event handler from a static event on the specified source.
/// The event handler should previously have been subscribed using WeakEventHandler.SubscribeStatic().
/// </summary>
/// <param name="sourceType">The type containing the static event to unsubscribe from.</param>
/// <param name="eventName">The name of the event to be unsubscribed from.</param>
/// <param name="eventHandler">The EventHandler that should no longer be invoked when the source's event is fired.</param>
public static void UnsubscribeStatic<TEventArgs>(Type sourceType, string eventName, EventHandler<TEventArgs> eventHandler)
{
if(sourceType == null)
throw new ArgumentNullException(nameof(sourceType));
if(eventName == null)
throw new ArgumentNullException(nameof(eventName));
if(eventHandler == null)
throw new ArgumentNullException(nameof(eventHandler));
EventInfo eventInfo = GetStaticEventInfo(sourceType, eventName);
if(eventInfo.EventHandlerType != typeof(EventHandler<TEventArgs>))
throw new InvalidOperationException($"Expected to unsubscribe from static event '{eventName}' of type {typeof(EventHandler<TEventArgs>)} but found event of type {eventInfo.EventHandlerType}.");
if(UnsubscribeHelper(eventName, eventHandler, s_staticEvents, eventInfo))
return;
throw new InvalidOperationException($"The specified event handler was not subscribed to static event {eventName} on type {sourceType}.");
}
#endregion
#region Non-Public Methods
/// <summary>
/// Finishes the subscription registration for the Subscribe() and SubscribeStatic() methods.
/// </summary>
private static void SubscribeHelper<TSource, TEventArgs>(TSource source, Dictionary<EventInfo, object> events, EventInfo eventInfo, Subscriber<TEventArgs> subscriber)
{
// Get the state object for the event, creating it if necessary.
EventState<TEventArgs> state = null;
lock(events)
{
if(events.TryGetValue(eventInfo, out object stateObj))
{
state = (EventState<TEventArgs>)stateObj;
}
else
{
state = new EventState<TEventArgs>(source, eventInfo);
events.Add(eventInfo, state);
}
}
// Add the subscriber to the list of subscribers for the event.
Debug.Assert(state != null);
state.AddSubscriber(subscriber);
}
/// <summary>
/// Finishes the subscription de-registratoin from the Unsubscribe() and UnsubscribeStatic() methods.
/// </summary>
private static bool UnsubscribeHelper<TEventArgs>(string eventName, EventHandler<TEventArgs> eventHandler, Dictionary<EventInfo, object> events, EventInfo eventInfo)
{
EventState<TEventArgs> state = null;
lock(events)
{
if(events.TryGetValue(eventInfo, out object stateObj))
{
state = stateObj as EventState<TEventArgs>;
if(state == null)
throw new InvalidOperationException($"Expected to unsubscribe to event '{eventName}' of type {typeof(EventHandler<TEventArgs>)} but found event of a different type.");
}
}
if(state.RemoveSubscriber(eventHandler))
{
// We successfully unsubscribed from the event, so don't throw an exception.
return true;
}
return false;
}
/// <summary>
/// Returns the EventInfo for the specified source type and event name, loading it from a cache if possible.
/// </summary>
private static EventInfo GetStaticEventInfo(Type sourceType, string eventName)
{
lock(s_staticEventInfoCache)
{
EventInfo eventInfo;
var key = (sourceType, eventName);
if(!s_staticEventInfoCache.TryGetValue(key, out eventInfo))
{
eventInfo = sourceType.GetEvent(eventName, BindingFlags.Public | BindingFlags.Static);
if(eventInfo == null)
throw new InvalidOperationException($"Type {sourceType} does not have static event '{eventName}'.");
s_staticEventInfoCache.Add(key, eventInfo);
}
return eventInfo;
}
}
#endregion
#region Types
/// <summary>
/// Stores the state for a particular event on a particular source object.
/// </summary>
private class EventState<TEventArgs>
{
private readonly object _source;
private readonly EventInfo _eventInfo;
private readonly EventHandler<TEventArgs> _handler;
private readonly List<Subscriber<TEventArgs>> _subscribers = new List<Subscriber<TEventArgs>>();
private int _countAfterLastPurge;
public EventState(object source, EventInfo eventInfo)
{
_source = source;
_eventInfo = eventInfo;
_handler = OnSourceEventFired;
}
/// <summary>
/// Adds the specified Subscriber to the list that will be invoked when the source event fires.
/// </summary>
public void AddSubscriber(Subscriber<TEventArgs> eventHandler)
{
lock(_subscribers)
{
if(_subscribers.Count == _subscribers.Capacity || _subscribers.Count + 1 > _countAfterLastPurge * 2)
Purge();
_subscribers.Add(eventHandler);
// Need to register for the first subscriber?
if(_subscribers.Count == 1)
_eventInfo.AddEventHandler(_source, _handler);
}
}
/// <summary>
/// Removes the Subscriber that matches the specified event handler.
/// </summary>
public bool RemoveSubscriber(EventHandler<TEventArgs> eventHandler)
{
bool ret = false;
lock(_subscribers)
{
for(int i = 0; i < _subscribers.Count;)
{
Subscriber<TEventArgs> s = _subscribers[i];
if(s.Target.TryGetTarget(out object target))
{
if(target == eventHandler.Target && eventHandler.Method == s.Method)
{
_subscribers.RemoveAt(i);
_countAfterLastPurge--;
ret = true;
break;
}
i++;
}
else
{
_subscribers.RemoveAt(i);
_countAfterLastPurge--;
}
}
// Do we need to unregister from the source event?
if(_subscribers.Count == 0)
_eventInfo.RemoveEventHandler(_source, _handler);
}
return ret;
}
/// <summary>
/// Called when the source event has fired.
/// </summary>
private void OnSourceEventFired(object sender, TEventArgs args)
{
bool hasDead = false;
using(TempList<Subscriber<TEventArgs>> toCall = TempList<Subscriber<TEventArgs>>.Alloc())
{
// Create a copy of the subscribers in case the subscription list is modified during a callback.
lock(_subscribers)
toCall.AddRange(_subscribers);
for(int i = 0; i < toCall.Count; i++)
{
Subscriber<TEventArgs> s = toCall[i];
if(s.Target.TryGetTarget(out object target))
s.OpenEventHandler.Invoke(target, sender, args);
else
hasDead = true;
}
}
// Clean up any now-dead subscribers.
// This needs to be run separately from the above because
// callbacks may add/remove subscribers.
if(hasDead)
{
lock(_subscribers)
{
Purge();
// Do we now need to un-register the event handler?
if(_subscribers.Count == 0)
_eventInfo.RemoveEventHandler(_source, _handler);
}
}
}
/// <summary>
/// Purges all dead subscribers.
/// Need to lock on _subscribers.
/// </summary>
private void Purge()
{
for(int i = 0; i < _subscribers.Count;)
{
Subscriber<TEventArgs> s = _subscribers[i];
if(s.Target.TryGetTarget(out _))
i++;
else
_subscribers.RemoveAt(i);
}
_countAfterLastPurge = _subscribers.Count;
}
}
/// <summary>
/// Stores a weak reference to a particular subscriber's event handler.
/// </summary>
private class Subscriber<TEventArgs>
{
private static readonly ConcurrentDictionary<MethodInfo, IOpenDelegate<TEventArgs>> s_openDelegates = new ConcurrentDictionary<MethodInfo, IOpenDelegate<TEventArgs>>();
public readonly IOpenDelegate<TEventArgs> OpenEventHandler;
public readonly WeakReference<object> Target;
public readonly MethodInfo Method;
public Subscriber(EventHandler<TEventArgs> eventHandler)
{
if(eventHandler.Target == null)
throw new ArgumentException("Only instance methods are supported.", nameof(eventHandler));
Method = eventHandler.Method;
OpenEventHandler = s_openDelegates.GetOrAdd(Method, mi =>
{
if(mi.IsStatic)
throw new ArgumentException("Only instance methods are supported.", nameof(eventHandler));
if(mi.DeclaringType.IsValueType)
throw new ArgumentException("Value types are not supported.", nameof(eventHandler));
Type openDelegateType = typeof(OpenDelegate<,>).MakeGenericType(mi.DeclaringType, typeof(TEventArgs));
ConstructorInfo openDelegateConstructor = openDelegateType.GetConstructor(new[] {typeof(MethodInfo)});
return (IOpenDelegate<TEventArgs>)openDelegateConstructor.Invoke(new object[] {mi});
});
Target = new WeakReference<object>(eventHandler.Target);
}
}
/// <summary>
/// Exposes an Invoke() method.
/// </summary>
private interface IOpenDelegate<in TEventArgs>
{
void Invoke(object target, object sender, TEventArgs e);
}
/// <summary>
/// Wraps a delegate to a particular method without binding the actual target object.
/// </summary>
private class OpenDelegate<TTarget, TEventArgs> : IOpenDelegate<TEventArgs>
where TTarget : class
{
private delegate void OpenEventHandler(TTarget target, object sender, TEventArgs e);
private readonly OpenEventHandler _openInvoker;
public OpenDelegate(MethodInfo method)
{
_openInvoker = (OpenEventHandler)Delegate.CreateDelegate(typeof(OpenEventHandler), null, method);
}
public void Invoke(object target, object sender, TEventArgs e)
{
_openInvoker.Invoke((TTarget)target, sender, e);
}
}
/// <summary>
/// Caches EventInfo objects so that they can be retrieved without using reflection every time.
/// </summary>
private static class EventInfoCache<TSource>
{
private static readonly Dictionary<string, EventInfo> s_cache = new Dictionary<string, EventInfo>();
public static EventInfo Get(string eventName)
{
lock(s_cache)
{
EventInfo eventInfo;
if(!s_cache.TryGetValue(eventName, out eventInfo))
{
eventInfo = typeof(TSource).GetEvent(eventName, BindingFlags.Public | BindingFlags.Instance);
if(eventInfo == null)
throw new InvalidOperationException($"Object of type {typeof(TSource)} does not have non-static event '{eventName}'.");
s_cache.Add(eventName, eventInfo);
}
return eventInfo;
}
}
}
#endregion
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment