< Day Day Up > |
Clicking a submit button, moving the mouse across a Form, pushing an Enter key, a character being received on an I/O port each of these is an event that usually triggers a call to one or more special event handling routines within a program. In the .NET world, events are bona fide class members equal in status to properties and methods. Just about every class in the Framework Class Library has event members. A prime example is the Control class, which serves as a base class for all GUI components. Its events including Click, DoubleClick, KeyUp, and GotFocus are designed to recognize the most common actions that occur when a user interacts with a program. But an event is only one side of the coin. On the other side is a method that responds to, or handles, the event. Thus, if you look at the Control class methods, you'll find OnClick, OnDoubleClick, OnKeyUp, and the other methods that correspond to their events. Figure 3-3 illustrates the fundamental relationship between events and event handlers that is described in this section. You'll often see this relationship referred to in terms of publisher/subscriber, where the object setting off the event is the publisher and the method handling it is the subscriber. Figure 3-3. Event handling relationshipsDelegatesConnecting an event to the handling method(s) is a delegate object. This object maintains a list of methods that it calls when an event occurs. Its role is similar to that of the callback functions that Windows API programmers are used to, but it represents a considerable improvement in safeguarding code. In Microsoft Windows programming, a callback occurs when a function calls another function using a function pointer it receives. The calling function has no way of knowing whether the address actually refers to a valid function. As a result, program errors and crashes often occur due to bad memory references. The .NET delegate eliminates this problem. The C# compiler performs type checking to ensure that a delegate only calls methods that have a signature and return type matching that specified in the delegate declaration. As an example, consider this delegate declaration: public delegate void MyString (string msg); When the delegate is declared, the C# compiler creates a sealed class having the name of the delegate identifier (MyString). This class defines a constructor that accepts the name of a method static or instance as one of its parameters. It also contains methods that enable the delegate to maintain a list of target methods. This means that unlike the callback approach a single delegate can call multiple event handling methods. A method must be registered with a delegate for it to be called by that delegate. Only methods that return no value and accept a single string parameter can be registered with this delegate; otherwise, a compilation error occurs. Listing 3-11 shows how to declare the MyString delegate and register multiple methods with it. When the delegate is called, it loops through its internal invocation list and calls all the registered methods in the order they were registered. The process of calling multiple methods is referred to as multicasting. Listing 3-11. Multicasting Delegate // file: delegate.cs using System; using System.Threading; class DelegateSample { public delegate void MyString(string s); public static void PrintLower(string s){ Console.WriteLine(s.ToLower()); } public static void PrintUpper(string s){ Console.WriteLine(s.ToUpper()); } public static void Main() { MyString myDel; // register method to be called by delegate myDel = new MyString(PrintLower); // register second method myDel += new MyString(PrintUpper); // call delegate myDel("My Name is Violetta."); // Output: my name is violetta. // MY NAME IS VIOLETTA. } } Note that the += operator is used to add a method to the invocation list. Conversely, a method can be removed using the -= operator: myDel += new MyString(PrintUpper); // register for callback myDel -= new MyString(PrintUpper); // remove method from list In the preceding example, the delegate calls each method synchronously, which means that each succeeding method is called only after the preceding method has completed operation. There are two potential problems with this: a method could "hang up" and never return control, or a method could simply take a long time to process blocking the entire application. To remedy this, .NET allows delegates to make asynchronous calls to methods. When this occurs, the called method runs on a separate thread than the calling method. The calling method can then determine when the invoked method has completed its task by polling it, or having it call back a method when it is completed. Asynchronous calls are discussed in Chapter 13, "Asynchronous Programming and Multithreading." Delegate-Based Event HandlingIn abstract terms, the .NET event model is based on the Observer Design Pattern. This pattern is defined as "a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically."[1] We can modify this definition to describe the .NET event handling model depicted in Figure 3-3: "when an event occurs, all the delegate's registered methods are notified and executed automatically." An understanding of how events and delegates work together is the key to handling events properly in .NET.
To illustrate, let's look at two examples. We'll begin with built-in events that have a predefined delegate. Then, we'll examine how to create events and delegates for a custom class. Working with Built-In EventsThe example in Listing 3-12 displays a form and permits a user to draw a line on the form by pushing a mouse key down, dragging the mouse, and then raising the mouse key. To get the endpoints of the line, it is necessary to recognize the MouseDown and MouseUp events. When a MouseUp occurs, the line is drawn. The delegate, MouseEventHandler, and the event, MouseDown, are predefined in the Framework Class Library. The developer's task is reduced to implementing the event handler code and registering it with the delegate. The += operator is used to register methods associated with an event. this.MouseDown += new MouseEventHandler(OnMouseDown); The underlying construct of this statement is this.event += new delegate(event handler method); To implement an event handler you must provide the signature defined by the delegate. You can find this in documentation that describes the declaration of the MouseEventHandler delegate: public delegate void MouseEventHandler( object sender, MouseEventArgs e) Listing 3-12. Event Handler Exampleusing System; using System.Windows.Forms; using System.Drawing; class DrawingForm:Form { private int lastX; private int lastY; private Pen myPen= Pens.Black; // defines color of drawn line public DrawingForm() { this.Text = "Drawing Pad"; // Create delegates to call MouseUp and MouseDown this.MouseDown += new MouseEventHandler(OnMouseDown); this.MouseUp += new MouseEventHandler(OnMouseUp); } private void OnMouseDown(object sender, MouseEventArgs e) { lastX = e.X; lastY = e.Y; } private void OnMouseUp(object sender, MouseEventArgs e) { // The next two statements draw a line on the form Graphics g = this.CreateGraphics(); if (lastX >0 ){ g.DrawLine(myPen, lastX,lastY,e.X,e.Y); } lastX = e.X; lastY = e.Y; } static void Main() { Application.Run(new DrawingForm()); } } Using Anonymous Methods with Delegates.NET 2.0 introduced a language construct known as anonymous methods that eliminates the need for a separate event handler method; instead, the event handling code is encapsulated within the delegate. For example, we can replace the following statement from Listing 3-12: this.MouseDown += new MouseEventHandler(OnMouseDown); with this code that creates a delegate and includes the code to be executed when the delegate is invoked: this.MouseDown += delegate(object sender, EventArgs e) { lastX = e.X; lastY = e.Y; } The code block, which replaces OnMouseDown, requires no method name and is thus referred to as an anonymous method. Let's look at its formal syntax: delegate [(parameter-list)] {anonymous-method-block}
To further clarify the use of anonymous methods, let's use them to simplify the example shown earlier in Listing 3-11. In the original version, a custom delegate is declared, and two callback methods are implemented and registered with the delegate. In the new version, the two callback methods are replaced with anonymous code blocks: // delegate declaration public delegate void MyString(string s); // Register two anonymous methods with the delegate MyString myDel; myDel = delegate(string s) { Console.WriteLine(s.ToLower()); }; myDel += delegate(string s) { Console.WriteLine(s.ToUpper()); }; // invoke delegate myDel("My name is Violetta"); When the delegate is called, it executes the code provided in the two anonymous methods, which results in the input string being printed in all lower- and uppercase letters, respectively. Defining Custom EventsWhen writing your own classes, it is often necessary to define custom events that signal when some change of state has occurred. For example, you may have a component running that monitors an I/O port and notifies another program about the status of data being received. You could use raw delegates to manage the event notification; but allowing direct access to a delegate means that any method can fire the event by simply invoking the delegate. A better approach and that used by classes in the Framework Class Library is to use the event keyword to specify a delegate that will be called when the event occurs. The syntax for declaring an event is public event <delegate name> <event name> Let's look at a simple example that illustrates the interaction of an event and delegate: public class IOMonitor { // Declare delegate public delegate void IODelegate(String s); // Define event variable public event IODelegate DataReceived ; // Fire the event public void FireReceivedEvent (string msg) { if (DataReceived != null) // Always check for null { DataReceived(msg); // Invoke callbacks } } } This code declares the event DataReceived and uses it in the FireReceivedEvent method to fire the event. For demonstration purposes, FireReceivedEvent is assigned a public access modifier; in most cases, it would be private to ensure that the event could only be fired within the IOMonitor class. Note that it is good practice to always check the event delegate for null before publishing the event. Otherwise, an exception is thrown if the delegate's invocation list is empty (no client has subscribed to the event). Only a few lines of code are required to register a method with the delegate and then invoke the event: IOMonitor monitor = new IOMonitor(); // You must provide a method that handles the callback monitor.DataReceived += new IODelegate(callback method); monitor.FireReceivedEvent("Buffer Full"); // Fire event Defining a Delegate to Work with EventsIn the preceding example, the event delegate defines a method signature that takes a single string parameter. This helps simplify the example, but in practice, the signature should conform to that used by all built-in .NET delegates. The EventHandler delegate provides an example of the signature that should be used: public delegate void EventHandler(object sender, EventArgs eventArgs); The delegate signature should define a void return type, and have an object and EventArgs type parameter. The sender parameter identifies the publisher of the event; this enables a client to use a single method to handle and identify an event that may originate from multiple sources. The second parameter contains the data associated with the event. .NET provides the EventArgs class as a generic container to hold a list of arguments. This offers several advantages, the most important being that it decouples the event handler method from the event publisher. For example, new arguments can be added later to the EventArgs container without affecting existing subscribers. Creating an EventArgs type to be used as a parameter requires defining a new class that inherits from EventArgs. Here is an example that contains a single string property. The value of this property is set prior to firing the event in which it is included as a parameter. public class IOEventArgs: EventArgs { public IOEventArgs(string msg){ this.eventMsg = msg; } public string Msg{ get {return eventMsg;} } private string eventMsg; } IOEventArgs illustrates the guidelines to follow when defining an EventArgs class:
If an event does not generate data, there is no need to create a class to serve as the EventArgs parameter. Instead, simply pass EventArgs.Empty. Core Note
An Event Handling ExampleLet's bring these aforementioned ideas into play with an event-based stock trading example. For brevity, the code in Listing 3-13 includes only an event to indicate when shares of a stock are sold. A stock purchase event can be added using similar logic. Listing 3-13. Implementing a Custom Event-Based Application//File: stocktrader.cs using System; // (1) Declare delegate public delegate void TraderDelegate(object sender, EventArgs e); // (2) A class to define the arguments passed to the delegate public class TraderEventArgs: EventArgs { public TraderEventArgs(int shs, decimal prc, string msg, string sym){ this.tradeMsg = msg; this.tradeprice = prc; this.tradeshs = shs; this.tradesym = sym; } public string Desc{ get {return tradeMsg;} } public decimal SalesPrice{ get {return tradeprice;} } public int Shares{ get {return tradeshs;} } public string Symbol{ get {return tradesym;} } private string tradeMsg; private decimal tradeprice; private int tradeshs; private string tradesym; } // (3) class defining event handling methods public class EventHandlerClass { public void HandleStockSale(object sender,EventArgs e) { // do housekeeping for stock purchase TraderEventArgs ev = (TraderEventArgs) e; decimal totSale = (decimal)(ev.Shares * ev.SalesPrice); Console.WriteLine(ev.Desc); } public void LogTransaction(object sender,EventArgs e) { TraderEventArgs ev = (TraderEventArgs) e; Console.WriteLine(ev.Symbol+" "+ev.Shares.ToString() +" "+ev.SalesPrice.ToString("###.##")); } } // (4) Class to sell stock and publish stock sold event public class Seller { // Define event indicating a stock sale public event TraderDelegate StockSold; public void StartUp(string sym, int shs, decimal curr) { decimal salePrice= GetSalePrice(curr); TraderEventArgs t = new TraderEventArgs(shs,salePrice, sym+" Sold at "+salePrice.ToString("###.##"), sym); FireSellEvent(t); // Fire event } // method to return price at which stock is sold // this simulates a random price movement from current price private decimal GetSalePrice(decimal curr) { Random rNum = new Random(); // returns random number between 0 and 1 decimal rndSale = (decimal)rNum.NextDouble() * 4; decimal salePrice= curr - 2 + rndSale; return salePrice; } private void FireSellEvent(EventArgs e) { if (StockSold != null) // Publish defensively { StockSold(this, e); // Invoke callbacks by delegate } } } class MyApp { public static void Main() { EventHandlerClass eClass= new EventHandlerClass(); Seller sell = new Seller(); // Register two event handlers for stocksold event sell.StockSold += new TraderDelegate( eClass.HandleStockSale); sell.StockSold += new TraderDelegate( eClass.LogTransaction); // Invoke method to sell stock(symbol, curr price, sell price) sell.StartUp("HPQ",100, 26); } } The class Seller is at the heart of the application. It performs the stock transaction and signals it by publishing a StockSold event. The client requesting the transaction registers two event handlers, HandleStockSale and LogTransaction, to be notified when the event occurs. Note also how the TRaderEvents class exposes the transaction details to the event handlers. |
< Day Day Up > |