Delegates

for RuBoard

Interfaces facilitate writing code so that your program can be called into by some other code. This style of programming has been available for a long time, under the guise of "callback" functions. In this section we examine del - egates in C#, which can be thought of as type-safe and object-oriented callback functions. Delegates are the foundation for a design pattern, known as events , which we'll look at in the next section.

A callback function is one which your program specifies and "registers" in some way, and which then gets called by another program. In C and C++ callback functions are implemented by function pointers.

In C# you can encapsulate a reference to a method inside a delegate object. A delegate can refer to either a static method or an instance method. When a delegate refers to an instance method, it stores both an object instance and an entry point to the instance method. The instance method can then be called through this object instance. When a delegate object refers to a static method, it stores just the entry point of this static method.

You can pass this delegate object to other code, which can then call your method. The code that calls your delegate method does not have to know at compile time which method is being called.

In C# a delegate is considered a reference type that is similar to a class type. A new delegate instance is created just like any other class instance, using the new operator. In fact, C# delegates are implemented by the .NET Framework class library as a class, derived ultimately from System.Delegate .

Delegates are object oriented and type safe, and they enjoy the safety of the managed code execution environment.

Declaring a Delegate

You declare a delegate in C# using a special notation with the keyword delegate and the signature of the encapsulated method. A naming convention suggests that your name should end with "Callback."

We illustrate delegates in the sample program DelegateAccount. Here is an example of a delegate declaration from the file DelegateAccount.cs .

 public delegate void NotifyCallback(decimal balance); 

Defining a Method

When you instantiate a delegate, you will need to specify a method, which must match the signature in the delegate declaration. The method may be either a static method or an instance method. Here are some examples of methods that can be hooked to the NotifyCallback delegate:

 private static void NotifyCustomer(decimal balance)  {     Console.WriteLine("Dear customer,");     Console.WriteLine(        "   Account overdrawn, balance = {0}", balance);  }  private static void NotifyBank(decimal balance)  {     Console.WriteLine("Dear bank,");     Console.WriteLine(        "   Account overdrawn, balance = {0}", balance);  }  private void NotifyInstance(decimal balance)  {     Console.WriteLine("Dear instance,");     Console.WriteLine(        "   Account overdrawn, balance = {0}", balance);  } 

Creating a Delegate Object

You instantiate a delegate object with the new operator, just as you would with any other class. The following code illustrates creating two delegate objects. The first one is hooked to a static method, and the second to an instance method. The second delegate object internally will store both a method entry point and an object instance that is used for invoking the method.

 NotifyCallback custDlg =     new NotifyCallback(NotifyCustomer);  ...  DelegateAccount da = new DelegateAccount();  NotifyCallback instDlg =     new NotifyCallback(da.NotifyInstance); 

Calling a Delegate

You "call" a delegate just as you would a method. The delegate object is not a method, but it has an encapsulated method. The delegate object "delegates" the call to this encapsulated method, hence the name "delegate." In the following code the delegate object notifyDlg is called whenever a negative balance occurs on a withdrawal. In this example the notifyDlg delegate object is initialized in the method SetDelegate .

  private NotifyCallback notifyDlg;  ...  public void SetDelegate(  NotifyCallback dlg  )  {  notifyDlg = dlg;  }  ...  public void Withdraw(decimal amount)  {     balance -= amount;     if (balance < 0)  notifyDlg(balance);  } 

Combining Delegate Objects

A powerful feature of delegates is that you can combine them. Delegates are "multicast," in which they have an invocation list of methods. When such a delegate is called, all the methods on the invocation list will be called in the order they appear in the invocation list. The + operator can be used to combine the invocation methods of two delegate objects. The - operator can be used to remove methods.

 NotifyCallback custDlg =     new NotifyCallback(NotifyCustomer);  NotifyCallback bankDlg = new NotifyCallback(NotifyBank);  NotifyCallback currDlg = custDlg + bankDlg;  

In this example we construct two delegate objects, each with an associated method. We then create a new delegate object whose invocation list will consist of both the methods NotifyCustomer and NotifyBank . When currDlg is called, these two methods will be invoked. Later on in the code we may remove a method.

 currDlg -= bankDlg; 

Now NotifyBank has been removed from the delegate, and the next time currDlg is called, only NotifyCustomer will be invoked.

Complete Example

The program DelegateAccount illustrates using delegates in our bank account scenario. The file DelegateAccount.cs declares the delegate NotifyCallback . The class DelegateAccount contains methods matching the signature of the delegate. The Main method instantiates delegate objects and combines them in various ways. The delegate objects are passed to the Account class, which uses its encapsulated delegate object to invoke suitable notifications when the account is overdrawn.

Observe how this structure is dynamic and loosely coupled . The Account class does not know or care which notification methods will be invoked in the case of an overdraft. It simply calls the delegate, which in turn calls all the methods on its invocation list. These methods can be adjusted at runtime.

Here is the code for the Account class:

 // Account.cs  public class Account  {     private decimal balance;     private NotifyCallback notifyDlg;     public Account(decimal bal, NotifyCallback dlg)     {        balance = bal;        notifyDlg = dlg;     }     public void SetDelegate(NotifyCallback dlg)     {        notifyDlg = dlg;     }     public void Deposit(decimal amount)     {        balance += amount;     }     public void Withdraw(decimal amount)     {     balance -= amount;     if (balance < 0)        notifyDlg(balance);     }     public decimal Balance     {        get        {           return balance;        }     }  } 

Here is the code declaring and testing the delegate:

 // DelegateAccount.cs  using System;  public delegate void NotifyCallback(decimal balance);  public class DelegateAccount  {     public static void Main(string[] args)     {        NotifyCallback custDlg =           new NotifyCallback(NotifyCustomer);        NotifyCallback bankDlg =           new NotifyCallback(NotifyBank);        NotifyCallback currDlg = custDlg + bankDlg;        Account acc = new Account(100, currDlg);        Console.WriteLine("balance = {0}", acc.Balance);        acc.Withdraw(125);        Console.WriteLine("balance = {0}", acc.Balance);        acc.Deposit(200);        acc.Withdraw(125);        Console.WriteLine("balance = {0}", acc.Balance);        currDlg -= bankDlg;        acc.SetDelegate(currDlg);        acc.Withdraw(125);        DelegateAccount da = new DelegateAccount();        NotifyCallback instDlg =           new NotifyCallback(da.NotifyInstance);        currDlg += instDlg;        acc.SetDelegate(currDlg);        acc.Withdraw(125);     }     private static void NotifyCustomer(decimal balance)     {        Console.WriteLine("Dear customer,");        Console.WriteLine(           "   Account overdrawn, balance = {0}", balance);     }     private static void NotifyBank(decimal balance)     {        Console.WriteLine("Dear bank,");        Console.WriteLine(           "   Account overdrawn, balance = {0}", balance);     }     private void NotifyInstance(decimal balance)     {        Console.WriteLine("Dear instance,");        Console.WriteLine(           "   Account overdrawn, balance = {0}", balance);     }  } 

Here is the output from running the program. Notice which notification methods get invoked, depending upon the operations that have been performed on the current delegate object.

 balance = 100  Dear customer,     Account overdrawn, balance = -25  Dear bank,     Account overdrawn, balance = -25  balance = -25  balance = 50  Dear customer,     Account overdrawn, balance = -75  Dear customer,     Account overdrawn, balance = -200  Dear instance,     Account overdrawn, balance = -200 

Stock Market Simulation

As a further illustration of the use of delegates, consider the simple stock-market simulation, implemented in the directory StockMarket . The simulation consists of two modules:

  • The Admin module provides a user interface for configuring and running the simulation. It also implements operations called by the simulation engine.

  • The Engine module is the simulation engine. It maintains an internal clock and invokes randomly generated operations, based on the configuration parameters passed to it.

    Figure 5-3 shows the high-level architecture of the simulation. The following operations are available:

    Figure 5-3. Architecture of stock-market simulation.

    graphics/05fig04.gif

  • PrintTick: shows each clock tick.

  • PrintTrade: shows each trade.

The following configuration parameters can be specified:

  • Ticks on/off

  • Trades on/off

  • Count of how many ticks to run the simulation

Running the Simulation

Build and run the example program in StockMarket . Start with the default configuration: Ticks are OFF, Trades are ON, Run count is 100. (Note that the results are random and will be different each time you run the program.)

 Ticks are OFF  Trades are ON  Run count = 100  Enter command, quit to exit  : run     2  ACME    23   600    27  MSFT    63   400    27  IBM    114   600    38  MSFT    69   400    53  MSFT    75   900    62  INTC    27   800    64  MSFT    82   200    68  MSFT    90   300    81  MSFT    81   600    83  INTC    30   800    91  MSFT    73   700    99  IBM    119   400  : 

The available commands are listed when you type "help" at the colon prompt. The commands are:

 count    set run count  ticks    toggle ticks  trades   toggle trades  config   show configuration  run      run the simulation  quit     exit the program 

