Created
July 19, 2017 21:50
-
-
Save waltdestler/2a339bdd0d7d647501eb4690772e3b50 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// <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