I'm coding a multi-tier VB.NET winforms application, and decided to take advantage of the BackgroundWorker object from the System.ComponentModel namespace for any cases where code in one of my forms is calling down to methods in one of the application's DLLs. For those not familiar, the BackgroundWorker is a simplified wrapper for situations where you want to implement multi-threading for your GUI-centric application without messing around with the thread plumbing yourself. (And for those wondering why not C#, I've chosen VB.NET because of the very handy Application Framework [1] that is not available for C#, but that's a story for another post.)
I've found the BackgroundWorker to be very handy, but a little tricky to start using at first. I like that it has built-in support for reporting incremental progress for a long-running operation, and that it has a simple way of allowing a long-running operation to be canceled. And the big benefit, of course, is that your form remains responsive to the user and the operating system as it runs on the main UI thread. For a basic tutorial, MSDN has you pretty well covered [2]. But there are some subtleties I had to overcome that are not covered in the documentation, especially in the area of exception handling.
This entry on MSDN about the BackgroundWorker.DoWork() event [3] contains the following paragraph:
If the operation [that you call from your DoWork() handler] raises an exception that your code does not handle, the BackgroundWorker catches the exception and passes it into the RunWorkerCompleted event handler, where it is exposed as the Error property of RunWorkerCompletedEventArgs. If you are running under the Visual Studio debugger, the debugger will break at the point in the DoWork event handler where the unhandled exception was raised.
That's all good, but what MSDN fails to mention is that "unhandled" means that this DoWork() handler must not have Try-Catch block around any of its code. We need to let the error happen unhandled, and then the backgroundworker will take care of getting it to the RunWorkerCompleted() handler. I spent some frustrating time being stuck on this point, because it is my normal practice to put a Try-Catch block in *all* event handlers, because an event handler is effectively the top of the stack. The DoWork() handler is now my one exception to this rule. If you want the control to behave as advertised, then you need to let errors bubble up from your DoWork() handler as advertised.
There is another reason to make sure that you conform to the BackgroundWorker's exception handling model: if you want to manipulate any of your form's controls when an exception comes out of DoWork(), then you *cannot* do that in your DoWork() handler. It's very common to need to do something on your form after an exception, such as disabling a button or changing a label. This must be done in your RunWorkerCompleted() handler, as confirmed by MSDN:
You must be careful not to manipulate any user-interface objects in your DoWork event handler. Instead, communicate to the user interface through the BackgroundWorker events.
As long as you don't put a Try-Catch block in your DoWork() handler, you should fine on this point. However, as soon as you craft your code to work this way, you'll run into another effectively undocumented issue: in a debugging scenario, whether you launched the app from within the IDE or from a compiled debug assembly, whenever you have an exception in any of the code that is called from the DoWork() handler, the debugger will stop on what it of course sees as an unhandled exception--damned if you do, damned if you don't.
If you "handle" the exception with a Try-Catch block in the DoWork() handler, the BackgroundWorker won't work, but as soon as you leave it out, the debugger implements it well intentioned by mostly just annoying behavior of stopping on the line that produced the exception. The trick to get around this is to decorate your code with the DebuggerNonUserCodeAttribute attribute from the System.Diagnostics namespace. I discovered this trick in this thread from the MSDN forums [4]. Here's what the attribute decoration of the DoWork() handler looks like in VB.NET:
<System.Diagnostics.DebuggerNonUserCodeAttribute()> _
Private Sub myBackgroundWorker_DoWork(ByVal sender As Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles verifyFileBWWorker.DoWork
However, I had to take this a step further since in my case what I was calling from my DoWork() handler was a method on a class in a separate DLL assembly--in other words, I was calling code that did not reside within my form, but in another class. What I found was that the debugger still saw exceptions that happened in this child class's code as "unhandled." I solved this by adding the DebuggerNonUserCodeAttribute to the class decoration as well:
<System.Diagnostics.DebuggerNonUserCodeAttribute()> _
Public Class MyClass
Now my BackgroundWorker code behaves in a consistent way whether I'm debugging or not. (Update: an unfortunate side-effect of setting DebuggerNonUserCodeAttribute on the whole class is that the debugger won't visit that class at all, even when you set a breakpoint--so it looks like I'll have to put up with the debugger stopping on "unhandled" exceptions...)
Here's one more gem I was not able to find in the documentation: if, like me, you are implementing the code that you want to call with BackgroundWorker in a separate class and/or DLL (rather than keeping the code in the form, which the simplistic MSDN examples do--a practice I don't recommend if the code in question is not specifically presentation-oriented logic) then you will wonder how you are going to report progress back to the GUI, or check for a cancelation, down in that code that's outside the form. My solution to this was to provide a way for the form to pass the BackgroundWorker object down to the class method. However, part of my goal with putting my business logic is a separate DLL is to make it reusable in other contexts, and I don't want to tie in a hard assumption that the code is being called from a BackgroundWorker. So the way I did this is to make the BackgroundWorker optional in the class. This could be done with a Property, and then the code in the class method could check to see if it's been set:
if (m_myBW != null)
{
m_myBW.ReportProgress(50);
}
You could also accomplish this by making the BackgroundWorker object an argument, but I recommend making the argument optional (in VB.NET) or using overloaded methods (in C#).
Implementing my first BackgroundWorker was more work than it needed to be, but I'll be implementing dozens of these in this application, so I expect the effort to pay dividends. Hopefully this article helps someone else get up to speed more quickly that I was able.
Hope that helps,
Dan