Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Generic InvocationHelper

4.91/5 (21 votes)
8 Jun 2007CPOL6 min read 1   248  
A generic class for providing thread-safe invocation of delegates. Can be used for (but not limited to) updating GUI elements from another thread.

Contents

Introduction

Recently, I was involved in writing a utility program for interpreting GPS data from a variety of sources, including a Serial Port. The main structure of the library/program involved three layers:

  • Data providers
  • Parsers
  • Presentation

Data Providers

The data providers were in charge of reading in data from different sources. For example, one read in data from a file while another one was supposed to read in data from a USB GPS receiver. Upon receiving some data, the providers would fire an event signaling that there was data ready to be parsed.

Parsers

These classes parsed the data from the providers and fired off some events notifying the "parent" classes that data was ready. An example of this was parsing the NMEA data into some structures; various events would be fired when location, bearing, etc. changed.

Presentation

The final layer listened to the events fired from the parsers and displayed the data in various forms, such as TextBoxes, Graphs, Plots and so on.

The Problem

Things were progressing nicely until the presentation layer. That's when the debugger told me that I was trying to update the UI from another thread. Slightly confused, Google and MSDN came to my rescue. It seems that the SerialPort.DataReceived event is fired from a different thread. Thinking back, it now makes sense because the SerialPort class is just a wrapper around some APIs. I seem to remember that these could involve multiple threads.

So, off to Google for a solution, or not. The only examples I found to begin with said that you should use Control.Invoke to update the UI from another thread. This was no good, as I didn't particularly want to tie the Parsers down to any Windows Forms controls.

Eventually, after a bit more digging -- even looking through framework code in Reflector to see how things were accomplished in Control.Invoke -- I came across this article by Alexey Popov. This did exactly what I wanted, except for one thing. The one thing that put me off was the current need to copy and paste the code into all of my OnEventHandler routines. Needless to say, the NMEA parser had quite a few of them.

Generics

Initially, the first version used generics until some kind person on Code Project pointed out that they weren't actually needed for this code. As a result, I have modified it to accept a simple delegate type rather than a generic. It was necessary to check that it was a delegate type, since this couldn't be imposed by a generic constraint.

The Code

This code is based on the article by Alexey Popov. His article does a good job of explaining what is going on, so I have not included that discussion.

C#
using System;
using System.ComponentModel;

namespace PooreDesign
{
    public static class InvocationHelper
    {
        public static void Invoke(Delegate handler, params object[] arguments)
        {
            int requiredParameters = handler.Method.GetParameters().Length;
            // Check that the correct number of arguments have been supplied
            if (requiredParameters != arguments.Length)
            {
                throw new ArgumentException(string.Format(
                     "{0} arguments provided when {1} {2} required.",
                     arguments.Length, requiredParameters, 
                     ((requiredParameters == 1) ? "is" : "are")));
            }
            // Get a local copy of the invocation list in case it changes
            Delegate[] invocationList = handler.GetInvocationList();
            // Check that it's not null
            if (invocationList == null)
                return;
            // Loop through delegates and check for ISynchronizeInvoke
            foreach (Delegate singleCastDelegate in invocationList)
            {
                ISynchronizeInvoke synchronizeTarget = 
                    singleCastDelegate.Target as ISynchronizeInvoke;
                // Check to see if the interface was supported
                if (synchronizeTarget == null)
                {
                    // Invoke delegate normally
                    singleCastDelegate.DynamicInvoke(arguments);
                }
                else
                {
                    // Invoke through synchronization interface
                    synchronizeTarget.BeginInvoke(
                        singleCastDelegate, arguments);
                }
            }
        }
    }
}

Invoke (Delegate eventHandler, params object[] Arguments)

Argument Check

The next check performed checks that the appropriate number of arguments has been provided. Note that it does not check that the arguments are of the correct type. There are several reasons for this, the chief reason being that it would be quite expensive to check all of the arguments all the time. The reason a check is performed on the number of arguments is that the default exception thrown is quite ambiguous. So, at least this will narrow down problems for the developer.

C#
int requiredParameters = handler.Method.GetParameters().Length;
// Check that the correct number of arguments have been supplied
if (requiredParameters != arguments.Length)
{
    throw new ArgumentException(
        string.Format("{0} arguments provided when {1} {2} required.",
        arguments.Length, requiredParameters, 
        ((requiredParameters == 1) ? "is" : "are")));
}

A delegate can easily become a MultiCastDelegate simply by subscribing more handlers to the delegate. In order for this solution to work, each delegate must be interrogated independently. GetInvocationList() returns an array of handlers currently subscribed to the delegate. A simple check is performed afterwards to make sure that there are some subscribed delegates.

C#
// Get a local copy of the invocation list in case it changes
Delegate[] invocationList = handler.GetInvocationList();
// Check that it's not null
if (invocationList == null)
{
    return;
}

Invocation

The next piece of code deals with finding out if each target object implements ISynchronizeTarget, which all Windows Forms controls do.

C#
ISynchronizeInvoke synchronizeTarget = 
    singleCastDelegate.Target as ISynchronizeInvoke;

The above line casts the Target to the required interface. If it cannot be converted, then a null instance is returned by the cast operation. If the target object does not support the interface, then a normal invocation of the delegate can occur. The Delegate.DynamicInvoke method handles this, accepting the correct number of arguments to pass to the method. If the target object does support the interface, then ISynchronizeInvoke.BeginInvoke is called. BeginInvoke is called in preference to Invoke because it will not block the calling thread. In this instance, it does not matter that the calling thread is not notified upon completion. This can be changed quite easily, if required.

How to Use

Let's take a similar example to what caused this article. Say that we want to output data received on the SerialPort to a TextBox from the DataReceived event. The form itself is very simple to set up. Simply drop a SerialPort component and a TextBox onto the designer and name them appropriately. A Button can also be included to open the SerialPort. So, we'll end up with some code similar to this:

C#
private void btnOpenport_Click(object sender, EventArgs e)
{
    this.serialPort.Open();
}
private void serialPort_DataReceived(object sender, 
    SerialDataReceivedEventArgs e)
{
    //To Do:
}

Now it's time to write the data to the output textbox. You can try this code inside the DataReceived event handler:

C#
this.outputTextBox.Text = this.serialPort.ReadExisting();

However, if you run this from inside the debugger, it will break and say that you're making a cross-thread call. One alternative, as is commonly suggested, is to use the Control.Invoke method. In this simple example, that will suffice. In my application, though, I didn't have a reference to the control being updated, so it was a no-go. The solution for this example is similar to the Control.Invoke method:

C#
InvocationHelper.Invoke(new EventHandler(delegate (object sender, EventArgs e)
{
    this.outputTextBox.Text = this.serialPort.ReadExisting();
}));

OK, I agree that this method doesn't really save much, but where it comes down to it is something like the following.

More Complex Example

Here's a very basic outline of the NMEA parser, which should demonstrate where this class becomes useful. First of all, there is a Parse routine which is called to parse an NMEA sentence (GPS string). Accompanying this is an event declaration to indicate the bad data. Inside this method is a bit of code that fires off an event, for example, to indicate a bad checknum.

C#
public event EventHandler<InvalidDataEventArgs> InvalidData;
        
public void Parse(string sentence)
{
    if (!this.IsValidChecksum(sentence))
    {
        this.OnInvalidData(new InvalidDataEventArgs(
            "Checksum failed", sentence));
    }
}

The InvalidDataEventArgs class just stores some information about what caused the bad data and why. OnInvalidData is a routine like the ones that Microsoft implements in their classes. If you look at a Control object, you'll notice that all of the "event handlers" can be overridden to provide custom functionality. This achieves the same thing.

Now, here is where the magic works. The Parse routine, if you remember from the Background section, was called by the SerialPort.DataReceived event. So, things are executing in that context.

C#
protected virtual void OnInvalidData(InvalidDataEventArgs e)
{
    InvokeHelper.Invoke(this.InvalidData, this, e);
}

This piece of code will now -- if the handler associated with the event implements ISynchronizeInvoke -- execute this code in the appropriate thread. So, no exceptions are thrown when the GUI is being updated. Hopefully, the advantages of using the generic implementation are obvious, especially when you have to do this multiple times.

Credits

History

  • 27th September, 2006: Original version posted
  • 8th June, 2007: Article and download updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)