developmentor - a developer services company				  
				      C# Tutorial
   Modules

Delegates and Events


Goals:

  • Define a delegate type.
  • Use a delegate to implement a simple callback.
  • Register both instance and static callback methods.
  • Use composition to create a delegate with multiple targets.
  • Use the event keyword to impose well know access semantics on a delegate.
  • Manually invoke the targets of a delegate.
  • Code a delegate with a return value.

Overview

A delegate serves as a proxy for a method. Invoking the delegate causes it to invoke the method it represents. Similar concepts are implemented using function pointers in C/C++ and interfaces in Java. The classic application for this technique comes from graphical user interface programming. Controls such as buttons, trees, lists, etc. publish events to which interested parties subscribe. User actions such as clicking a button trigger the event and the control notifies all its subscribers that the event occurred.


part 1- Basics

In this lab we explore the basic definition and use of delegates and events. There will be three main pieces: the delegate definition, the publisher that invokes through the delegate, and the subscriber that receives the call.

steps:

  1. Define a delegate named MessageHandler for methods that take a string argument and return void. Recall that the delegate definition syntax is similar to a method signature, the generic pattern is shown below.

    delegate returnType delegateName(arguments);
  2. Define a class Publisher which will publish string messages by invoking through a delegate. Add a public field of type MessageHandler to store the delegate. Add a public method called Dispatch that invokes the delegate. Recall that a delegate field is a reference with a default value of null so it is typical to place the call inside a conditional to ensure the delegate is valid. The delegate specifies a string argument so you can pass a message of your choice.

  3. Define a class Subscriber which will subscribe to the messages published by the Publisher. Add an instance method to the subscriber that matches the signature of the delegate; that is, it takes a string argument and returns void. Name the method CallMe and have it simply print out a message containing the string it receives from the publisher.

  4. Define a class called Driver and add a Main method. Create a publisher and a subscriber. Create a delegate for the SubscriberCallMe method and install it on the publisher. Call the publisher's Dispatch method and verify that the delegate invokes the subscriber method.

  5. Add an instance method named MeToo to the subscriber class. Make sure the method signature conforms to what is required by the MessageHandler delegate. Modify the driver code to register both the CallMe and the MeToo methods. Be sure to use the += operator to do the registration. Run the program and observe the output: notice that both callbacks are invoked and they are called in the order they were registered.

  6. Add a static method named AndMe to the subscriber class. Make sure the method signature conforms to what is required by the MessageHandler delegate. Modify the driver code to register all three subscriber methods: CallMe, MeToo, and AndMe. Use the class name when registering the static method. Run the program and observe that all three methods are called.

  7. Currently, the delegate field in the Publisher class is public so it can be accessed by the driver code. Because the delegate is public, the driver code can assign to the delegate or invoke it.

    Use the assignment operator to assign directly to the delegate from the driver code and verify that the program compiles. Direct assignment is considered bad style since it clobbers over any previously registered callbacks. The preferred alternative is the += operator which simply adds a new callback without disturbing the existing ones. If the delegate is public we have no contol over how the delegate is used by the client.

    Invoke the delegate from the driver code. Again, this is allowed because the delegate is public. This is also considered poor style since the callbacks should only be made when conditions in the publisher warrant it.

    Add the keyword event to the delegate declaration in the publisher. Making it an event modifies the accessibility of the delegate. It now exposes the += and -= operators to the user code but denies direct access to the underlying delegate. Code inside the declaring class still has full access to the delegate. Verify that the event cannot be directly assigned to nor invoked from the driver code.

Solutions:


part 2- Invocation List (optional)

A delegate can have multiple targets. Invoking the delegate invokes all the registered targets. This automatic invocation is typically convenient and desirable; however, there are a few cases where more control is required.

Suppose a target were to throw an exception. The default behavior is to stop processing the targets and propagate the exception to the publisher. The publisher might prefer to invoke each target individually, catch any exception that is thrown, and continue the processing of the remaining targets.

Another case to consider is a delegate with a return type other than void. The default behavior is to return the result from the last target in the sequence and discard the others. The publisher might prefer to obtain the result from each target.

To allow a publisher more control over target invocation, delegates offer a GetInvocationList method that returns the individual targets. The targets are returned as an array of delegates - each element in the array represents one target. The publisher can iterate through the array and invoke each target individually.

In this lab, we recode the Publisher class Dispatch method from the last exercise so it manually invokes each target.

steps:

  1. In the PublisherDispatch method, remove the invocation of the delegate. In its place, retrieve the invocation list from the delegate, step through the array, and invoke each target individually.

    Notes:

    • The type of each array element is the system type Delegate; however, the array is actually filled with MessageHandler objects. As you step through the array, downcast each element of the array to MessageHandler.
    • Use standard method call syntax to invoke the individual targets after the downcast.
    • A foreach loop is probably the simplest way to accomplish the goal here since it will both step through the array and downcast each element.

Solutions:


part 3- Delegate return value (optional)

Delegates often return void. This is not surprising since their most common use is as callbacks where the publisher is notifying subscribers of an event. Typically, the subscribers do not need to return any data to the publisher.

The use of delegates does not need to be limited to callbacks. Delegates provide a clean way of decoupling the actors in many other design patterns. For example, a delegate might be used as a data source for a consumer where the consumer invokes the delegate to obtain needed data. This design insulates the consumer from the data source: the data location, the name of the class supplying the data, and even the name of the method used to retrieve the data are all hidden from the consumer by the delegate. Some delegates might draw from a database, others from a network connection, and still others from a disk file. The data source can easily be changed by supplying a different delegate without the need to modify the consumer code.

In this lab we will code a histogram class that uses a delegate for the data source of each entry. To keep things simple we will make a number of simplifications in the drawing. It should be straightforward to remove these limitations in a more realistic implementation.

Notes:

  • Each entry will have only a magnitude, text labels will not be supported.
  • Magnitude will be an integer.
  • Drawing will be done using text: the magnitude will be represented by the corresponding number of "*" characters.
  • The histogram will be drawn "sideways" with each entry on one line.

steps:

  1. Define a delegate named HistogramSource that takes no arguments and returns an int. The int return value will be the magnitude of one histogram entry.

  2. Create a class named Histogram and add a public event of type HistogramSource. Add a public method called Draw that invokes the delegates in the event. You will need to invoke the delegates manually in order to capture the return value from each call. Each delegate in the event represents one entry in the histogram and the return value is the magnitude of that entry. Output a line of "*" characters representing the magnitude.

  3. Create one or more classes with methods to serve as the data sources. Feel free to choose any algorithm you would like to generate the magnitude of the entries in the histogram. To save time, you can use the class shown below which generates random values.

    class RandomSource
    {
    	private int    max;
    	private Random r;
    
    	public RandomSource(int max)
    	{
    		this.max = max;
    
    		r = new Random();
    	}
    
    	// callback method - generates a random number between 0 and max-1
    	public int Magnitude()
    	{
    		return r.Next(max);
    	}
    }
    
  4. Create a driver program. Create a histogram object. Register a few data sources. Draw the histogram.

Solutions:

This material is excerpted from the Programming C# course offered by DevelopMentor.