About .NET, ASP.NET, MVC, C#, WPF, WCF and everything related to .NET and more.

Weak events

Categories: Programming

Introduction

When using usual C# events, registering an event handler creates a strong reference from the event source to the listening object. If the source object has a longer lifetime than the listener, and if the listener no longer needed (there are no other references), to avoid memory leak the listener object must unsubscribe from the source event, if it does not, the use normal .NET events causes a memory leak.
There are lots of different approaches to resolve this problem. This article will explain some of them and discuss their advantages and disadvantages.

Method 1. Using weak-delegate without code-generation.

This method is very simple, we need to create wrapper for a weak-delegate:
public class WeakDelegate<TDelegate> : IEquatable<TDelegate> where TDelegate : class
{
    private readonly MethodInfo _method;
    private readonly WeakReference _targetReference;

    public WeakDelegate(Delegate realDelegate)
    {
        _targetReference = realDelegate.Target != null ? new WeakReference(realDelegate.Target) : null;
        _method = realDelegate.Method;
    }

    public bool IsAlive
    {
        get { return _targetReference == null || _targetReference.IsAlive; }
    }

    public TDelegate GetDelegate()
    {
        //If it's non-static create delegate with target.
        if (_targetReference != null)
            return Delegate.CreateDelegate(typeof(TDelegate), _targetReference.Target, _method) as TDelegate;
        return Delegate.CreateDelegate(typeof(TDelegate), _method) as TDelegate;
    }

    public bool Equals(TDelegate other)
    {
        var d = (Delegate)(object)other;
        return d != null
                && d.Target == _targetReference.Target
                && d.Method.Equals(_method);
    }

    public void Invoke(params object[] args)
    {
        var handler = (Delegate) (object) GetDelegate();
        handler.DynamicInvoke(args);
    }
}

and for a weak-event:
public class WeakEvent<TEventHandler> where TEventHandler : class
{
    private readonly List<WeakDelegate<TEventHandler>> _handlers;

    public WeakEvent()
    {
        _handlers = new List<WeakDelegate<TEventHandler>>();
    }

    public void AddHandler(TEventHandler handler)
    {
        var d = (Delegate)(object)handler;
        lock (_handlers)
            _handlers.Add(new WeakDelegate<TEventHandler>(d));
    }

    public void RemoveHandler(TEventHandler handler)
    {
        // also remove "dead" (garbage collected) handlers
        lock (_handlers)
            _handlers.RemoveAll(wd => !wd.IsAlive || wd.Equals(handler));
    }

    public void Raise(params object[] values)
    {
        lock (_handlers)
        {
            for (int index = 0; index < _handlers.Count; index++)
            {
                var weakDelegate = _handlers[index];
                if (weakDelegate.IsAlive)
                    weakDelegate.Invoke(values);
                else
                {
                    _handlers.Remove(weakDelegate);
                    index--;
                }
            }
        }
    }
}
After do this, we can create a class that will be used a WeakEvent wrapper to store events:
public class Alpha
{
    private readonly WeakEvent<Action<object>> _myEvents;

    public Alpha()
    {
        _myEvents = new WeakEvent<Action<object>>();
    }

    public event Action<object> MyEvent
    {
        add { _myEvents.AddHandler(value); }
        remove { _myEvents.RemoveHandler(value); }
    }

    protected virtual void OnMyEvent(object value)
    {
        _myEvents.Raise(value);
    }
}

Advantages
  • Works on all platforms
  • Simple and effective
Disadvantages
  • Slow, because to raise an event creates new instance of delegate

Method 2. Using weak-delegate created by DynamicMethod.

The second method uses the DynamicMethod to generate a weak version of delegate. The code below demonstrates one approach to generate weak delegate using emit.
The first step it's create class that represents a information about weak event:
public sealed class WeakEventInfo
{
    #region Const

    public static readonly FieldInfo TargetFieldInfo = typeof(WeakEventInfo).GetField("Target");

    public static readonly FieldInfo UnsubcribeDelegateFieldInfo =
        typeof(WeakEventInfo).GetField("UnsubcribeDelegate");

    public static readonly FieldInfo DelegateFieldInfo = typeof(WeakEventInfo).GetField("Delegate");

