65.9K
CodeProject is changing. Read more.
Home

Making Controls Thread-safely

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.10/5 (16 votes)

May 6, 2009

CPOL

2 min read

viewsIcon

38832

downloadIcon

272

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