Simple pattern to invoke GUI from another thread
If you are making a GUI application and you are using multiple threads, there is one very important rule: GUI controls can only be accessed from the GUI thread. This is inherent to Windows development and is not a limitation of .NET.
Let me first say that although this seems to be an annoyance for the developer, this is actually a great thing! It means that while you are developing a GUI application you generally don't have to worry about threading issues (locks, deadlocks) because you know that all GUI-access is done from a single thread.
But of course there are situations where you want to start background processing on a seperate thread and access the GUI from this thread. Take for instance this simple example:
public void SetTitleUnsafe(string title) { // When invoked from another thread, this next statement is illegal: this.Text = title; }
If you call this method from a thread that is not a GUI thread, everything may seem to go well at first sight. But because we violated the very important rule, the behaviour of our application is now undefined. Things may (and will) start to go wrong very unpredictably, sometimes much later when there is no obvious relationship with the violation that was made. This makes this problem very hard to find. Among the things that could happen is GUI-events to become 'lost' and the GUI to become unresponsive.
Luckily, form Visual Studio 2005 onward you get a nice error-message when you violate this rule while the debugger is attached:
So now you at least get an immedeate notification that you made a mistake. This one of the reasons why I advise to run your code from Visual Studio (using F5) while you are developing.
To get around our threading-violation, Winforms provides these helper-methods: BeginInvoke, EndInvoke, Invoke, InvokeRequired. But even with these methods available it may not be obvious how to use them in a correct and simple way.
That's why I present this pattern:
delegate void Invoker(string parameter); public void SetTitleSafe(string title) { if (this.InvokeRequired) { // Execute the same method, but this time on the GUI thread this.BeginInvoke(new Invoker(SetTitleSafe), title); // we return immedeately return; } // From here on it is safe to access methods and properties on the GUI // For example: this.Text = title; }
As you can see, you will need to define a delegate that matches your method (or use an existing delegate that is defined elsewhere, such as the System.Windows.Forms.MethodInvoker). This is how the pattern works:
- When the method is called from a thread that is not the GUI thread, InvokeRequired will be true. The method will be wrapped in a delegate and passed to the BeginInvoke method (together with the parameters). We return immedeately. BeginInvoke guarantees that some time later our method will be called on the GUI thread.
- When the method is called on the GUI thread, InvokeRequired returns false so we just go forward and access the GUI in any way we like.
This is in my opinion the simplest and shortest pattern that is always correct.
From .NET 2.0 onwards the same pattern can be written using an anonymous method but that is less readable in my opinion, so I prefer to keep this pattern.
Also in .NET 2.0 you can use the BackgroundWorker class that handles all the details behind your back. But the same principle still applies: never access GUI from another thread!
11 comments:
I prefer a small helper class like this one, it makes the code even cleaner and shorter:
using System.Windows.Forms;
public static class ControlsHelper
{
public static void SyncBeginInvoke(control, MethodInvoker del)
{
if ((control != null) && control.InvokeRequired)
control.BeginInvoke(del, null);
else
del();
}
}
private void SetLabelText(int number)
{
ControlsHelper.SyncBeginInvoke(this, delegate()
{
label.Text = number.ToString();
});
}
Sorry, don't know how to format the code correctly, pre doesn't work.
I have a quick question for you guys. I was running into this illegal cross thread problem recently, so I'm having to refactor my code to use delegates.
I'm very new to delegates and very new to C# and winforms, so bear with me =)
I have a Deployer class to which I pass a RichTextBox control from my main form like so:
Deployer myDeployer = new Deployer(outputWindow);
Then, in my Deployer class, I have the following code for clearing and writing to the output window:
delegate void PrintToOutputWindowDelegate(string msg);
protected void PrintToOutputWindow(string msg)
{
if (outputWindow.InvokeRequired)
{
outputWindow.BeginInvoke(new PrintToOutputWindowDelegate(PrintToOutputWindow), msg);
}
// print the message in the output window
PrintToOutputWindow(msg);
}
delegate void ClearOutputWindowDelegate();
protected void ClearOutputWindow()
{
if (outputWindow.InvokeRequired)
{
outputWindow.BeginInvoke(new ClearOutputWindowDelegate(ClearOutputWindow));
}
// clear the output window
ClearOutputWindow();
}
But I'm getting a StackOverflowException on the recursive call. Basically, it seems that outputWindow.InvokeRequired is always returning true, even after the BeginInvoke call.
Am I doing something wrong?
Ah nevermind. I was calling the function again instead of clearing the output window. =)
adam: you're getting a stack overflow on the GUI-thread because you keep on invoking the same method - but you don't have any code that actually does something with the RichTextBox control.
This is how I would correct your first example:
delegate void PrintToOutputWindowDelegate(string msg);
protected void PrintToOutputWindow(string msg)
{
if (outputWindow.InvokeRequired)
{
outputWindow.BeginInvoke(new PrintToOutputWindowDelegate(PrintToOutputWindow), msg);
}
// print the message in the output window
outputWindow.Text += msg;
}
Thanks for the pattern. I was trying to self-teach multi-threading with gui and your pattern clarified the many things i was reading.
Hello,
Can some1 explain me, why the "Invalid Thread Operation" is only thrown in debug mode? When I set for example the title unsave and I run the code without debugger, I got no response that there maybe oncurred an error ...
Thx for your help. Nice pattern anyway.
Best Regards,
Jan
This pattern solved my problem with multi threads and a richtextbox. Clean and simple.
Thanks
Jan
public static class InvokeUtil
{
public static void SafeInvoke(this Control control, MethodInvoker del)
{
if (control.InvokeRequired)
{
control.Invoke(delegate);
}
else
{
del();
}
}
USE:
private void SetLabelText(int number)
{
this.SafeInvoke(delegate
{
label.Text = number.ToString();
}
}
@Anonymous: I would change your code to use BeginInvoke instead of Invoke. See one of my other posts to see why.
Really nice post !!!
Seeeee perfecto, exacto lo que necesitaba, gracias!
Post a Comment