Weak Event Pattern In WPF

|

Recently I just came across the weak event pattern in WPF, and when I learnt the tricks to use this pattern, I just want to pick up some code to demonstrate it, before I bombardment with some code, Let me first explain a bit about what is weak event pattern, and what kind of problem this pattern can solve?

Weak event pattern is used in the situation where the event soure object's lifetime is potentially independent of event listener objects. imagine you have a Button on the window, and you have a listener object called ButtonActionHelper which registers to the Button's Click event using classical += operator, when you are done with the actions you want to perform on the Button, you probably want to dispose the ButtonActionHelper object to save memory, and when you do so, you will think that the garbage collector will compact the memory space occupied by the ButtonActionHelper object when the GC finds that it's the appropiate time to do the dirty work, But in actuality, when the GC is triggered to collect the managed heap, It probably cannot collect ButtonActionHelper object, because the Click event source object aka Button object still hold a strong reference to it, why? the problem here is that event implementation in the .NET Framework is based on the CLR's delegate mechanism, you when use the += operator to register event handler to an event, you actually end up creating a new delegate presenting the event handler you attached to this event, and the newly created delegate will be added to the delegate chain, each delegate has two private fields, one is _target, and the other  _methodPtr. and the "culprit" bit aka _target arises from the horizon,  _target is a field which will hold a strong reference to the event listener on which the event handler is defined. that's why even if you've disposed the event listener, you still canot have the GC collect it, one workaround to this problem is to explicitly unregister the Click event, then the event listener can be cleaned up because no strong reference to it exists in the managed heap, but this requires you to determine the appropriate time and circumstance at which you don't want your event listeners anymore, sometimes, it's really hard to determine this, enter weak event pattern.

Weak event pattern solves the aforementioned problem by not having the delegate's _target field holding a strong reference to the event listener, the weak eventing system in WPF employs the same technique and concept which Greg Schechter blogged about ages ago in his awesome article "Simulating Weak Delegates in the CLR". By "decoupling" the event listener from the event source, the event listener's lifetime can be independent of the event source, this is especially important when the event listener holds up a lot of memory space, and unnecessarily keeping it in the managed heap any longer would increase the working set of your application. Okay, I think I just rant too much about the concept, let me start with some code to demonstrate this pattern.

If you want to implement the weak event pattern, there are two classes you should take care of, they are System.Windows.IWeakEventListener, and System.Windows.WeakEventManager. if you want your event listeners to participate in the WPF's weak eventing system, your event listeners should implement the IWeakEventListener interface, and simultaneously you should subclass WeakEventManager class to provide weak event mangement logic for the event you want to "expose" as a weak event, in my demo code, I create a custom WeakEventManager to "expose" Button's Click event as a weak event:

public class ButtonClickEventManager : WeakEventManager
{
    public static void AddListener(Button source, IWeakEventListener listener)
    {
        ButtonClickEventManager.CurrentManager.ProtectedAddListener(source, listener);
    }

    public static void RemoveListener(Button source, IWeakEventListener listener)
    {
        ButtonClickEventManager.CurrentManager.ProtectedRemoveListener(source, listener);
    }

    private void OnButtonClick(Object sender, RoutedEventArgs args)
    {
        base.DeliverEvent(sender, args);
    }

    protected override void StartListening(Object source)
    {
        Button button = (Button)source;
        button.Click += this.OnButtonClick;
    }

    protected override void StopListening(Object source)
    {
        Button button = (Button)source;
        button.Click -= this.OnButtonClick;
    }

    private static ButtonClickEventManager CurrentManager
    {
        get
        {
            Type managerType = typeof(ButtonClickEventManager);
            ButtonClickEventManager manager = (ButtonClickEventManager)WeakEventManager.GetCurrentManager(managerType);
            if (manager == null)
            {
                manager = new ButtonClickEventManager();
                WeakEventManager.SetCurrentManager(managerType, manager);
            }
            return manager;
        }
    }
}

In the above ButtonClickEventManager implementation, I override the StartListening() and StopListenting() methods to register and unregister Button's Click event respectively, in the OnButtonClick handler, I just simply call the DeliverEvent() method, the DeliverEvent() method will end up dispatching the ClickEvent to the weak event listeners if there are any, and I also define two static methods to enable to caller to add and remove weak event listener. so now we have an event manager which will mange and dispatch the ClickEvent, next we should create an IWeakEventListener listening to the ClickEvent:

