using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; namespace Unity.CodeEditor.Utils { /// /// WeakEventManager base class. Inspired by the WPF WeakEventManager class and the code in /// https://social.msdn.microsoft.com/Forums/silverlight/en-US/34d85c3f-52ea-4adc-bb32-8297f5549042/command-binding-memory-leak?forum=silverlightbugs /// /// Copied here from ReactiveUI due to bugs in its design (singleton instance for multiple events). /// /// The type of the event source. /// The type of the event handler. /// The type of the event arguments. internal abstract class WeakEventManagerBase where TEventManager : WeakEventManagerBase, new() { // ReSharper disable once StaticMemberInGenericType private static readonly object StaticSource = new object(); /// /// Mapping between the target of the delegate (for example a Button) and the handler (EventHandler). /// Windows Phone needs this, otherwise the event handler gets garbage collected. /// private readonly ConditionalWeakTable> _targetToEventHandler = new ConditionalWeakTable>(); /// /// Mapping from the source of the event to the list of handlers. This is a CWT to ensure it does not leak the source of the event. /// private readonly ConditionalWeakTable _sourceToWeakHandlers = new ConditionalWeakTable(); private static readonly Lazy CurrentLazy = new Lazy(() => new TEventManager()); private static TEventManager Current => CurrentLazy.Value; /// /// Adds a weak reference to the handler and associates it with the source. /// /// The source. /// The handler. internal static void AddHandler(TEventSource source, TEventHandler handler) { Current.PrivateAddHandler(source, handler); } /// /// Removes the association between the source and the handler. /// /// The source. /// The handler. internal static void RemoveHandler(TEventSource source, TEventHandler handler) { Current.PrivateRemoveHandler(source, handler); } /// /// Delivers the event to the handlers registered for the source. /// /// The sender. /// The instance containing the event data. protected static void DeliverEvent(object sender, TEventArgs args) { Current.PrivateDeliverEvent(sender, args); } /// /// Override this method to attach to an event. /// /// The source. protected abstract void StartListening(TEventSource source); /// /// Override this method to detach from an event. /// /// The source. protected abstract void StopListening(TEventSource source); protected void PrivateAddHandler(TEventSource source, TEventHandler handler) { if (source == null) throw new ArgumentNullException(nameof(source)); if (handler == null) throw new ArgumentNullException(nameof(handler)); if (!typeof(TEventHandler).GetTypeInfo().IsSubclassOf(typeof(Delegate))) { throw new ArgumentException("Handler must be Delegate type"); } AddWeakHandler(source, handler); AddTargetHandler(handler); } private void AddWeakHandler(TEventSource source, TEventHandler handler) { if (_sourceToWeakHandlers.TryGetValue(source, out var weakHandlers)) { // clone list if we are currently delivering an event if (weakHandlers.IsDeliverActive) { weakHandlers = weakHandlers.Clone(); _sourceToWeakHandlers.Remove(source); _sourceToWeakHandlers.Add(source, weakHandlers); } weakHandlers.AddWeakHandler(source, handler); } else { weakHandlers = new WeakHandlerList(); weakHandlers.AddWeakHandler(source, handler); _sourceToWeakHandlers.Add(source, weakHandlers); StartListening(source); } Purge(source); } private void AddTargetHandler(TEventHandler handler) { var @delegate = handler as Delegate; var key = @delegate?.Target ?? StaticSource; if (_targetToEventHandler.TryGetValue(key, out var delegates)) { delegates.Add(@delegate); } else { delegates = new List { @delegate }; _targetToEventHandler.Add(key, delegates); } } protected void PrivateRemoveHandler(TEventSource source, TEventHandler handler) { if (source == null) throw new ArgumentNullException(nameof(source)); if (handler == null) throw new ArgumentNullException(nameof(handler)); if (!typeof(TEventHandler).GetTypeInfo().IsSubclassOf(typeof(Delegate))) { throw new ArgumentException("handler must be Delegate type"); } RemoveWeakHandler(source, handler); RemoveTargetHandler(handler); } private void RemoveWeakHandler(TEventSource source, TEventHandler handler) { if (_sourceToWeakHandlers.TryGetValue(source, out var weakHandlers)) { // clone list if we are currently delivering an event if (weakHandlers.IsDeliverActive) { weakHandlers = weakHandlers.Clone(); _sourceToWeakHandlers.Remove(source); _sourceToWeakHandlers.Add(source, weakHandlers); } if (weakHandlers.RemoveWeakHandler(source, handler) && weakHandlers.Count == 0) { _sourceToWeakHandlers.Remove(source); StopListening(source); } } } private void RemoveTargetHandler(TEventHandler handler) { var @delegate = handler as Delegate; var key = @delegate?.Target ?? StaticSource; if (_targetToEventHandler.TryGetValue(key, out var delegates)) { delegates.Remove(@delegate); if (delegates.Count == 0) { _targetToEventHandler.Remove(key); } } } private void PrivateDeliverEvent(object sender, TEventArgs args) { var source = sender ?? StaticSource; var hasStaleEntries = false; if (_sourceToWeakHandlers.TryGetValue(source, out var weakHandlers)) { using (weakHandlers.DeliverActive()) { hasStaleEntries = weakHandlers.DeliverEvent(source, args); } } if (hasStaleEntries) { Purge(source); } } private void Purge(object source) { if (_sourceToWeakHandlers.TryGetValue(source, out var weakHandlers)) { if (weakHandlers.IsDeliverActive) { weakHandlers = weakHandlers.Clone(); _sourceToWeakHandlers.Remove(source); _sourceToWeakHandlers.Add(source, weakHandlers); } else { weakHandlers.Purge(); } } } internal class WeakHandler { private readonly WeakReference _source; private readonly WeakReference _originalHandler; internal bool IsActive => _source != null && _source.IsAlive && _originalHandler != null && _originalHandler.IsAlive; internal TEventHandler Handler { get { if (_originalHandler == null) { return default(TEventHandler); } return (TEventHandler)_originalHandler.Target; } } internal WeakHandler(object source, TEventHandler originalHandler) { _source = new WeakReference(source); _originalHandler = new WeakReference(originalHandler); } internal bool Matches(object o, TEventHandler handler) { return _source != null && ReferenceEquals(_source.Target, o) && _originalHandler != null && (ReferenceEquals(_originalHandler.Target, handler) || _originalHandler.Target is TEventHandler && handler is TEventHandler && handler is Delegate del && _originalHandler.Target is Delegate origDel && Equals(del.Target, origDel.Target)); } } internal class WeakHandlerList { private int _deliveries; private readonly List _handlers; internal WeakHandlerList() { _handlers = new List(); } internal void AddWeakHandler(TEventSource source, TEventHandler handler) { var handlerSink = new WeakHandler(source, handler); _handlers.Add(handlerSink); } internal bool RemoveWeakHandler(TEventSource source, TEventHandler handler) { foreach (var weakHandler in _handlers) { if (weakHandler.Matches(source, handler)) { return _handlers.Remove(weakHandler); } } return false; } internal WeakHandlerList Clone() { var newList = new WeakHandlerList(); newList._handlers.AddRange(_handlers.Where(h => h.IsActive)); return newList; } internal int Count => _handlers.Count; internal bool IsDeliverActive => _deliveries > 0; internal IDisposable DeliverActive() { Interlocked.Increment(ref _deliveries); return new Disposable(() => Interlocked.Decrement(ref _deliveries)); } // ReSharper disable once MemberHidesStaticFromOuterClass internal virtual bool DeliverEvent(object sender, TEventArgs args) { var hasStaleEntries = false; foreach (var handler in _handlers) { if (handler.IsActive) { var @delegate = handler.Handler as Delegate; @delegate?.DynamicInvoke(sender, args); } else { hasStaleEntries = true; } } return hasStaleEntries; } internal void Purge() { for (var i = _handlers.Count - 1; i >= 0; i--) { if (!_handlers[i].IsActive) { _handlers.RemoveAt(i); } } } } } internal sealed class Disposable : IDisposable { private volatile Action _dispose; internal Disposable(Action dispose) { _dispose = dispose; } internal bool IsDisposed => _dispose == null; public void Dispose() { Interlocked.Exchange(ref _dispose, null)?.Invoke(); } } }