Delegates


To understand how events work in C#, we must first describe how to use delegates. The event model uses delegates to identify event handler methods in our application. Delegates are also useful in their own right, above and beyond their role in the event mechanism.

A delegate is like a pointer to a method in an object in our application. As discussed earlier, the problem with function pointers as they are used in C and C++, is that they are not type-safe, because the compiler cannot know the signature of the function called. So, if the programmer makes a mistake, they could pass an argument of the wrong type to the function. The designers of Java resolved this issue by banning function pointers from their language specification.

For any programming problem that would normally be solved using a callback function (passing a pointer to the function), Java programmers resort to defining a specialized callback interface. The caller defines a type (either interface or class) that can be called. When writing code that must be called, you must implement this interface and your method must have the same name as defined by the caller. Like using delegates, this technique allows writing a calling object without knowing the object that will eventually be called. The called object, however, must contain considerable extra code implementing interfaces and must follow the design of the calling object. Delegates allow the developer the same flexibility as the unsafe function pointers, but with type safety.

The designers of the .NET Framework decided to overcome the problem of function pointers by wrapping them in a special kind of class, one per function signature. These classes are called delegate classes or delegates. We define a delegate class to specify the signature of methods we would like to call through the delegate. We can then create a delegate object and bind it to any method whose signature matches that of the delegate.

Delegates are useful in the following scenarios:

  • To register one of our object's methods as a callback method with another object
    When something important happens to that object, it can invoke our callback method. The object that invokes the delegate doesn't need to know anything about our object or its methods; the delegate contains all the information about which method to invoke on which object. Therefore, the object that invokes the delegate will continue to work correctly in the future even if we define new types of object with new method names.

  • To choose one of a series of methods (with the same signature) for use in an algorithm
    For example, in Chapter 1 we saw how to define a delegate to represent mathematical functions such as Math.Sin, Math.Cos, and Math.Tan.

  • To separate the selection of a method from its invocation
    We can create a delegate to indicate which method we want to call, but delay its execution until we are ready. For example, we can create a delegate to represent a method to call every ten seconds in a real-time application. We can then create a separate thread, and use the delegate to invoke the required method every ten seconds.

Creating and Using Simple Delegates

To use delegates in our application, and usingwe perform the following three steps:

  • Declare the delegate type

  • Create a delegate object, and bind it to a particular method

  • Invoke the method by using the delegate object

We'll show an example of how to declare a delegate to draw different types of shape on a Windows form. As a fully visual example, we won't show all the form code here in the book. The source code for this example is located in the download code folder DelegatesEvents\UsingDelegates.cs.

The form will have the following appearance:

click to expand

When the user clicks the Draw rectangle button, the application displays a rectangle with the specified position, size, and color. Likewise, when the user clicks the Draw ellipse button, the application displays an ellipse with the specified characteristics.

To achieve this functionality, we could write two completely separate button-click event handler methods to handle the Draw rectangle and Draw ellipse click events. However, this would result in a great deal of duplicate code, as both methods would need to perform the following tasks:

  • Make sure all the textboxes have been filled in, to specify the position and size of the shape

  • Make sure the shape fits in the drawing area on the screen

  • Create a brush with the specified color

  • Draw the required shape

To avoid duplication of code, we'll use a delegate to represent the drawing operation required. The delegate will either point to the FillRectangle() method or the FillEllipse() method, both of which are defined in the Graphics class in the .NET Framework class library. Then we'll write a generic method that takes this delegate as a parameter, and uses the delegate to invoke the appropriate drawing operation.

Let's look at a part of the supporting code for the application:

    // omitted the delegate declaration here    public class UsingDelegates : System.Windows.Forms.Form    {      //Declare a Rectangle field, to indicate available drawing area      private Rectangle mRect;      #region Windows Form Designer generated code      // omitted for brevity. This region contains some event      // related code as well. We will look at that later.      #endregion      //When the form receives a paint event,      //color the available drawing area white      private void UsingDelegates_Paint(object sender,            System.Windows.Forms.PaintEventArgs e)      {       mRect = new Rectangle(0, 0, Width, Height / 2);       e.Graphics.FillRectangle(new SolidBrush(Color.White), mRect);      }      private void btnColor_Click(object sender, System.EventArgs e)      {        ColorDialog dlgColor = new ColorDialog();        dlgColor.ShowDialog(this);        btnColor.BackColor = dlgColor.Color;      }      // Delegate-related code (see later)...    } 

Note the following points in this code:

  • We declare a private field named mRect to represent the drawing area on the form.

  • We initialize the mRect field whenever the form paints itself, and paint the rectangle white.

  • We've provided a click-event handler method for the Color button on our form. The method displays the standard color dialog box, so the user can choose a color for the next shape. To indicate which color has been selected, we repaint the Color button using the selected color. Note that it is not the name of the method that specifies that it handles the Click event from btnColor. The wiring of methods and events is done in the omitted Forms Designer code. We will look into this code in the second half of this chapter.

