Click here to Skip to main content
Click here to Skip to main content

Making Controls Thread-safely

By , 6 May 2009
 

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

License

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

About the Author

Sergiu Josan
Software Developer Computaris
Moldova (Republic Of) Moldova (Republic Of)
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralExtension to your codemembervicoB12 Jul '10 - 7:27 
Thank you very much, very usefull code!
 
However:
Your code could not handle 'controls' like ' ToolStripMenuItem' , because this item cannot be cast into a control.
 
I have [extended] your code. Hope this helps some people.
 
Sincerely,
Vico
GeneralMore Elegant Approach - Using Extension Method, Lamda's and GenericsmemberChris Stefano12 May '09 - 7:53 
There's a far more elegant, and type-safe way to do this, with extension methods, delegates (or lamda's) and generics.
Check out the following code:
 
/* taken from http://dotnet.org.za/virtualstaticvoid/archive/2009/01/24/safe-controlinvoke-with-lambda-s.aspx */
using System;
using System.Windows.Forms;
 
namespace Patterns
{
 
  public static class ControlInvokeSupport
  {
 
    public delegate void SafeControlInvokeHandler<TControl>(TControl control)
      where TControl : Control;
 
    public static void SafeControlInvoke<TControl>(this TControl control, SafeControlInvokeHandler<TControl> action)
      where TControl : Control
    {
      if (control != null && control.InvokeRequired)
      {
        IAsyncResult asyncResult = control.BeginInvoke(action, new object[] { control });
        control.EndInvoke(asyncResult);
      }
      else
      {
        action(control);
      }
    }
 
    public delegate TReturn SafeControlInvokeHandler<TReturn, TControl>(TControl control)
      where TControl : Control;
 
    public static TReturn SafeControlInvoke<TReturn, TControl>(this TControl control, SafeControlInvokeHandler<TReturn, TControl> action)
      where TControl : Control
    {
      if (control != null && control.InvokeRequired)
      {
        IAsyncResult asyncResult = control.BeginInvoke(action, new object[] { control });
        return (TReturn)control.EndInvoke(asyncResult);
      }
      return action(control);
    }
 
    public delegate TReturn SafeControlInvokeHandler<TReturn, TControl, TArg>(TControl control, TArg args)
      where TControl : Control;
 
    public static TReturn SafeControlInvoke<TReturn, TControl, TArg>(this TControl control, TArg args, SafeControlInvokeHandler<TReturn, TControl, TArg> action)
      where TControl : Control
    {
      if (control != null && control.InvokeRequired)
      {
        IAsyncResult asyncResult = control.BeginInvoke(action, new object[] { control, args });
        return (TReturn)control.EndInvoke(asyncResult);
      }
      return action(control, args);
    }
 
  }
}
 
Some examples of using this approach:
 
// to set a button's enabled state from another thread
this.button.SafeControlInvoke(button => button.Enabled = true);
 
// to load a ListView items collection from another thread
IList listViewItems = ... 
this.listViewDisplay.SafeControlInvoke
  (
    listView =>
      {
        listView.BeginUpdate();
        listView.Items.Clear();
        listView.Items.AddRange(listViewItems);
        listView.EndUpdate();
      }
  );

GeneralAnother optionmemberPaul B.12 May '09 - 3:15 
See [^] for an option that is strong typed.
GeneralInvoke vs BeginInvokemembersupercat97 May '09 - 6:23 
I generally prefer to avoid using Invoke on controls when possible, since it will make the performance of the non-UI thread highly dependent upon that of the UI thread. If some function on the UI thread takes 100ms to execute, the Control.Invoke will block until that has happened.
 
Obviously there are some types of controls for which precise sequencing is mandatory and there isn't any real alternative to using Control.Invoke. Generally, though, I prefer to use an integer to keep track of whether an update is pending; when I change information related to the control, I Interlocked.Exchange the integer with '1'; if it was zero, I call Control.BeginInvoke. The first step of the control's update routine is to set the update flag to zero. If an attempt is made to change the control's data after the update has begun, the update will be performed again (to allow for the fact that the control may or may not have been updated with new data). If many updates to the control are attempted before the UI thread gets around to drawing, the earlier updates will essentially be skipped. Thus, something like a progress bar may be updated as many times as desired without overly dogging performance.
GeneralMany thanksmemberLuca Bonotto6 May '09 - 22:56 
Usefull class ! Thanks
GeneralWhere is the codememberJohann Krenn6 May '09 - 20:23 
The link to the source code seems to be broken.
GeneralRe: Where is the codememberEnzocom6 May '09 - 20:47 
If You have any questions please email me at enzocom@gmail.com
 
Enzocom

GeneralNice classmemberaldo hexosa6 May '09 - 16:52 
Thanks for your hard work Smile | :)

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

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130516.1 | Last Updated 6 May 2009
Article Copyright 2009 by Sergiu Josan
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid