|
Delegates: how to add them to an event, how they get removed and when that is necessary category 'KB', language C#, created 24-Jul-2007, version V1.0, by Luc Pattyn This article also appeared on the CodeProject. |
|
License: The author hereby grants you a worldwide, non-exclusive license to use and redistribute the files and the source code in the article in any way you see fit, provided you keep the copyright notice in place; when code modifications are applied, the notice must reflect that. The author retains copyright to the article, you may not republish or otherwise make available the article, in whole or in part, without the prior written consent of the author. Disclaimer: This work is provided |
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.
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"); and 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 by 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) {
...
}
}
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, 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; and it may take you a while to figure out where the mistake is !
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 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 the run-time system, when executing the -= line parses the delegate, and looks for one with the same object ("this") and method pointer ("button_click"); if it finds one (or more), it removes one; it not, nothing happens, no Exception gets thrown. All of this gets confirmed by the test program.
The program is a very simple Console application. The program 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, and simulates some clicks, using the Button.PerformClick() method, which fortunately also works for invisible buttons ! Most importantly the program logs all what 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
So these are the observations we made:
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 being 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 no longer is apparent from the source
code ! It now seems like the button_click first got added, then
removed again.
Of course we will remove a delegate if the object remains active but does not want to receive the notifications any more. But what if the object is basically done, it has no use anymore, and we hope it will soon be garbage collected ? We realize 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:
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.class1 attaches its delegate to an
event of another class (class2), then that counts as a reference
from class2 to class1, and 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.
The following items (some of them not discussed but present in the source code) relate to recent topics on one of the CodeProject message boards:
log()
method to get timestamps on major actions;
Console.ReadLine()
to keep the command window from closing;
#define, #if, #else, #endif
statements for conditional compilation.
Perceler |
Copyright © 2012, Luc Pattyn |
Last Modified 21-May-2025 |