Introduction
When developing rich business applications, developers often have to deal with manipulating data and UI on separate threads and so have to manage cross-thread controls access.
In WinForms, we do that by using callback methods and invoke them on the main thread.
For example, consider a heavy load worker that searches some files into the computer
and sends back log information during the process. We should implement a method that handles the event:
private void AppendLogText(string logText)
{
if (tbLog.InvokeRequired)
{
AppendLogTextCallBack callBack = new AppendLogTextCallBack(AppendLogText);
Invoke(callBack, new object[] { logText });
}
else
{
if (!string.IsNullOrEmpty(tbLog.Text))
tbLog.AppendText(Environment.NewLine);
tbLog.AppendText(string.Concat(DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss"), " : ", logText));
}
}
This article will show how to implement methods to simplify cross thread controls access implementation.
Background
In this article, we use the TPL (Task Parallel Library) to run worker async. The TPL is a set of APIs included in the framework 4, please visit this link
for more information: here.
We also use the class Action
, that encapsulates a method. It's very
useful in this case to encapsulate the callback methods without having to declare
them as class fields. To learn more about this class, please visit this link: here.
As the worker is running async, the worker implements some events to send back
information.
First step: Creating a helper managing the invoke method
The basis of all guidelines is to not develop a process twice: when you have to manage cross threads controls access for multiple controls in your code,
you should have the pattern implemented once.
To do so, we use a helper providing a method that manages control access and callback method firing:
public static class ControlThreadingHelper
{
public static void InvokeControlAction<t>(t control, Action action) where t : Control
{
if (control.InvokeRequired)
control.Invoke(new Action<t, Action>(InvokeControlAction), new object[] { control, action });
else
action();
}
}
This helper is fully reusable for all controls thanks to generics: it checks
the InvokeRequired
property and fires the callback method embedded
in an Action
. The class Action
can be assigned by a lambda expression, so it's very simple to write code.
Thanks to this helper, we can now manage all controls access, cross threads or not, by a unique way:
private void AppendLogText(string logText)
{
ControlThreadingHelper.InvokeControlAction(tbLog, () =>
{
if (!string.IsNullOrEmpty(tbLog.Text))
tbLog.AppendText(Environment.NewLine);
tbLog.AppendText(string.Concat(DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss"), " : ", logText));
});
}
Second step: Simplifying code writing
To have simplified code writing, we should implement the control access
method directly in the class Control
.
We can do this thanks to an extension method:
public static class ControlThreadingExtensions
{
public static void Invoke<t>(this t control, Action action) where t : Control
{
ControlThreadingHelper.InvokeControlAction<t>(control, action);
}
}
This extension method allows us to implement control access directly onto the controls of our WinForm:
private void AppendLogText(string logText)
{
tbLog.Invoke(() =>
{
if (!string.IsNullOrEmpty(tbLog.Text))
tbLog.AppendText(Environment.NewLine);
tbLog.AppendText(string.Concat(DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss"), " : ", logText));
});
}
We have separated the concerns: cross threads accesses are managed by a unique method in the helper while specific form behaviour is assigned into the form behind code.
Points of interest
In this article, we have seen how coupling the Action
class, helpers, and extension methods have helpes us write methods that provide easy-to-use cross-threads controls access methods.
History
- 19/06/2012: Bugfix : use an empty Action instead of an Action<t> for callback method
- 19/06/2012: Original version.