Windows Forms アプリケーションでは未処理の例外を Application.ThreadExceptionイベントや AppDomain.UnhandledException イベントで処理することができました。また Visual Basic のアプリケーション フレームワークでは WindowsFormsApplicationBase.UnhandledException イベントで処理していました。WPF アプリケーションではどのように未処理の例外を処理できるのでしょうか。

Application.DispatcherUnhandledException イベント

 WPF では Application.DispatcherUnhandledException イベントで未処理の例外を処理することができます。これは WindowsFormsApplicationBase.UnhandledException イベントに似ています。未処理の例外を処理しなかった場合は、既定のウィンドウが表示されアプリケーションが終了します(既定のウィンドウといっても、これは OS 側で表示しているウィンドウだと思います)。

異常終了時のウィンドウ1 異常終了時のウィンドウ2

 DispatcherUnhandledException イベントでは発生した例外の情報と、例外の発生元の Dispatcher の情報を取得することができます。このイベントの引数の Handled プロパティを True にすることで、アプリケーションを終了しないようにできます。独自のメッセージを表示してからアプリケーションを終了する場合にも、Handled を True にしてから Application.Shutdown メソッドを呼びます。そうしないと既定のウィンドウが表示されてしまいます。

独自のメッセージを表示してアプリケーションを終了する例
Class Application
    Private Sub Application_DispatcherUnhandledException(ByVal sender As System.Object, ByVal e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs)
        MessageBox.Show(e.Exception.ToString) ' 独自のメッセージを表示
        e.Handled = True ' 既定のウィンドウが表示されないようにする
        Me.Shutdown() ' Handled = True にするとアプリケーションが終了しないので、終了させる
    End Sub
End Class

 このイベントで全ての未処理の例外をキャッチできるわけではありません。キャッチできる例外は次のものです。

DispatcherUnhandledException イベントでキャッチできる例外
  • Application クラスが実行しているスレッドで発生した未処理の例外

 Application クラスが実行しているスレッドはメイン UI スレッドです。そのスレッド上で発生した例外のみキャッチすることができます。そのため、次のような例外はキャッチすることができません。

DispatcherUnhandledException イベントでキャッチできない例外
  • セカンダリスレッド上で発生した未処理の例外

 バックグラウンドで処理を行うためなどに新たにスレッドを生成した場合、そのスレッドで発生した例外をキャッチすることはできません。一応、次のようにすればキャッチすることができます。

セカンダリスレッドの未処理の例外をキャッチする方法
Imports System.Threading
Imports System.Windows.Threading

Class Window1

    Private Sub Button1_Click(...) Handles Button1.Click
        ' ボタンをクリックした時にセカンダリスレッドのメソッドを呼びます。
        Dim t As New Thread(New ThreadStart(AddressOf ThrowSecondaryThreadException))
        t.Start()
    End Sub

    ' セカンダリスレッドで実行されます。
    Private Sub ThrowSecondaryThreadException()
        Try
            Throw New Exception("セカンダリスレッドで例外を発生させます。")
        Catch ex As Exception
            ' 例外をキャッチし、メイン UI スレッドのメソッドに例外を渡します。
            Application.Current.Dispatcher.Invoke( 
                DispatcherPriority.Send, 
                New DispatcherOperationCallback(AddressOf ThrowMainThreadException), 
                ex)
        End Try
    End Sub

    ' メイン UI スレッドで実行されます。
    Private Function ThrowMainThreadException(ByVal arg As Object) As Object
        ' 例外を再スローします。
        Throw New Exception("メイン UI スレッドで例外を再スローします。", CType(arg, Exception))
    End Function

End Class

 このようにセカンダリスレッド上で例外をキャッチし、それをメイン UI スレッド上に渡し、そこから例外を再スローする必要があります。なんだか例外をキャッチしている時点で、未処理の例外という感じがしませんが・・・。

 実はこんなことをしなくても Windows Forms で使用していた AppDomain.UnhandledException イベントを使用すれば、セカンダリスレッドで発生した例外をキャッチすることができます。ただし AppDomain.UnhandledException イベントで例外をキャッチしても、既定のウィンドウが表示されアプリケーションは終了してしまいます。

まとめ

  • 未処理の例外を処理しない場合は、アプリケーションが終了する。
  • Application.DispatcherUnhandledException イベントでメイン UI スレッド上の未処理の例外を処理することができる。
  • AppDomain.UnhandledException イベントでセカンダリスレッド上から発生した例外をキャッチすることができるが、アプリケーションは終了する。

 WPF の未処理の例外の処理は WindowsFormsApplicationBase.UnhandledException イベントが DispatcherUnhandledException イベントに置き換わっている思えば、違いはほとんどないと思います。もし AppDomain.UnhandledException イベントが Application クラスにあれば、未処理の例外がもっと扱いやすくなったのでは?と思いました。

 それと一つ気になったのですが、DispatcherUnhandledException イベントの引数にある Dispatcher は何のためにあるのでしょうか? キャッチできる例外は Application クラスと同じスレッドの例外のため Application.Current.Dispatcher で取得できると思うのですが・・・。