19.3 EventsGraphical User Interfaces (GUIs) require that programs respond to events. An event might be a button push, a menu selection, the completion of a file transfer, or a similar occurrence. 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. Whenever something happens in relation to the program (whenever an event is raised), the code must provide a way to 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. 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. 19.3.1 Publishing and SubscribingIn 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. 19.3.2 Events and DelegatesIn C#, you use delegates to implement events. 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. As explained previously, 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. The EventArgs class contains information about the event that can be of use to the event handler method. It is recommended that your event handlers follow this design pattern. Events are properties of the class publishing the event. The event keyword controls how the event property is accessed by the subscribing classes. The 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. Example 19-4 shows the complete source for this example, followed by the output and a detailed analysis. Example 19-4. Events to update the time for the Clock classusing System; using System.Threading; namespace DelegatesAndEvents { // 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 class; 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; } // Declares an observer class, DisplayClock, which subscribes to the // Clock object's events. The job of DisplayClock is // to display the current time. public class DisplayClock { // Given a Clock object, 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, but // we write to the console to see the effect. // Note that 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()); } } class Tester { public void Run() { // Create a new Clock object. Clock theClock = new Clock(); // Create the DisplayClock object 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 object. LogCurrentTime lct = new LogCurrentTime(); lct.Subscribe(theClock); // Get the clock started. theClock.Run(); } [STAThread] static void Main() { Tester t = new Tester(); t.Run(); } } } Output: Current Time: 9:16:36 Logging to file: 9:16:36 Current Time: 9:16:37 Logging to file: 9:16:37 Current Time: 9:16:38 Logging to file: 9:16:38 Current Time: 9:16:39 Logging to file: 9:16:39 In Example 19-4, you create an event, OnSecondChange: public event SecondChangeHandler OnSecondChange; The general declaration of an event type is as follows: [attributes] [modifiers] event type member-name The optional attributes are an advanced topic that is not covered in this book. See Programming C#, Second Edition (O'Reilly) for more information. The optional modifier can be abstract, new, override, static, virtual, or one of the four standard access modifiers (public, private, etc.); access modifiers are discussed in Chapter 8. The modifier is followed by the event keyword. 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 that is implemented by a delegate of type SecondChangeHandler. The declaration for the SecondChangeHandler delegate is: public delegate void SecondChangeHandler( object clock, TimeInfoEventArgs timeInformation ); As stated earlier, by convention an event handler returns void and takes two parameters: the source of the event (clock) and an object derived from EventArgs (TimeInfoEventArgs). TimeInfoEventArgs is a class designed for this book, defined as follows: 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; } } The TimeInfoEventArgs object has information about the current hour, minute, and second. It defines a constructor and three public read-only 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 ten 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 ten milliseconds: Thread.Sleep(10); This makes use of Sleep(), which is a static method of the Thread class from the System.Threading namespace. The call to Sleep() causes a 10 second pause in execution, preventing the loop from running so tightly that little else on the computer gets done. After sleeping for 10 milliseconds, the Run() method checks the current time: System.DateTime dt = System.DateTime.Now; About every 100 times that the Run() method checks the time, the second will have incremented. The Run() 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 evaluates to null. The preceding test 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 Example 19-4, the Clock object'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 preceding line. Raising the event invokes whatever methods have been registered with the Clock class through the delegate. More about this in a moment. Once the event is raised, 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. Example 19-4 simplifies this class down to two methods. The first is a helper method named Subscribe(), which subscribes to the Clock object'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 the SecondChangeHandler delegate with the OnSecondChange event of Clock. Create a second class, LogCurrentTime, that will also respond to this event. 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, but // 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 the DisplayClock object to subscribe to the event. Then create a LogCurrentTime class and tell it to subscribe as well. Finally, tell the Clock to run. The net effect of this code is to create two classes, DisplayClock and LogCurrentTime, both of which subscribe to a third class's event (Clock.OnSecondChange). |