2016年5月20日金曜日

.NET(VB) アプリケーションイベントを使用した際の例外一括処理

WindowsFormアプリケーションです。
VBで「アプリケーションフレームワークを有効にする」をチェックONにしたときに、例外を一括処理する方法です。

C#ではアプリケーションのエントリーポインはMainメソッドになります。
補足されなかった例外を一括で処理する場合、このMainメソッド内で
System.Windows.Forms.Application.ThreadExceptionイベントと
System.AppDomain.CurrentDomain.UnhandledExceptionイベントをハンドルして処理すると思います。

VBでもエントリーポイントをMainメソッドにした場合、C#と同じ方法で補足されなかった例外を一括処理できます。

しかし、VBではデフォルトで「アプリケーションフレームワークを有効にする」チェックONなので
スタートアップフォームを指定してアプリケーションを起動することが多いように思います。

私に権限があれば自作のMainメソッドをエントリーポイントにしてアプリを起動するようにするのですが
(そのほうが細かいこともまで融通が利くので)
政治的な理由で「アプリケーションフレームワークを有効にする」チェックOFFにできない場合
アプリケーションイベントを使用して、補足されなかった例外を一括処理できます。

まずアプリケーションイベントとはVBにのみ用意されている、アプリケーションに関するイベントを記述することができるクラスです。
このクラスファイルはデフォルトでは非表示です。
まずはこのファイルを表示させます。

プロジェクトのプロパティ画面を開き「アプリケーション」タブにある「アプリケーションイベントの表示」をクリックします。

するとプロジェクト内に「ApplicationEvent.vb」ファイルが表示されます。

このファイルの中を見ると
名前空間は「My」クラス名は「MyApplication」になっています。
「Partial」キーワードが付いているので、どこかに分離されたコードがあります。

「My Project」の「Application.myapp」の「Application.Designer.vb」が分離されたコードになります。

MyApplicationクラスのコードのコメントに
「UnhandledException: ハンドルされていない例外がアプリケーションで発生したときに発生するイベントです。」とあるので
一見、これを使えばよさそうな気がします。

結論からいいますと、ダメでした。

例外の補足には
メインフォームが実行されているスレッド(UIスレッド)で発生する例外と
メインフォームが実行されているスレッド以外(UIスレッド以外)で発生する例外を補足する必要があります。

UIスレッドで発生する例外を補足するには
System.Windows.Forms.Application.ThreadExceptionイベントを使用します。

UIスレッド以外で発生する例外を補足するには
System.AppDomain.CurrentDomain.UnhandledExceptionイベントを使用します。

MyApplicationクラスのUnhandledExceptionイベントは、UIスレッドで発生する例外を補足できるようです。

UIスレッド以外で発生する例外を補足するために、StartUpイベントを使用して
System.AppDomain.CurrentDomain.UnhandledExceptionイベントをハンドルしてみました。
以下がダメな場合のコードです。
実行するとUIスレッドで発生した例外もすべて、CurrentDomain_UnhandledExceptionメソッドで処理されてしまいます。

ダメなコードです!!
Namespace My

    ' 次のイベントは MyApplication に対して利用できます:
    ' 
    ' Startup: アプリケーションが開始されたとき、スタートアップ フォームが作成される前に発生します。
    ' Shutdown: アプリケーション フォームがすべて閉じられた後に発生します。このイベントは、通常の終了以外の方法でアプリケーションが終了されたときには発生しません。
    ' UnhandledException: ハンドルされていない例外がアプリケーションで発生したときに発生するイベントです。
    ' StartupNextInstance: 単一インスタンス アプリケーションが起動され、それが既にアクティブであるときに発生します。
    ' NetworkAvailabilityChanged: ネットワーク接続が接続されたとき、または切断されたときに発生します。
    Partial Friend Class MyApplication

        Private Sub MyApplication_Startup(sender As Object, e As ApplicationServices.StartupEventArgs) Handles Me.Startup

            AddHandler System.AppDomain.CurrentDomain.UnhandledException, AddressOf CurrentDomain_UnhandledException
        End Sub


        Private Sub MyApplication_UnhandledException(sender As Object, e As ApplicationServices.UnhandledExceptionEventArgs) Handles Me.UnhandledException
            'アプリケーションを終了しない
            e.ExitApplication = False
            '例外メッセージを表示
            MessageBox.Show(e.Exception.Message,"MyApplication_UnhandledException")
        End Sub

        Private Sub CurrentDomain_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs)
            '例外メッセージを表示
            MessageBox.Show(DirectCast(e.ExceptionObject, Exception).Message,"CurrentDomain_UnhandledException")
        End Sub

    End Class


