Click here to Skip to main content
15,885,278 members
Articles / Programming Languages / C#

.NET Weak Events for the Busy Programmer

Rate me:
Please Sign up or sign in to vote.
4.97/5 (17 votes)
15 Feb 2013CPOL3 min read 64.7K   587   80   47
General WeakEvent class

Introduction

By now we are all familiar with delegates in .NET. They are so simple yet so powerful. We use them for events, callbacks, and many other wonderful derivative functions. However delegates have a nasty little secret. When you subscribe to an event, the delegate backing that event will keep a strong reference to you. What does this mean? It means that you (the caller) will not be able to be garbage collected since the garbage collection algorithm will be able to find you. Unless you like memory leaks this is a problem. This article will demonstrate how to avoid this issue without forcing callers to manually unsubscribe. The aim of this article is make using weak events extremely simple.

Background

Now I've seen my share of weak event implementations. They more or less do the job including the WeakEventManager in WPF. The issue I always encountered is either their setup code or memory usage. My implementation of the weak event pattern will address memory issues and will be able to match any event delegate signature while making it very easy to incorporate into your projects. 

Implementation

The first thing developers should know about delegates is that they are classes just like any other .Net class. They have properties that we can use to create our own weak events. The issue with the delegate as I mentioned before is that they keep a strong reference to the caller. What we need to do is to create a weak reference to the caller. This is actually simple. All we have to do is use a WeakReference object to accomplish this.

C#
RuntimeMethodHandle mInfo = delegateToMethod.Method.MethodHandle;
 
if (delegateToMethod.Target != null)
{
  WeakReference weak = new WeakReference(delegateToMethod.Target);
  subscriptions.Add(weak, mInfo);
}
else
{
  staticSubscriptions.Add(mInfo);
}

In the above example 'delegateToMethod' is our delegate. We can get to the method that it will eventually invoke and most importantly we can get to it's Target, the subscriber. We then create a weak reference to the target. This allows the target to be garbage collected if it is no longer in scope.

I have also saved a 'pointer' to the method using its handle in a RuntimeMethodHandle field. The reason for this is that even though I'm creating a weak reference to the target I am still holding on to a MethodInfo. The collection of MethodInfo objects will grow as subscribers increase. This in not memory efficient. By using a RuntimeMethodHandler I am essentially creating a pointer to the MethodInfo instead. Then later only when I need them I'll 'bring them to life' so to speak. The RuntimeMethodHandle only has one property of type IntPtr which uses a lot less memory than the MethodInfo object. The following code demonstrates how this works.

C#
public void RaiseEvent(object[] parameters = null)
{
    List<weakreference> deadTargets = new List<weakreference>();

    foreach (var subcription in subscriptions)
    {
        object target = subcription.Key.Target;

        if (target != null)
        {
            try
            {
               MethodBase.GetMethodFromHandle(subcription.Value).Invoke(target, parameters);
             }
             catch (Exception ex)
             {
                 //Error("Exception caught calling delegate", ex);
             }
        }
        else
        {
            deadTargets.Add(subcription.Key);
        } 
   }

   foreach (var deadTarget in deadTargets)
   {
      subscriptions.Remove(deadTarget);
   }
}

This solution is memory efficient. However if you are worried about performance just ask yourself how often events are fired in 99% of the cases.

Now we will also need a mechanism to remove delegate subscriptions as well. The entire functionality can be wrapped in my SmartDelegate class.

C#
private class SmartDelegate
{
  
    private readonly Dictionary<<weakreference,> subscriptions = new Dictionary<weakreference,>();
    private readonly List<runtimemethodhandle> staticSubscriptions = new List<runtimemethodhandle>();
 
  
 
    #region Constructors
 
    public SmartDelegate(Delegate delegateToMethod)
    {
        RuntimeMethodHandle mInfo = delegateToMethod.Method.MethodHandle;
 
 
        if (delegateToMethod.Target != null)
        {
            WeakReference weak = new WeakReference(delegateToMethod.Target);
            subscriptions.Add(weak, mInfo);
        }
        else
        {
            staticSubscriptions.Add(mInfo);
        }
    }
 
    #endregion
 
    #region Public Methods
 
    public void RaiseEvent(object[] parameters = null)
    {
        List<weakreference> deadTargets = new List<weakreference>();
 
        foreach (var subcription in subscriptions)
        {
            object target = subcription.Key.Target;

            if (target != null)
            {
                try
                {
                   MethodBase.GetMethodFromHandle(subcription.Value).Invoke(target, parameters);
                 }
                 catch (Exception ex)
                 {
                     //Error("Exception caught calling delegate", ex);
                 }
            }
            else
            {
                deadTargets.Add(subcription.Key);
             } 
       }
 
        foreach (var deadTarget in deadTargets)
        {
            subscriptions.Remove(deadTarget);
        }
    }
 
