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.
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." 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 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;
}
IOEventArgs illustrates the guidelines to follow
when defining an
EventArgs class:
-
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.