The output shows clock tick, stock, price, volume.

Delegate Code

Two delegates are declared in the Admin.cs file.

 public delegate void TickCallback(int ticks);  public delegate void TradeCallback(int ticks, string stock,                                     int price, int volume); 

As we saw in the previous section, a delegate is similar to a class, and a delegate object is instantiated by new .

 TickCallback tickDlg = new TickCallback(PrintTick);  TradeCallback tradeDlg = new TradeCallback(PrintTrade); 

A method is passed as the parameter to the delegate constructor. The method signature must match that of the delegate.

 public static void PrintTick(int ticks)  {     Console.Write("{0} ", ticks);     if (++printcount == LINECOUNT)     {        Console.WriteLine();        printcount = 0;     }  } 
Passing the Delegates to the Engine

The Admin class passes the delegates to the Engine class in the constructor of the Engine class.

 Engine engine = new Engine(tickDlg, tradeDlg); 
Random-Number Generation

The heart of the simulation is the Run method of the Engine class. At the core of the Run method is assigning simulated data based on random numbers . We use the System.Random class, which we discussed in Chapter 3.

 double r =  rangen.NextDouble()  ;  if (r < tradeProb[i])  {     int delta = (int) (price[i] * volatility[i]);     if (  rangen.NextDouble()  < .5)     {        delta = -delta;     }     price[i] += delta;     int volume =  rangen.Next(minVolume, maxVolume)  * 100;     tradeOp(tick, stocks[i], price[i], volume);  } 
Using the Delegates

In the Engine class, delegate references are declared:

  TickCallback tickOp;  TradeCallback tradeOp; 

The delegate references are initialized in the Engine constructor:

 public Engine(  TickCallback tickOp  , TradeCallback tradeOp)  {  this.tickOp = tickOp;  this.tradeOp = tradeOp;  } 

The method that is wrapped by the delegate object can then be called through the delegate reference:

 if (showTicks)  tickOp(tick);  
Events

Delegates are the foundation for a design pattern known as events . Conceptually, servers implement incoming interfaces, which are called by clients . In a diagram, such an interface may be shown with a small bubble (a notation used in COM). Sometimes a client may wish to receive notifications from a server when certain "events" occur. In such a case the server will specify an outgoing interface. The server defines the interface and the client implements it. In a diagram, such an interface may be shown with an arrow (again, a notation used in COM). Figure 5-4 illustrates a server with one incoming and one outgoing interface. In the case of the outgoing interface, the client will implement an incoming interface, which the server will call.

Figure 5-4. A server with an incoming interface and an outgoing interface.

graphics/05fig05.gif

A good example of a programming situation with events is a graphical user interface. An event is some external action, typically triggered by the user, to which the program must respond. Events include user actions such as clicking a mouse button or pressing a key on the keyboard. A GUI program must contain event handlers to respond to or "handle" these events. We will see many examples of GUI event handling in Chapter 6, where we discuss Windows Forms.

Events in C# and .NET

The .NET Framework provides an easy-to-use implementation of the event paradigm built on delegates. C# simplifies working with .NET events by providing the keyword event and operators to hook up event handlers to events and to remove them. The Framework also defines a base class EventArgs to be used for passing arguments to event handlers. There are a number of derived classes defined by the Framework for specific types of events, such as MouseEventArgs , ListChangedEventArgs , and so forth. These derived classes define data members to hold appropriate argument information.

An event handler is a delegate with a specific signature,

 public delegate void EventHandler(     object sender,     EventArgs e); 

The first argument represents the source of the event, and the second argument contains data associated with the event.

We will examine this event architecture through salient code from the example program EventDemo , which illustrates a chat room.

Server-Side Event Code

We begin with server-side code, in ChatServer.cs . The .NET event architecture uses delegates of a specific signature:

 public delegate void JoinHandler(object sender,                                   ChatEventArg e); 

The first parameter specifies the object that sent the event notification. The second parameter is used to pass data along with the notification. Typically, you will derive a class from EventArg to hold your specific data.

 public class ChatEventArg : EventArgs  {     public string Name;     public ChatEventArg(string name)     {        Name = name;     }  } 

A delegate object reference is declared using the keyword event .

 public event JoinHandler Join; 

A helper method is typically provided to facilitate calling the delegate object(s) that have been hooked up to the event.

 protected void OnJoin(ChatEventArg e)  {     if (Join != null)     {        Join(this, e);     }  } 

A test for null is made in case no delegate objects have been hooked up to the event. Typically, access is specified as protected , so that a derived class has access to this helper method. You can then "fire" the event by calling the helper method.

 public void JoinChat(string name)  {     members.Add(name);     OnJoin(new ChatEventArg(name));  } 

Client-Side Event Code

The client provides event handler functions.

 public static void OnJoinChat(object sender,     ChatEventArg e)  {     Console.WriteLine(        "sender = {0}, {1} has joined the chat",        sender, e.Name);  } 

The client hooks the handler to the event, using the += operator.

 ChatServer chat = new ChatServer("OI Chat Room");  // Register to receive event notifications from the server  chat.Join += new JoinHandler(OnJoinChat); 

The event starts out as null , and event handlers get added through += . All of the registered handlers will get invoked when the event delegate is called. You may unregister a handler through -= .

Chat Room Example

The chat room example in EventDemo illustrates the complete architecture on both the server and client sides. The server provides the following methods:

  • JoinChat

  • QuitChat

  • ShowMembers

Whenever a new member joins or quits, the server sends a notification to the client. The event handlers print out an appropriate message. Here is the output from running the program:

 sender = OI Chat Room, Michael has joined the chat  sender = OI Chat Room, Bob has joined the chat  sender = OI Chat Room, Sam has joined the chat  --- After 3 have joined--- Michael  Bob  Sam  sender = OI Chat Room, Bob has quit the chat  --- After 1 has quit--- Michael  Sam 
Client Code

The client program provides event handlers. It instantiates a server object and then hooks up its event handlers to the events. The client then calls methods on the server. These calls will trigger the server, firing events back to the client, which get handled by the event handlers.

 // ChatClient.cs  using System;  class ChatClient  {     public static void OnJoinChat(object sender,                                   ChatEventArg e)     {        Console.WriteLine(           "sender = {0}, {1} has joined the chat",           sender, e.Name);     }     public static void OnQuitChat(object sender,                                   ChatEventArg e)     {        Console.WriteLine(           "sender = {0}, {1} has quit the chat",           sender, e.Name);     }     public static void Main()     {        ChatServer chat = new ChatServer("OI Chat Room");        // Register to receive event notifications from the        // server        chat.Join += new JoinHandler(OnJoinChat);        chat.Quit += new QuitHandler(OnQuitChat);        // Call methods on the server        chat.JoinChat("Michael");        chat.JoinChat("Bob");        chat.JoinChat("Sam");        chat.ShowMembers("After 3 have joined");        chat.QuitChat("Bob");        chat.ShowMembers("After 1 has quit");     }  } 
Server Code

The server provides code to store in a collection the names of people who have joined the chat. When a person quits the chat, the name is removed from the collection. Joining and quitting the chat triggers firing an event back to the client. The server also contains the "plumbing" code for setting up the events, including declaration of the delegates, the events, and the event arguments. There are also helper methods for firing the events.

 // ChatServer.cs  using System;  using System.Collections;  public class ChatEventArg : EventArgs  {     public string Name;     public ChatEventArg(string name)     {        Name = name;     }  }  public delegate void JoinHandler(object sender,                                   ChatEventArg e);  public delegate void QuitHandler(object sender,                                   ChatEventArg e);  public class ChatServer  {     private ArrayList members = new ArrayList();     private string chatName;     public event JoinHandler Join;     public event QuitHandler Quit;     public ChatServer(string chatName)     {        this.chatName = chatName;     }     override public string ToString()     {        return chatName;     }     protected void OnJoin(ChatEventArg e)     {        if (Join != null)        {           Join(this, e);        }     }     protected void OnQuit(ChatEventArg e)     {        if (Quit != null)        {           Quit(this, e);        }     }     public void JoinChat(string name)     {        members.Add(name);        OnJoin(new ChatEventArg(name));     }     public void QuitChat(string name)     {        members.Remove(name);        OnQuit(new ChatEventArg(name));     }     public void ShowMembers(string msg)     {        Console.WriteLine("--- " + msg + "---");        foreach (string member in members)        {           Console.WriteLine(member);        }     }  } 

It may appear that there is a fair amount of such "plumbing" code, but it is much simpler than the previous connection-point mechanism used by COM for events. Also, in certain areas various wizards and other tools (such as the Forms designers) will generate the infrastructure for you automatically. We will see how easy it is to work with events in Windows programming in Chapter 6.

for RuBoard


Application Development Using C# and .NET
Application Development Using C# and .NET
ISBN: 013093383X
EAN: 2147483647
Year: 2001
Pages: 158

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net