GUIs, such as Microsoft Windows and web browsers, require that programs respond to events. An event might be a button push, a menu selection, the completion of a file transfer, and so forth. In short, something happens and you must respond to it. You cannot predict the order in which events will arise. The system is quiescent until the event, and then springs into action to handle it.
In a GUI environment, any number of controls can
raise an event. For example, when you click a
button, it might raise the Click
event. When you add to a drop-down list, it might raise a
ListChanged
event.
Other classes will be interested in responding to these events. How they respond is not of interest to the class raising the event. The button says, “I was clicked,” and the responding classes react appropriately.
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. 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.
This design implements the Publish/Subscribe (Observer) Pattern described in the seminal work Design Patterns by Gamma, et al. (Addison Wesley, 1995).
Note that the publishing class does not know or care who (if anyone) subscribes; it just raises the event. Who responds to that event, and how they respond, is not the concern of the publishing class.
As a second example, a Clock
might notify interested classes whenever the time changes by one
second. The Clock
class could
itself be responsible for the User Interface representation of 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 the Clock
class need not know
how its information will be used; the monitoring of the time is thus
decoupled from the representation of that information. In addition,
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.
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.
Events in C# are implemented with delegates . The publishing class defines a delegate. The subscribing class does two things: first, it creates a method that matches the signature of the delegate, and then it creates an instance of that delegate type encapsulating that method. When the event is raised, the subscribing class’s methods are invoked through the delegate.
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 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 SecondChangeEventHandler
:
The declaration for the SecondChangeEventHandler
delegate is:
public delegate void SecondChangeEventHandler: ( 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, read-only 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 (SecondChanged != null) { SecondChanged(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
its subscribers and then updates its own state.
The first step is to sleep for 10 milliseconds:
Thread.Sleep(10);
This makes use of a static method of the Thread
class from the System.Threading
namespace. The call to
Sleep( )
prevents the loop from
running so tightly that little else on the computer gets done.
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) { // create the TimeInfoEventArgs object // to pass to the subscriber TimeInfoEventArgs timeInformation = new TimeInfoEventArgs(dt.Hour,dt.Minute,dt.Second);
It then notifies the subscribers by firing the SecondChanged
event:
// if anyone has subscribed, notify them if (SecondChanged != null) { SecondChanged(this,timeInformation); } }
If an event has no subscribers registered, it will evaluate to
null
. The preceding test checks
that the value is not null
,
ensuring that there are subscribers before calling SecondChanged
.
You will remember that SecondChanged
takes two arguments: the
source of the event and the object derived from EventArgs
. In the 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 in the
preceding snippet.
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. 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( )
that is used to subscribe to the clock’s SecondChanged
delegate. The second method is
the event handler TimeHasChanged( )
:
public class DisplayClock { public void Subscribe(Clock theClock) { theClock.SecondChanged += 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 SecondChanged
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.SecondChanged += 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 this is shown in Example 17-2 (you’ll need to
press Ctrl-C to terminate this application).
Example 17-2. Implementing events with delegates
using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace ImplementingEventsWithDelegates { // 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: // SecondChanged. public class Clock { private int hour; private int minute; private int second; // the delegate the subscribers must implement public delegate void SecondChangeHandler ( object clock, TimeInfoEventArgs timeInformation ); // an instance of the delegate public SecondChangeHandler SecondChanged; // 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 ( SecondChanged != null ) { SecondChanged( this, timeInformation ); } } // update the state this.second = dt.Second; this.minute = dt.Minute; this.hour = dt.Hour; } } } // 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.SecondChanged += 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.SecondChanged += 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( ); } } }
The output on my machine looks like this:
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.SecondChanged
).
SecondChanged
is a multicast
delegate field, initially referring to nothing. In time, it refers to
a single delegate, and then later to multiple delegates . When the observer classes wish to be notified, they
create an instance of the delegate and then add these delegates to
SecondChanged
. For example, in
DisplayClock
’s Subscribe( )
method, you see this line of
code:
theClock.SecondChanged += 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.SecondChanged += new Clock.SecondChangeHandler(WriteLogEntry); }
There is a problem with Example 17-2, however. What
if the LogCurrentTime
class was not
so considerate, and it used the assignment operator (=
) rather than the subscribe operator
(+=
), as in the following:
public void Subscribe(Clock theClock) { theClock.SecondChanged = 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 SecondChanged
multicast delegate. This is
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.SecondChanged(theClock, timeInformation);
Here, Main( )
has created its
own TimeInfoEventArgs
object and
invoked SecondChanged
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’s 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 SecondChanged
from:
public SecondChangeHandler SecondChanged;
to the following:
publicevent
SecondChangeHandler SecondChanged;
Adding the event
keyword
fixes both problems. Classes can no longer attempt to subscribe to the
event using the assignment operator (=
), as they could previously, nor can they
invoke the event directly, as was done in Main( )
in the preceding example. Either of
these attempts will now generate a compile error:
The event 'Programming_CSharp.Clock.SecondChanged' 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 SecondChanged
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,
SecondChanged
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 Example 17-3.
Example 17-3. Using the event keyword
using System; using System.Collections.Generic; using System.Text; using System.Threading; namespace UsingTheEventKeyword { // 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 readonly int hour; public readonly int minute; public readonly int second; public TimeInfoEventArgs( int hour, int minute, int second ) { this.hour = hour; this.minute = minute; this.second = second; } } // our subject -- it is this class that other classes // will observe. This class publishes one event: // SecondChanged. The observers subscribe to that event public class Clock { private int hour; private int minute; private int second; // 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 SecondChanged; // 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 ( SecondChanged != null ) { SecondChanged( this, timeInformation ); } } // update the state this.second = dt.Second; this.minute = dt.Minute; this.hour = dt.Hour; } } } // 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.SecondChanged += 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.SecondChanged += 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( ); } } }