Now let's start looking at the delegate-specific code. Just before the start of the class definition, we placed a delegate definition. It looks like this:

    //Define a Delegate type, to indicate signature of    //method to call    delegate void DrawShape(Brush aBrush , Rectangle aRect); 

The delegate definition is like a class definition: it specifies a new type. It is a not part of the UsingDelegates class, but forms an independent class of its own. This delegate represents methods that return void and take two parameters:

  • A Brush object, to specify the color of the shape

  • A Rectangle object, to specify the position and size of the shape

This delegate corresponds with the signatures of the FillRectangle() and FillEllipse() methods in the Graphics class. The next step is to create an instance of the type, and bind it to a specific method on a particular object. Here is an example showing how to create a DrawShape instance and bind it to the FillRectangle() method on a Graphics object:

    //Handle the Click event for the btnDrawRect button    private void btnDrawRect_Click(object sender, System.EventArgs e)    {       // Create a Graphics object (we need its FillRectangle method)       Graphics aGraphics = CreateGraphics();       // Declare a DrawShape variable       DrawShape DrawRectangleMethod;       // Create a delegate object, and bind to the FillRectangle method       DrawRectangleMethod = new DrawShape(aGraphics.FillRectangle);       // Call MyDrawShape, and pass the delegate as a parameter       MyDrawShape(DrawRectangleMethod);    } 

Note the following points in the btnRectangle_Click() method above:

  • We create a Graphics object because the drawing methods (such as FillRectangle() and FillEllipse()) are defined as instance methods in this class. Therefore, we need an instance of the Graphics class to enable us to call these methods. CreateGraphics() is an inherited method from the Control class (of which our form is a subclass).

  • We declare a local variable of type DrawShape. This reinforces the fact that delegates are data types. Later in the chapter, we'll show the MSIL code for the DrawShape data type.

  • We create an instance of the DrawShape type, and bind it to the FillRectangle() method on the Graphics object. Note that we pass the name of the method without parentheses. This syntax can only be used in constructors of delegates.

Note

It's also possible to bind delegates to static methods. To bind a delegate to a static method rather than an instance method, specify a class name before the method (for example, Math.Sin), rather than specifying an object name before the method (for example, aGraphics.FillRectangle).

  • We pass the DrawShape instance into another method named MyDrawShape(). MyDrawShape() uses the delegate to invoke the specified drawing operation (aGraphics.FillRectangle).

Now we create a similar method for handling btnDrawEll. Often, delegate instances are created and immediately passed to a method. Using delegates in this code can be confusing. The code for btnDrawEll_Click() is functionally equivalent to btnDrawRect_Click(). Take your time to compare the two.

    // Handle the Click event for the btnDrawEll button    private void btnDrawEll_Click(object sender, System.EventArgs e)    {       // Use a shorter but more cryptic syntax.       MyDrawShape(new DrawShape(CreateGraphics().FillEllipse));    } 

Our final task is to use the delegate in the MyDrawShape() method, to invoke the required drawing operation. Here is the code for the MyDrawShape() method:

    // MyDrawShape uses a delegate to indicate which method to call    private void MyDrawShape(DrawShape theDelegate )    {       // Are any text fields blank?       if (txtLeft.Text.Length == 0 || txtTop.Text.Length == 0 ||           txtWidth.Text.Length == 0 || txtHeight.Text.Length == 0)       {          MessageBox.Show("Please fill in all text boxes", "Error",                          MessageBoxButtons.OK,                          MessageBoxIcon.Error);          return;       }       // Get the coordinate values entered in the text fields       Rectangle aRect = new Rectangle(Int32.Parse(txtLeft.Text),                                       Int32.Parse(txtTop.Text),                                       Int32.Parse(txtWidth.Text),                                       Int32.Parse(txtHeight.Text));       // Make sure the coordinates are in range       if (mRect.Contains(aRect))       {         // Get the color of the btnColor button         Brush aBrush = new SolidBrush(btnColor.BackColor);         // Call the delegate, to draw the specified shape         theDelegate(aBrush, aRect);       }       else       {         // Display error message, and return immediately         MessageBox.Show("Coordinates are outside drawing area", "Error",                MessageBoxButtons.OK, MessageBoxIcon.Error);       }    } 

Note the following points in the MyDrawShape() method:

  • MyDrawShape() takes a delegate of type DrawShape. The delegate object specifies the required drawing operation, and also specifies the Graphics object to use for the drawing operation. It's important to realize that MyDrawShape() has no idea what method is specified in the delegate. This makes it easier to extend the method in the future, for example if new kinds of drawing operation are introduced.

  • MyDrawShape() performs various administrative tasks, such as getting the size, position, and color information entered by the user.

  • MyDrawShape() uses the delegate to invoke the specified drawing method. Delegates can be called as if they were methods. As parameters, you must pass them the exact types specified in the delegate definition. For example, DrawShape takes a Brush parameter and a Rectangle parameter. If the target method has overloads with different parameters, these cannot be called using this delegate.

