Contents
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.
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.
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.
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;
if (requiredParameters != arguments.Length)
{
throw new ArgumentException(string.Format(
"{0} arguments provided when {1} {2} required.",
arguments.Length, requiredParameters,
((requiredParameters == 1) ? "is" : "are")));
}
Delegate[] invocationList = handler.GetInvocationList();
if (invocationList == null)
return;
foreach (Delegate singleCastDelegate in invocationList)
{
ISynchronizeInvoke synchronizeTarget =
singleCastDelegate.Target as ISynchronizeInvoke;
if (synchronizeTarget == null)
{
singleCastDelegate.DynamicInvoke(arguments);
}
else
{
synchronizeTarget.BeginInvoke(
singleCastDelegate, 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.
int requiredParameters = handler.Method.GetParameters().Length;
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.
Delegate[] invocationList = handler.GetInvocationList();
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.
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.
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:
private void btnOpenport_Click(object sender, EventArgs e)
{
this.serialPort.Open();
}
private void serialPort_DataReceived(object sender,
SerialDataReceivedEventArgs e)
{
}
Now it's time to write the data to the output textbox. You can try this code inside the DataReceived
event handler:
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:
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.
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.
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