Delegates and Events
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 relationships
Delegates
Connecting 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 Handling
In 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.
[1] Design Patterns by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides; Addison-Wesley, 1995.
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 Events
The 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 Example
using 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}
-
The delegate keyword is placed in front of the code that is executed when the delegate is invoked.
-
An optional parameter list may be used to pass data to the code block. These parameters should match those declared by the delegate. In this example, the parameters correspond to those required by the predefined delegate MouseEventHandler.
-
When the C# compiler encounters the anonymous code block, it creates a new class and constructs a method inside it to contain the code block. This method is called when the delegate is invoked.
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 Events
When 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 Events
In 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; }
-
It must inherit from the EventArgs class.
-
Its name should end with EventArgs.
-
Define the arguments as readonly fields or properties.
-
Use a constructor to initialize the values.
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
If your delegate uses the
EventHandler signature, you can use EventHandler as your
delegate instead of creating your own. Because it is part of the .NET Framework
Class Library, there is no need to declare
it.
|
An Event Handling Example
Let'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.
No comments:
Post a Comment
Comment Here