Click here to Skip to main content
Click here to Skip to main content

.NET Weak Events for the Busy Programmer

, 15 Feb 2013 CPOL
Rate this:
Please Sign up or sign in to vote.
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.

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.

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.

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:

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:

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.

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)

Share

About the Author

FatCatProgrammer
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

 
QuestionWhy Dictionary to store subscriptions in SmartDelegate? PinmemberTim Overbay28-Feb-13 11:35 
AnswerRe: Why Dictionary to store subscriptions in SmartDelegate? PinmemberFatCatProgrammer28-Feb-13 17:28 
GeneralRe: Why Dictionary to store subscriptions in SmartDelegate? PinmemberTim Overbay3-Mar-13 5:44 
Questionminor editorial comment [modified] PinmemberBillWoodruff15-Feb-13 23:37 
AnswerRe: minor editorial comment PinmemberFatCatProgrammer16-Feb-13 4:27 
GeneralRe: minor editorial comment PinmemberBillWoodruff16-Feb-13 7:09 
QuestionTry/Catching Pinmemberkelton502014-Feb-13 9:59 
AnswerRe: Try/Catching PinmemberFatCatProgrammer14-Feb-13 14:04 
BugRe: Try/Catching Pinmemberkelton502014-Feb-13 14:34 
GeneralRe: Try/Catching PinmemberFatCatProgrammer14-Feb-13 16:25 
GeneralRe: Try/Catching Pinmemberkelton502014-Feb-13 16:29 
GeneralRe: Try/Catching PinmemberFatCatProgrammer14-Feb-13 17:08 
GeneralRe: Try/Catching Pinmemberkelton502014-Feb-13 17:15 
GeneralRe: Try/Catching PinmemberFatCatProgrammer14-Feb-13 17:16 
GeneralRe: Try/Catching Pinmemberkelton502014-Feb-13 17:18 
GeneralRe: Try/Catching PinmemberFatCatProgrammer15-Feb-13 5:22 
GeneralRe: Try/Catching PinmemberTim Overbay3-Mar-13 5:53 
QuestionGood stuff, and useful [modified] PinmvpSacha Barber5-Feb-13 3:24 
AnswerRe: Good stuff, and useful PinmemberFatCatProgrammer5-Feb-13 4:30 
GeneralRe: Good stuff, and useful PinmvpSacha Barber5-Feb-13 4:54 
GeneralRe: Good stuff, and useful PinmemberFatCatProgrammer5-Feb-13 5:01 
GeneralRe: Good stuff, and useful [modified] PinmvpSacha Barber5-Feb-13 5:23 
GeneralRe: Good stuff, and useful [modified] PinmemberFatCatProgrammer5-Feb-13 6:01 
GeneralRe: Good stuff, and useful PinmvpSacha Barber5-Feb-13 23:16 
GeneralMy vote of 5 PinmemberpeteSJ4-Feb-13 15:27 
GeneralRe: My vote of 5 PinmemberFatCatProgrammer5-Feb-13 5:18 
QuestionSpinlock? PinmemberThomas Olsson4-Feb-13 0:41 
AnswerRe: Spinlock? PinmemberFatCatProgrammer4-Feb-13 3:52 
GeneralRe: Spinlock? PinmemberThomas Olsson4-Feb-13 4:22 
GeneralRe: Spinlock? PinmemberFatCatProgrammer4-Feb-13 4:42 
GeneralRe: Spinlock? PinmemberThomas Olsson4-Feb-13 5:31 
GeneralRe: Spinlock? PinmemberFatCatProgrammer4-Feb-13 5:48 
GeneralRe: Spinlock? PinmemberThomas Olsson4-Feb-13 6:23 
GeneralRe: Spinlock? PinmemberFatCatProgrammer4-Feb-13 7:08 
GeneralRe: Spinlock? PinmemberFatCatProgrammer4-Feb-13 7:41 
GeneralRe: Spinlock? PinmemberThomas Olsson4-Feb-13 8:58 
GeneralRe: Spinlock? PinmemberFatCatProgrammer4-Feb-13 9:05 
BugRe: Spinlock? [modified] PinmemberThomas Olsson6-Feb-13 21:25 
GeneralRe: Spinlock? PinmemberFatCatProgrammer14-Feb-13 17:02 
BugRe: Spinlock? PinmemberThomas Olsson15-Feb-13 3:19 
GeneralRe: Spinlock? PinmemberFatCatProgrammer15-Feb-13 5:19 
SuggestionRe: Spinlock? PinmemberThomas Olsson15-Feb-13 6:00 
GeneralRe: Spinlock? PinmemberFatCatProgrammer15-Feb-13 8:53 
GeneralRe: Spinlock? [modified] PinmemberThomas Olsson15-Feb-13 9:58 
GeneralRe: Spinlock? PinmemberFatCatProgrammer15-Feb-13 16:52 
GeneralMy vote of 5 PinprotectorMarc Clifton3-Feb-13 7:37 
GeneralRe: My vote of 5 PinmemberFatCatProgrammer4-Feb-13 6:10 

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

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

| Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.1411023.1 | Last Updated 15 Feb 2013
Article Copyright 2013 by FatCatProgrammer
Everything else Copyright © CodeProject, 1999-2014
Layout: fixed | fluid