public class ExpensiveObject : DispatcherObject, IWeakEventListener
{
    private Button sourceButton;
    private String id;
    public ExpensiveObject(Button sourceButton, Boolean usingWeakEvent, String id)
    {
        this.sourceButton = sourceButton;
        this.id = id;
        if (usingWeakEvent)
        {
            ButtonClickEventManager.AddListener(sourceButton, this);
        }
        else
        {
            sourceButton.Click += OnButtonClick;
        }
    }

    ~ExpensiveObject()
    {
        // Clean up expensive resources here.
        this.Dispatcher.BeginInvoke(DispatcherPriority.Send, new DispatcherOperationCallback(delegate
            {
                sourceButton.Content = String.Format("ExpansiveObject (Id:{0}) is cleaned up.", id);
                return null;
            }), null);

    }

    Boolean IWeakEventListener.ReceiveWeakEvent(Type managerType, Object sender, EventArgs e)
    {
        if (managerType == typeof(ButtonClickEventManager))
        {
            OnButtonClick(sender, (RoutedEventArgs)e);
            return true;
        }
        else
        {
            return false;
        }
    }

    private void OnButtonClick(Object sender, RoutedEventArgs e)
    {
        MessageBox.Show(String.Format("EventHandler called on ExpansiveObject (Id:{0})", id));
    }
}

I call this weak event listener an ExpensiveObject, because I just want to emphasize how classical CLR event will hold up the ExpansiveObject, resulting in memory leak, by implementing IWeakEventListener interface, we will not encounter such problem, presumably this ExpensiveObject will occupy non-trivial memory space, in the above code, I hook up the click event handler based on the boolean parameter specified in the constructor, later on, I will create two ExpensiveObject, one using weak event, and the other using classical event. I also implement the IWeakEventListener's ReceiveWeakEvent() using C#'s Explicit Interface Method Implementation feature, this method will be called by the WeakEventManger's DeliverEvent() method to notify the listener that the ClickEvent is fired, I also define a finalizer for this ExpensiveObject, inside this method, you just simply update the button's display to indicate that the ExpensiveObject is cleaned up.

Up until now, I've finish defining the ButtonClickEventManager, and the ExpensiveObject weak event listener, Let' me put those things together by creating an simple demo application:

<Window x:Class="WeakEventPatternDemo.Window1"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   Title="WeakEventPatternDemo" Height="300" Width="300"
    >
    <
StackPanel Margin="10">
      <
Button Name="button1" Content="Commit ExpensiveObject1" Margin="10"/>
      <
Button Name="button2" Content="Commit ExpensiveObject2" Margin="10"/>
      <
Button Name="fooButton" Content="Clean Up Expensive Objects" Margin="10"/>
    </
StackPanel>
</
Window>

public partial class Window1 : System.Windows.Window
{
    private ExpensiveObject object1, object2;
    public Window1()
    {
        InitializeComponent();

        //Associate the button1 with the ExpensiveObject 1 using weak event.
        object1 = new ExpensiveObject(button1, true, "1");

        //Associate the button2 with the ExpensiveObject 2 using classical event .
        object2 = new ExpensiveObject(button2, false, "2");

        fooButton.Click += fooButton_Click;
    }

    private void fooButton_Click(Object sender, RoutedEventArgs e)
    {
        // explicitly clean up the ExpensiveObjects to save memory.
        object1 = null;
        object2 = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

In this simple demo application, I create three Buttons on the Window, and in the Window's constructor, I create two ExpensiveObjects, and associate them with the button1 and button2 using weak event and classical event mechanism respectively, in the fooButton's Click event handler, I simply nullify the two ExpensiveObjects I create previously, and explicitly force garbage collector to collect the memory instantaneously, I call GC.WaitForPendingFinalizers() to block the current thread until the finalizers for those ExpensiveObjects are get called.

When you run this application, and press the fooButton, you will find button1's display is updated to indicate that the ExpensiveObject 1 associated with it is cleaned up by the GC, but the button2's display is not updated, because the association between ExpensiveObject 2 and button2 is through classical event mechanism, and you can see that as long as button2 is not cleaned up, the button2 will prevent the ExpensiveObject 2 from garbage collected in a timely fashion.

Actually just as Ian Griffiths suggests, you can entirely avoid the problem inherent with the CLR's event mechanism withouting resorting to weak event pattern by carefully authoring your code , my next blog article will dedicate to this topic.

You can download the demo app project from here.

2 comments:

Unknown said...

Thanks for such detailed explanation. I am unable to download the source code. Could you please look into that.

Anup Hosur said...

Very nice article. Highly appriciable.