Click here to Skip to main content
15,867,568 members
Articles / Programming Languages / C#

Making Controls Thread-safely

Rate me:
Please Sign up or sign in to vote.
4.10/5 (16 votes)
6 May 2009CPOL2 min read 38K   271   58   8
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:

C#
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()).

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Computaris
Moldova (Republic of) Moldova (Republic of)
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
GeneralNice class Pin
aldo hexosa6-May-09 16:52
professionalaldo hexosa6-May-09 16:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.