You can compile the application using the free command-line compiler or any graphical IDE, like Visual Studio .NET. To run the application, type the following command:

    C:\Class Design\Ch 06> UsingDelegates.exe 

The application runs, and we can then create rectangles and ellipses in any colors of our choice.

Fun though this is, it's perhaps useful to see how our application is compiled into MSIL code. Close the application, and type the following command at the command prompt (make sure ILDASM is in your system path):

    C:\Class Design\Ch 06> ildasm UsingDelegates.exe 

The MSIL Disassembler window displays the following information (we've expanded the nodes we're interested in this screenshot, to show the details for each of our types):

click to expand

There's a great deal of information here, as you might expect in a Windows application. The important part as far as we are concerned is the MSIL code for DrawShape. Note the following points about DrawShape:

  • DrawShape is a class. Whenever we define a new delegate type in our application, the compiler generates a class to encapsulate the information in the delegate's signature.

  • The MSIL syntax extends [mscorlib]System.MulticastDelegate indicates that the generated class DrawShape inherits from a standard .NET Framework class named MulticastDelegate. We'll describe what we mean by multicast delegates later in this chapter. MulticastDelegate is logically located in the System namespace, and is physically located in the mscorlib assembly. We'll discuss inheritance in more detail in Chapter 7, and we'll investigate namespaces and assemblies in Chapter 8.

  • DrawShape defines several methods in addition to those inherited from its super-class MulticastDelegate. First, it has methods named BeginInvoke() and EndInvoke(), which enable us to invoke methods asynchronously via the delegate, if required. In other words, we can use a delegate to invoke a method in a background thread; the main thread in the application can continue execution, without having to wait for the method to complete. We'll see an example of asynchronous delegates later in this chapter.

  • DrawShape has another method named Invoke(), which enables us to invoke methods synchronously via the delegate. The compiler generated the Invoke() method from the signature we specified when we defined the DrawShape type. When we call the delegate, the compiler really generates IL code that calls the Invoke() method on our delegate instance. If you try to call the Invoke() method from your code, you'll see that the C# compiler raises an error, stating 'Invoke() cannot be called directly on a delegate'. In many other .NET languages, programmers have to call the Invoke() method themselves.

Creating and Using Multicast Delegates

The .NET Framework supports two distinct kinds of delegate:

  • Single-cast delegates, which enable us to call a single method on a single object. We have seen how this works in the previous section.

  • Multicast delegates, which enable us to call a series of methods on potentially different objects. Multicast delegates maintain an invocation list to remember which method to call on which object. When we call a multicast delegate, the delegate calls the specified methods on the designated objects, in sequence.

Note

Actually, all delegates are multicast. Single-cast delegates are just delegates with only one method in their invocation list. But as the creation of a single-cast delegate (through the constructor) and a multicast delegate (by combining existing delegates) are so different, we treat them here as different entities.

Multicast delegates are useful if we need to perform the same operation on a collection of objects, or if we need to perform a series of operations on the same object, or any combination of these two cases. We can use multicast delegates implicitly to keep a collection of all the methods that need to be executed, and the objects upon which these methods are to be executed. Combining instances of the same delegate type creates multicast delegates.

To create and use a multicast delegate, we must follow these steps:

  • Define a delegate type to represent the signature of the methods we want to call via the delegate. Multicast delegates can only be used to execute methods with the same signature, which is consistent with the general ethos of strong data typing with delegates.

  • Write methods with the same signature as the delegate.

  • Create a delegate object, and bind it to the first method we want to call via the delegate.

  • Create another delegate object, and bind it to the next method we want to call.

  • Call the Combine() method in the System.Delegate class to combine the two delegates into an integrated multicast delegate. The Combine() method returns a new delegate, whose invocation list contains both delegates.

  • Repeat the previous two steps to create as many delegates as needed and combine them into an integrated multicast delegate.

  • If we need to remove a delegate from a multicast delegate, call the Remove() method defined in the System.Delegate class. The Remove() method returns a new delegate, whose invocation list does not contain the removed delegate. If the delegate we just removed was the only delegate in the invocation list, the Remove() method returns null instead.

  • When we are ready to invoke the methods specified by the multicast delegate, simply call the delegate as before. This invokes the methods in the order they appear in the invocation list, and returns only the result of the last method in the invocation list.

Besides the static methods Combine() and Remove() on the System.Delegate class, C# offers us another syntax to add or remove delegates to a multicast delegate: using the addition and subtraction operators (+ and -). You can just add two delegates together to create a new multicast delegate. The following two lines of code are equivalent:

    // Combining DelegateA and DelegateB to CombinedDelegate    System.Delegate CombinedDelegate =                    System.Delegate.Combine(DelegateA, DelegateB);    // Idem    System.Delegate CombinedDelegate = DelegateA + DelegateB; 

As always, these operators can be combined with the assignment operator to += and =. This is actually the most common way to combine delegates in C#. The operator syntax often looks cleaner than using the Combine() and Remove() methods. The C# compiler translates the addition back to calls to the static methods (it is not an overloaded operator on the Delegate class).

To illustrate these concepts, and to show the C# syntax for multicast delegates, we'll work through a complete example that uses multicast delegates to paint any number of child forms in a Windows Forms application. The application has a main form that allows the user to create new child forms, and to change the color of all these forms at any time. The main form uses a multicast delegate to invoke a Repaint() method on each child form.

The source code for this example is located in the download folder DelegatesEvents\ MulticastDelegates. Before we look at the code, let's see how the application will work. The main form in the application appears as follows:

click to expand

The user enters some screen coordinates, and then clicks Add Window to create the child form. At this stage in the application, we create a new delegate and bind it to the Repaint() method on the new child form. We combine this delegate into a multicast delegate in the main form, to keep track of all the open child forms.

The main form displays a message in its status bar to indicate the number of entries in the invocation list of the multicast delegate. This tells us how many child forms are currently open.

If the user clicks Add windows several times, we'll see several child forms on the screen. The multicast delegate keeps track of all these child forms. To be more precise, the invocation list in the multicast delegate holds the Repaint() method for each of the child forms:

click to expand

When the user clicks Change Color to select a new color, we invoke the multicast delegate to repaint all the child forms. In other words, we call the multicast delegate, which invokes the Repaint() method on each child form. Each form repaints itself in the specified color:

click to expand

There is one more issue to consider. If the user closes a child form, we need to remove the corresponding delegate from the multicast delegate in the main form. To achieve this effect, the main form has a method named ChildFormClosing(), which the child form can call just before it closes. In the ChildFormClosing() method, we remove the child form's delegate from the multicast delegate, because we won't need to repaint the form in future.

  • It is good practice to remove delegates from the invocation list when the objects they point to do not exist anymore. You may think an object is out of scope and will soon be garbage-collected, but the delegate is not only pointing to the method on the object, but also holds a reference to the object itself. Therefore, an object will remain in memory as long as the delegate is still in the invocation list.