    public static readonly MethodInfo ClearMethodInfo = typeof(WeakEventInfo).GetMethod("Clear");

    #endregion

    #region Fields

    public Delegate Delegate;

    public WeakReference Target;

    public Action<Delegate> UnsubcribeDelegate;

    public MethodInfo OriginalMethod;

    #endregion

    #region Methods

    public void Clear()
    {
        if (UnsubcribeDelegate != null)
            UnsubcribeDelegate(Delegate);
        Delegate = null;
        Target = null;
        UnsubcribeDelegate = null;
        OriginalMethod = null;
    }

    #endregion
}
Description of the fields:
  • Delegate - contains weak version of delegate, used to unsubscribe when target was garbage collected.
  • Target - contains a delegate target as WeakReference.
  • UnsubcribeDelegate - contains a delegate to unsubscribe when target was garbage collected.
  • OriginalMethod - contains information about method that should be invoked.
This code represents methods to make delegate as weak:
internal static DynamicMethod ConvertToWeakDelegate(Delegate originalAction)
{
    MethodInfo originalMethodDelegate = originalAction.Method;
    ParameterInfo[] parameterInfos = originalMethodDelegate.GetParameters();

    var types = new Type[parameterInfos.Length + 1];
    types[0] = typeof(WeakEventInfo);
    for (int i = 0; i < parameterInfos.Length; i++)
    {
        types[i + 1] = parameterInfos[i].ParameterType;
    }

    var dynamicMethod = new DynamicMethod(
        string.Format("_DynamicMethod_{0}_{1}", originalMethodDelegate.DeclaringType.Name,
                        Guid.NewGuid().ToString("N")), originalMethodDelegate.ReturnType,
        types, originalMethodDelegate.DeclaringType.Module, true);

    ILGenerator ilGenerator = dynamicMethod.GetILGenerator();
    Label originalDelegateIsNotNull = ilGenerator.DefineLabel();
    Label returnNullLabel = ilGenerator.DefineLabel();
    LocalBuilder declareLocal = ilGenerator.DeclareLocal(originalAction.Target.GetType());


    for (Int16 i = 0; i < parameterInfos.Length; i++)
    {
        ParameterInfo parameterInfo = parameterInfos[i];
        if (!parameterInfo.IsDefined(typeof(OutAttribute), true)) continue;
        Type elementType = parameterInfo.ParameterType.GetElementType();
        ilGenerator.Emit(OpCodes.Ldarg, i + 1);
        ilGenerator.Emit(OpCodes.Initobj, elementType);
    }

    ilGenerator.Emit(OpCodes.Ldarg_0);
    ilGenerator.Emit(OpCodes.Ldfld, WeakEventInfo.TargetFieldInfo);
    ilGenerator.Emit(OpCodes.Brfalse_S, returnNullLabel);
    //if (weakEventInfo.Target == null)
    //   return null;

    ilGenerator.Emit(OpCodes.Ldarg_0);
    ilGenerator.Emit(OpCodes.Ldfld, WeakEventInfo.TargetFieldInfo);
    ilGenerator.Emit(OpCodes.Callvirt, typeof(WeakReference).GetProperty("Target").GetGetMethod());

    ilGenerator.Emit(OpCodes.Castclass, declareLocal.LocalType);
    ilGenerator.Emit(OpCodes.Stloc, declareLocal);
    //var target = weakEventInfo.OriginalDelegate.Target;
    ilGenerator.Emit(OpCodes.Ldloc_0);
    ilGenerator.Emit(OpCodes.Brtrue_S, originalDelegateIsNotNull);
    //if (target == null){

    ilGenerator.Emit(OpCodes.Ldarg_0);
    ilGenerator.Emit(OpCodes.Callvirt, WeakEventInfo.ClearMethodInfo);
    //weakEventInfo.Clear();
    //}
    ilGenerator.MarkLabel(returnNullLabel);
    if (dynamicMethod.ReturnType != typeof(void))
    {
        if (dynamicMethod.ReturnType.IsValueType)
        {
            LocalBuilder result = ilGenerator.DeclareLocal(dynamicMethod.ReturnType);
            ilGenerator.Emit(OpCodes.Ldloca_S, result);
            ilGenerator.Emit(OpCodes.Initobj, dynamicMethod.ReturnType);
            ilGenerator.Emit(OpCodes.Ldloc_S, result);
        }
        else
            ilGenerator.Emit(OpCodes.Ldnull);
    }
    ilGenerator.Emit(OpCodes.Ret);
    //return null;
    //}

    ilGenerator.MarkLabel(originalDelegateIsNotNull);
    ilGenerator.Emit(OpCodes.Ldloc_0);
    for (Int16 i = 1; i < types.Length; i++)
    {
        ilGenerator.Emit(OpCodes.Ldarg_S, i);
    }
    ilGenerator.Emit(OpCodes.Callvirt, originalMethodDelegate);
    ilGenerator.Emit(OpCodes.Ret);
    //return target.Invoke(arg1, arg2, .....);


    return dynamicMethod;
}