    public bool Remove(Delegate handler)
    {
        WeakReference removalCandidate = null;
 
        foreach (var subscription in subscriptions)
        {
            if (subscription.Key.Target != null && subscription.Key.Target == handler.Target)
            {
                removalCandidate = subscription.Key;
                break;
            }
        }
 
        if (removalCandidate != null)
        {
            subscriptions.Remove(removalCandidate);
 
            return true;
        }
 
        return false;
    }
 
    #endregion
}

Don't worry about the type of the Dictionary or list they are actually:

C#
private readonly Dictionary<WeakReference, RuntimeMethodHandle> subscriptions = 
             new Dictionary<WeakReference, RuntimeMethodHandle>();
private readonly List<RuntimeMethodHandle> staticSubscriptions = new List<RuntimeMethodHandle>();

For some reason Code Project article editor is not allowing me to put them in.

We now need some type of container for the SmartDelegates that will manage subscribing, unsubscribing and raising events. The EventHostSubscription class accomplishes this:

C#
public class EventHostSubscription
{
    #region Private Fields
 
    private readonly Dictionary<string,>> subscriptions = new Dictionary<string,>>();
    private int flag;
 
    #endregion
 
    #region Public Methods
 
    public void Add(string eventName, Delegate handler)
    {
        if (handler == null)
        {
            throw new ArgumentNullException("handler");
        }
 
        while (Interlocked.CompareExchange(ref flag, 1, 0) != 0);
        
        try
        {
             if (!subscriptions.ContainsKey(eventName))
             {
                 subscriptions.Add(eventName, new List<smartdelegate>());
             }
 
             SmartDelegate smartDelegate = new SmartDelegate(handler);
             subscriptions[eventName].Add(smartDelegate);
            
        }
        finally
        {
            Interlocked.Exchange(ref flag,0);
        } 
    }
 
    public void Remove(string eventName, Delegate handler)
    {
        if (handler == null)
        {
            throw new ArgumentNullException("handler");
        }
 
        while (Interlocked.CompareExchange(ref flag, 1, 0) != 0);
        
        try
        {
            if (subscriptions.ContainsKey(eventName))
            {
                List<smartdelegate> smartDelegates;

                if (subscriptions.TryGetValue(eventName, out smartDelegates))
                {
                    for (int i = 0; i < smartDelegates.Count; i++)
                    {
                        SmartDelegate smartDelegate = smartDelegates[i];

                        smartDelegate.Remove(handler);
                    }
                }
             }
                 
         }
         finally
         {
            Interlocked.Exchange(ref flag,0);
         }   
    }
 
    public void RaiseEvent(string eventName, params object[] parameters)
    {
        List<smartdelegate> smartDelegates;
 
          while (Interlocked.CompareExchange(ref flag, 1, 0) != 0);
          
          try
          {
              if (subscriptions.TryGetValue(eventName, out smartDelegates))
              {
                  object[] delegateParameters = null;

                  if (parameters.Length > 0)
                  {
                      delegateParameters = parameters;
                  }

                  for (int i = 0; i < smartDelegates.Count; i++)
                  {
                      SmartDelegate smartDelegate = smartDelegates[i];

                      smartDelegate.RaiseEvent(delegateParameters);
                   }
                }

           }
           finally
           {
               Interlocked.Exchange(ref flag,0);
           }
       }
    }
}

Again the dictionary type declaration is screwed up in the editor. The download will work just fine.

Using the Code

Using the code is simple. Let's say we have a class with an event called OnChanged, the code hookup is demonstrated below.

C#
public class WeakEventControl
{
    private readonly EventHostSubscription subscriptions = new EventHostSubscription();

    public delegate void OnChangedDelegate(object sender, EventArgs args);

    public event OnChangedDelegate OnChanged
    {
        add
        {
            subscriptions.Add("OnChanged", value);
        }
        remove
        {
            subscriptions.Remove("OnChanged", value);
        }
    }

    public void RaiseEvent()
    {
        subscriptions.RaiseEvent("OnChanged", this, EventArgs.Empty);
    }
}

That's all there is to it. Just call the RaiseEvent method with the name of the event and the arguments as you would normally.

Any feedback or improvement ideas welcomed.

History

CodeProject member Sacher Barber pointed out that between the check if the target of a weak reference is null and its actual usage the target could be garbage collected.  I've modified my code to cater for this.

CodeProject member Thomas Olsson pointed out that I should 'lock' the Remove and RaiseEvent methods. I've modified my code to cater for this as well.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Finance Industry
United States United States
Currently pursuing 'Programming Nirvana' (The ineffable ultimate in which one has attained disinterested wisdom and compassion as it relates to programming)

