In Winforms only exists one thread for the UI namely UI thread, that can be accessed from all the classes that extends and use the System.Windows.Forms.Control
class and its subclasses members. If you try to access this Thread from another, you will cause this cross-thread exception.
For example, check the following code:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Threading;
namespace YourNamespace
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
Thread TypingThread = new Thread(delegate () {
// Lock the thread for 5 seconds
uiLockingTask();
// Change the status of the buttons inside the TypingThread
// This will throw an exception:
button1.Enabled = true;
button2.Enabled = false;
});
// Change the status of the buttons in the Main UI thread
button1.Enabled = false;
button2.Enabled = true;
TypingThread.Start();
}
/**
* Your Heavy task that locks the UI
*
*/
private void uiLockingTask(){
Thread.Sleep(5000);
}
}
}
How to solve it?
According to the situation of your development process, project and time, you may use 2 solutions for this problem:
A. The quick and dirty solution
You need to verify wheter an invoke function is required to do some changes in the control, if it's the case you can delegate a method invoker as shown in the following example:
private void button1_Click(object sender, EventArgs e)
{
Thread TypingThread = new Thread(delegate () {
heavyBackgroundTask();
// Change the status of the buttons inside the TypingThread
// This won't throw an exception anymore !
if (button1.InvokeRequired)
{
button1.Invoke(new MethodInvoker(delegate
{
button1.Enabled = true;
button2.Enabled = false;
}));
}
});
// Change the status of the buttons in the Main UI thread
button1.Enabled = false;
button2.Enabled = true;
TypingThread.Start();
}
This won't throw an exception anymore. The reason why this way is dirty, is because if you want to perform computing expensive operations, you need always use a separate thread instead of the main thread.
B. The right but not so quick way
Are you here? Well, usually no one reads the right way, however let's continue. In this case, to make it the right way, you will need to think and redesign carefully the algorithm of your application because your current model is failing theoretically someway. In our example, our heavy task is a simple function that waits for 5 seconds and then allow you to continue. In the real life, heavy tasks may become really expensive and shouldn't be executed in the main thread.
So what's the right way to do it? Execute your heavy task within another thread and send messages from this thread to the main thread (UI) to update the controls. This can be achieved thanks to the AsyncOperationManager of .NET that gets or sets the synchronization context for the asynchronous operation.
The first you need to do is to create a class that returns some kind of response. As previously mentioned, think of this way as callbacks, where a callback set in the main thread will be executed when something happens in another thread. This response can be a simple class where a message is returned:
public class HeavyTaskResponse
{
private readonly string message;
public HeavyTaskResponse(string msg)
{
this.message = msg;
}
public string Message { get { return message; } }
}
The class is required as you may want to set multiple information from the second thread to the main as multiple strings, numbers, objects etc. Now it's important to include the namespace to access the Synchronization Context so don't forget to add it at the beginning of your class:
using System.ComponentModel;
Now, the first you need to do is to think about which callbacks will be triggered from the second thread. The callbacks need to be declared as an EventHandler of the type of the response (in our example HeavyTaskResponse
) and they're obviously empty. They need to be public as you need to attach the callbacks in the declaration of a new HeavyTask instance. Our thread will run indefinitely, so create a boolean variable accessible by the entire class (in this case HeavyProcessStopped
).Then expose a readonly instance of the Synchronization Context class to be accesible by all the methods of the class. It's very important now to update the value of this SyncContext variable in the constructor with the value of AsyncOperationManager.SynchronizationContext, otherwise the main point of this class won't be used.
As next come some logic of Threads, in this case our HeavyTask
class exposes itself as a class that will run some expensive function within a new thread. This HeavyTask can be started and stoped as a timer, so you need to create 3 methods namely Start
, Stop
and Run
. The Start
method runs a thread with the Run
method as argument and runs in the background. The run method runs indefinitely with a while loop until the value of our boolean flag HeavyProcessStopped
is set to true by the Stop
method.
Inside the while loop you can now execute the task that would look the Main thread (UI) without a problem because it's running inside a thread. Now, if you need to update any kind of control, in this example some buttons, you won't do it inside the HeavyTask class but you will send a "notification" to the main thread that it should update some button by using our callbacks. The callbacks can be triggered thanks to the SyncContext.Post method that receives a SendOrPostCallback as first argument (that receives in turn the HeavyTaskResponse instance that will be sent to the main thread with your callback) and the sender object that can be null in this case. This SendOrPostCallback needs to be a method from the second thread class that receives as first agument the instance of HeavyTask and triggers the callback if it's set. In our example we'll trigger 2 callbacks:
Note
Remember that the callbacks can be triggered multiple times according to your needs, in this example we have just triggered them once.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
class HeavyTask
{
// Boolean that indicates wheter the process is running or has been stopped
private bool HeavyProcessStopped;
// Expose the SynchronizationContext on the entire class
private readonly SynchronizationContext SyncContext;
// Create the 2 Callbacks containers
public event EventHandler<HeavyTaskResponse> Callback1;
public event EventHandler<HeavyTaskResponse> Callback2;
// Constructor of your heavy task
public HeavyTask()
{
// Important to update the value of SyncContext in the constructor with
// the SynchronizationContext of the AsyncOperationManager
SyncContext = AsyncOperationManager.SynchronizationContext;
}
// Method to start the thread
public void Start()
{
Thread thread = new Thread(Run);
thread.IsBackground = true;
thread.Start();
}
// Method to stop the thread
public void Stop()
{
HeavyProcessStopped = true;
}
// Method where the main logic of your heavy task happens
private void Run()
{
while (!HeavyProcessStopped)
{
// In our example just wait 2 seconds and that's it
// in your own class it may take more if is heavy etc.
Thread.Sleep(2000);
// Trigger the first callback from background thread to the main thread (UI)
// the callback enables the first button !
SyncContext.Post(e => triggerCallback1(
new HeavyTaskResponse("Some Dummy data that can be replaced")
), null);
// Wait another 2 seconds for more heavy tasks ...
Thread.Sleep(2000);
// Trigger second callback from background thread to the main thread (UI)
SyncContext.Post(e => triggerCallback2(
new HeavyTaskResponse("This is more information that can be sent to the main thread")
), null);
// The "heavy task" finished with its things, so stop it.
Stop();
}
}
// Methods that executes the callbacks only if they were set during the instantiation of
// the HeavyTask class !
private void triggerCallback1(HeavyTaskResponse response)
{
// If the callback 1 was set use it and send the given data (HeavyTaskResponse)
Callback1?.Invoke(this, response);
}
private void triggerCallback2(HeavyTaskResponse response)
{
// If the callback 2 was set use it and send the given data (HeavyTaskResponse)
Callback2?.Invoke(this, response);
}
}
Now that our heavy task can notify us in the main thread when a control can be updated, you will need to declare the callbacks in the new instance of HeavyTask:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace YourNamespace
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void Form1_Load(object sender, EventArgs e)
{
// Create an instance of the heavy task
HeavyTask hvtask = new HeavyTask();
// You can create multiple callbacks
// or just one ...
hvtask.Callback1 += CallbackChangeFirstButton;
hvtask.Callback2 += CallbackChangeSecondButton;
hvtask.Start();
}
private void CallbackChangeFirstButton(object sender, HeavyTaskResponse response)
{
// Access the button in the main thread :)
button1.Enabled = true;
// prints: Some Dummy data that can be replaced
Console.WriteLine(response.Message);
}
private void CallbackChangeSecondButton(object sender, HeavyTaskResponse response)
{
// Access the button in the main thread :)
button2.Enabled = false;
// prints: This is more information that can be sent to the main thread
Console.WriteLine(response.Message);
}
}
}
For our example it was pretty easy to create another class for our HeavyTask that in our initial example was a single function in the main thread. This method is the recommended way to go as it makes your code pretty maintainable, dynamic and useful. If you don't get it with this example, we recommend you to read another awesome article about the same method here in another blog here.
Happy coding !