2 changed files with 290 additions and 0 deletions
@ -0,0 +1,219 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Reflection; |
|||
using System.Runtime.CompilerServices; |
|||
|
|||
namespace Avalonia.Utilities |
|||
{ |
|||
/// <summary>
|
|||
/// Manages subscriptions to events using weak listeners.
|
|||
/// </summary>
|
|||
public static class WeakEventHandlerManager |
|||
{ |
|||
/// <summary>
|
|||
/// Subscribes to an event on an object using a weak subscription.
|
|||
/// </summary>
|
|||
/// <typeparam name="TTarget">The type of the target.</typeparam>
|
|||
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
|
|||
/// <param name="target">The event source.</param>
|
|||
/// <param name="eventName">The name of the event.</param>
|
|||
/// <param name="subscriber">The subscriber.</param>
|
|||
public static void Subscribe<TTarget, TEventArgs, TSubscriber>(TTarget target, string eventName, EventHandler<TEventArgs> subscriber) |
|||
where TEventArgs : EventArgs where TSubscriber : class |
|||
{ |
|||
var dic = SubscriptionTypeStorage<TEventArgs, TSubscriber>.Subscribers.GetOrCreateValue(target); |
|||
Subscription<TEventArgs, TSubscriber> sub; |
|||
|
|||
if (!dic.TryGetValue(eventName, out sub)) |
|||
{ |
|||
dic[eventName] = sub = new Subscription<TEventArgs, TSubscriber>(dic, typeof(TTarget), target, eventName); |
|||
} |
|||
|
|||
sub.Add(subscriber); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Unsubscribes from an event.
|
|||
/// </summary>
|
|||
/// <typeparam name="TEventArgs">The type of the event arguments.</typeparam>
|
|||
/// <param name="target">The event source.</param>
|
|||
/// <param name="eventName">The name of the event.</param>
|
|||
/// <param name="subscriber">The subscriber.</param>
|
|||
public static void Unsubscribe<TEventArgs, TSubscriber>(object target, string eventName, EventHandler<TEventArgs> subscriber) |
|||
where TEventArgs : EventArgs where TSubscriber : class |
|||
{ |
|||
SubscriptionDic<TEventArgs, TSubscriber> dic; |
|||
|
|||
if (SubscriptionTypeStorage<TEventArgs, TSubscriber>.Subscribers.TryGetValue(target, out dic)) |
|||
{ |
|||
Subscription<TEventArgs, TSubscriber> sub; |
|||
|
|||
if (dic.TryGetValue(eventName, out sub)) |
|||
{ |
|||
sub.Remove(subscriber); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private static class SubscriptionTypeStorage<TArgs, TSubscriber> |
|||
where TArgs : EventArgs where TSubscriber : class |
|||
{ |
|||
public static readonly ConditionalWeakTable<object, SubscriptionDic<TArgs, TSubscriber>> Subscribers |
|||
= new ConditionalWeakTable<object, SubscriptionDic<TArgs, TSubscriber>>(); |
|||
} |
|||
|
|||
private class SubscriptionDic<T, TSubscriber> : Dictionary<string, Subscription<T, TSubscriber>> |
|||
where T : EventArgs where TSubscriber : class |
|||
{ |
|||
} |
|||
|
|||
private static readonly Dictionary<Type, Dictionary<string, EventInfo>> Accessors |
|||
= new Dictionary<Type, Dictionary<string, EventInfo>>(); |
|||
|
|||
private class Subscription<T, TSubscriber> where T : EventArgs where TSubscriber : class |
|||
{ |
|||
private readonly EventInfo _info; |
|||
private readonly SubscriptionDic<T, TSubscriber> _sdic; |
|||
private readonly object _target; |
|||
private readonly string _eventName; |
|||
private readonly Delegate _delegate; |
|||
|
|||
private Descriptor[] _data = new Descriptor[2]; |
|||
private int _count = 0; |
|||
|
|||
delegate void CallerDelegate(TSubscriber s, object sender, T args); |
|||
|
|||
struct Descriptor |
|||
{ |
|||
public WeakReference<TSubscriber> Subscriber; |
|||
public CallerDelegate Caller; |
|||
} |
|||
|
|||
private static Dictionary<MethodInfo, CallerDelegate> s_Callers = |
|||
new Dictionary<MethodInfo, CallerDelegate>(); |
|||
|
|||
public Subscription(SubscriptionDic<T, TSubscriber> sdic, Type targetType, object target, string eventName) |
|||
{ |
|||
_sdic = sdic; |
|||
_target = target; |
|||
_eventName = eventName; |
|||
Dictionary<string, EventInfo> evDic; |
|||
if (!Accessors.TryGetValue(targetType, out evDic)) |
|||
Accessors[targetType] = evDic = new Dictionary<string, EventInfo>(); |
|||
|
|||
if (!evDic.TryGetValue(eventName, out _info)) |
|||
{ |
|||
var ev = targetType.GetRuntimeEvents().FirstOrDefault(x => x.Name == eventName); |
|||
|
|||
if (ev == null) |
|||
{ |
|||
throw new ArgumentException( |
|||
$"The event {eventName} was not found on {target.GetType()}."); |
|||
} |
|||
|
|||
evDic[eventName] = _info = ev; |
|||
} |
|||
|
|||
var del = new Action<object, T>(OnEvent); |
|||
_delegate = del.GetMethodInfo().CreateDelegate(_info.EventHandlerType, del.Target); |
|||
_info.AddMethod.Invoke(target, new[] { _delegate }); |
|||
|
|||
} |
|||
|
|||
void Destroy() |
|||
{ |
|||
_info.RemoveMethod.Invoke(_target, new[] { _delegate }); |
|||
_sdic.Remove(_eventName); |
|||
} |
|||
|
|||
public void Add(EventHandler<T> s) |
|||
{ |
|||
if (_count == _data.Length) |
|||
{ |
|||
//Extend capacity
|
|||
var ndata = new Descriptor[_data.Length*2]; |
|||
Array.Copy(_data, ndata, _data.Length); |
|||
_data = ndata; |
|||
} |
|||
|
|||
var subscriber = (TSubscriber)s.Target; |
|||
if (!s_Callers.TryGetValue(s.Method, out var caller)) |
|||
s_Callers[s.Method] = caller = |
|||
(CallerDelegate)Delegate.CreateDelegate(typeof(CallerDelegate), null, s.Method); |
|||
_data[_count] = new Descriptor |
|||
{ |
|||
Caller = caller, |
|||
Subscriber = new WeakReference<TSubscriber>(subscriber) |
|||
}; |
|||
_count++; |
|||
} |
|||
|
|||
public void Remove(EventHandler<T> s) |
|||
{ |
|||
var removed = false; |
|||
|
|||
for (int c = 0; c < _count; ++c) |
|||
{ |
|||
var reference = _data[c].Subscriber; |
|||
TSubscriber instance; |
|||
|
|||
if (reference != null && reference.TryGetTarget(out instance) && instance == s) |
|||
{ |
|||
_data[c] = default; |
|||
removed = true; |
|||
} |
|||
} |
|||
|
|||
if (removed) |
|||
{ |
|||
Compact(); |
|||
} |
|||
} |
|||
|
|||
void Compact() |
|||
{ |
|||
int empty = -1; |
|||
for (int c = 0; c < _count; c++) |
|||
{ |
|||
var r = _data[c]; |
|||
//Mark current index as first empty
|
|||
if (r.Subscriber == null && empty == -1) |
|||
empty = c; |
|||
//If current element isn't null and we have an empty one
|
|||
if (r.Subscriber != null && empty != -1) |
|||
{ |
|||
_data[c] = default; |
|||
_data[empty] = r; |
|||
empty++; |
|||
} |
|||
} |
|||
if (empty != -1) |
|||
_count = empty; |
|||
if (_count == 0) |
|||
Destroy(); |
|||
} |
|||
|
|||
void OnEvent(object sender, T eventArgs) |
|||
{ |
|||
var needCompact = false; |
|||
for(var c=0; c<_count; c++) |
|||
{ |
|||
var r = _data[c].Subscriber; |
|||
TSubscriber sub; |
|||
if (r.TryGetTarget(out sub)) |
|||
{ |
|||
_data[c].Caller(sub, sender, eventArgs); |
|||
} |
|||
else |
|||
needCompact = true; |
|||
} |
|||
if (needCompact) |
|||
Compact(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using System.Text; |
|||
using System.Threading.Tasks; |
|||
using Avalonia.Utilities; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Base.UnitTests |
|||
{ |
|||
public class WeakEventHandlerManagerTests |
|||
{ |
|||
class EventSource |
|||
{ |
|||
public event EventHandler<EventArgs> Event; |
|||
|
|||
public void Fire() |
|||
{ |
|||
Event?.Invoke(this, new EventArgs()); |
|||
} |
|||
} |
|||
|
|||
class Subscriber |
|||
{ |
|||
private readonly Action _onEvent; |
|||
|
|||
public Subscriber(Action onEvent) |
|||
{ |
|||
_onEvent = onEvent; |
|||
} |
|||
|
|||
public void OnEvent(object sender, EventArgs ev) |
|||
{ |
|||
_onEvent?.Invoke(); |
|||
} |
|||
} |
|||
|
|||
[Fact] |
|||
public void EventShoudBePassedToSubscriber() |
|||
{ |
|||
bool handled = false; |
|||
var subscriber = new Subscriber(() => handled = true); |
|||
var source = new EventSource(); |
|||
WeakEventHandlerManager.Subscribe<EventSource, EventArgs, Subscriber>(source, "Event", |
|||
subscriber.OnEvent); |
|||
source.Fire(); |
|||
Assert.True(handled); |
|||
} |
|||
|
|||
|
|||
[Fact] |
|||
public void EventHandlerShouldNotBeKeptAlive() |
|||
{ |
|||
bool handled = false; |
|||
var source = new EventSource(); |
|||
AddCollectableSubscriber(source, "Event", () => handled = true); |
|||
for (int c = 0; c < 10; c++) |
|||
{ |
|||
GC.Collect(); |
|||
GC.Collect(3, GCCollectionMode.Forced, true); |
|||
} |
|||
source.Fire(); |
|||
Assert.False(handled); |
|||
} |
|||
|
|||
private void AddCollectableSubscriber(EventSource source, string name, Action func) |
|||
{ |
|||
WeakEventHandlerManager.Subscribe<EventSource, EventArgs, Subscriber>(source, name, new Subscriber(func).OnEvent); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue