Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Facts and Fallacies of Events in C#

0.00/5 (No votes)
24 Jul 2007 1  
Delegates: how to add them to an event, how they get removed and when that is necessary

Introduction

If you don't know what delegates and events are, don't read on. Go read an introductory book on C# first. If you do know what events and delegates are and you have used them successfully, but still have some questions about them, this article may shed some light.

Delegates and events

MSDN holds some pretty accurate definitions for delegates and events, as it should:

A delegate declaration defines a reference type that can be used to encapsulate a method with a specific signature. A delegate instance encapsulates a static or an instance method. Delegates are roughly similar to function pointers in C++. However, delegates are type-safe and secure.
Events are used on classes and structs to notify objects of occurrences that may affect their state. To add an event to a class requires using the event keyword and providing a delegate type and a name for the event.

To recap that in layman's terms: a delegate contains the reference to an object and a method that can operate on that object -- say "a function pointer" and a "this" -- whereas an event resembles a collection capable of holding such delegates and firing them one after another. As an example, one can install an "event handler" to be called on a mouse click using the following lines of code:

public class myForm : Form 
{
    private Button button=new Button();
    ...
    // constructor

    public myForm() 
    {
        ...
        button.Click+=new EventHandler(button_Click);
        ...
    }
    
    // event handler, fires when button gets clicked

    private void button_Click(object sender, EventArgs e) 
    {
        ...
    }
}

A popular mistake: adding the same delegate more than once

Normally, you want a delegate to be called just once: one btn_click() for every click of the button. When you add the same delegate more than once to an event, the handler will be called more than once. Most of the time, that was not the intention and the extra adds are a mistake. That mistake may constitute a bug: hitting the "A" key should not enter two characters "A" in a textbox. Simple mistakes like that will be noticed immediately.

Sometimes, executing the handler more than once does not result in an error; it just wastes CPU cycles. For instance, executing a Focus handler twice probably will not result in a logical error. Of course, if for some reason you keep adding the same Paint handler over and over to the Paint event, in the end the responsiveness of the application will be completely ruined. It may take you a while to figure out where the mistake is!

A little magic

The event works somewhat like a collection: you can add delegates to it and, later on when you are no longer interested, you can remove them again. Some people scrupulously store the delegate they are about to add to the event so that they can later use the reference for removing it again, as in:

private EventHandler buttonClickHandler = 
    new EventHandler(button_Click);
...
button.Click += buttonClickHandler;
...
button.Click -= buttonClickHandler;

Other people, and most books on C#, use a different approach: they create a delegate and add it. Later on, they create another delegate and remove it, as in:

button.Click += new EventHandler(button_Click);
...
button.Click -= new EventHandler(button_Click);

So basically now the events "collection" seems able to remove an object that is different from, but equivalent to, another object that was added earlier. What this actually means is that the run-time system, when executing the -= line, parses the delegate and looks for one with the same object and method pointer, i.e. this and button_click respectively. If it finds one or more, it removes one. If not, nothing happens and no exception gets thrown. All of this gets confirmed by the test program.

The test program

The program is a very simple console application. It creates one button that does not get a parent and hence remains invisible. The program then plays around with a delegate to handle the click events. It simulates some clicks using the Button.PerformClick() method, which fortunately also works for invisible buttons! Most importantly, the program logs all that is going on, so the log can confirm the theory. This is the heart of the test sequence:

public void Run() 
{
    doClick();
    addDelegate();
    doClick();
    addDelegate();
    doClick();
    removeDelegate();
    doClick();
    removeDelegate();
    doClick();
    removeDelegate();
    doClick();
    log("Done");
}

private static void log(string s) 
{
    if (s.Length!=0) s=DateTime.Now.ToString("ss.fff  ")+s;
    Console.WriteLine(s);
}

private void doClick() 
{
    log("CLICK");
    btn.PerformClick();
}

private void addDelegate() 
{
    delegateCount++;
    log("ADD DELEGATE #"+delegateCount);
    btn.Click+=new EventHandler(btn_Click);
}

private void removeDelegate() 
{
    log("REMOVE DELEGATE #"+delegateCount);
    delegateCount--;
    try {btn.Click-=new EventHandler(btn_Click);}
    catch { log("failed to remove handler"); }
}

private void btn_Click(object sender, EventArgs e) 
{
    clicks++;
    log("    got click #"+clicks);
}

And this is the log that gets produced:

29.750  CLICK

29.750  ADD DELEGATE #1
29.750  CLICK
29.750      got click #1

29.750  ADD DELEGATE #2
29.750  CLICK
29.750      got click #2
29.750      got click #3

29.750  REMOVE DELEGATE #2
29.750  CLICK
29.750      got click #4

29.750  REMOVE DELEGATE #1
29.750  CLICK

29.750  REMOVE DELEGATE #0
29.750  CLICK

29.750  Done

These are the observations we made:

  • Every CLICK is followed by a number of "got click" messages equal to the number of delegates currently in the event: first none and then 1, 2, 1, 0
  • The first "remove delegate" did not remove all delegates; it removed just one
  • Removing more delegates than we added in the first place did not throw an exception; it had no effect at all

C# 2.0

Originally, C# required an explicit delegate to be added to or removed from an event, as in:

// add a click handler, the original way (works since .NET 1.0)

button.Click += new EventHandler(button_Click);
...
// remove a click handler, the original way (works since .NET 1.0)

button.Click -= new EventHandler(button_Click);

The above works for all .NET versions. The new version of C#, C# 2.0, also accepts a shorthand notation, as in:

// add a click handler, the shorthand (works since .NET 2.0)

button.Click += button_Click;
...
// remove a click handler, the shorthand (works since .NET 2.0)

button.Click -= button_Click;

This generates exactly the same MSIL code. The compiler deduces which delegate needs to be instantiated from the declaration of the Click event itself. So, the magic described earlier is still going on. A new delegate is being created in order to remove an earlier one, but this is no longer apparent from the source code! It now seems like the button_click first got added and then removed again.

The final question: do we have to remove event handlers?

Of course we will remove a delegate if the object remains active, but does not want to receive the notifications anymore. However, what if the object is basically done; it has no use anymore and we hope it will soon be garbage collected? We realize that a delegate contains a reference to the object to which the method applies, so will the existence of delegates prevent it from being collected? There are two different situations:

  • Most of the time, all delegates are used internally and the situation is simple. In our earlier example, the button is part of a class and the click handler method also belongs to that class. So in a sense it is an "internal delegate" and an "internal event," something that is invisible from the outside. When an instance of that class no longer is reachable by the Main() method and its descendants, then the object is a candidate for garbage collection. The event inside the object should not and will not prevent that.
  • As soon as one delegate is used "outside" of its class, the situation is different. If an instance of class1 attaches its delegate to an event of another class, i.e. class2, then that counts as a reference from class2 to class1. Hence, as long as the class2 object is alive, it will keep the instance of class1 alive. To end this dependency, the class1 instance should remove its delegate.

Conclusion: you should remove delegates from events when they reach outside the class itself; i.e. when you subscribe to external events, you should end your subscription when you are done. Failing to do so will keep your object alive longer than necessary.

Other points of interest

The following items relate to recent topics on one of the CodeProject message boards. Note that some of them are not discussed in this article, but are present in the source code:

  • Create a simple log() method to get timestamps on major actions
  • Add Console.ReadLine() to keep the command window from closingng
  • Use #define, #if, #else, #endif statements for conditional compilation

History

  • 24 July, 2007 -- LP#EventHandlerRemoval 1.0, first release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here