Graphical user interfaces (GUIs), Windows and web browsers, such as Microsoft, 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 it springs into action to handle the event.
In a GUI environment, any number of
widgets 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.
This design implements the Publish/Subscribe (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.
Events in C# are implemented with delegates. The publishing class defines a delegate that the subscribing classes must implement. 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
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.
Events are properties of the class publishing the event. The keyword
event
controls how the event property is accessed
by the subscribing classes. The event
keyword is
designed to maintain the publish/subscribe idiom.
Suppose you want to create a Clock
class that uses
events to notify potential subscribers whenever the local time
changes value by one second. Call this event
OnSecondChange
. You declare the event and its
event handler delegate type as follows:
[attributes] [modifiers] event type member-name
For example:
public event SecondChangeHandler OnSecondChange;
This example has no attributes (attributes are covered in Chapter 18). The modifier can be
abstract
, new
,
override
, static
,
virtual
, or one of the four
access
modifiers, in this case public
.
The modifier is followed by the keyword event
.
The type is the delegate to which you want to associate the event, in
this case SecondChangeHandler
.
The member name is the name of the event, in this case
OnSecondChange
. It is customary to begin events
with the word On
.
Altogether, this declaration states that
OnSecondChange
is an event which is implemented by
a delegate of type SecondChangeHandler
.
The declaration for the SecondChangeHandler
delegate is:
public delegate void SecondChangeHandler( object clock, TimeInfoEventArgs timeInformation );
This declares the delegate. As stated earlier, by convention an event
handler must return void
and must take two
parameters: the source of the event (in this case
clock
) and an object derived from
EventArgs
, in this case
TimeInfoEventArgs
.
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 a delegate and an event, 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 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, which will be covered in
some detail in Chapter 20. 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
OnSecondChange
event:
// if anyone has subscribed, notify them if (OnSecondChange != null) { OnSecondChange(this,timeInformation); } }
If an event has no subscribers registered, it will evaluate to
null
. The test above checks that the value is not
null
, ensuring that there are subscribers before
calling OnSecondChange
.
You will remember that OnSecondChange
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 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;
Note that no attempt has been made to make this code thread safe. Thread safety and synchronization are discussed in Chapter 20.
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
event. 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.
Notice that events are added using the
+=
operator. This allows new events to be
added to the Clock
object’s
OnSecondChange
event without destroying the events
already registered. When LogCurrentTime
subscribes
to the OnSecondChange
event, you do not want the
event to lose track of the fact that DisplayClock
has already subscribed.
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 12-4.
Example 12-4. Working with events
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 event we publish
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( );
}
}
}
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
).
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. Similarly, a button can publish an
Onclick
event, and any number of unrelated objects
can subscribe to that event, receiving notification when the button
is clicked.
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.
3.146.37.250