Making Controls Thread-safely






4.10/5 (16 votes)
Making controls thread-safely
Introduction
If you use multithreading to improve the performance of your Windows Forms applications, you must make sure that you make calls to your controls in a thread-safe manner. Access to Windows Forms controls is not inherently thread safe. If you have two or more threads manipulating the state of a control, it is possible to force the control into an inconsistent state. Other thread-related bugs are possible, such as race conditions and deadlocks. It is important to make sure that access to your controls is performed in a thread-safe way.
Background
After reading the article, SafeInvoke: Making GUI Thread Programming Easier in C#, by John Wood, I decided to write a simple util class that would allow to access any control in a thread-safe mode and that would avoid the issues listed by John.
Here is the source code listed below:
using System;
using System.Reflection;
using System.Windows.Forms;
namespace ControlUtils
{
/// <summary>
/// A helper class that allows to invoke control's
/// methods and properties thread-safely.
/// </summary>
public class SafeInvokeUtils
{
/// <summary>
/// Delegate to invoke a specific method on the control thread-safely.
/// </summary>
/// <param name="control">Control on which to invoke the method</param>
/// <param name="methodName">Method to be invoked</param>
/// <param name="paramValues">Method parameters</param>
/// <returns>Value returned by the invoked method</returns>
private delegate object MethodInvoker
(Control control, string methodName, params object[] paramValues);
/// <summary>
/// Delegate to get a property value on the control thread-safely.
/// </summary>
/// <param name="control">Control on which to GET the property value</param>
/// <param name="propertyName">Property name</param>
/// <return>Property value</return>
private delegate object PropertyGetInvoker(Control control, string propertyName);
/// <summary>
/// Delegate to set a property value on the control thread-safely.
/// </summary>
/// <param name="control">Control on which to SET the property value</param>
/// <param name="propertyName">Property name</param>
/// <param name="value">New property value</param>
private delegate void PropertySetInvoker
(Control control, string propertyName, object value);
/// <summary>
/// Invoke a specific method on the control thread-safely.
/// </summary>
/// <param name="control">Control on which to invoke the method</param>
/// <param name="methodName">Method to be invoked</param>
/// <param name="paramValues">Method parameters</param>
/// <return>Value returned by the invoked method</return>
public static object InvokeMethod
(Control control, string methodName, params object[] paramValues)
{
if (control != null && !string.IsNullOrEmpty(methodName))
{
if (control.InvokeRequired)
{
return control.Invoke(new MethodInvoker(InvokeMethod),
control, methodName, paramValues);
}
else
{
MethodInfo methodInfo = null;
if (paramValues != null && paramValues.Length > 0)
{
Type[] types = new Type[paramValues.Length];
for (int i = 0; i < paramValues.Length; i++)
{
if (paramValues[i] != null)
{
types[i] = paramValues[i].GetType();
}
}
methodInfo = control.GetType().GetMethod(methodName, types);
}
else
{
methodInfo = control.GetType().GetMethod(methodName);
}
if (methodInfo != null)
{
return methodInfo.Invoke(control, paramValues);
}
else
{
throw new InvalidOperationException();
}
}
}
else
{
throw new ArgumentNullException();
}
}
/// <summary>
/// Get a PropertyInfo object associated with a specific property on the control.
/// </summary>
/// <param name="control">Control</param>
/// <param name="propertyName">Property name</param>
/// <return>A PropertyInfo object associated with
/// 'propertyName' on specified 'control'</return>
private static PropertyInfo GetProperty(Control control, string propertyName)
{
if (control != null && !string.IsNullOrEmpty(propertyName))
{
PropertyInfo propertyInfo = control.GetType().GetProperty(propertyName);
if (propertyInfo == null)
{
throw new Exception(control.GetType().ToString() + "
does not contain '" + propertyName + "' property.");
}
return propertyInfo;
}
else
{
throw new ArgumentNullException();
}
}
/// <summary>
/// Set a property value on the control thread-safely.
/// </summary>
/// <param name="control">Control on which to SET the property value</param>
/// <param name="propertyName">Property name</param>
/// <param name="value">New property value</param>
public static void SetPropertyValue
(Control control, string propertyName, object value)
{
if (control != null && !string.IsNullOrEmpty(propertyName))
{
if (control.InvokeRequired)
{
control.Invoke(new PropertySetInvoker
(SetPropertyValue), control, propertyName, value);
}
else
{
PropertyInfo propertyInfo = GetProperty(control, propertyName);
if (propertyInfo != null)
{
if (propertyInfo.CanWrite)
{
propertyInfo.SetValue(control, value, null);
}
else
{
throw new Exception(control.GetType().ToString() +
"." + propertyName + " is read-only property.");
}
}
}
}
else
{
throw new ArgumentNullException();
}
}
/// <summary>
/// Get a property value on the control thread-safely.
/// </summary>
/// <param name="control">Control on which to GET the property value</param>
/// <param name="propertyName">Property name</param>
/// <return>Property value</return>
public static object GetPropertyValue(Control control, string propertyName)
{
if (control != null && !string.IsNullOrEmpty(propertyName))
{
if (control.InvokeRequired)
{
return control.Invoke(new PropertyGetInvoker(GetPropertyValue),
control, propertyName);
}
else
{
PropertyInfo propertyInfo = GetProperty(control, propertyName);
if (propertyInfo != null)
{
if (propertyInfo.CanRead)
{
return propertyInfo.GetValue(control, null);
}
else
{
throw new Exception(control.GetType().ToString() +
"." + propertyName + " is write-only property.");
}
}
return null;
}
}
else
{
throw new ArgumentNullException();
}
}
}
}
Using the Code
The code is very simple to use.
Let's suppose that we have a WinForm with two textboxes and four buttons.
Each button starts a new thread that executes some method: UnsafeMethod()
, SafeMethod()
, SafeMethod2()
or SafeMethod3()
.
UnsafeMethod()
will fail with a cross-thread exception because the .NET Framework does not allow to access a control created in another thread. To avoid such a situation, we need to check Control.InvokeRequired
property and if it is true
when using Control.Invoke()
method and so on...
SafeInvokeUtils
does exactly the same things for you and a bit more, because now you do not need to check for invoke requirements, just use GetPropertyValue()
or SetPropertyValue()
for control's properties and InvokeMethod()
for control's methods (See: SafeMethod()
, SafeMethod2()
and SafeMethod3()
).
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ThreadStart(UnsafeMethod));
thread.Start();
}
private void button2_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ThreadStart(SafeMethod));
thread.Start();
}
private void button3_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ThreadStart(SafeMethod2));
thread.Start();
}
private void button4_Click(object sender, EventArgs e)
{
Thread thread = new Thread(new ThreadStart(SafeMethod3));
thread.Start();
}
private void UnsafeMethod()
{
this.textBox1.Text = "test";
}
private void SafeMethod()
{
SafeInvokeUtils.SetPropertyValue(this.textBox1, "Text", "Some text");
}
private void SafeMethod2()
{
string text1 = Convert.ToString
(SafeInvokeUtils.GetPropertyValue(this.textBox1, "Text"));
SafeInvokeUtils.SetPropertyValue(this.textBox2, "Text", text1);
}
private void SafeMethod3()
{
SafeInvokeUtils.InvokeMethod(this.textBox1, "Paste", "Some text");
}
}
Points of Interest
I don't know why the guys from Microsoft force us to write more and more workarounds for their code. Probably they are not as good as we are :).
History
- This is version 1.0