Tuesday, February 13, 2007

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:

publicvoid 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:

delegatevoid Invoker(string parameter);
publicvoid SetTitleSafe(string title)
{
if (this.InvokeRequired)
{
// Execute the same method, but this time on the GUI threadthis.BeginInvoke(new Invoker(SetTitleSafe), title);
// we return immedeatelyreturn;
}
// 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!

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);}

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 ...