Wednesday, June 30, 2010

Control.Invoke and exceptions - part 1

Windows Forms in the .Net Framework are now becoming a bit outdated, but they are still widely used.

A common problem with their use is that long computations or tasks that involve gathering data from the web tend to freeze the user interface if executed in the main thread (more precisely, the thread that created the handle, and where form events are run). This impacts the user experience, so the common solution is to resort to background threads to do the hard work, and free the main thread to keep the application always responsive.

Using background threads used to be a tricky task, in Windows programming. Well, it still is, even if newer releases of the .Net Framework have brought tools such as the BackgroundWorker that frees the developer from handling much of the hassle while leaving him with full control of what's going on.

The most common issues involving multithreading in Windows Forms is that almost any call to all classes derived from Control must be done from the main thread. If you call a method from a different thread, and you are lucky, the IDE at runtime warns you that you are making an illegal call, but such problem may slip undetected into release, and then your code might even work as expected in 99% of the times... in the remaining 1% of the runs though your application will behave erratically, and you won't be able to discover why...

Here is a note in the Msdn documentation that remarks that nearly all methods of Control require to be called from the control's thread.

Note

In addition to the InvokeRequired property, there are four methods on a control that are thread safe:
Invoke
, BeginInvoke, EndInvoke, and CreateGraphics if the handle for the control has already been created. Calling CreateGraphics before the control's handle has been created on a background thread can cause illegal cross thread calls. For all other method calls, you should use one of the invoke methods to marshal the call to the control's thread.

A practical way to call methods in Control and derived classes, from your code that runs in the background, is to call Invoke() passing a delegate to the method you need to be called, and any arguments that you need to pass to the method.

The background thread will wait for the call to complete on the main thread, then your background thread will proceed; any return value of the called method is returned by Invoke(), so it can be used by your code. Even exceptions thrown in the method invoked are rethrown.

Everything seems to work fine, but let's make an example, and take a look at the stack trace of the exception:

using System;
using System.Windows.Forms;
using System.Threading;

static class Program {
[STAThread]
static void Main() {
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new TestForm());
}
}

public class TestForm : Form {
private Button btn;
public TestForm() {
// Just add a simple button with a Click handler
btn = new Button();
btn.Text = "Click me";
btn.Click += new EventHandler(btn_Click);
this.Controls.Add(btn);
}

void btn_Click(object sender, EventArgs e) {
// We are creating a WorkItem to be run
// on a thread from the thread pool
WaitCallback wc = new WaitCallback(this.DoTheWork);
ThreadPool.QueueUserWorkItem(wc);
}

void DoTheWork(object sender) {
// This code is run in a background thread
try {
// We are using Invoke so the call
// to ThrowWithNiceStackTrace() runs in the main thread
this.Invoke(new MethodInvoker(
this.ThrowWithNiceStackTrace));
}
catch (Exception ex) {
string stackTrace = ex.StackTrace;
}
}
private void ThrowWithNiceStackTrace() {
// This code is run in the main thread;
// calling Control methods is legal
this.btn.Text = "Clicked";
this.ThrowWithLongStackTrace();
}
private void ThrowWithLongStackTrace() {
this.ThrowDivByZero();
}
private void ThrowDivByZero() {
int i = 1 / string.Empty.Length;
}
}

If you set a breakpoint in the DoTheWork method, inside the catch clause, you can see the stack trace property of our DivideByZeroException caught:

at System.Windows.Forms.Control.MarshaledInvoke(Control caller, Delegate method, Object[] args, Boolean synchronous)
at System.Windows.Forms.Control.Invoke(Delegate method, Object[] args)
at System.Windows.Forms.Control.Invoke(Delegate method)
at TestForm.DoTheWork(Object sender) in C:\TestInvoke\Program.cs:line 36

Hmmm... missing something? Wasn't our exception thrown in the ThrowDivByZero() method? There seems to be no trace of this in the stack trace!

But stay tuned, there's more to come...

No comments:

Post a Comment