Search code examples
wpfvb.netmvvmexceptionunobserved-exception

Catch-all Exception Handler for non-UI Threads in WPF


Specifically, I'm using WPF with MVVM. I have a MainWindow, which is a WPF Window where all of the action happens. It uses a corresponding View Model class for its properties, commands, etc.

I have set up main UI thread and non-UI thread exception handlers in Application.xaml.vb StartUp like this:

Private Sub Application_DispatcherUnhandledException(sender As Object, e As Windows.Threading.DispatcherUnhandledExceptionEventArgs) Handles Me.DispatcherUnhandledException
    ' catches main UI thread exceptions only
    ShowDebugOutput(e.Exception)
    e.Handled = True
End Sub

Private Sub Application_Startup(sender As Object, e As StartupEventArgs) Handles Me.Startup
    ' catches background exceptions
    Dim currentDomain As AppDomain = AppDomain.CurrentDomain
    AddHandler currentDomain.UnhandledException, AddressOf UnhandledExceptionHandler
    AddHandler System.Threading.Tasks.TaskScheduler.UnobservedTaskException, AddressOf BackgroundTaskExceptionHandler
End Sub

Sub UnhandledExceptionHandler(sender As Object, args As UnhandledExceptionEventArgs)
    Dim ex As Exception = DirectCast(args.ExceptionObject, Exception)
    ShowDebugOutput(ex)
End Sub

Sub BackgroundTaskExceptionHandler(sender As Object, args As System.Threading.Tasks.UnobservedTaskExceptionEventArgs)
    Dim ex As Exception = DirectCast(args.Exception, Exception)
    ShowDebugOutput(ex)
End Sub

This part works

When I try to test this out, by deliberately throwing an exception, it works. It is actually in the View Model in the Sub that handles the Select All button click.

The button:

<Button Content="Select All" Height="23" Width="110" Command="{Binding SelectAllCommand}" />

The Command where I'm throwing the exception that is successfully caught:

Private Sub SelectAll()
    Throw (New Exception("UI Thread exception"))
    SetAllApplyFlags(True)
End Sub

This part doesn't work

There's another button in the same MainWindow similarly bound to a command. However, it uses a Task to perform its work in the background, and an exception thrown in there does NOT get caught by my catch-all handlers.

Private Sub GeneratePreview()
    ' run in background
    System.Threading.Tasks.Task.Factory.StartNew(
        Sub()
            ' ... stuff snipped out, issue is the same with or without the rest of the code here ...
            Throw (New Exception("Throwing a background thread exception"))
        End Sub)
End Sub

There are several similar questions, but I haven't been able to actually figure out my answer from them. The AppDomain UnhandledException seems to be the answer in most cases, but it isn't for mine. What exactly do I have to add to be able to catch an exception that might be thrown in a non-UI thread this way?

What I ended up doing

I could not get the TaskScheduler.UnobservedTaskException event to call my event handler when I was handling it in Application.xaml.vb. But I took hints from the other answer, and I'll mark it as the answer because it ultimately helped.

However, it is not at the application level, so if this was a larger application, I'd have to duplicate this in every instance where I used a Task. This wasn't really what I was looking for, but not willing to spend more time on it now.

I ended up putting a try-catch inside the Task. In the catch, I was able to use Dispatcher.Invoke to still display a user-friendly dialog with the exception info.

Private Sub GeneratePreview()
    ' run in background
    System.Threading.Tasks.Task.Factory.StartNew(
        Sub()
            Try
                ' ... stuff snipped out, issue is the same with or without the rest of the code here ...
                Throw (New Exception("Throwing a background thread exception"))
            Catch ex As Exception
                Application.Current.Dispatcher.Invoke(DispatcherPriority.Normal, DirectCast(
                    Sub()
                        HRNetToADImport.Application.ShowDebugOutput(ex)
                    End Sub, Action))
            End Try
        End Sub)
End Sub

Solution

  • Sandra,

    There is a way to catch the exception from inside your background Task.
    I admit my solution is not global, but at least it catches and prevents crash !

    Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)
        Dim t As Task = Task.Factory.StartNew(
           Sub()
               ' ... stuff snipped out, issue is the same with or without the rest of the code here ...
               Throw (New Exception("Throwing a background thread exception"))
           End Sub)
        Try
            Await t
        Catch ex1 As Exception
            Debug.WriteLine("Exception from inside task " & ex1.Message)
        End Try
    End Sub
    

    Think it could help, may be you, may be others.