From 4af9b22c59723845e3ef8760fde6c7522aba796f Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 26 Feb 2019 16:29:25 +0300 Subject: [PATCH] Introduced WeakEventHandlerManager --- .../Utilities/WeakEventHandlerManager.cs | 219 ++++++++++++++++++ .../WeakEventHandlerManagerTests.cs | 71 ++++++ 2 files changed, 290 insertions(+) create mode 100644 src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs create mode 100644 tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs diff --git a/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs new file mode 100644 index 0000000000..a724878317 --- /dev/null +++ b/src/Avalonia.Base/Utilities/WeakEventHandlerManager.cs @@ -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 +{ + /// + /// Manages subscriptions to events using weak listeners. + /// + public static class WeakEventHandlerManager + { + /// + /// Subscribes to an event on an object using a weak subscription. + /// + /// The type of the target. + /// The type of the event arguments. + /// The event source. + /// The name of the event. + /// The subscriber. + public static void Subscribe(TTarget target, string eventName, EventHandler subscriber) + where TEventArgs : EventArgs where TSubscriber : class + { + var dic = SubscriptionTypeStorage.Subscribers.GetOrCreateValue(target); + Subscription sub; + + if (!dic.TryGetValue(eventName, out sub)) + { + dic[eventName] = sub = new Subscription(dic, typeof(TTarget), target, eventName); + } + + sub.Add(subscriber); + } + + /// + /// Unsubscribes from an event. + /// + /// The type of the event arguments. + /// The event source. + /// The name of the event. + /// The subscriber. + public static void Unsubscribe(object target, string eventName, EventHandler subscriber) + where TEventArgs : EventArgs where TSubscriber : class + { + SubscriptionDic dic; + + if (SubscriptionTypeStorage.Subscribers.TryGetValue(target, out dic)) + { + Subscription sub; + + if (dic.TryGetValue(eventName, out sub)) + { + sub.Remove(subscriber); + } + } + } + + private static class SubscriptionTypeStorage + where TArgs : EventArgs where TSubscriber : class + { + public static readonly ConditionalWeakTable> Subscribers + = new ConditionalWeakTable>(); + } + + private class SubscriptionDic : Dictionary> + where T : EventArgs where TSubscriber : class + { + } + + private static readonly Dictionary> Accessors + = new Dictionary>(); + + private class Subscription where T : EventArgs where TSubscriber : class + { + private readonly EventInfo _info; + private readonly SubscriptionDic _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 Subscriber; + public CallerDelegate Caller; + } + + private static Dictionary s_Callers = + new Dictionary(); + + public Subscription(SubscriptionDic sdic, Type targetType, object target, string eventName) + { + _sdic = sdic; + _target = target; + _eventName = eventName; + Dictionary evDic; + if (!Accessors.TryGetValue(targetType, out evDic)) + Accessors[targetType] = evDic = new Dictionary(); + + 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(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 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(subscriber) + }; + _count++; + } + + public void Remove(EventHandler 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(); + } + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs b/tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs new file mode 100644 index 0000000000..9ed6590821 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/WeakEventHandlerManagerTests.cs @@ -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 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(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(source, name, new Subscriber(func).OnEvent); + } + } +}