Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WinForms

Invoking events without worrying about cross-thread exceptions

13 May 2010CPOL3 min read 61.4K  
Eliminate InvalidOperationException on events called across threads that change control properties.

Introduction

InvalidOperationException: "Cross-thread operation not valid: Control '<name>' accessed from a thread other than the thread it was created on."

This exception is something often seen by someone who needs work done on a background thread to leave the application UI responsive to users and maybe even allow the user to cancel the current operation, while changing properties of controls on the UI from within the background operation.

I will explain here a solution for dealing with this particular problem, giving a scenario in which the solution is appropriate, where I use Extension Methods to provide a general solution to the problem.

To implement custom user controls that deal with cross thread property changes themselves, please see the section on Custom/User Controls.

Background

It should be noted that when particular operations need to be executed in the background, the BackgroundWorker is by far the best solution.

However, it is not always possible to use the BackgroundWorker 'as is', and you could even have to write your own thread handling classes. This is where the following extension method comes in handy.

The Problem

Consider the scenario in which you want to wrap a BackgroundWorker and provide an extra event that reports the status message of the work being done in the worker.

C#
public class ThreadedCall
{
    // The worker being wrapped by our class
    private BackgroundWorker bw;
    // Our new event that will be called
    // whenever our class wants to report a status
    public event StatusChangedEventHandler StatusChanged;

    // The entry point of our class to start background opperation
    public void Start()
    {
        bw = new BackgroundWorker();
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.RunWorkerAsync();
    }

    // The method that does the actual work, run inside our wrapped worker
    void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        // Report a status here
        if (StatusChanged!= null)
            StatusChanged.Invoke(this, 
               new StatusChangedEventArgs("Phase 1"));
        Thread.Sleep(100);  // Do some work
        // Report another status here
        if (StatusChanged!= null)
            StatusChanged.Invoke(this, 
               new StatusChangedEventArgs("Phase 2"));
    }
}

Our event handler code:

C#
// Delegate to eventhandler that takes a StatusChangedEventArgs as paramater
public delegate void StatusChangedEventHandler(object sender, StatusChangedEventArgs e);
public class StatusChangedEventArgs : EventArgs
{
    private string status;
    // The status property added to the eventargs used to supply status to the callee
    public string Status
    {
        get { return status; }
        set { status = value; }
    }
    public StatusChangedEventArgs(string status)
    {
        this.status = status;
    }
}

Some code to test our class (the UI code):

C#
private void button1_Click(object sender, EventArgs e)
{
    // Instantiate and start or worker wrapper
    ThreadedCall t = new ThreadedCall();
    t.StatusChanged += new StatusChangedEventHandler(t_StatusChanged);
    t.Start();
}

void t_StatusChanged(object sender, StatusChangedEventArgs e)
{
    // The following line will raise an exception!
    textBox1.Text = string.Format("Status: {0}", e.Status);
}

The Exception

The sample given above will receive an InvalidOperationException where indicated. This is perfectly valid, as we are not allowed to change certain properties of controls on threads other than the thread it was created on.

I sometimes, due to laziness, disable these exceptions using the following code:

C#
Form.CheckForIllegalCrossThreadCalls = false; 

But this is very bad coding practice, and will lead to unintended\unexpected behaviour on the part of your controls. The correct way to deal with this problem would be to create a delegate inside your callee\UI, and check the Control.InvokeRequired property, and then call Control.Invoke with your new delegate to change the desired property.

Although this is correct, I find that it is difficult to read, and does not appear as elegant as it should be.

The Solution

Following the correct solution mentioned above, I moved the invoking of methods away from the UI and into an extension method, making it extremely reusable. This will check if invoking is required, and then invoke the delegate on the control's creation thread and not on the background thread.

C#
public static class DelegateExpansion
{
    // Prevent CrossThreadException by invoking delegate through target control's thread.
    public static object CrossInvoke(this Delegate delgt,object sender,EventArgs e)
    {
        if (delgt.Target is Control && ((Control)delgt.Target).InvokeRequired)
        {
            return ((Control)delgt.Target).Invoke(delgt, new object[] { sender, e });
        }
        return delgt.Method.Invoke(delgt.Target, new object[] { sender, e });
    }
}

The background code then needs to be changed to not use the standard delegate Invoke but our new CrossInvoke method, as follows:

C#
// The method that does the actual work, run inside our wrapped worker
void bw_DoWork(object sender, DoWorkEventArgs e)
{
    // Report a status here
    if (StatusChanged!= null)
        StatusChanged.CrossInvoke(this, 
           new StatusChangedEventArgs("Phase 1"));
    Thread.Sleep(100);  // Do some work
    // Report another status here
    if (StatusChanged!= null)
        StatusChanged.CrossInvoke(this, 
          new StatusChangedEventArgs("Phase 2"));
}

This eliminates the need to change or complicate any of our GUI code, keeping it readable and 'clean'.

Custom/User Controls

When designing controls for use by other developers, the solution above would not mean much. In such cases, you could make the control more 'thread safe' by simply changing the properties to accommodate for cross thread invoking.

Example:

C#
private string innerText;
public string Text
{
    get { return innerText; }
    set
    {
        if (this.InvokeRequired)
            this.Invoke(new MethodInvoker(() => { innerText = value; }));
        else innerText = value;
    }
}

This would ensure that your control properties could be accessed without the need for external callers to worry about invoking on the correct thread.

History

  • 12/05/2010 - Original article.
  • 13/05/2010 - Added the Custom/User Controls section.

License

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