Thursday, April 28, 2011

Cross Thread calls on WinForms

In a multithreaded WinForm app, one of the most common exceptions you'll see is in trying to access a form, or controls on a form, from a different thread than the one it was created on. Fortunately there is an easy way to check if a control is being accessed from the thread it was created on - the InvokeRequired property.

This simply checks if the handle was created on the same thread as the caller. If this Property is set to true then you need to marshal the call to the UI across the thread. There are methods provided for this too - use Invoke and BeginInvoke to correctly marshal the call to the thread that created the Control. From MSDN, Control.Invoke "Executes the specified delegate on the thread that owns the control's underlying window handle".

Most of the code snippets you see on the web for this are out-of-date. Although they work well there are better solutions using built-in features of .NET that results in less code (woo-hoo!).

The old way is to declare a custom delegate
public delegate void UpdateFormTextChanged(string message);

Then you will see that delegate being invoked from the form. If parameters are to be passed they are done in an object array (yuck).
   
public void UpdateProgressMessage(string message)
{
if (!InvokeRequired)
{
DownloadStatusTextBox.Text = message;
}
else
{
UpdateFormTextChanged updateText = SetProductName;
Invoke(updateText, new object[] { productName });
}
}

A simpler way of doing this without the need to create custom delegates is to use Action<T> which takes one parameter and returns void. You replace T with your parameter type making it strongly typed - no objects or object arrays.
public void UpdateProgressMessage(string message)
{
if (!InvokeRequired)
{
DownloadStatusTextBox.Text = message;
}
else
{
Invoke(new Action<string>(UpdateProgressMessage), message);
}
}

For a method with no parameter and void return type you can use the Action() method (in .NET 3.5 and higher) or MethodInvoker in .NET 2.0
   
public void ShowProgressForm()
{
if (!InvokeRequired)
{
ShowDialog();
BringToFront();
}
else
{
// .net 2.0
Invoke(new MethodInvoker(ShowProgressForm));
// .net 3.5 +
Invoke(new Action(ShowProgressForm));
}
}

For more than one parameter you can use one of the many overloads of Action<T>. But wait! I don't see any available in .NET 2.0. Yeah, .NET 2.0 includes Action<T> but not Action<T1, T2> or Func<T, TResult> etc. but it does support Generics of course so you can write your own delegates to mimic these 3.5 features. For example, if you want to call a method on a form that has two parameters - for example, updating a progress bar
public delegate void Action<T1, T2>(T1 arg1, T2 arg2);
public void UpdateProgress(int total, int downloaded)
{
if (!InvokeRequired)
{
ProgressBar.Maximum = total;
ProgressBar.Value = downloaded;
}
else
{
Invoke(new Action<int, int>(UpdateProgress), new object[] {total, downloaded});
}
}

No comments:

Post a Comment