Respected Technologies
1. Confusor (https://confuser.codeplex.com/)
2. Power Threading (http://www.wintellect.com/Resources/visit-the-power-threading-library)
3. EDI Parsers (http://www.rdpcrystal.com)


Acknowledgements:

Microsoft Certified Technologist for WPF and .Net 3.5 (MCTS)
Microsoft Certified Technologist for WCF and .Net 3.5 (MCTS)
Microsoft Certified Application Developer for .Net (MCAD)
Microsoft Certified Systems Engineer (MCSE)
Microsoft Certified Professional (MCP)

Sun Certified Developer for Java 2 Platform (SCD)
Sun Certified Programmer for Java 2 Platform (SCP)
Sun Certified Web Component Developer (SCWCD)

CompTIA A+ Certified Professional

Registered Business School Teacher for Computer Programming and Computer Applications (2004)
(University of the State of New York Education Department)

Graduated from University At Stony Brook

Comments and Discussions

 
GeneralRe: Spinlock? Pin
FatCatProgrammer4-Feb-13 6:41
FatCatProgrammer4-Feb-13 6:41 
GeneralRe: Spinlock? Pin
Thomas Olsson4-Feb-13 7:58
Thomas Olsson4-Feb-13 7:58 
GeneralRe: Spinlock? Pin
FatCatProgrammer4-Feb-13 8:05
FatCatProgrammer4-Feb-13 8:05 
BugRe: Spinlock? Pin
Thomas Olsson6-Feb-13 20:25
Thomas Olsson6-Feb-13 20:25 
GeneralRe: Spinlock? Pin
FatCatProgrammer14-Feb-13 16:02
FatCatProgrammer14-Feb-13 16:02 
BugRe: Spinlock? Pin
Thomas Olsson15-Feb-13 2:19
Thomas Olsson15-Feb-13 2:19 
GeneralRe: Spinlock? Pin
FatCatProgrammer15-Feb-13 4:19
FatCatProgrammer15-Feb-13 4:19 
SuggestionRe: Spinlock? Pin
Thomas Olsson15-Feb-13 5:00
Thomas Olsson15-Feb-13 5:00 
Which brings me to my final suggestion how I would have changed this class if I were to use it.

In almost all scenarios I have experienced, signaling events is much more common than subscribing/unsubscribing. Another thing I have found is that it is not uncommon that you want to be able to unsubscribe from your event service function. (Your code deadlocks in this scenario)

To solve that, I would use an "immutable" list of subscribers.
Then I could take a snapshot of the list in the RaiseEvent method. In that way I would not need to do any locking in that method, which is good for performance and it allows me to add/remove listeners from within the event service function. I am willing to take on a bit more work in Add/Remove in order to make RaiseEvent fast and non-blocking.
(The "immutable" list could either be some of the ordinary collection classes that I replace in Add and Remove instead of changing it, or I could use something like "Microsoft.Bcl.Immutable".)

Another change I would make is to use an ordinary "lock {}" instead of attempting to implement my own spinlock. Locking performance is not trivial. I would have done something like you do of I really needed the shortest possible delay in a real time system, but if timing is that critical, I would likely not use .NET, but C++. Otherwise it is simply not worth going with your own implementation. First of all, there is always a bigger risk that you do it wrong (as you did at first) and then the overall performance is not so trivial to assess. Sometimes you can actually save time by doing Yield and even by waiting on a handle, since the lock you are waiting for might be held by a thread recently scheduled for the same CPU. (The thread scheduler prefers to schedule the threads to the same CPU as they ran on the last time).

Microsoft has put a lot of effort in making the Monitor class, which is used by the "lock {}" statement, efficient for a number of scenarios. In .NET 4.5 it has come quite a long way and my experience is that it is hard to beat the performance in any significant way in most cases. As far as I understand it, it actually starts by doing some spinning without yielding and then spins a while with some yielding and only then does it actually wait on a handle (or likely even a "critical section" structure, which is more lightweight). If running on a single core CPU, it skips the spinning since it very inefficient.
GeneralRe: Spinlock? Pin
FatCatProgrammer15-Feb-13 7:53
FatCatProgrammer15-Feb-13 7:53 
GeneralRe: Spinlock? Pin
Thomas Olsson15-Feb-13 8:58
Thomas Olsson15-Feb-13 8:58 
GeneralRe: Spinlock? Pin
FatCatProgrammer15-Feb-13 15:52
FatCatProgrammer15-Feb-13 15:52 
GeneralMy vote of 5 Pin
Marc Clifton3-Feb-13 6:37
mvaMarc Clifton3-Feb-13 6:37 
GeneralRe: My vote of 5 Pin
FatCatProgrammer4-Feb-13 5:10
FatCatProgrammer4-Feb-13 5:10 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.