public static Delegate ConvertToWeakDelegate(Delegate originalAction, Action<Delegate> unsubcribeDelegate)
{
    if (originalAction == null)
        throw new ArgumentNullException("originalAction");
    if (originalAction.Target == null || originalAction.Target is WeakEventInfo)
        return originalAction;
    DynamicMethod weakMethodInternal = ConvertToWeakDelegate(originalAction);
    var weakEventInfo = new WeakEventInfo
        {
            Target = new WeakReference(originalAction.Target),
            OriginalMethod = originalAction.Method
        };
    if (unsubcribeDelegate != null)
        weakEventInfo.UnsubcribeDelegate = unsubcribeDelegate;

    Delegate weakDelegate = weakMethodInternal.CreateDelegate(originalAction.GetType(), weakEventInfo);
    if (unsubcribeDelegate != null)
        weakEventInfo.Delegate = weakDelegate;
    return weakDelegate;
}
Note: In this example I don't cache dynamic methods, but I recommend you do it.
Now we can use this methods to make weak-delegates:
public class Beta
{
    public event Action<object> MyEvent;

    protected virtual void OnMyEvent(object value)
    {
        Action<object> action = MyEvent;
        if (action != null)
            action(value);
    }
}
....

private void Method()
{
    var beta = new Beta();
    beta.MyEvent += (Action<object>) ConvertToWeakDelegate(new Action<object>(OnMyEvent), @delegate =>
        {
            beta.MyEvent -= (Action<object>) @delegate;
        });
}

private void OnMyEvent(object value)
{
            
}
In order to make this syntax better, we create this extension method:
public static T ConvertToWeakDelegate<T>(T originalAction, Action<T> unsubcribeDelegate) where T : class
{
    var @delegate = originalAction as Delegate;
    if (@delegate == null)
        throw new ArgumentNullException("originalAction", "Parameter can only be a delegate.");
    Action<Delegate> uns = null;
    if (unsubcribeDelegate != null)
        uns = d => unsubcribeDelegate((T)((object)d));
    return ConvertToWeakDelegate(@delegate, uns) as T;
}
And now we can rewrite previous method like this:
private void Method()
{
    var beta = new Beta();
    beta.MyEvent += ConvertToWeakDelegate(new Action<object>(OnMyEvent), 
                                   @delegate => beta.MyEvent -= @delegate);
}

Advantages
  • Fast, nearly no code overhead.
Disadvantages
  • Does not work in partial trust because it uses reflection on private methods, read this article for more information.

Method 3. Using weak-delegate created by LambdaExpression.