Now that we've seen how the application works in principle, let's look at the code. The source code for the child form is fairly straightforward, because there is no delegate- related code in this class and child forms have no awareness of the other child forms in the application. The source code for the ChildForm class is provided in ChildForm.cs:

    public class ChildForm : System.Windows.Forms.Form    {      private System.ComponentModel.Container components = null;      // omitted the constructor      #region Windows Form Designer generated code      // omitted for brevity      #endregion      private void ChildForm_Load(object sender, System.EventArgs e)      {        // Display the current time in the window's title bar        this.Text = "Created " + DateTime.Now.ToLongTimeString();      }      // This method will be called via multicast delegate in main form      public string Repaint(Color theColor)      {        // Set the color for this form, and update the caption bar        this.BackColor = theColor;        this.Text = "Updated " + DateTime.Now.ToLongTimeString();        return this.Text;      }      // Handle the Cancel event for this form      private void ChildForm_Closing(object sender,                   System.ComponentModel.CancelEventArgs e)      {        // Tell the main form we are closing, so the main form can        // remove us from its multicast delegate        Mainform MyOwner = (Mainform)this.Owner;        MyOwner.ChildFormClosing(this);      }    } 

Note the following points in the ChildForm class:

  • The ChildForm_Load() method displays the form's creation time on the caption bar.

  • The Repaint() method has a Color parameter, to tell the form which color to repaint itself. The main form's multicast delegate calls this method whenever the user selects a new color.

  • The ChildForm_Cancel() method informs the main form that this child form is about to close. We use the Owner property to get a reference to the main form, and then call its ChildFormClosing() method.

Now let's see the code for the main form in the application. This main form contains all the delegate-related processing. The first step is to define our delegate type, and to declare a field to refer to the multicast delegate object:

    // define the Delegate type (and it's signature)    delegate string ChangeColorDelegate(Color aColor);    public class Mainform : System.Windows.Forms.Form    {      // Declare a ChangeColorDelegate field,      // to refer to the multicast delegate      private ChangeColorDelegate mAllRepaintMethods;    } 

Note the following points in the MainForm class definition above:

  • ChangeColorDelegate defines the signature of the methods we want to call via this delegate type. As you'd expect, the delegate's signature matches that of the Repaint() method in the ChildForm class.

    There is no difference in how we define single-cast delegate types and multicast delegate types in C#. In fact, every delegate type in C# is implicitly a multicast delegate. It's up to us whether we use the delegate to represent a single method or several methods.

  • mAllRepaintMethods is a field that will point to the multicast delegate. In other words, mAllRepaintMethods will refer to a ChangeColorDelegate instance. Initially, mAllRepaintMethods is null because there are no child forms yet. When we create the first child form, we'll create a new ChangeColorDelegate instance and assign it to mAllRepaintMethods.

Now let's see how to create a new child form, and combine its Repaint() method into our multicast delegate:

    // Handle the click event on the btnAddWindow button    // the registration of this method for the Click event is done    // elsewhere    private void btnAddWindow_Click(object sender, System.EventArgs e)    {      // Are any text fields blank?      if (txtLeft.Text.Length == 0 || txtTop.Text.Length == 0 ||        txtWidth.Text.Length == 0 || txtHeight.Text.Length == 0)      {        MessageBox.Show("Please fill in all text boxes",            "Error",            MessageBoxButtons.OK,            MessageBoxIcon.Error);            return;      }        ChildForm aChildForm = new ChildForm();        aChildForm.Owner = this;        aChildForm.DesktopBounds = new Rectangle(          Int32.Parse(txtLeft.Text),          Int32.Parse(txtTop.Text),          Int32.Parse(txtWidth.Text),          Int32.Parse(txtHeight.Text));        aChildForm.Show();        // Create a new delegate for the child form's Repaint method        ChangeColorDelegate newDelegate = new ChangeColorDelegate                                              (aChildForm.Repaint);        // Combine new delegate into the multicast delegate        mAllRepaintMethods = (ChangeColorDelegate)System.Delegate.Combine(                                         mAllRepaintMethods, newDelegate);        // Use multicast delegate to count the child forms        sbStatus.Text = "Created child form " +        mAllRepaintMethods.GetInvocationList().Length + ".";    } 

Note the following points in the btnAddWindow_Click() method shown above:

  • We begin by verifying if the user has entered numbers in all the text fields. If all is well, we create a new child form with the specified location and size, and display the form on the screen. We specify the main form as the Owner of the child form.

  • We create a new ChangeColorDelegate instance, and bind it to the Repaint() method on the new child form.

  • We use the static Combine() method on System.Delegate to add the newDelegate instance to the invocation list of the multicast delegate mAllRepaintMethods. The first time when mAllRepaintMethods is not yet initialized, the Combine() method returns the passed newDelegate instance.

  • Delegates have a GetInvocationList() method, which returns an array of delegate objects representing all the entries in the multicast delegate. We can use the Length property to find the size of this array. In our example, this tells us how many child forms are currently open.

The line of code that performs the actual combination of the new delegate with the existing delegates into one new multicast delegate could also have been written like this:

    mAllRepaintMethods += newDelegate; 

In the resulting MSIL code, this would look identical. Once the user has created some child forms, they can click Change Color to change the color of these forms. The following code achieves this:

    private void btnChangeColor_Click(object sender, System.EventArgs e)    {      if(mAllRepaintMethods == null)      {        MessageBox.Show("There are no child forms to change.",           "Error changing color",           MessageBoxButtons.OK,           MessageBoxIcon.Error);      }      else      {        // Ask user to choose a color        ColorDialog dlgColor = new ColorDialog();        dlgColor.ShowDialog();        // Invoke multicast delegate, to repaint all the child forms        mAllRepaintMethods (dlgColor.Color );        // Use multicast delegate to count the child forms        sbStatus.Text = "Updated " +          mAllRepaintMethods.GetInvocationList().Length +          " child form(s).";      }    } 

Note the following points in the btnColors_Click() method shown above:

  • If mAllRepaintMethods is null, there are no child forms open, so we display an error message and return immediately. It is very easy to use the multicast delegate to keep track of the presence or absence of child forms.

  • If mAllRepaintMethods is not null, we ask the user to choose a new color for the existing forms. We then call our multicast delegate, to invoke the Repaint() method on all the child forms. Again, it is convenient to use the multicast delegate to iterate through the child forms and repaint each one.

  • We display a message in the status bar of the main form to indicate how many child forms have been repainted. As before, we get this information by calling GetInvocationList on the multicast delegate.

The final method we need to look at in the MainForm class is the ChildFormClosing() method. Child forms call this method just before they close, to enable the main form to keep track of its child forms. The code for the ChildFormClosing() method is shown below:

    public void ChildFormClosing(ChildForm aChildForm )    {      // Create a dummy delegate for the ChildForm that is closing      ChangeColorDelegate unneededDelegate =                  new ChangeColorDelegate ( aChildForm.Repaint);      // Remove the delegate from the multicast delegate      mAllRepaintMethods = (ChangeColorDelegate)System.Delegate.Remove(      mAllRepaintMethods, unneededDelegate);      // If multicast delegate is Nothing, there are no child forms left      if (mAllRepaintMethods == null)      {        sbStatus.Text = "Final child form has been closed.";      }      else      {        // Use multicast delegate to count the child forms        sbStatus.Text = "Child form closed, " +          mAllRepaintMethods.GetInvocationList().Length +          " form(s) remaining.";      }    } 

Note the following points in the ChildFormClosing() method shown above:

  • The ChildFormClosing() method receives a ChildForm parameter, to indicate which child form is closing.

  • We create a new delegate, and bind it to the Repaint() method on the soon-to-be-defunct child form. We then call the Remove() method in the System.Delegate class, to remove this delegate from the multicast delegate. The Remove() method returns one of two possible values:

    • If we've just removed the final delegate from the multicast delegate, the multicast delegate no longer exists. In this case, the Remove() method returns null.

    • Otherwise, the Remove() method returns a new multicast delegate that does not contain the removed delegate.

  • Note that delegates exhibit value semantics; even though we have created a new delegate pointing to the method, we can use it to remove another delegate pointing to the same method. The two delegates are considered equivalent. If multiple delegates in the list point to the same method on the same object, only the first will be removed.

  • We display an appropriate message in the status bar of the main form, to indicate how many child forms are left.

We could have written the code removing one of the delegates from the invocation list much shorter like this:

      // Remove the delegate from the multicast delegate      mAllRepaintMethods -= unneededDelegate; 

The code for the application is complete. To run the application, type the following command:

    C:\Class Design\Ch 06> MainForm.exe 

The application appears as follows, and enables us to open child windows and set their colors as previously advertised:

click to expand

Multicast delegates make it easy to maintain a collection of objects that need to be notified in unison when something important happens. When we invoke the multicast delegate, it implicitly iterates through all the entries in its invocation list to call the specified method on each designated object.

Creating and Using Asynchronous Delegates

All of the examples we have seen so far have used delegates to invoke methods synchronously by calling the delegate directly. When we invoke a method synchronously, we have to wait for the method to return before we can continue processing.

We can also use delegates to invoke methods asynchronously. This means we can invoke the method in a background thread, and continue doing useful work in the main thread at the same time. This can be useful if we have lengthy tasks to perform.

To support asynchronous method calls via delegates, the C# compiler generates the following two methods in our delegate class (in addition to the Invoke() method, to support synchronous method calls):

  • BeginInvoke()

    We call BeginInvoke() to invoke the required method asynchronously, in a background thread.

  • EndInvoke()

    After the specified method has completed execution, we can call EndInvoke() to get the return value from the method. EndInvoke() also allows us to access any output parameters from the method; for example, if the method takes parameters ByRef, and modifies these parameters during execution, we can use EndInvoke() to get the modified values for these parameters. If the method has not completed execution yet, the application will wait (and halt) until the return values are available, so be careful with calling EndInvoke().

There are several ways to use asynchronous delegates, depending on our needs. Here are the possibilities, in order of increasing complexity:

  • Invoke a method that does not return a result

  • Invoke a method that returns a result, and wait a finite time for the result

  • Invoke a method that returns a result, and define a callback method to receive the result whenever it arrives

To get a feel for the possibilities, we will have a look at a sample application that uses all three possibilities and displays timing results for each. The source code for this example is located in the download folder DelegatesEvents\AsyncDelegates. Before we look at the code, let's see what the application will do. The application has only one form. When you compile the application and run it, you will see this:

click to expand

When you click any of the three buttons, the application will start downloading three XML files from the Internet (you need a working network connection for testing this example). They contain the top articles in three categories from the Slashdot website. When a file is completely downloaded and parsed by the System.Xml.XmlDocument class, we display the time in milliseconds since you clicked the button. So after downloading the three files, the screen would look something like this:

click to expand

As you can see, each of the downloads takes approximately 900 ms (in your environment it may of course take longer or shorter, depending on your connection). As the files are more or less of the same size and are served from the same server, which is what we would expect.

First let's have a look at the synchronous code. It is called when the user clicks the Load Synchronous button.

    public class Mainform : System.Windows.Forms.Form    {      private ArrayList mUrlList;      private void Mainform_Load(object sender, System.EventArgs e)      {        // Initialising a list of three Urls containing valid Xml content        mUrlList = new ArrayList();        mUrlList.Add("http://slashdot.org/slashdot.rdf");        mUrlList.Add("http://slashdot.org/science.rdf");        mUrlList.Add("http:// slashdot.org/books.rdf");      }      private void btnSync_Click(object sender, System.EventArgs e)      {        // Clear the output        ClearLog();        // Initialise an array of XmlDocument objects        XmlDocument[] Documents = new XmlDocument[mUrlList.Count];        for (int i = 0; i<mUrlList.Count; i++)        {          Documents[i] = new XmlDocument();          // Both the loading of the content over the network          // and the parsing of this content happens in the next line.          Documents[i].Load((string)mUrlList[i]);          AppendLog("Loaded document from " + mUrlList[i]);        }      }    } 

Note the following points:

  • The Mainform_Load() method initializes the ArrayList of URLs we want to download; these URLs are hard coded.

  • In the btnSync_Click() method, we use the methods ClearLog() and AppendLog(). The code of these methods is not printed here, but you can check them out in the code download. They manage the logging of timed milliseconds to the textbox on the form.

  • btnSync_Click() itself is fairly straightforward: in a for loop, all of the URLs in mUrlList are used to load an XmlDocument object from the Internet. The Load() method on XmlDocument will wait until all of the remote resource has been downloaded and parsed and no errors have been encountered.

It doesn't matter if you have never worked with the classes in the System.Xml namespace before because it is not relevant here. We just use the XmlDocument.Load() method as an example of a long lasting operation.

Most of the 3 900ms, we are waiting for the network request, due to bandwidth and network delays. It would be much more efficient to make the three requests first (this takes hardly any bandwidth) and then wait for all requests at the same time. This traditionally required spawning additional threads and having each thread do a request and wait for its response. Multithreaded programming is hard to do well and errors are difficult to debug. Now with asynchronous calling of delegates, we can actually achieve a lot of the power of multithreaded programming without most of the pain. For a number of situations, the delegates actually do the multithreaded programming for us. These are:

  • Calling a long-lasting operation without freezing the user interface.

  • Calling multiple operations at the same time to use resources more efficiently

More complex multithreaded scenarios (like having a worker thread that monitors a resource) cannot be programmed with delegates. Check out the System.Threading namespace's documentation for more information. Be aware of the fact that your call creates a new thread that consumes resources. Calling a delegate asynchronously 10,000 times in a loop will slow down your system.

Now let's look at the code for the asynchronous approach. We start by defining a delegate suitable for calling the Load() method on XmlDocument:

    delegate void DocumentLoad(string fileUrl); 

Instead of calling the method directly, we can now create a delegate of type DocumentLoad and have the delegate call the method for us. To call it asynchronously, we would use the BeginInvoke() method on the delegate. The simplest way to use it would be like this:

    private void btnAsync_Click(object sender, System.EventArgs e)    {      ClearLog();      XmlDocument[] Documents = new XmlDocument[mUrlList.Count];      for (int i = 0; i<mUrlList.Count; i++)      {        XmlDocument Document[i] = new XmlDocument();        DocumentLoad MyDelegate = new DocumentLoad(Document[i].Load);        MyDelegate.BeginInvoke((string)mUrlList[i], null, null);        AppendLog("Loaded document from " + mUrlList[i]);      }    } 

The two extra parameters that are passed to the delegate's BeginInvoke() method will be explained later. In this case, we just pass in null. If you work in Visual Studio .NET, you will notice that the IntelliSense support does not work on the BeginInvoke() and EndInvoke() methods.

This code will indeed call the Load() method for all three of the XmlDocuments. When BeginInvoke() is called, the method returns immediately, but on a different thread that was created by the delegate it is still busy loading the document. So when we call the AppendLog() method, we are not sure if the loading is completed yet (in fact we are quite sure that it is not).

In some cases, you don't really care when a method is finished, you just want to start it and go on with your main task. In these cases, calling BeginInvoke() like this may do the trick. But normally, you eventually need to know the results of the operation. In this case, you must hold on to the delegate and the return value from BeginInvoke().

You cannot get at a return value when the method calculating. It is still running on another thread. To get the return value from an asynchronous method call we use the EndInvoke() method. When you call this method, the delegate will hold the execution until the original call to the underlying method is completed. Be careful not to call EndInvoke() unless waiting for the completion of the method is OK. The EndInvoke() method returns the return value from the underlying method. The return type of the EndInvoke() method is always equal to the return value of the delegate itself.

If the method you call uses ref or out parameters, you can also get at the new values for these through EndInvoke(). We will not go into that here, but you can check the order of the parameters by looking into your delegate with ILDASM.exe.

EndInvoke() always expects an object implementing IAsyncResult as a parameter. For this parameter, you must use the return value you got from BeginInvoke(). This object is like a receipt for retrieving the return value through EndInvoke(). This mechanism is necessary because you can call the same delegate multiple times through BeginInvoke() (perhaps using different parameters). Now if you call EndInvoke(), the delegate must make sure exactly which call to BeginInvoke() you want to end. If you are not going to call EndInvoke anyway, you don't need the IAsyncResult object.

So, let's look at asynchronous downloading where we don't log the duration until the download is really complete:

    private void btnAsync_Click(object sender, System.EventArgs e)    {      ClearLog();      // As we will now be working with several objects at the same time,      // we declare arrays of XmlDocument, Delegate and the IAsyncResult      XmlDocument[] Documents = new XmlDocument[mUrlList.Count];      DocumentLoad[] Delegates = new DocumentLoad[mUrlList.Count];      // VS.NET does not support arrays of interface type with      // Intellisense, but this code is valid C# and compiles fine      IAsyncResult[] Tickets = new IAsyncResult[mUrlList.Count];      for (int i = 0; i<mUrlList.Count; i++)      {        Documents[i] = new XmlDocument();        Delegates[i] = new DocumentLoad(Documents[i].Load);        // The next line starts the loading of the XmlDocument on a        // different thread. We don't have to wait for its        // completion now. The two null parameters at the end        // are for use with callback functions.        Tickets[i] = Delegates[i].BeginInvoke(                     (string)mUrlList[i], null, null);        AppendLog("Started loading document from " + mUrlList[i]);      }      for (int i = 0; i<mUrlList.Count; i++)      {        // Force to wait here until the call is completed. We        // could have called BeginInvoke multiple times on the        // same delegate instance, so we need the ticket (of        // type IAsyncResult) to specify which call we mean exactly.        Delegates[i].EndInvoke(Tickets[i]);        AppendLog("Loaded document from " + mUrlList[i]);      }    } 

First we loop through the list of URLs and for each URL create an XmlDocument instance and a DocumentLoad instance pointing at the Load() method of the XmlDocument object. Both the XmlDocument instances and the delegates pointing at them are stored in arrays. We need them again when we want to end the asynchronous operation. We then call BeginInvoke() on the delegate and store the returned object in a third array.

When all three delegates are started off, we could theoretically start doing some other processor-intensive work, as we have hundreds of milliseconds before the first response comes in. But as we don't have anything to do in this application, we just enter a second loop, where we wait for the three threads to finish. In the starting order, we call EndInvoke() on the delegate, passing it the appropriate ticket. Execution now halts until the method called by the delegate has completed. After completion, we log the time and go on to the next delegate.

What would we expect from the times logged for this situation? Well, for the first request, we would not expect much difference. Maybe a small delay caused by the creation of threads and the overhead of dealing with them. By clicking the second button, we get the following result:

click to expand

Calling the EndInvoke() method as soon as you need the results is a good solution when you have an approximate idea of how long the operation will take. Sometimes waiting just isn't an option. Sometimes you want the application to start working without the results from the operation (you could gray out all functionality that needs the results to work properly and enable it as soon as the necessary information comes available). In these cases, callback functions are the solution. You must implement a method with a predefined signature and tell the delegate to call the callback method as soon as execution completes.

In our case, we would just use the callback method to create a line in the log, but you could also call EndInvoke() from the callback method and access the return value from the executed method. This is what the results might look like on screen:

click to expand

Note that the order of starting the three downloads is not necessarily the same as the order of completing them. Let's look at the code:

    private void btnCallback_Click(object sender, System.EventArgs e)    {      ClearLog();      XmlDocument[] Documents = new XmlDocument[mUrlList.Count];      for (int i = 0; i<mUrlList.Count; i++)      {        Documents[i] = new XmlDocument();        DocumentLoad TheDelegate = new DocumentLoad(Documents[i].Load);        AsyncCallback CallbackDelegate = new               AsyncCallback(this.ReadyLoading);        // We start the loading of the XMLDocument, passing it        // a delegate for calling the ReadyLoading method when        // it is ready. A reference to the current XmlDocument        // is also passed in. This will be passed to the callback        // method as the AsyncState property of the IAsyncResult.        TheDelegate.BeginInvoke((string)mUrlList[i],                CallbackDelegate, Documents[i]);        AppendLog("Started loading document from " + mUrlList[i]);      }    }    private void ReadyLoading(IAsyncResult r)    {      if (r.AsyncState is XmlDocument)      {        XmlDocument doc = (XmlDocument)r.AsyncState;        AppendLog("Loaded " + doc.BaseURI);      }    } 

Starting at the bottom, we first see the actual callback function. It has the signature defined by the standard delegate System.AsyncCallback. If the IAsyncResult.AsyncState property of the passed object is of type XmlDocument, it adds a line to the log. The AsyncState property is filled by the code that initiates the asynchronous call.

If you look at the code starting the execution, you will see that we now actually use two delegates, one to point at the method to start executing and another one (of type System.AsyncCallback) to point at the callback method. This delegate is passed as the second parameter in BeginInvoke(). The third parameter is optional. Anything you pass there will be passed to the callback function as IAsyncResult.AsyncState. In this case we pass the XmlDocument instance itself. We use it in the callback method to log the URL of the loaded document.

So, we have seen three ways of calling a delegate instance:

  • Synchronously, waiting for the method (or methods) to complete. This is by far the most common use of delegates.

  • Asynchronously, waiting for the method to complete (by calling EndInvoke()), but only just before you need it to be completed.

  • Asynchronously, registering a callback method to notify you as soon as execution completes.

Each is appropriate in different situations: it really depends on how hard you need the results from the method and how long the wait might take. Note that you cannot use asynchronous calling on multicast delegates. The delegate will throw an exception.




C# Class Design Handbook(c) Coding Effective Classes
C# Class Design Handbook: Coding Effective Classes
ISBN: 1590592573
EAN: 2147483647
Year: N/A
Pages: 90

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