When a head of state dies, the President of the United States typically does not have time to attend the funeral personally. Instead, he dispatches a delegate. Often this delegate is the Vice President, but sometimes the VP is unavailable and the President must send someone else, such as the Secretary of State or even the First Lady. He does not want to “hardwire” his delegated authority to a single person; he might delegate this responsibility to anyone who is able to execute the correct international protocol.
The President defines in advance what responsibility will be delegated (attend the funeral), what parameters will be passed (condolences, kind words), and what value he hopes to get back (good will). He then assigns a particular person to that delegated responsibility at “runtime” as the course of his presidency progresses.
In programming, you are often faced with situations where you need to execute a particular action, but you don’t know in advance which method, or even which object, you’ll want to call upon to execute it. For example, a button might know that it must notify some object when it is pushed, but it might not know which object or objects need to be notified. Rather than wiring the button to a particular object, you will connect the button to a delegate and then resolve that delegate to a particular method when the program executes.
In the early, dark, and primitive days of computing, a program would begin execution and then proceed through its steps until it completed. If the user was involved, the interaction was strictly controlled and limited to filling in fields.
Today’s Graphical User Interface (GUI) programming model requires a different approach, known as event-driven programming. A modern program presents the user interface and waits for the user to take an action. The user might take many different actions, such as choosing among menu selections, pushing buttons, updating text fields, clicking icons, and so forth. Each action causes an event to be raised. Other events can be raised without direct user action, such as events that correspond to timer ticks of the internal clock, email being received, file-copy operations completing, and so forth.
An event is the encapsulation of the idea that “something happened” to which the program must respond. Events and delegates are tightly coupled concepts because flexible event handling requires that the response to the event be dispatched to the appropriate event handler. An event handler is typically implemented in C# via a delegate .
Delegates are also used as callbacks so that one class can say to another “do this work and when you’re done, let me know.”
In C#, delegates are first-class objects, fully supported by the language. Technically, a delegate is a reference type used to encapsulate a method with a specific signature and return type. You can encapsulate any matching method in that delegate.
A delegate is created with the delegate
keyword, followed by a return type
and the signature of the methods that can be delegated to it, as in the
following:
public delegate int WhichIsFirst(object obj1, object obj2);
This declaration defines a delegate named WhichIsFirst
, which will encapsulate any
method that takes two objects as parameters and that returns an int
.
Once the delegate is defined, you can encapsulate a member method with that delegate by instantiating the delegate, passing in a method that matches the return type and signature. As an alternative, you can use anonymous methods as described below. In either case, the delegate can then be used to invoke that encapsulated method.
Delegates decouple the class that
declares the delegate from the class that uses the delegate. For
example, suppose that you want to create a simple generic container
class called a Pair
that can hold
and sort any two objects passed to it. You can’t know in advance what
kind of objects a Pair
will hold,
but by creating methods within those objects to which the sorting task
can be delegated, you can delegate responsibility for determining
their order to the objects themselves.
Different objects will sort differently (for example, a Pair
of Counter
objects might sort in numeric order,
while a Pair
of Buttons
might sort alphabetically by their
name). As the author of the Pair
class, you want the objects in the pair to have the responsibility of
knowing which should be first and which should be second. To
accomplish this, you will insist that the objects to be stored in the
Pair
must provide a method that
tells you how to sort the objects.
You can define this requirement with interfaces, as well.
Delegates are smaller and of finer granularity than interfaces. The
Pair
class does not need to
implement an entire interface; it just needs to define the signature
and return type of the method it wants to invoke. That is what
delegates are for: they define the return type and signature of
methods that can be invoked through the interface.
In this case, the Pair
class
will declare a delegate named WhichIsFirst
. When the Pair
needs to know how to order its objects,
it will invoke the delegate passing in its two member objects as
parameters. The responsibility for deciding which of the two objects
comes first is delegated to the method encapsulated by the
delegate:
public delegateComparison WhichIsFirst<T>
( T obj1, T obj2 );
In this definition, WhichIsFirst
is defined to encapsulate a
method that takes two objects as parameters, and that returns an
object of type Comparison
. Comparison
turns out to be an enumeration
you will define:
public enum Comparison { TheFirstComesFirst = 1, TheSecondComesFirst = 2 }
To test the delegate, you will create two classes: a Dog
class and a Student
class. Dogs
and Students
have little in common, except that
they both implement methods that can be encapsulated by WhichComesFirst
, and thus both Dog
objects and Student
objects are eligible to be held
within Pair
objects.
In the test program, you will create a couple of Students
and a couple of Dogs
, and store them each in a Pair
. You will then create instances of
WhichIsFirst
to encapsulate their
respective methods that will determine which Student
or which Dog
object should be first, and which
second. Let’s take this step by step.
You begin by creating a Pair
constructor that takes two objects and stashes them away in a private
array:
public Pair( T firstObject, T secondObject ) { thePair[0] = firstObject; thePair[1] = secondObject; }
Note that the Pair<T>
class uses generics, as discussed in Chapter 14. Therefore, firstObject
and secondObject
in the constructor above are of
the generic type T, and the actual types will be assigned at
runtime.
Next, you override ToString( )
to obtain the string value of the two objects:
public override string ToString( ) { return thePair[0].ToString( ) + ", " + thePair[1].ToString( ); }
You now have two objects in your Pair
and you can print out their values.
You’re ready to sort them and print the results of the sort. You can’t
know in advance what kind of objects you will have, so you delegate
the responsibility of deciding which object comes first in the sorted
Pair
to the objects
themselves.
Both the Dog
class and the
Student
class implement methods
that can be encapsulated by WhichIsFirst
. Any method that takes two
objects and returns a Comparison
can be encapsulated by this delegate at runtime.
You can now define the Sort( )
method for the Pair
class:
public void Sort(WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0],thePair[1]) == Comparison.theSecondComesFirst) { T temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } }
This method takes a parameter: a delegate of type WhichIsFirst
named theDelega-tedFunc
. The Sort( )
method delegates responsibility for deciding which of the two objects in
the Pair
comes first to the method
encapsulated by that delegate. In the body of the Sort( )
method, it invokes the delegated
method and examines the return value, which will be one of the two
enumerated values of Comparison
.
If the value returned is theSecondComesFirst
, the objects within the
pair are swapped; otherwise, no action is taken.
This is analogous to how the other parameters work. If you had a
method that took an int
as a
parameter:
int SomeMethod (int myParam){//...}
The parameter name is myParam
, but you can pass in any int
value or variable. Similarly, the
parameter name in the delegate example is theDelegatedFunc
, but you can pass in any
method that meets the return value and signature defined by the
delegate WhichIsFirst
.
Imagine you are sorting Students
by name. You write a method that
returns theFirstComesFirst
if the
first student’s name comes first, and theSecondComesFirst
if the second student’s
name does. If you pass in “Amy, Beth,” the method will return theFirstComesFirst
, and if you pass in
“Beth, Amy,” it will return theSecondComesFirst
. If you get back
theSecondComesFirst
, the Sort( )
method reverses the items in its
array, setting Amy to the first position and Beth to the
second.
Now add one more method, ReverseSort( )
, which will force the items in the array into the reverse
of their normal order:
public void ReverseSort(WhichIsFirst theDelegatedFunc) { if (theDelegatedFunc(thePair[0], thePair[1]) == Comparison.theFirstComesFirst) { T temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } }
The logic here is identical to Sort( )
, except that this method performs the swap if the
delegated method says that the first item comes first. Because the
delegated function thinks the first item comes first, and this is a
reverse sort, the result you want is for the second item to come
first. This time, if you pass in “Amy, Beth,” the delegated function
returns theFirstComesFirst
(Amy
should come first), but because this is a reverse
sort, it swaps the values, setting Beth first. This allows you to use
the same delegated function as you used with Sort( )
, without forcing the object to
support a function that returns the reverse sorted value.
Now all you need are some objects to sort. You’ll create two
absurdly simple classes: Student
and Dog
. Assign Student
objects a name at creation:
public class Student { public Student(string name) { this.name = name; }
The Student
class requires
two methods: one to override ToString( )
and the other to be encapsulated as the delegated
method.
Student
must override
ToString( )
so that the ToString( )
method in Pair
, which invokes ToString( )
on the contained objects, will
work properly; the implementation does nothing more than return the
student’s name (which is already a string object):
public override string ToString( ) { return name; }
It must also implement a method to which Pair.Sort( )
can delegate the responsibility
of determining which of two objects comes first, called WhichStudentComesFirst( )
in this
case:
public static Comparison WhichStudentComesFirst( Student s1, Student s2 ) { return (String.Compare(s1.name, s2.name) < 0 ? Comparison.theFirstComesFirst : Comparison.theSecondComesFirst); }
String.Compare( )
is a .NET
Framework method on the String
class that compares two strings and returns less than zero if the
first is smaller, greater than zero if the second is smaller, and zero
if they are the same. This method is discussed in some detail in Chapter 15. Notice that the logic
here returns theFirstComesFirst
only if the first string is smaller; if they are the same or the
second is larger, this method returns theSecondComesFirst
.
Notice that the WhichStudentComesFirst( )
method takes two objects as parameters and returns a
Comparison
. This qualifies it to be
a Pair.WhichIsFirst
delegated
method, whose signature and return value it matches.
The second class is Dog
. For
our purposes, Dog
objects will be
sorted by weight, lighter dogs before heavier. Here’s the complete
declaration of Dog
:
public class Dog { public Dog(int weight) { this.weight=weight; } // dogs are ordered by weight public static Comparison WhichDogComesFirst( Object o1, Object o2) { Dog d1 = (Dog) o1; Dog d2 = (Dog) o2; return d1.weight > d2.weight ? Comparison.theSecondComesFirst : Comparison.theFirstComesFirst; } public override string ToString( ) { return weight.ToString( ); } private int weight; }
The Dog
class also overrides
ToString
and implements a static
method called Which-DogComesFirst( )
with the correct signature for the delegate. Notice also
that the Dog
and Student
delegate methods do not have the
same name. They do not need to have the same name, as they will be
assigned to the delegate dynamically at runtime.
You can call your delegated method names anything you like,
but creating parallel names (such as WhichDogComesFirst
and WhichStudent-ComesFirst
) makes the code
easier to read, understand, and maintain.
Example 17-1 is the complete program, which illustrates how the delegate methods are invoked.
using System; using System.Collections.Generic; using System.Text; namespace WorkingWithDelegates { public enum Comparison { TheFirstComesFirst = 1, TheSecondComesFirst = 2 } // a simple collection to hold 2 items public class Pair<T> { // private array to hold the two objects private T[] thePair = new T[2]; // the delegate declaration public delegate Comparison WhichIsFirst( T obj1, T obj2 ); // passed in constructor take two objects, // added in order received public Pair( T firstObject, T secondObject ) { thePair[0] = firstObject; thePair[1] = secondObject; } // public method which orders the two objects // by whatever criteria the object likes! public void Sort( WhichIsFirst theDelegatedFunc ) { if ( theDelegatedFunc( thePair[0], thePair[1] ) == Comparison.TheSecondComesFirst ) { T temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } } // public method which orders the two objects // by the reverse of whatever criteria the object likes! public void ReverseSort( WhichIsFirst theDelegatedFunc ) { if ( theDelegatedFunc( thePair[0], thePair[1] ) == Comparison.TheFirstComesFirst ) { T temp = thePair[0]; thePair[0] = thePair[1]; thePair[1] = temp; } } // ask the two objects to give their string value public override string ToString( ) { return thePair[0].ToString( ) + ", " + thePair[1].ToString( ); } } // end class Pair public class Dog { private int weight; public Dog( int weight ) { this.weight = weight; } // dogs are ordered by weight public static Comparison WhichDogComesFirst( Dog d1, Dog d2 ) { return d1.weight > d2.weight ? Comparison.TheFirstComesFirst : Comparison.TheSecondComesFirst ); } public override string ToString( ) { return weight.ToString( ); } } // end class Dog public class Student { private string name; public Student( string name ) { this.name = name; } // students are ordered alphabetically public static Comparison WhichStudentComesFirst( Student s1, Student s2 ) { return ( String.Compare( s1.name, s2.name ) < 0 ? Comparison.theFirstComesFirst : Comparison.theSecondComesFirst ); } public override string ToString( ) { return name; } } // end class Student public class Test { public static void Main( ) { // create two students and two dogs // and add them to Pair objects Student Jesse = new Student( "Jesse" ); Student Stacey = new Student( "Stacey" ); Dog Milo = new Dog( 65 ); Dog Fred = new Dog( 12 ); Pair<Student> studentPair = new Pair<Student>( Jesse, Stacey ); Pair<Dog> dogPair = new Pair<Dog>( Milo, Fred ); Console.WriteLine( "studentPair\t\t\t: {0}", studentPair.ToString( ) ); Console.WriteLine( "dogPair\t\t\t\t: {0}", dogPair.ToString( ) ); // Instantiate the delegates Pair<Student>.WhichIsFirst theStudentDelegate = new Pair<Student>.WhichIsFirst( Student.WhichStudentComesFirst ); Pair<Dog>.WhichIsFirst theDogDelegate = new Pair<Dog>.WhichIsFirst( Dog.WhichDogComesFirst ); // sort using the delegates studentPair.Sort( theStudentDelegate ); Console.WriteLine( "After Sort studentPair\t\t: {0}", studentPair.ToString( ) ); studentPair.ReverseSort( theStudentDelegate ); Console.WriteLine( "After ReverseSort studentPair\t: {0}", studentPair.ToString( ) ); dogPair.Sort( theDogDelegate ); Console.WriteLine( "After Sort dogPair\t\t: {0}", dogPair.ToString( ) ); dogPair.ReverseSort( theDogDelegate ); Console.WriteLine( "After ReverseSort dogPair\t: {0}", dogPair.ToString( ) ); } } }
The output looks like this:
studentPair : Jesse, Stacey dogPair : 65, 12 After Sort studentPair : Jesse, Stacey After ReverseSort studentPair : Stacey, Jesse After Sort dogPair : 12, 65 After ReverseSort dogPair : 65, 12
The Test
program creates two
Student
objects and two Dog
objects and then adds them to Pair
containers. The student
constructor takes a string
for the student’s name and the
dog
constructor takes an int
for the dog’s weight.
Student Jesse = new Student( "Jesse" ); Student Stacey = new Student( "Stacey" ); Dog Milo = new Dog( 65 ); Dog Fred = new Dog( 12 ); Pair<Student> studentPair = new Pair<Student>( Jesse, Stacey ); Pair<Dog> dogPair = new Pair<Dog>( Milo, Fred ); Console.WriteLine( "studentPair\t\t\t: {0}", studentPair.ToString( ) ); Console.WriteLine( "dogPair\t\t\t\t: {0}", dogPair.ToString( ) );
It then prints the contents of the two Pair
containers to see the order of the
objects. The output looks like this:
studentPair : Jesse, Stacey dogPair : 65, 12
As expected, the objects are in the order in which they were
added to the Pair
containers. You
next instantiate two delegate objects:
Pair<Student>.WhichIsFirst theStudentDelegate = new Pair<Student>.WhichIsFirst( Student.WhichStudentComesFirst ); Pair<Dog>.WhichIsFirst theDogDelegate = new Pair<Dog>.WhichIsFirst( Dog.WhichDogComesFirst );
The first delegate, theStudentDelegate
, is created by passing in
the appropriate static method from the Student
class. The second delegate, theDogDelegate
, is passed a static method
from the Dog
class.
The delegates are now objects that can be passed to methods. You
pass the delegates first to the Sort( )
method of the Pair
object, and then to the ReverseSort( )
method. The results are printed to the console:
After Sort studentPair : Jesse, Stacey After ReverseSort studentPair : Stacey, Jesse After Sort dogPair : 12, 65 After ReverseSort dogPair : 65, 12
At times, it is desirable to call two (or more) implementing methods through a single delegate. This becomes particularly important when handling events (discussed later in this chapter).
The goal is to have a single delegate that invokes more than one method. For example, when a button is pressed, you might want to take more than one action. This process of calling more than one method with a single delegate is called multicasting .
Two delegates can be combined with the addition operator (+
). The result is a new multicast delegate
that invokes both of the original implementing methods. For example,
assuming Writer
and Logger
are delegates, the following line will
combine them and produce a new multicast delegate named myMulticastDelegate
:
myMulticastDelegate = Writer + Logger;
You can add delegates to a multicast delegate using the
plus-equals (+=
) operator. This
operator adds the delegate on the right side of the operator to the
multicast delegate on the left. For example, assuming Transmitter
and myMulticastDelegate
are delegates, the
following line adds Transmitter
to
myMulticastDelegate
:
myMulticastDelegate += Transmitter;
The power of multicast delegates is best understood in terms of events, discussed in the next section. When an event such as a button press occurs, an associated multicast delegate can invoke a series of event handler methods that will respond to the event.
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).
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.
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( ); } } }
In the previous example, you subscribed to the event by invoking a new instance of the delegate, passing in the name of a method that implements the event:
theClock.SecondChanged += TimeHasChanged;
Later in the code, you must define TimeHasChanged
as a method that matches the
signature of the SecondChangeHandler
delegate:
public void TimeHasChanged( object theClock, TimeInfoEventArgs ti) { Console.WriteLine("Current Time: {0}:{1}:{2}", ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( )); }
Anonymous methods allow you to pass a code block rather than the name of the method. This can make for more efficient and easier to maintain code, and the anonymous method has access to the variables in the scope in which they are defined.
clock.SecondChanged += delegate( object theClock,TimeInfoEventArgs
ti ) {Console
.WriteLine( "Current Time: {0}:{1}:{2}", ti.hour.ToString( ), ti.minute.ToString( ), ti.second.ToString( ) ); };
Notice that rather than registering an instance of a delegate, you use the keyword delegate, followed by the parameters that would be passed to your method, followed by the body of your method encased in braces and terminated by a semicolon.
This “method” has no name; hence, it is anonymous. You cannot invoke the method except through the delegate; but that is exactly what you want.
Modern GUIs rely on events generated by the user (or by the system) to know what action to take.
A delegate is a reference to a method of a particular signature and return type.
The keyword event
constrains a delegate’s usage to the event handling
semantics.
Instead of implementing delegates as static fields, you can implement them as properties, so that you do not need to instantiate them if they are never used.
You can combine delegates using the +=
operator.
An object can publish a series of events to which other classes can subscribe. The publishing class defines a delegate and an event based on that delegate. The subscribing class creates a method that matches the signature of the delegate, and registers that method with an instance of the delegate.
In .NET, all event handlers return void
, and take two parameters. The first
parameter is of type object and is the object that raises the event;
the second argument is an object of type EventArgs
or of a type derived from
EventArgs
, which may contain
useful information about the event.
Instead of passing a method name to a delegate, you can pass a block of code; this creates an anonymous method.
Define an event to signal that the phone has rung.
Are delegates value types or reference types?
What is the purpose of a delegate?
How do you instantiate a delegate, such as the OnPhoneRings
delegate described in the
first question?
Give an example of how you might call the delegated method through the delegate.
What is multicasting?
What does the event
keyword do?
If you want to pass information into the method that is called through the event, how do you do so?
What properties or methods does System.EventArgs
have?
How can you create delegated methods anonymously?
Write a countdown alarm program that uses delegates to notify anyone who is interested that the designated amount of time has passed.
Break the program you write in Exercise 17-1 by assigning a new handler to the delegate (deleting the old!).
Fix the program you wrote in Exercise 17-1 by using the
event
keyword and test against
changes you added in Exercise 17-2.