sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF で発生した例外をすべて記録するテクニック

例外処置

本当に想定外の例外というものは、 try-catch ステートメントの外で発生してしまうものです。当たり前ですが 100% 制御できたプログラムを作成するのも、コストが高いです。

なので、想定外のエラーの対策のために「例外をまとめてトラップする」仕組みをプログラムに組み入れておくという、テクニックのお話。

発生した何らかの例外によって強制終了するソフトウェアの最後をフォローすることができます。監査証跡が要求されるソフトウェアでは必須のテクニックだと思います。

  • 発生した例外情報をログに残す
  • 発生した例外をキャンセルする
  • ソフトウェアの異常停止をユーザーに通達する

例外をまとめてトラップするテクニックを、大きく「3つ」取り上げました。

UnobservedTaskException イベントは、最後にすこし。

1.FirstChanceException イベント (最初に処理)

FirstChanceException イベントは、 .NET 4.0 で追加されたわりと新しい機能です。
これは、 try-catch ステートメントの中の例外発生であっても、まず、この例外が通知されます。(そのあとに try-catch ステートメントの例外)
通知は受け取るだけで、例外をキャンセルすることはできません

発生した例外情報をログに残すことに適していると私は思っています。どの例外処置よりも先に発生するため、漏れることがありませんので、すべての例外を記録するには、このイベントの利用が手っ取り早いです。
ログを残すなら、こんな感じです。

static App()
{
    AppDomain.CurrentDomain.FirstChanceException += (sender, args) =>
    {
        ReportException("FirstChanceException.log", sender, args.Exception);
    };
}

private static void ReportException(string fileName, object sender, Exception exception)
{
    const string reportFormat = 
    "===========================================================\r\n" +
    "ERROR Date = {0}, Sender = {1}, \r\n" +
    "{2}\r\n\r\n";

    var reportText = string.Format(reportFormat, DateTimeOffset.Now, sender, exception);
    File.AppendAllText(fileName, reportText);
}


2.UnhandledException イベント (未処理例外を最後に処理)

2.UnhandledException イベントは、 .NET に最初からある機能です。
これは、 try-catch ステートメントの中の例外発生であったときは、 catch 内で throw し直さないと発生しません。なので、未処理の例外があったときに発生するイベントです。

FirstChanceException イベント は、 必ず発生する イベントです。
UnhandledException イベントは、 未処理のイベントがあったときに発生する イベントです。
(また、通知は受け取るだけで、例外をキャンセルすることはできません)

このイベントは未処理の例外発生を致命的なエラーと判断して、ユーザーにソフトウェアの強制終了を通達する際に利用しやすいです。

static App()
{
    AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
    {
        MessageBox.Show("未処理の例外が発生しました。アプリケーションを強制終了します。");
        Current.Shutdown();
    };
}


3.DispatcherUnhandledException イベント (UI スレッドの例外処置)

DispatcherUnhandledException イベントは、 WPF で使える機能です。
Windows フォームアプリケーションの (Application.ThreadException)https://msdn.microsoft.com/ja-jp/library/system.windows.forms.application.threadexception%28v=vs.110%29.aspx の役目を果たすためのイベントです。

つまり、 UI スレッドで発生して 例外が処理されなかったとき に発生します。

このイベントの特徴は、例外をキャッチして 止めること ができることです。つまり、アプリケーションの強制終了をキャンセルすることができます。
(try-catch ステートメントなら catch して throw し直さないのと同じ)

汎用的な例外のキャッチは、注意が必要です。あまり常用される手段ではないと私は思います。

public App()
{
    // UI スレッドの処理されない例外
    DispatcherUnhandledException += (sender, args) =>
    {
        var name = args.Exception.TargetSite.Name;
        var message = args.Exception.Message;

        var result = MessageBox.Show(
            $"例外が {name} で発生しました。プログラムを継続しますか。{Environment.NewLine}詳細:{message}", 
            "DispatcherUnhandledException", 
            MessageBoxButton.YesNo, 
            MessageBoxImage.Warning);

        if (result == MessageBoxResult.Yes)
        {
            args.Handled = true;
        }
    };

}


Task.Run() で例外を捕捉できない書き方

Task.Run() は作業を別のスレッドにわたす便利なメソッドです。
しかし、この中で発生した例外は、コーディングによって 上手く捕捉できない ことがあります。次は、その一例です。

private void Button_Click(object sender, RoutedEventArgs e)
{
    var task = Task.Run(() => 
    {
        // UI スレッドで例外を発生させる
        throw new InvalidOperationException($"ThreadID:{Thread.CurrentThread.ManagedThreadId}");
    });

    // 待たないとエラーを捕捉できない
    // task.Wait();
}

タスクは、 Task.Wait() でタスクの実行を待たないと例外を捕捉できません。
他の方法だと、 async await を利用します。こっちのほうが楽だと思います。

private async void Button_Click(object sender, RoutedEventArgs e)
{
    await Task.Run(() => 
    {
        throw new InvalidOperationException($"ThreadID:{Thread.CurrentThread.ManagedThreadId}");
    });
}

バックグラウンドタスクでの例外は UnobservedTaskException イベントで汎用的にトラップできますが、癖のある挙動をします。 (サンプルプログラム参照)


例外が起こりやすいプログラムは NG

if-else ステートメントの else 内に入る感覚で、try-catch ステートメントの catch の内を利用してはいけません。

パフォーマンス面でも非常に低速ですが、そもそも誰かとレビューをすれば 100% 指摘を受けるはずです。初歩的なコーディングのお作法や道徳の話だと思います。

サンプルプログラム

作りました。
ExceptionTrapSample です。

f:id:shikaku_sh:20180523102857p:plain:h400

  • UI スレッドの例外 (InvalidOperation or 0 の除算)
  • 異なるスレッドでの例外 (Task or async Task)

この4通りを try-catch のありとなしで作成したので、8通りのテストが可能です。すべての例外発生時に、 FirstChanceException イベント でログを記録しています。


参考

dobon.net - 捕捉されなかった例外がスローされたことを知る
@IT - WPF:例外をまとめてトラップするには?
kekyoの丼 - .NET非同期処理(async-await)と例外の制御