![]() |
Using Delegates to Implement Event Handlingby Jesse Liberty, author of Programming C#, 3rd Edition06/23/2003 |
One of the key aspects of C# programming in particular, and .NET programming in general, is using delegates to handle events. In Programming C#, 3rd Edition, I approach teaching delegates and events somewhat differently than I had in previous editions.
This article will focus on one aspect of delegates: how they are used to implement event handling. It is important to understand that while delegates are a general-purpose mechanism for calling methods indirectly, their principal uses in .NET are for a) implementing events and b) implementing call-back methods.
To get a sense of how delegates are used to implement events, we'll look at the implementation of a custom event.
In C#, any object can publish a set of events to which other classes can subscribe. When the publishing class raises an event, all the subscribed classes are notified. This design is a form of the Observer Pattern described in the seminal work Design Patterns, by Gamma, et al. (Addison Wesley, 1995). Gamma describes the intent of this pattern: "Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically."
With this mechanism, your object can say "Here are things I can notify you about,"
and other classes might sign up, saying "Yes, let me know when that happens." For
example, a button might notify any number of interested observers when it is
clicked. The button is called the "publisher," because the button publishes the Click
event, and the other classes are the subscribers, because they subscribe to the Click
event.
As a second example, a Clock
might notify interested classes whenever the time
changes by one second. The Clock
class could simply
print the time rather than raising an event, so why bother with the indirection of
using delegates? The advantage of the publish/subscribe idiom is that any number
of classes can be notified when an event is raised. The subscribing classes do not
need to know how the Clock
works, and the Clock
does not need to know what
they are going to do in response to the event.
![]() |
Related Reading |
The publisher and the subscribers are decoupled by the delegate. This is highly
desirable; it makes for more flexible and robust code. The Clock
can change how it
detects time without breaking any of the subscribing classes. The subscribing
classes can change how they respond to time changes without breaking the Clock
.
The two classes spin independently of one another, and that makes for code that is
easier to maintain.
A method that handles an event is called an event handler. You can declare your event handlers as you would any other
delegate. By convention, event handlers in the .NET Framework return void and
take two parameters: The first parameter is the "source" of the event; that is, the
publishing object. The second parameter is an object derived from EventArgs
. It is
recommended that your event handlers follow this design pattern.
EventArgs
is the base class for all event data. Other than its constructor, the EventArgs
class inherits all of its methods from Object
, though it
does add a public static field, named empty
, which represents an event with no state
(to allow for the efficient use of events with no state). The EventArgs
-derived class
contains information about the event.
Suppose you want to create a Clock
class that uses delegates to notify potential
subscribers whenever the local time changes value by one second. Call this delegate
SecondChangeHandler
.
The declaration for the SecondChangeHandler
delegate is:
public delegate void SecondChangeHandler(
object clock,
TimeInfoEventArgs timeInformation
);
This delegate will encapsulate any method that returns void and that takes two
parameters. The first parameter is an object that represents the clock (the object
raising the event) and the second parameter is an object of type
TimeInfoEventArgs
that will contain useful information for anyone interested in
this event.
TimeInfoEventArgs
is defined as follows:
public class TimeInfoEventArgs : EventArgs
{
public TimeInfoEventArgs(int hour, int minute, int second)
{
this.hour = hour;
this.minute = minute;
this.second = second;
}
public readonly int hour;
public readonly int minute;
public readonly int second;
}
The TimeInfoEventArgs
object will have information about the current hour,
minute, and second. It defines a constructor and three public, readonly integer
variables.
In addition to its delegate, a Clock
has three member variables, hour
, minute
, and
second
, as well as a single method, Run()
:
public void Run()
{
for(;;)
{
// sleep 10 milliseconds
Thread.Sleep(10);
// get the current time
System.DateTime dt = System.DateTime.Now;
// if the second has changed
// notify the subscribers
if (dt.Second != second)
{
// create the TimeInfoEventArgs object
// to pass to the subscriber
TimeInfoEventArgs timeInformation =
new TimeInfoEventArgs(
dt.Hour,dt.Minute,dt.Second);
// if anyone has subscribed, notify them
if (OnSecondChange != null)
{
OnSecondChange(this,timeInformation);
}
}
// update the state
this.second = dt.Second;
this.minute = dt.Minute;
this.hour = dt.Hour;
}
}
Run
creates an infinite for
loop that periodically checks the system time. If the time
has changed from the Clock
object's current time, it notifies all of its subscribers and
then updates its own state.
The first step is to sleep for 10 milliseconds:
Thread.Sleep(10);
After sleeping for 10 milliseconds, the method checks the current time:
System.DateTime dt = System.DateTime.Now;
About every 100 times it checks, the second will have incremented. The method
notices that change and notifies its subscribers. To do so, it first creates a new
TimeInfoEventArgs
object:
if (dt.Second != second)
{
TimeInfoEventArgs timeInformation =
new TimeInfoEventArgs(dt.Hour,dt.Minute,dt.Second);
It then notifies the subscribers by firing the OnSecondChange
event (the if
statement checks that the value is not null, ensuring that there are subscribers
before calling OnSecondChange
).
if (OnSecondChange != null)
{
OnSecondChange(this,timeInformation);
}
You will remember that OnSecondChange
takes two arguments: the source of the
event and the object derived from EventArgs
. In the code snippet, you see that the
clock's this
reference is passed because the clock is the source of the event. The
second parameter is the TimeInfoEventArgs
object timeInformation
, created on the line above.
|
Raising the event will invoke whatever methods have been registered with the
Clock
class through the delegate. We'll examine this in a moment.
Once the event is raised, you update the state of the Clock
class:
this.second = dt.Second;
this.minute = dt.Minute;
this.hour = dt.Hour;
All that is left is to create classes that can subscribe to this event. You'll create two; your first will be the DisplayClock
class. The job of DisplayClock
is not to keep
track of time, but rather to display the current time to the console.
The example simplifies this class down to two methods. The first is a helper
method named Subscribe
. Subscribe
's job is to subscribe
to the clock's OnSecondChange
delegate. The second method is the event handler
TimeHasChanged
:
public class DisplayClock
{
public void Subscribe(Clock theClock)
{
theClock.OnSecondChange +=
new Clock.SecondChangeHandler(TimeHasChanged);
}
public void TimeHasChanged(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Current Time: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
When the first method, Subscribe
, is invoked, it creates a new SecondChangeHandler
delegate, passing in its event handler method TimeHasChanged
. It then registers that delegate with the OnSecondChange
event of Clock
.
You will create a second class that will also respond to this event: LogCurrentTime
. This class would normally log the event to a file, but for our demonstration
purposes, it will log to the standard console:
public class LogCurrentTime
{
public void Subscribe(Clock theClock)
{
theClock.OnSecondChange +=
new Clock.SecondChangeHandler(WriteLogEntry);
}
// this method should write to a file
// we write to the console to see the effect
// this object keeps no state
public void WriteLogEntry(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Logging to file: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
Although in this example these two classes are very similar, in a production program, any number of disparate classes might subscribe to an event.
All that remains is to create a Clock
class, create the DisplayClock
class, and tell
it to subscribe to the event. You then will create a LogCurrentTime
class and tell
it to subscribe as well. Finally, you'll tell the Clock
to run. All of this is shown in
the following complete example:
Implementing Events with Delegates
namespace Programming_CSharp
{
using System;
using System.Threading;
// a class to hold the information about the event
// in this case it will hold only information
// available in the clock class, but could hold
// additional state information
public class TimeInfoEventArgs : EventArgs
{
public TimeInfoEventArgs(int hour, int minute, int second)
{
this.hour = hour;
this.minute = minute;
this.second = second;
}
public readonly int hour;
public readonly int minute;
public readonly int second;
}
// our subject -- it is this class that other classes
// will observe. This class publishes one delegate:
// OnSecondChange.
public class Clock
{
// the delegate the subscribers must implement
public delegate void SecondChangeHandler
(
object clock,
TimeInfoEventArgs timeInformation
);
// an instance of the delegate
public SecondChangeHandler OnSecondChange;
// set the clock running
// it will raise an event for each new second
public void Run()
{
for(;;)
{
// sleep 10 milliseconds
Thread.Sleep(10);
// get the current time
System.DateTime dt = System.DateTime.Now;
// if the second has changed
// notify the subscribers
if (dt.Second != second)
{
// create the TimeInfoEventArgs object
// to pass to the subscriber
TimeInfoEventArgs timeInformation =
new TimeInfoEventArgs(
dt.Hour,dt.Minute,dt.Second);
// if anyone has subscribed, notify them
if (OnSecondChange != null)
{
OnSecondChange(
this,timeInformation);
}
}
// update the state
this.second = dt.Second;
this.minute = dt.Minute;
this.hour = dt.Hour;
}
}
private int hour;
private int minute;
private int second;
}
// an observer. DisplayClock subscribes to the
// clock's events. The job of DisplayClock is
// to display the current time
public class DisplayClock
{
// given a clock, subscribe to
// its SecondChangeHandler event
public void Subscribe(Clock theClock)
{
theClock.OnSecondChange +=
new Clock.SecondChangeHandler(TimeHasChanged);
}
// the method that implements the
// delegated functionality
public void TimeHasChanged(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Current Time: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
// a second subscriber whose job is to write to a file
public class LogCurrentTime
{
public void Subscribe(Clock theClock)
{
theClock.OnSecondChange +=
new Clock.SecondChangeHandler(WriteLogEntry);
}
// this method should write to a file
// we write to the console to see the effect
// this object keeps no state
public void WriteLogEntry(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Logging to file: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
public class Test
{
public static void Main()
{
// create a new clock
Clock theClock = new Clock();
// create the display and tell it to
// subscribe to the clock just created
DisplayClock dc = new DisplayClock();
dc.Subscribe(theClock);
// create a Log object and tell it
// to subscribe to the clock
LogCurrentTime lct = new LogCurrentTime();
lct.Subscribe(theClock);
// Get the clock started
theClock.Run();
}
}
}
Output:
Current Time: 14:53:56
Logging to file: 14:53:56
Current Time: 14:53:57
Logging to file: 14:53:57
Current Time: 14:53:58
Logging to file: 14:53:58
Current Time: 14:53:59
Logging to file: 14:53:59
Current Time: 14:54:0
Logging to file: 14:54:0
The net effect of this code is to create two classes, DisplayClock
and LogCurrentTime
,
both of which subscribe to a third class' event (Clock.OnSecondChange
).
OnSecondChange
is a delegate. It starts out set to null. When the observer classes
wish to be notified, they create an instance of the delegate and then add these
delegates to OnSecondChange
. For example, in DisplayClock
's Subscribe
method,
you see this line of code:
theClock.OnSecondChange +=
new Clock.SecondChangeHandler(TimeHasChanged);
It turns out that the LogCurrentTime
class also wants to be notified. In its
Subscribe
method is very similar code:
public void Subscribe(Clock theClock)
{
theClock.OnSecondChange +=
new Clock.SecondChangeHandler(WriteLogEntry);
}
There is a problem with this example, however. What if the LogCurrentTime
class
was not so considerate, and it used the assignment operator (=
) rather than the
subscribe operator (+=
)?
public void Subscribe(Clock theClock)
{
theClock.OnSecondChange =
new Clock.SecondChangeHandler(WriteLogEntry);
}
If you make that one tiny change to the example, you'll find that the Logger
method is called but the DisplayClock
method is not called. The assignment
operator replaced the delegate held in the OnSecondChange
multi-cast delegate. Not
good.
A second problem is that other methods can call SecondChangeHandler
directly.
For example, you might add the following code to the Main()
method of your Test
class:
Console.WriteLine("Calling the method directly!");
System.DateTime dt = System.DateTime.Now.AddHours(2);
TimeInfoEventArgs timeInformation =
new TimeInfoEventArgs(
dt.Hour,dt.Minute,dt.Second);
theClock.OnSecondChange(theClock, timeInformation);
Here, Main()
has created its own TimeInfoEventArgs
object and invoked OnSecondChange
directly. This runs fine, even though it is not what the designer of the Clock
class intended. Here is the output:
Calling the method directly!
Current Time: 18:36:7
Logging to file: 18:36:7
Current Time: 16:36:7
Logging to file: 16:36:7
The problem is that the designer of the Clock
class intended the methods
encapsulated by the delegate to be invoked only when the event is fired. Here
Main()
has gone around through the back door, and invoked those methods
itself. What is more, it has passed in bogus data -- passing in a time construct set
to two hours into the future!
|
How can you, as the designer of the Clock
class, ensure that no one calls the
delegated method directly? You can make the delegate private, but then it won't
be possible for clients to register with your delegate at all! What is needed is a
way to say "This delegate is designed for event handling: you may subscribe and
unsubscribe, but you may not invoke it directly."
The solution to this dilemma is to use the event
keyword. The event
keyword
indicates to the compiler that the delegate can only be invoked by the defining
class, and that other classes can only subscribe to, and unsubscribe from, the
delegate using the appropriate +=
and -=
operators, respectively.
To fix your program, change your definition of OnSecondChange
from
public SecondChangeHandler OnSecondChange;
to the following:
public event SecondChangeHandler OnSecondChange;
Adding the event
keyword fixes both problems. Classes can no longer attempt to
subscribe to the event using the assignment operator (=
), as you did above; nor can
they invoke the event directly, as was done in Main
in the example above. Either of
these attempts will now generate a compile error:
The event 'Programming_CSharp.Clock.OnSecondChange' can only appear on the
left hand side of += or -= (except when used from within the type
'Programming_CSharp.Clock')
There are two ways of looking at OnSecondChange
, now that you've modified
it. In one sense, it is simply a delegate instance to which you've restricted access
using the keyword event
. In another, more important sense, OnSecondChange
is
an event, implemented by a delegate of type SecondChangeHandler
. These two
statements mean the same thing, but the latter is a more object-oriented way of
looking at it, and better reflects the intent of this keyword: to create an event
that your object can raise, and to which other objects can respond.
The complete source, modified to use the event, rather than the unrestricted delegate, is shown in the following modified example:
Using the Keyword event
namespace Programming_CSharp
{
using System;
using System.Threading;
// a class to hold the information about the event
// in this case it will hold only information
// available in the clock class, but could hold
// additional state information
public class TimeInfoEventArgs : EventArgs
{
public TimeInfoEventArgs(int hour, int minute, int second)
{
this.hour = hour;
this.minute = minute;
this.second = second;
}
public readonly int hour;
public readonly int minute;
public readonly int second;
}
// our subject -- it is this class that other classes
// will observe. This class publishes one event:
// OnSecondChange. The observers subscribe to that event
public class Clock
{
// the delegate the subscribers must implement
public delegate void SecondChangeHandler
(
object clock,
TimeInfoEventArgs timeInformation
);
// the keyword event controls access to the delegate
public event SecondChangeHandler OnSecondChange;
// set the clock running
// it will raise an event for each new second
public void Run()
{
for(;;)
{
// sleep 10 milliseconds
Thread.Sleep(10);
// get the current time
System.DateTime dt = System.DateTime.Now;
// if the second has changed
// notify the subscribers
if (dt.Second != second)
{
// create the TimeInfoEventArgs object
// to pass to the subscriber
TimeInfoEventArgs timeInformation =
new TimeInfoEventArgs(
dt.Hour,dt.Minute,dt.Second);
// if anyone has subscribed, notify them
if (OnSecondChange != null)
{
OnSecondChange(
this,timeInformation);
}
}
// update the state
this.second = dt.Second;
this.minute = dt.Minute;
this.hour = dt.Hour;
}
}
private int hour;
private int minute;
private int second;
}
// an observer. DisplayClock subscribes to the
// clock's events. The job of DisplayClock is
// to display the current time
public class DisplayClock
{
// given a clock, subscribe to
// its SecondChangeHandler event
public void Subscribe(Clock theClock)
{
theClock.OnSecondChange +=
new Clock.SecondChangeHandler(TimeHasChanged);
}
// the method that implements the
// delegated functionality
public void TimeHasChanged(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Current Time: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
// a second subscriber whose job is to write to a file
public class LogCurrentTime
{
public void Subscribe(Clock theClock)
{
theClock.OnSecondChange +=
new Clock.SecondChangeHandler(WriteLogEntry);
}
// this method should write to a file
// we write to the console to see the effect
// this object keeps no state
public void WriteLogEntry(
object theClock, TimeInfoEventArgs ti)
{
Console.WriteLine("Logging to file: {0}:{1}:{2}",
ti.hour.ToString(),
ti.minute.ToString(),
ti.second.ToString());
}
}
public class Test
{
public static void Main()
{
// create a new clock
Clock theClock = new Clock();
// create the display and tell it to
// subscribe to the clock just created
DisplayClock dc = new DisplayClock();
dc.Subscribe(theClock);
// create a Log object and tell it
// to subscribe to the clock
LogCurrentTime lct = new LogCurrentTime();
lct.Subscribe(theClock);
// Get the clock started
theClock.Run();
}
}
}
You can see that the event
keyword serves to modify how the delegate is used,
to be consistent with the semantics of event handling. There is much more to
the use of delegates, and this topic is covered in depth in Chapter 12 of
Programming C#, 3rd Edition, from which this article is adapted.
O'Reilly & Associates recently (in May 2003) released Programming C#, 3rd Edition.
You can also look at the Table of Contents, the Index, and the full description of the book.
For more information, or to order the book, click here.
Jesse Liberty is a computer consultant, trainer, and best-selling book author specializing in .NET and Web development. Jesse provides support for his books at www.LibertyAssociates.com.
Return to ONDotnet.com
Copyright © 2004 O'Reilly Media, Inc.