Tuesday, November 30, 2010

WPF - Background Worker Exception Model

In this post, let us discuss about special exception handling mechanism for BackgroundWorker.

Let us create an application with a view with single button. The button click causes some time consuming operation. During this operation, some exception is generated. The XAML definition of the view is as follows:

<Window x:Class="BackgroundWorkerExceptionHandling.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BackgroundWorkerExceptionHandling"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainWindowViewModel />
</Window.DataContext>
<Grid>
<Button Content="Start Work!" Height="97"
HorizontalAlignment="Left" Margin="70,109,0,0"
Name="button1" VerticalAlignment="Top" Width="372"
Command = "{Binding StartWorkCommand}"/>
</Grid>
</Window>

The above view is expecting a class named MainWindowViewModel and using that as its DataContext. It is also expecting that this view model has an ICommand property, StartWorkCommand. We are defining StartWorkCommand as RelayCommand. When this command is executed, we start a BackgroundWorker. Before that we specify handlers for DoWork and RunWorkerCompleted events.

class MainWindowViewModel : DependencyObject
{
[DebuggerNonUserCode]
void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
}

[DebuggerNonUserCode]
void worker_DoWork(object sender, DoWorkEventArgs e)
{
throw new NotImplementedException();
}

private BackgroundWorker worker;

RelayCommand _startWorkCommand;
public ICommand StartWorkCommand
{
get
{
if (_startWorkCommand == null)
{
_startWorkCommand = new RelayCommand(param => this.StartWork(),
param => this.CanStartWork);
}
return _startWorkCommand;
}
}

public void StartWork()
{
worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(worker_DoWork);
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
worker.RunWorkerAsync();
}

bool CanStartWork
{
get { return true; }
}
}

You can get RelayCommand definition from Josh Smith’s article:

http://msdn.microsoft.com/en-us/magazine/dd419663.aspx

Clicking the button, we expect that the application would crash as we have thrown exception in the DoWork. What happened? Let us try to understand! We try to see if we are able to catch the exception using Application’s DispatcherUnhandledException AppDomain’s UnhandledException event. AppDomain’s exception handler makes sense as BackgroundWorker executes in a background thread. From msdn:

“The BackgroundWorker class allows you to run an operation on a separate, dedicated thread.”

[ http://msdn.microsoft.com/en-us/library/4852et58.aspx ]

public partial class MainWindow : Window
{

public MainWindow()
{
InitializeComponent();

Application.Current.DispatcherUnhandledException += new System.Windows.Threading.DispatcherUnhandledExceptionEventHandler(Current_DispatcherUnhandledException);
AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
}

void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
MessageBox.Show(string.Format("CurrentDomain_UnhandledException : {0}", e.ExceptionObject.ToString()));
//MessageBox.Show(e.ExceptionObject.ToString());
}

void Current_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
{
MessageBox.Show(string.Format("Current_DispatcherUnhandledException: {0}", e.Exception.Message));
e.Handled = true;
}
}

Now run the application. Still we are not able to catch it. What is going on? This seems weird as application is also not crashing which happens when there is an exception which is not handled in the application. This makes us belief application is handled somewhere. But where?

I think I have created enough suspense for light hearted ones J. Let me reveal now what is going on. Basically BackgroundWorker handles all exception in DoWork’s handler and provide the details in the handler for RunWorkerCompleted in the argument for RunWorkerCompletedEventArgs. Let us update our example as follows to get the exception in DoWork. Let us update handler for RunWorkerCompleted as follows:

void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(string.Format("worker_RunWorkerCompleted: {0}", e.Error.Message));
}
}

Now when we run the application and click the button we get this message box.



As we have seen that RunWorkerCompleted gets raised if DoWork is done execution as a result of completion of work, exception or cancellation from user. If it is raised for any reason other than completion of work then accessing result from RunWorkerCompletedEventArgs.Result would raise a new exception. If we don’t want to handle this exception here then we can just rethrow this exception as below. Then it would be caught by Application.DispatcherUnhandledException ‘s handler.

void worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
throw e.Error;
}
}

This results in this message box:


In the handler for DispatcherUandledException, we have set Handled as true. If we don’t do it then it would be caught by AppDomain’s exception handler for any thread. The following messagebox is displayed. In this handler, we can no longer stop application from shutting down.



If we are running it outside Visual Studio then generalized OS error message is also displayed:

No comments: