Peregrine's View

Yet another C# / WPF / MVVM blog …

WPF General

C# – Weak Event Handlers

Hammering in My Head: Garbage

I’m going to take a brief sidestep from my series on MVVM to discuss an aspect of WPF development that recently bit me on the bum. When dealing with event handlers in C#, it is important to take account of the relative lifetimes of the source and listener objects.

In WPF / MVVM, the most common event we use is INotifyPropertyChanged.PropertyChanged – either in Xaml bindings or created manually in code behind. In most instances, PropertyChanged bindings are between a View and ViewModel pair which are created and destroyed together, but that’s not always the case.

The demo project for this post demonstrates this asymmetry in action.

MainViewModel contains a timer which updates the current date/time and the total allocated memory (as reported by the GarbageCollector) each 1/10th second.

public class MainViewModel: ViewModelBase
{
    public MainViewModel()
    {
        TimerTick();

        var timer = new DispatcherTimer
        {
            Interval = TimeSpan.FromSeconds(0.1)
        };

        timer.Tick += (s,e) => TimerTick();
        timer.Start();
    }

    private void TimerTick()
    {
        CurrentDateTime = DateTime.Now;
        CurrentTotalMemory = GC.GetTotalMemory(false);
    }

    private DateTime _currentDateTime;

    public DateTime CurrentDateTime
    {
        get => _currentDateTime;
        set => Set(nameof(CurrentDateTime), ref _currentDateTime, value);
    }

    private long _currentTotalMemory;

    public long CurrentTotalMemory
    {
        get => _currentTotalMemory;
        set => Set(nameof(CurrentTotalMemory), ref _currentTotalMemory, value);
    }
}

The requirement is to create a number of child windows, observing what happens to the memory usage as we open and close them. To ensure that the memory hit is obvious, each ChildViewModel creates an (otherwise unused) array of ten million integers.

There are three design cases to consider.

A. No link between main and child views

This is the simplest case – just create a new ChildView with a corresponding ChildViewModel and show it. Note that for simplicity, this proof of concept application breaks the core MVVM principal by creating View objects within the ViewModel.

var childView = new ChildView();
var childViewModel = new ChildViewModel();
childView.DataContext = childViewModel;
childView.Show();

As each child window is created, the allocated memory increases as expected. When a child window is closed, the allocated memory remains the same, but that’s not a total surprise. In C#, when an object goes out of scope, the memory it used is not immediately reclaimed and released. That process is handled by the GarbageCollector. Normally garbage collection runs automatically in the background, but in order to see an immediate effect we have to manually force it by calling GC.Collect(). After clicking on the button you will see the allocated memory drop again. OnForceGarbageCollect() actually calls GC.Collect() twice to ensure maximum memory reclaim each time.

private static void OnForceGarbageCollect()
{
    GC.Collect();
    GC.WaitForPendingFinalizers();
    GC.Collect();
}

B. A link between main and child views / viewmodels

What if we add a new requirement – to have the current date / time, as displayed on the main view, shown on each child window as well. That shouldn’t be too hard – we can create a CurrentDateTime property on the ChildViewModel and set up a PropertyChanged handler to update it each time the CurrentDateTime property on the MainViewModel is updated.

var childView = new ChildView();
var childViewModel = new ChildViewModel();
PropertyChanged += (s, e) =>
{
    if (nameof(CurrentDateTime).Equals(e.PropertyName))
        childViewModel.CurrentDateTime = CurrentDateTime);
};
childView.DataContext = childViewModel;
childView.Show();

All looks fine – the child windows open as expected, the CurrentDateTime values are synchronised and the allocated memory value increases as before. However, look at what happens when we close a child window – no matter how many times we force garbage collection, the allocated memory doesn’t decrease! What’s going on?

As you might suspect, it’s the new PropertyChanged event handler that is causing the problem. The event handler creates a reference back from the MainViewModel to the ChildViewModel, which is sufficient to make the garbage collector think that the ChildViewModel is still in active use and therefore not reclaim its memory, even though the corresponding View has been closed. In this case, the ChildViewModel objects remain uncollected until the application is closed – the call to Debug.WriteLine() in the ChildViewModel destructor confirms this.

The memory usage in this demo application is somewhat extreme, just to make a point, but if you have a long running application you will still be leaking memory if you use this type of construct.

C. A link between main and child views / viewmodels – doing it the right way

Now that we’ve identified the cause of the issue, how do we resolve it? The solution is through the use of WeakReferences. These allow a reference to an object to be maintained, but won’t block garbage collection if there are no other live references. This element of my library was inspired by a 2009 blog post by Andy Kutruff: High Performance Property Changed Weak Event Notifications for C#, which I’ve updated to use some more modern C# constructs.

perWeakPropertyChangedEventHandler handles the PropertyChanged event links between source and listener objects, using weak references for both.

The static perWeakPropertyChangedEventHandler class can handle many INotifyPropertyChanged source objects

… each of which can have many properties being observed

… each of which can have many listener objects

… each of which has one handler action to invoke when that property value changes.

The internal collections will clear down any stale entries after a source or listener object is garbage collected.

The handler action might look a little strange with three parameters, but it is done this way to prevent the creation of a closure or any other solid reference to the listener during registration.

Now we can use this manager to maintain the PropetyChanged event handlers between MainViewModel and each ChildViewModel …

var childView = new ChildView();
var childViewModel = new ChildViewModel();
perWeakPropertyChangedEventHandler.Register(this, 
                                            nameof(CurrentDateTime), 
                                            childViewModel, 
                                            (l, s, e) => l.CurrentDateTime = CurrentDateTime);
childView.DataContext = childViewModel;
childView.Show();

Now when we run the application and create child windows, the allocated memory increases and the CurrentDateTime values remain in sync as expected. This time however, when a child window is closed the allocated memory correctly decreases again (after forcing garbage collection) – happy days 😀.

You might be asking if you need to use this construct for all PropertyChanged handlers, including Xaml data binding. The answer to that is no. As I already started, the View and ViewModel objects usually co-exist as a pair, so explicitly managing their relative lifespans is not necessary. The main usage for this technique is where you have an imbalance – especially when the source object can exist for the whole application lifetime.

In my next post, I’ll resume my discussion on MVVM by looking at commands, which will include usage of this perWeakPropertyChangedEventHandler class.

Don’t forget that all of the code samples for this blog are available on Github, along with my own personal C# / WPF library.

If you find this article useful, or you have any other feedback, please leave a comment below.

Leave a Reply

Your email address will not be published. Required fields are marked *