using PostSharp.Aspects; using PostSharp.Serialization; using System; using System.Collections.Immutable; using System.Reflection; using System.Threading; namespace PostSharp.Samples.WeakEvent { /// <summary> /// Aspect that, when applied to an event, prevents the target event from holding a strong reference to event handlers. /// Therefore, the aspect prevents the event to prevent clients to be garbage collected. /// </summary> [PSerializable] [LinesOfCodeAvoided(6)] public sealed class WeakEventAttribute : EventInterceptionAspect, IInstanceScopedAspect { [PNonSerialized] private volatile int cleanUpCounter; [PNonSerialized] private ImmutableArray<object> handlers; [PNonSerialized] private bool initialized; [PNonSerialized] private SpinLock spinLock; /// <summary> /// Creates an instance of the aspect for a specific instance of the target class (when the target event is not /// static). A call of this method is followed by a call of RuntimeInitializeInstance. /// </summary> object IInstanceScopedAspect.CreateInstance(AdviceArgs adviceArgs) { return MemberwiseClone(); } /// <summary> /// Initializes the current instance of the aspect (when the target event is not static). /// </summary> void IInstanceScopedAspect.RuntimeInitializeInstance() { if (!initialized) { cleanUpCounter = 0; initialized = true; handlers = ImmutableArray<object>.Empty; } } /// <summary> /// Initializes the current instance of the aspect (whether target event is static or not). /// </summary> public override void RuntimeInitialize(EventInfo eventInfo) { if (eventInfo.AddMethod.IsStatic) { if (!initialized) { cleanUpCounter = 0; initialized = true; handlers = ImmutableArray<object>.Empty; } } } #region Add /// <summary> /// Method invoked when (instead of) a new handler is added to the target event. /// </summary> /// <param name="args">Context information.</param> public override void OnAddHandler(EventInterceptionArgs args) { // Add the handler to our own list. if (AddHandler(args.Handler)) { args.AddHandler(null); } // Register the handler to the client to prevent garbage collection of the handler. DelegateReferenceKeeper.AddReference(args.Handler); } private bool AddHandler(Delegate handler) { var lockTaken = false; try { spinLock.Enter(ref lockTaken); handlers = handlers.Add(new WeakReference(handler)); return handlers.Length == 1; } finally { if (lockTaken) { spinLock.Exit(); } } } #endregion #region Remove /// <summary> /// Method invoked when (instead of) a new handler is removed from the target event. /// </summary> /// <param name="args">Context information.</param> public override void OnRemoveHandler(EventInterceptionArgs args) { // Remove the handler from our own list. if (RemoveHandler(args.Handler)) { args.RemoveHandler(null); } // Remove the handler from the client. DelegateReferenceKeeper.RemoveReference(args.Handler); } private bool RemoveHandler(Delegate handler) { var lockTaken = false; try { spinLock.Enter(ref lockTaken); handlers = handlers.RemoveAll( o => ((WeakReference) o).Target?.Equals(handler) ?? false); return handlers.IsEmpty; } finally { if (lockTaken) { spinLock.Exit(); } } } #endregion #region Invoke /// <summary> /// Method invoked when (instead of) a the target event is raised. /// </summary> /// <param name="args">Context information.</param> public override void OnInvokeHandler(EventInterceptionArgs args) { // Note that args.Handler == null because it's the value we added in OnAddHandler, but it really does not matter. InvokeHandler(args.Arguments.ToArray()); } private void InvokeHandler(object[] args) { var lastCleanUpCounter = -1; // Take a snapshot of the handlers list. var invocationList = handlers; var needCleanUp = false; foreach (var obj in invocationList) { var handler = (Delegate) ((WeakReference) obj).Target; if (handler == null) { if (!needCleanUp) { needCleanUp = true; lastCleanUpCounter = cleanUpCounter; } continue; } handler.DynamicInvoke(args); } if (needCleanUp && lastCleanUpCounter == cleanUpCounter) { if (lastCleanUpCounter == cleanUpCounter) { var lockTaken = false; try { spinLock.Enter(ref lockTaken); handlers = handlers.RemoveAll(w => !((WeakReference) w).IsAlive); Interlocked.Increment(ref cleanUpCounter); } finally { if (lockTaken) { spinLock.Exit(); } } } } } #endregion } }