End Namespace
UIスレッド以外のスレッドは使用しないから、UnhandledExceptionイベントだけハンドルすればいいかと思ってたのですが
メインスレッドのコンストラクタ内で例外が発生すると、補足できません。

※コンストラクタで例外が発生するのがどうかと思うのですが、ごくたまにサードパーティ製のコントロールが例外をスローして
例外が補足できずに標準の例外が出てしまうことがありました。


OnInitializeメソッドをオーバーライドして
System.Windows.Forms.Application.ThreadExceptionイベントと
System.AppDomain.CurrentDomain.UnhandledExceptionイベントをハンドルすると、希望通りの動作になりました。
Namespace My

    ' 次のイベントは MyApplication に対して利用できます:
    ' 
    ' Startup: アプリケーションが開始されたとき、スタートアップ フォームが作成される前に発生します。
    ' Shutdown: アプリケーション フォームがすべて閉じられた後に発生します。このイベントは、通常の終了以外の方法でアプリケーションが終了されたときには発生しません。
    ' UnhandledException: ハンドルされていない例外がアプリケーションで発生したときに発生するイベントです。
    ' StartupNextInstance: 単一インスタンス アプリケーションが起動され、それが既にアクティブであるときに発生します。
    ' NetworkAvailabilityChanged: ネットワーク接続が接続されたとき、または切断されたときに発生します。
    Partial Friend Class MyApplication

        ''' 
        ''' OnInitializeメソッドをオーバーライド
        ''' 
        ''' 
        ''' 
        ''' 
        Protected Overrides Function OnInitialize(commandLineArgs As ObjectModel.ReadOnlyCollection(Of String)) As Boolean
            'ハンドルされていない例外を補足する
            '--UIスレッドでの例外を補足
            AddHandler System.Windows.Forms.Application.ThreadException, AddressOf Application_ThreadException
            '--UIスレッド以外での例外を補足
            AddHandler System.AppDomain.CurrentDomain.UnhandledException, AddressOf CurrentDomain_UnhandledException

            Return MyBase.OnInitialize(commandLineArgs)
        End Function

        ''' 
        ''' UIスレッドでの例外を補足するイベント
        ''' 
        ''' 
        ''' 
        ''' 
        Private Sub Application_ThreadException(sender As Object, e As System.Threading.ThreadExceptionEventArgs)
            'スプラッシュ画面を閉じる
            CloseSplashScreen()
            '例外処理を行う
            ShowError(e.Exception)
        End Sub
        
        ''' 
        ''' UIスレッド以外での例外を補足するイベント
        ''' 
        ''' 
        ''' 
        ''' 
        Private Sub CurrentDomain_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs)
            '※UIスレッド以外のスレッドなので、UIスレッドの画面操作はできない
            '-----
            ''スプラッシュ画面を閉じる
            'CloseSplashScreen()
            '-----
            '例外処理を行う
            ShowError(DirectCast(e.ExceptionObject, Exception))
        End Sub

        ''' 
        ''' スプラッシュ画面を閉じる
        ''' 
        ''' 
        Private Sub CloseSplashScreen()
            If (My.Application.SplashScreen IsNot Nothing AndAlso My.Application.SplashScreen.IsDisposed <> True) Then
                My.Application.SplashScreen.Close()
            End If
        End Sub

        ''' 
        ''' 例外処理を行う
        ''' 
        ''' 
        ''' 
        Private Sub ShowError(ex As Exception)
            Dim info As New System.Text.StringBuilder
            info.AppendLine(ex.Message)
            info.AppendLine(ex.StackTrace)
            info.AppendLine("------メソッド呼出履歴------")
            For Each frm As StackFrame In New StackTrace(ex, True).GetFrames
                Dim mb As System.Reflection.MethodBase = frm.GetMethod
                info.AppendLine(mb.ReflectedType.FullName & " : " & mb.Name)
            Next

            MessageBox.Show(info.ToString)
        End Sub

    End Class

End Namespace