Simulating “Weak Delegates” in the CLR

Introduction

What follows may seem like a fairly obscure topic relating to lifetime management in the CLR, but for those who have hit upon this scenario (and they may not even know they have), this is a very helpful pattern to know about.

In some of the development of Avalon, we hit upon a pattern for associating event handlers from a generic notion of Containee to a generic notion of Container. This results in strong CLR references to the Container, and can thus prevent garbage collection of the Container so long as the Containee is alive. This is similar to the problem that WeakReferences in the .NET Framework were designed to solve, but somewhat different in that this scenario is specific to events.

This becomes a real issue if:

a) the Containee outlives the Container (or at least outlives the intended lifetime of the Container), and,

b) the Container is substantially more heavyweight than the Containee, and thus results in substantial resources being held onto

This certainly doesn’t happen all the time, but when it does, one needs a good pattern and solution for dealing with it. The rest of this post lays out such a pattern and solution.

The Problem

The specific instance of this problem we’re facing in the Avalon is that there’s a key class in the system called a Visual. A Visual contains a Transform (for 2D transformation). That Transform need not be used only in one place, and may be contained by a bunch of other Visuals (and other objects) as well. Furthermore, the Transform can change, and when it changes, the Visual (or Visuals) needs to be notified. From the programmer’s point of view, there’s no reference from the Transform to the Visual that contains it, since the programmer simply thinks of assigning a Transform into a Visual, not vice-versa. Therefore, there’s no reason to expect that if I hang on to a Transform, that I’m keeping the Visuals alive that reference that Transform. However, that’s exactly the case, since the Visuals are listeners on the events published by the Transform.

This is simply a specific case of a much more general problem of Containers and Containees, where the Containee has a hidden reference (from the point of view of the programmer) to the Container, and thus forces the Container to stay alive. Here’s the situation I’m talking about:

Furthermore, since the Containee can be used in multiple places, we get these graphs:

And this is even more insidious than the previous one since having one of the Containers still be alive forces the other one to stay alive. This gets worse the more places the Containee is used.

A Solution

An ideal solution to this would be some explicit CLR notion of “Weak Delegates”. That is, a delegate that can be used to attach to an event whose target is inherently a weak reference. The CLR doesn’t currently have such a feature, so we must make do with what we have. Thus, given that there is no such first-class notion of a “weak delegate”, we can employ a pattern to get a very good approximation of the problem we’re trying to solve.

The approach is to have the Containee register for an event not on the Container, but on a class that derives from WeakReference and implements an event with the same signature as that on the Container. It then holds a reference to the container as the WeakReference’s “target”. This class, in the code below, is called a WeakContainer, and approximates what a “Weak Delegate” would really do. When it comes time to raise the event on the Containee, the following occurs:

  • Containee just fires its events, and since the WeakContainer is a target for one of these, its handler runs.
  • That handler first checks to see if Target (from WeakReference) is not null (meaning the target object is still alive)
    • If it is, it simply calls through that targets handler.
    • If it isn’t, then it removes itself from the sender’s handler list, allowing the WeakContainer itself to be collected.

This approach results in the following for the “single use of Containee” case, as in the first diagram above:

And the multiple use of Containee looks like:

Note that:

  • See code for all of this at the end of this post.
  • The Containee knows nothing about any of this. The Container is responsible for setting up the “weak delegate” construct.
  • The WeakContainer class is a private nested class inside the Container class. No need for anyone outside to know about it.
  • There’s likely room for generalizing this for multiple event signatures through the use of generics. I didn’t want to confuse the issue with that for now.
  • The WeakContainer class (to which Containee has a strong reference) may stick around indefinitely, as they are only reaped when an event is raised after their target has been collected. That is one downside here, but note that only the WeakContainer class itself (which is basically just a shell) is temporarily leaked. Far, far better than leaking the Container itself.
  • There are a few other subtleties here, but the above is the basic idea. See the code below for the exact usage.

Code

using System;

using System.Diagnostics;

namespace WeakDelegate

{

// Containee needn't know anything about weak references, etc.

// It just fires its events as normal.

class Containee

{

public Containee(string id) { _id = id; }

public event EventHandler MyEvent;

public void DoIt()

{

if (MyEvent != null)

{

MyEvent(this, new EventArgs());

}

}

public string ID { get { return _id; } }

~Containee()

{

Console.WriteLine(" Containee '{0}' cleaned up", ID);

}

private string _id;

}

// Container has a reference to Containee. But doesn't want to

// force Containee to have a strong reference to it. Uses a

// WeakContainer private nested class for this.

class Container

{

public void Handler1(object sender, EventArgs args)

{

Console.WriteLine(

" Container.Handler1 called with Containee '{0}'",

_containee == null ? "<none>" : _containee.ID);

}

public Containee Containee

{

get

{

return _containee;

}

set

{

if (_weakContainer == null)

{

_weakContainer = new WeakContainer(this);

}

// unsubscribe old

if (_containee != null)

{

_containee.MyEvent -= new

EventHandler(_weakContainer.Handler1);

}

// subscribe new

_containee = value;

if (_containee != null)

{

_containee.MyEvent += new

EventHandler(_weakContainer.Handler1);

}

}

}

~Container()

{

Console.WriteLine(" Container cleaned up");

}

#region WeakContainer

private class WeakContainer : WeakReference

{

public WeakContainer(Container target) : base(target) {}

public void Handler1(object sender, EventArgs args)

{

Console.WriteLine(" WeakContainer.Handler1 called");

Container b = (Container)this.Target;

if (b != null)

{

b.Handler1(sender, args);

}

else

{

Containee c = sender as Containee;

if (c != null)

{

c.MyEvent -= new EventHandler(this.Handler1);

Console.WriteLine(

" Removed WeakContainer handler");

}

}

}

~WeakContainer()

{

Console.WriteLine(" WeakContainer cleaned up");

}

}

#endregion

private Containee _containee;

private WeakContainer _weakContainer = null;

}

class Class1

{

/// <summary>

/// The main entry point for the application.

/// </summary>

[STAThread]

static void Main(string[] args)

{

(new Class1()).Run();

}

private Containee containee1, containee2;

private void InvokeMe()

{

Console.WriteLine("Invoking");

if (containee1 != null) containee1.DoIt();

if (containee2 != null) containee2.DoIt();

}

public void Run()

{

containee1 = new Containee("FIRST");

containee2 = new Containee("SECOND");

Container container = new Container();

container.Containee = containee1;

InvokeMe();

Console.WriteLine("Switching to containee2");

container.Containee = containee2;

InvokeMe();

Console.WriteLine("Switching back to containee1");

container.Containee = containee1;

InvokeMe();

Console.WriteLine("Setting container to null and GC'ing");

container = null;

GC.Collect();

GC.WaitForPendingFinalizers();

InvokeMe();

InvokeMe();

Console.ReadLine();

}

}

}

Finally, note that in this particular chunk of code, the diagrams above aren’t quite complete. The missing piece is that Container maintains a strong reference to WeakContainer. But that’s fine, it doesn’t prevent Container from going away just because Containee is still alive. Here’s the more accurate diagram: