Click here to Skip to main content
15,867,141 members
Articles / Desktop Programming / Windows Forms

Generic InvocationHelper

Rate me:
Please Sign up or sign in to vote.
4.91/5 (21 votes)
8 Jun 2007CPOL6 min read 78.6K   246   62   21
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)


Written By
Engineer PooreDesign
United Kingdom United Kingdom
Ed is a student who due to a form of cancer (now clear) took a year out before going to Imperial College, London to study Electronic Engineering.

His interests include shooting (clay-pigeon (shotgun), air-rifle and rifle), playing with his three labradors (Sandy, Rosie and Tundra), programming (most experienced in C# and C, although those are not the only ones), walking (has completed Gold Duke of Edinburgh's Award), playing games and reading.

He lives in two places on a 57 acre farm in West Waleswith the rest of the family during the holidays; and Greater London during term time.

Languages and Technologies: C#, C, VB6, VB.NET, XAML, (X)HTML, CSS, XSLT, Assembler (PIC), ASP.NET, WPF, Windows.Forms, ASP, VBScript, JavaScript, Pascal / Delphi, XML

Current Stuff:
1st Year MEng Electronics Engineering (Imperial College, London)

Comments and Discussions

 
GeneralThanks Pin
bigbro_19853-Nov-10 1:47
professionalbigbro_19853-Nov-10 1:47 
GeneralSolving this problem on .NET 1.1 Pin
Bill Seddon27-Aug-07 8:28
Bill Seddon27-Aug-07 8:28 
GeneralRe: Solving this problem on .NET 1.1 Pin
Ed.Poore27-Aug-07 10:32
Ed.Poore27-Aug-07 10:32 
GeneralGeneric InvokeHelper Pin
Fogo14-Aug-07 19:07
Fogo14-Aug-07 19:07 
GeneralRe: Generic InvokeHelper Pin
Ed.Poore15-Aug-07 2:33
Ed.Poore15-Aug-07 2:33 
GeneralThis is excellent Pin
Sacha Barber15-Jun-07 8:09
Sacha Barber15-Jun-07 8:09 
GeneralRe: This is excellent Pin
Ed.Poore15-Jun-07 11:51
Ed.Poore15-Jun-07 11:51 
GeneralVery Nice Article Pin
Paul Conrad8-Nov-06 10:56
professionalPaul Conrad8-Nov-06 10:56 
GeneralRe: Very Nice Article Pin
Ed.Poore8-Nov-06 11:17
Ed.Poore8-Nov-06 11:17 
QuestionWhy use Generics? Pin
Don Riesbeck4-Oct-06 8:13
Don Riesbeck4-Oct-06 8:13 
AnswerRe: Why use Generics? Pin
Ed.Poore4-Oct-06 8:53
Ed.Poore4-Oct-06 8:53 
GeneralPerformance enhancement Pin
Thomas Freudenberg28-Sep-06 14:16
Thomas Freudenberg28-Sep-06 14:16 
GeneralRe: Performance enhancement Pin
Ed.Poore2-Oct-06 23:04
Ed.Poore2-Oct-06 23:04 
GeneralNice Article! [modified] Pin
Charles T II28-Sep-06 11:48
Charles T II28-Sep-06 11:48 
GeneralRe: Nice Article! Pin
Ed.Poore28-Sep-06 12:15
Ed.Poore28-Sep-06 12:15 
GeneralRe: Nice Article! Pin
Charles T II28-Sep-06 13:57
Charles T II28-Sep-06 13:57 
GeneralUse constraints Pin
TimWorse28-Sep-06 9:08
TimWorse28-Sep-06 9:08 
GeneralRe: Use constraints Pin
Ed.Poore28-Sep-06 9:21
Ed.Poore28-Sep-06 9:21 
GeneralPlease, please, please -- format your article Pin
fwsouthern28-Sep-06 5:21
fwsouthern28-Sep-06 5:21 
GeneralRe: Please, please, please -- format your article Pin
Ed.Poore28-Sep-06 9:23
Ed.Poore28-Sep-06 9:23 
GeneralOh contrair Pin
fwsouthern28-Sep-06 14:50
fwsouthern28-Sep-06 14:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.