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.
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);
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); }
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);
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); }
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.
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
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:
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
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.
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; } }
The Admin class passes the delegates to the Engine class in the constructor of the Engine class.
Engine engine = new Engine(tickDlg, tradeDlg);
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); }
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);
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.
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.
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.
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)); }
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 -= .
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
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"); } }
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 |