Introduction
I recently began contributing to a WinForms project that is used to acquire data from a hardware module in the host PC. The application is primarily used to configure and command the acquisition module, then manage the resulting data. The application included a plot panel that would show a sample of data recorded from one of the module channels based on a user selection. When the application started or a new channel was selected, the application would freeze while new data was acquired. I had done some work with Asynchronous programming on another .NET project and this seemed like a good place to apply that pattern.
Background
The project in question was restricted to .NET 4.0 so I wasn't able to use the new and improved async
features in .NET 4.5. To summarize my design intent, when the app starts or a new channel is selected, I want the application to:
- Acquire a sample of data from the selected channel
- Refresh the UI with this data
- Repeat until a new channel is selected or the user commands a different mode
Since each acquisition might take several seconds, doing this synchronously is not an option. The UI would always be waiting for the next acquisition, i.e. the main thread would be blocked.
I can't share the original project here so I made a simple WinForms app to demonstrate the pattern.
The Code
Step by step:
- Create a new Winforms project in Visual Studio.
- Add these controls to Form1:
ComboBox
, TextBox
and a Button
. I added a few labels to explain what is going on. - For the combo box, I entered a few choices to the items collection that are akin to the channel selections mentioned above. [Orange, Apple, Grape, Fig, ...]
- In the
Form1
code, you will need these using
s. I don't think you need any additional project references as these are part of the core framework.
using System.Runtime.Remoting.Messaging;
using System.Threading;
- You will need a couple of delegates and fields which I will explain later:
public Form1()
{
InitializeComponent();
}
IAsyncResult myAsyncResult;
CancellationTokenSource cTokenSource = new CancellationTokenSource();
delegate void AsyncMethod1Caller(string fruit, CancellationToken cts);
delegate void SetTextCallback(string text);
- The method that we want to run asynchronously is as follows:
void Method1(string fruit, CancellationToken cts)
{
Random rnd = new Random();
string[] words = {"pie", "fritters", "split", "jam", "marmalade", "leaf" };
while (!cts.IsCancellationRequested)
{
this.SetText(fruit + " " + words[rnd.Next(0, words.Count() - 1)]);
Thread.Sleep(1000);
}
return;
}
In my original project, this is where I would be acquiring data from the module. This method is pretty standard with a couple of small differences. Notice the CancellationToken
in the methods
arguments. This is the mechanism that the UI uses to stop the "worker" thread when it needs to. This doesn't kill the thread abruptly which is considered risky. Instead, it allows the worker to finish what it is doing and exit gracefully. The other twist is the SetText()
call which is used to safely set the text box's value from the worker thread as explained in this article. This call uses one of the delegates shown above.
- Here is the
SetText
method:
private void SetText(string text)
{
if (this.textBox1.InvokeRequired)
{
SetTextCallback d = new SetTextCallback(SetText);
this.Invoke(d, new object[] { text });
}
else
{
this.textBox1.Text = text;
}
}
- Now for the event handlers. This handler fires whenever the user selects an item from the combo box list. The first line changes our
CancellationTokenSource
IsCancellationRequested
property to true
. I then Dispose
of the CancellationTokenSource
and wait for the worker process to stop. In my application, I know how long each acquisition cycle is so I know how long I have to wait. Then, I recreate the CancellationTokenSource
, instantiate an AsyncMethod1Caller
delegate and use it to invoke Method1.
private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
{
cTokenSource.Cancel();
cTokenSource.Dispose();
Thread.Sleep(500);
cTokenSource = new CancellationTokenSource();
AsyncMethod1Caller caller = new AsyncMethod1Caller(Method1);
var cToken = cTokenSource.Token;
myAsyncResult = caller.BeginInvoke(comboBox1.Text, cToken, new AsyncCallback(Method1Callback), "info");
}
Dissecting the last (and most important) statement: I'm using the BeginInvoke
method of the AsyncMethod1Caller
delegate. The arguments are the selected text from the combo box (the input to the worker method), a cancelation token, a reference to an AsyncCallback
and a state object that I don't do anything with. The arguments here must be the same as the signature for Method1
plus the two added arguments that are used to manage the worker thread.
Using the AsyncCallback
option is important in my case because I don't want the main UI thread to block while waiting for the worker thread to complete. The callback:
void Method1Callback(IAsyncResult ar)
{
AsyncResult result = (AsyncResult)ar;
AsyncMethod1Caller caller = (AsyncMethod1Caller)result.AsyncDelegate;
caller.EndInvoke(ar);
}
This runs on yet another thread (from the thread pool) when the worker thread completes. It is important to call EndInvoke
on the delegate we created to clean up from our little threading adventure, and be ready to launch the next worker.
Finally, I included a handler for the button that also can cancel the worker process.
private void button1_Click(object sender, EventArgs e)
{
cTokenSource.Cancel();
}
Running the Example
If you build and run it, you should get something like this:
If you pick a fruit from the combo box, the worker process will join it with another word randomly picked from a list, pause, then pick another random word, etc. Some of the combos are gastronomically weird but that's not the point. Notice you can keep picking different items from the first list, i.e., the asynchronous worker process is not interfering with the main UI thread.