This method is very similar to the method 2, different only in way to create the weak-delegate. To make delegate as weak this method uses the LambdaExpression and the WeakEventInfo class.
internal static Delegate ConvertToWeakDelegate(WeakEventInfo weakEventInfo, Delegate method)
{
    var methodInfo = weakEventInfo.OriginalMethod;
    Type targetType = method.Target.GetType();
    LabelTarget resultLabel = Expression.Label();
    ConstantExpression constantExpression = Expression.Constant(weakEventInfo);
    ParameterExpression resultVariable = Expression.Variable(methodInfo.ReturnType == typeof(void)
                                                                    ? typeof(object)
                                                                    : methodInfo.ReturnType);
    Expression.Assign(resultVariable, Expression.Default(resultVariable.Type));
    ParameterExpression localField = Expression.Variable(targetType);
    ParameterExpression[] methodCallParameters = methodInfo.GetParameters()
                                                            .Select(
                                                                info => Expression.Parameter(info.ParameterType))
                                                            .ToArray();

    Expression methodCallExpression = Expression.Call(localField, methodInfo, methodCallParameters);
    if (methodInfo.ReturnType != typeof(void))
        methodCallExpression = Expression.Assign(resultVariable, methodCallExpression);
    MemberExpression weakReferenceField = Expression.Field(constantExpression, WeakEventInfo.TargetFieldInfo);

    BlockExpression body = Expression.Block
        (new[] { resultVariable },
            Expression.IfThenElse(Expression.Equal(weakReferenceField, Expression.Constant(null)),
                                Expression.Return(resultLabel),
        //if (weakEventInfo.Target == null)
        //   return null;
        //else
                                Expression.Block
                                    (
                                        new[] { localField },
                                        Expression.Assign(localField,
                                                            Expression.Convert(
                                                                Expression.Property(weakReferenceField,
                                                                                    typeof(WeakReference)
                                                                                        .GetProperty("Target")),
                                                                targetType)),
        //var target = weakEventInfo.OriginalDelegate.Target;
        //if (target == null){
                                        Expression.IfThenElse(
                                            Expression.Equal(localField, Expression.Constant(null)),
                                            Expression.Block
                                                (
                                                    Expression.Call(constantExpression,
                                                                    WeakEventInfo.ClearMethodInfo),
                                                    Expression.Return(resultLabel)
                                                ),
        //weakEventInfo.Clear();
        //return null;
        //}
        //else
        //return target.Invoke(arg1, arg2, .....);
                                            Expression.Block(methodCallExpression, Expression.Return(resultLabel))))),
            Expression.Label(resultLabel), resultVariable);


    return Expression.Lambda(method.GetType(), body, methodCallParameters).Compile();
}

public static Delegate ConvertToWeakDelegate(Delegate originalAction, Action<Delegate> unsubcribeDelegate)
{
    if (originalAction == null) throw new ArgumentNullException("originalAction");
    if (originalAction.Target == null)
        return originalAction;

    var weakEventInfo = new WeakEventInfo
    {
        Target = new WeakReference(originalAction.Target),
        OriginalMethod = originalAction.Method
    };
    Delegate result = ConvertToWeakDelegate(weakEventInfo, originalAction);
    if (unsubcribeDelegate != null)
        weakEventInfo.UnsubcribeDelegate = unsubcribeDelegate;
    if (unsubcribeDelegate != null)
        weakEventInfo.Delegate = result;
    return result;
}
Code to work with this method is the same as in method 2.

Advantages Disadvantages
  • The overhead of creating delegate, because the expression is difficult to cache.

Performance

Here are the benchmark results of an event with two registered delegates (one instance method and one static method):
  • Original event: 1000000 calls times is 0.0278125 seconds.
  • Method 1: 1000000 calls times is 12.1321455 seconds.
  • Method 2: 1000000 calls times is 0.0370756 seconds.
  • Method 3: 1000000 calls times is 0.0692381 seconds.

MugenInjection

The MugenInjection, also provides methods to create weak-delegates, it supports the second and the third method. This code demonstrates how it works:
private void Method()
{
    var beta = new Beta();
    //You can change the type of ReflectionAccessProvider.
    //InjectorUtils.ReflectionAccessProvider = new EmitReflectionAccessProvider();
    //InjectorUtils.ReflectionAccessProvider = new ExpressionReflectionAccessProvider();
    beta.MyEvent += ReflectionExtension.ConvertToWeakDelegate(new Action<object>(OnMyEvent), 
@delegate => beta.MyEvent -= @delegate);    
}

Conclusion

If you do not need the high performance you can use the first method, otherwise, your choice will be the second or the third method. The third method can work in partial trust, which is very important if you are using Silverlight.
Moreover, there are lots of different approaches to this problem and this article explain a few of them.

Comments
Leave a Reply
*bold*
_italics_
+underline+
* Bullet List
** Bullet List 2
# Number List
## Number List 2
{"Do not apply formatting"}
{code:language} code here {code:language}.
Supports: aspx c#, c#, c++, html, sql, xml
[url:http://www.example.com]