Events: Custom Add and Remove

Because the compiler creates a private delegate field for every event that’s declared, a class that declares numerous events will use one field per event. The Control class in System.Windows .Forms declares more than 25 events, but there are usually just a couple of these events hooked up for a given control. What’s needed is a way to avoid allocating the storage for the delegate unless it’s needed.

The C# language supports this by allowing the add() and remove() methods to be written directly, which lets delegates be stored in a more space-efficient manner. One typical way of doing this is to define a Hashtable as part of the object and then to store the delegate in the Hashtable, like this:

using System; using System.Collections;

using System.Runtime.CompilerServices;

public class Button {

public delegate void ClickHandler(object sender, EventArgs e);

Hashtable delegateStore = new Hashtable();

static object clickEventKey = new object();

public event ClickHandler Click

{

[Methodlmpl(MethodlmplOptions.Synchronized)]

add

{

delegateStore[clickEventKey] =

Delegate.Combine((Delegate) delegateStore[clickEventKey], value);

}

[MethodImpl(MethodImplOptions.Synchronized)]

remove

{

delegateStore[clickEventKey] =

Delegate.Remove((Delegate) delegateStore[clickEventKey], value);

}

} protected void OnClick()

{

ClickHandler ch = (ClickHandler) delegateStore[clickEventKey];

if (ch != null)

ch(this, null);

}

public void SimulateClick() {

OnClick();

}

class Test {

static public void ButtonHandler(object sender, EventArgs e)

{

Console.WriteLine(“Button clicked”);

}

public static void Main()

{

Button button = new Button();

button.Click += new Button.ClickHandler(ButtonHandler);

button.SimulateClick();

button.Click -= new Button.ClickHandler(ButtonHandler);

}

}

The add() and remove() methods are written using a syntax similar to the one used for properties, and they use the delegateStore hash table to store the delegate. One problem with using a hash table is coming up with a key that can be used to store and fetch the delegates. There’s nothing associated with an event that can serve as a unique key, so clickEventKey is an object that’s included only so you can use it as a key for the hash table. It’s static because the same unique value can be used for all instances of the Button class.

The MethodImpl attribute is required so two threads won’t try to add or remove delegates at the same time (which would be bad). You could also do this with the lock statement.

This implementation still results in the use of one field per object for the hash table. To get rid of this, you have a couple of options. The first is to make the hash table static so that it can be shared among all instances of the Button class. The second choice is to make a single global class to be shared among all the controls, which saves the most space.[1]

Both are good choices, but both will require a couple of changes to the approach. First, simply using an object as a key isn’t good enough since the hash table is shared among all the instances of the object, so the instance will also have to be used as a key.

A subtle outgrowth of using the instance is that the controls have to call a method to remove their event storage when they’re closed. If this didn’t happen, the control wouldn’t be visible anymore, but it’d still be referenced through the global event object, and the memory would never be reclaimed by the garbage collector.

Here’s the a final version of the example, with a global delegate cache:

using System;

using System.Collections;

using System.Runtime.CompilerServices;

//

// Global delegate cache. Uses a two-level hash table. The delegateStore

// hash table stores a hash table keyed on the object instance, and the

// instance hash table is keyed on the unique key. This allows fast teardown

// of the object when it’s destroyed.

//

public class DelegateCache

{

private DelegateCache() {}                     // nobody can create one of these

Hashtable delegateStore = new Hashtable();     // top level hash table

static DelegateCache dc = new DelegateCache(); // our single instance

Hashtable GetInstanceHash(object instance)

{

Hashtable instanceHash = (Hashtable) delegateStore[instance];

if (instanceHash == null)

{

instanceHash = new Hashtable();

delegateStore[instance] = instanceHash;

}

return(instanceHash);

}

public static void Combine(Delegate myDelegate, object instance, object key)

{

lock(instance)

{

Hashtable instanceHash = dc.GetInstanceHash(instance);

instanceHash[key] =

Delegate.Combine((Delegate) instanceHash[key], myDelegate);

}

}

public static void Remove(Delegate myDelegate, object instance, object key)

{

lock(instance)

{

Hashtable instanceHash = dc.GetInstanceHash(instance);

instanceHash[key] =

Delegate.Remove((Delegate) instanceHash[key], myDelegate);

}

}

public static Delegate Fetch(object instance, object key)

{

Hashtable instanceHash = dc.GetInstanceHash(instance);

return((Delegate) instanceHash[key]);

}

public static void ClearDelegates(object instance)

{

dc.delegateStore.Remove(instance);

}

}

public class Button {

public void TearDown()

{

DelegateCache.ClearDelegates(this);

}

public delegate void ClickHandler(object sender, EventArgs e);

static object clickEventKey = new object();

public event ClickHandler Click

{

add

{

DelegateCache.Combine(value, this, clickEventKey);

}

remove

{

DelegateCache.Remove(value, this, clickEventKey);

}

}

protected void OnClick()

{

ClickHandler ch = (ClickHandler) DelegateCache.Fetch(this, clickEventKey);

if (ch != null) ch(this, null);

}

public void SimulateClick()

{

OnClick();

}

}

class Test {

static public void ButtonHandler(object sender, EventArgs e)

{

Console.WriteLine(“Button clicked”);

}

public static void Main()

{

Button button = new Button();

button.Click += new Button.ClickHandler(ButtonHandler);

button.SimulateClick();

button.Click -= new Button.ClickHandler(ButtonHandler);

button.TearDown();

}

}

The DelegateCache class stores the hash tables for each instance that has stored a delegate in a main hash table. This allows for easier cleanup when a control is finished. The Combine(), Remove(), and Fetch() methods do what’s expected. The ClearDelegates() method is called by Button.TearDown() to remove all delegates stored for a specific control instance.

Source: Gunnerson Eric, Wienholt Nick (2005), A Programmer’s Introduction to C# 2.0, Apress; 3rd edition.

Leave a Reply

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