Click here to Skip to main content
15,896,063 members
Articles / Programming Languages / C#

Windows Forms Thread Safety: InvalidOperationException and Invoking

Rate me:
Please Sign up or sign in to vote.
4.50/5 (2 votes)
6 Mar 2012CPOL2 min read 13.9K   111   4   2
Windows Forms Thread Safety: InvalidOperationException and Invoking

If you are developing with Windows Forms, you know that due to thread safety reasons, you cannot change any UI control’s settings from threads different from UI thread itself.

image

Assume you have a multithreaded application where you want to acquire some values in parallel and then display them on the UI elements. It could be some server requests (even simultaneous requests to different servers) or just some calculations – it doesn’t really matter now. What matters is that you need to apply some values to the same controls several times (applying text to several "labels" or "text boxes").

For our simple case, let’s assume the situation when you have several "labels" and "combo boxes". During application startup, some calculations will be done and results will be put inside combo boxes. Each label will be marked in accordance with its calculation. As a calculation example, let’s take the simple progression of "x" order. (order = 1: 1, 1, 1, 1, ...; order = 2: 2, 4, 8, 16, ...'; order = 3: 3, 9, 27, 81; etc.) The task is to calculate each sequence in a separate thread.

For somebody who isn't familiar with invoking, the first and naïve solution could be something like this:

C#
using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;
 
namespace WindowsFormsInvoking
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }
 
        private void Form1_Load(object sender, EventArgs e)
        {
            Thread t1 = new Thread(ApplyUiValues);
            Thread t2 = new Thread(ApplyUiValues);
            Thread t3 = new Thread(ApplyUiValues);
            Thread t4 = new Thread(ApplyUiValues);
            Thread t5 = new Thread(ApplyUiValues);
 
            t1.Start(1.0);
            t2.Start(2.0);
            t3.Start(3.0);
            t4.Start(4.0);
            t5.Start(5.0);
        }
 
        private void ApplyUiValues(object order)
        {
            if (order is Double)
            {
                Double o = Convert.ToInt32(order);
                string labelStr = string.Format("Order {0}", o);
                List<Double> values = new List<Double>();
                List<string> strings = new List<string>();
 
                values = GenerateOneToTenSequense(order);
                foreach (Double i in values)
                {
                    strings.Add(i.ToString());
                }
 
                switch ((int)o)
                {
                    case 1:
                        label1.Text = labelStr;
                        comboBox1.Items.AddRange(strings.ToArray());
                        break;
                    case 2:
                        label2.Text = labelStr;
                        comboBox2.Items.AddRange(strings.ToArray());
                        break;
                    case 3:
                        label3.Text = labelStr;
                        comboBox3.Items.AddRange(strings.ToArray());
                        break;
                    case 4:
                        label4.Text = labelStr;
                        comboBox4.Items.AddRange(strings.ToArray());
                        break;
                    case 5:
                        label5.Text = labelStr;
                        comboBox5.Items.AddRange(strings.ToArray());
                        break;
                    default:
                        break;
                }
            }
        }
 
        private List<Double> GenerateOneToTenSequense(object order)
        {
            List<Double> sequence = new List<Double>();
            if (order is Double)
            {
                Double o = Convert.ToInt32(order);
                for (Double i = 1; i <= 10; ++i)
                {
                    sequence.Add(Math.Pow(o, i));
                }
            }
            return sequence;
        }
    }
}

But once you’ll try to run this, you’ll immediately get the following exception:

image

InvalidOperationException – you cannot change UI properties from other threads. MSDN provides a very good explanation on How to: Make Thread-Safe Calls to Windows Forms Controls.

But there is something missing. For example – do I really need to call "Background Worker" for these simple things? What if I have many similar controls (like in my example) – should I repeatedly call InvokeRequired property? I just want to use the simple one line of code similar to this one:

C#
label1.Text = labelStr;

So my solution was to create a helper class with static methods to call most often used controls:

C#
using System;
using System.Collections.Generic;
using System.Windows.Forms;
 
namespace WindowsFormsInvoking
{
    public class Invoker
    {
        #region delegates
 
        delegate void SetComboBoxItemsCallback(
            out bool isSet, 
            ComboBox invokingCtrl, 
            List<string> items);
        
        delegate void SetComboBoxSelectedItemCallback(
            out bool isSet, 
            ComboBox invokingCtrl, 
            int itmToSet);
        
        delegate void TrySetLabelTextCallback(
            out bool isSet, 
            Label invokingCtrl, 
            string valToSet);
 
        #endregion
 
        /// <summary>
        /// Loads Items for a ComboBox from List of strings
        /// </summary>
        /// <param name="caller">Parent control (usually it's a Form)</param>
        /// <param name="invokingCtrl">ComboBox to load items to</param>
        /// <param name="items">Strings to be loaded into the ComboBox</param>
        public static void LoadComboBoxItems(
            Control caller,
            ComboBox invokingCtrl,
            List<string> items)
        {
            try
            {
                bool wasSet = false;
                SetComboBoxItems(out wasSet, invokingCtrl, items);
                if (!wasSet)
                {
                    SetComboBoxItemsCallback cb = 
                        new SetComboBoxItemsCallback(SetComboBoxItems);
                    caller.Invoke(cb, 
                        new object[] { wasSet, invokingCtrl, items });
                }
 
                if (invokingCtrl.Items.Count > 0)
                {
                    Invoker.SetComboBoxSelectedItem(
                        out wasSet, 
                        invokingCtrl, 0);
 
                    if (!wasSet)
                    {
                        SetComboBoxSelectedItemCallback cb =
                            new SetComboBoxSelectedItemCallback(
                                Invoker.SetComboBoxSelectedItem);
 
                        caller.Invoke(cb, 
                            new object[] { wasSet, invokingCtrl, 0 });
                    }
                }
            }
            catch (Exception ex)
            {
                
            }
        }
 
        /// <summary>
        /// Sets a selected item for a ComboBox
        /// </summary>
        /// <param name="caller">Parent control (usually it's a Form)</param>
        /// <param name="invokingCtrl">Label to set text to</param>
        /// <param name="valToSet">String to set</param>
        public static void SetLabelText(
            Control caller,
            Label invokingCtrl,
            string valToSet)
        {
            try
            {
                bool wasSet = false;
                Invoker.TrySetLabelText(out wasSet, 
                    invokingCtrl, valToSet);
                if (!wasSet)
                {
                    TrySetLabelTextCallback cb =
                        new TrySetLabelTextCallback(Invoker.TrySetLabelText);
                    caller.Invoke(cb, new object[] { wasSet, invokingCtrl, valToSet });
                }
            }
            catch (Exception ex)
            {
                
            }
        }
 
        private static void SetComboBoxItems(
            out bool isSet, 
            ComboBox invokingCtrl, 
            List<string> items)
        {
            if (!invokingCtrl.InvokeRequired && 
                invokingCtrl.Items.Count == 0)
            {
                invokingCtrl.Items.AddRange(items.ToArray());
                isSet = true;
            }
 
            isSet = false;
        }
 
        private static void SetComboBoxSelectedItem(
            out bool isSet, 
            ComboBox invokingCtrl, 
            int itmToSet)
        {
            if (!invokingCtrl.InvokeRequired)
            {
                invokingCtrl.SelectedItem = invokingCtrl.Items[itmToSet];
                isSet = true;
            }
 
            isSet = false;
        }
 
        private static void TrySetLabelText(
            out bool isSet, 
            Label invokingCtrl, 
            string valToSet)
        {
            if (!invokingCtrl.InvokeRequired)
            {
                invokingCtrl.Text = valToSet;
                isSet = true;
            }
 
            isSet = false;
        }
    }
}

After creating the helper Invoker class, I’m able to use the property assignment in one line like I wanted:

C#
Invoker.SetLabelText(this, label1, labelStr);

And the result is:

C#
private void ApplyUiValues(object order)
{
    if (order is Double)
    {
        Double o = Convert.ToInt32(order);
        string labelStr = string.Format("Order {0}", o);
        List<Double> values = new List<Double>();
        List<string> strings = new List<string>();
 
        values = GenerateOneToTenSequense(order);
        foreach (Double i in values)
        {
            strings.Add(i.ToString());
        }
 
        switch ((int)o)
        {
            case 1:
                Invoker.SetLabelText(this, label1, labelStr);
                Invoker.LoadComboBoxItems(this, comboBox1, strings);
                break;
            case 2:
                Invoker.SetLabelText(this, label2, labelStr);
                Invoker.LoadComboBoxItems(this, comboBox2, strings);
                break;
            case 3:
                Invoker.SetLabelText(this, label3, labelStr);
                Invoker.LoadComboBoxItems(this, comboBox3, strings);
                break;
            case 4:
                Invoker.SetLabelText(this, label4, labelStr);
                Invoker.LoadComboBoxItems(this, comboBox4, strings);
                break;
            case 5:
                Invoker.SetLabelText(this, label5, labelStr);
                Invoker.LoadComboBoxItems(this, comboBox5, strings);
                break;
            default:
                break;
        }
    }
}

image

You can download this example code from Max's SVN :http://subversion.assembla.com/svn/max-s-blog-posts/

Please leave your comments if you found this post helpful.

Thanks and enjoy your coding!
Max B.

License

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


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionPlease provide your download as zip archiv Pin
Renate Neumann29-Feb-12 9:10
professionalRenate Neumann29-Feb-12 9:10 
It is very cumbersome and tedious these files individually download to.
AnswerRe: Please provide your download as zip archiv Pin
bovykinmaxim6-Mar-12 3:05
bovykinmaxim6-Mar-12 3:05 

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.