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.
public class ThreadedCall
{
private BackgroundWorker bw;
public event StatusChangedEventHandler StatusChanged;
public void Start()
{
bw = new BackgroundWorker();
bw.DoWork += new DoWorkEventHandler(bw_DoWork);
bw.RunWorkerAsync();
}
void bw_DoWork(object sender, DoWorkEventArgs e)
{
if (StatusChanged!= null)
StatusChanged.Invoke(this,
new StatusChangedEventArgs("Phase 1"));
Thread.Sleep(100);
if (StatusChanged!= null)
StatusChanged.Invoke(this,
new StatusChangedEventArgs("Phase 2"));
}
}
Our event handler code:
public delegate void StatusChangedEventHandler(object sender, StatusChangedEventArgs e);
public class StatusChangedEventArgs : EventArgs
{
private string status;
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):
private void button1_Click(object sender, EventArgs e)
{
ThreadedCall t = new ThreadedCall();
t.StatusChanged += new StatusChangedEventHandler(t_StatusChanged);
t.Start();
}
void t_StatusChanged(object sender, StatusChangedEventArgs e)
{
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:
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.
public static class DelegateExpansion
{
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:
void bw_DoWork(object sender, DoWorkEventArgs e)
{
if (StatusChanged!= null)
StatusChanged.CrossInvoke(this,
new StatusChangedEventArgs("Phase 1"));
Thread.Sleep(100);
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:
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.