UIスレッドの未処理例外を捕捉するDispatcherUnhandledException

C#
スポンサーリンク

DispatcherUnhandledExceptionでUIスレッドの未処理例外を捕捉

WPFアプリケーションで未処理 (=catchしていない) の例外が発生したとき、
発火するイベントがあります。

それが、「DispatcherUnhandledException」です。

DispatcherUnhandledExceptionイベントは、
平たく言うとUIスレッド上で未処理例外が起きた時に発火します。

Task.Runなどを使ってバックグラウンドに回している処理内で
発生した例外についてはこのイベントは発火しません。
(※awaitで待ってるときは除く。詳細は以下の「あわせて読みたい」を見てください)

バックグラウンドスレッドで起きた未処理例外を知りたい場合は、
別のイベントを購読する必要があります。
そちらについての詳細は以下の「あわせて読みたい」を見ていただければと思います。

このイベント内では渡された引数 DispatcherUnhandledExceptionEventArgs
を使って、以下のことができます(というかこれがそのまま主な利用法です)。

  • 起きた例外の内容を出力、お知らせする
  • 正常っぽい終了をする
  • 例外を握りつぶす

最後に何やら物騒なことが書いてありますが、
1つずつ解説していきます。

起きた例外の内容を出力、お知らせする

通常、アプリケーションを使うユーザーの手元で、
未処理例外が起きてしまった場合、
アプリケーションはその時点で強制終了してしまいます。

ユーザーにとっては訳も分からずアプリが落ちてるので、
せいぜい「なんか普通に使ってたら落ちたんだけど?」
くらいのフィードバックしか開発者にできません。

さんさめ
さんさめ

さすがにそれだけじゃ何が原因か分からんですわ…

そこで、いわば「最も大きいキャッチャーミット」とでもいうような、
アプリケーション全体をcatchするイベントとして、
このDispatcherUnhandledExceptionイベントが用意されています。

不具合時に開発者とユーザーをつなぐエラー報告についての
詳細は以下の記事で書いています。
WPFアプリでエラーを報告してもらうための実装

想定外の事態が起きた時でも、
このイベントを使って最低限必要な情報を出力してから落ちることで、
速やかな改善を見込むことができます。

「最低限必要な情報」として最も有力なものが、
例外の種類と、スタックトレースです。

例えば以下のコードはUIスレッドで例外が発生するコードです。
xamlは省略しますが、
例外を発生させるButtonが置いてあるんだなくらいに想像していただければ。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        string a = null;
        MessageBox.Show(a.ToString()); // NullReferenceException!!
    }
}

try句で囲んでいないので、
当然ボタンを押したら即アプリが例外終了します。

さて、それではイベント購読してみましょう。
以下のコードではMainWindowのコードビハインドで購読開始していますが、
普通はApp.xaml.csのコンストラクタか、
OnStartupをオーバーライドして書くことが多いです。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        Application.Current.DispatcherUnhandledException += OnDispatcherUnhandledException;
    }

    private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
    {
        MessageBox.Show("申し訳ありません。\n" +
            "お使いのアプリケーションは異常を検知したため終了します\n" +
            "-- エラー内容 --\n" +
            e.Exception.ToString(),
            "異常終了");
        Environment.Exit(1);
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        string a = null;
        MessageBox.Show(a.ToString());
    }
}

辞世の一句としてMessageBoxが表示されるようにしてみました。
このコードを実行してボタンを押すと次のようになります。

さんさめ
さんさめ

…しまった、Debug実行だとこうなるわな…

実はVisualStudioからデバッグ実行しているときは、
デバッガが例外発生したコード部分で一旦ブレークしてくれます。

開発中はありがたいのですが、今はその先の処理が見たいので、
気にせずF5キーか「続行ボタン」で続行しましょう。

気を取り直して続行してみると、
次のようなMessageBoxが表示されるはずです

e.Exceptionというプロパティに対して、
ToStringを呼び出しMessageBoxに表示された結果がこちらです。

System.NullReferenceException: Object reference not set to an instance of an object.

と書かれ、その下にはスタックトレースがズラリと表示されています。

例外メッセージとスタックトレースの読み方については、
後日改めて記事を書こうと思いますが、
これで不具合修正のための最低限必要な情報が手に入ったことになります。

もちろん、実際の現場では、MessageBoxに表示するだけでは色々考慮が足りませんが、
情報の取得の仕方についてはこんなところです。

正常っぽい終了をする

MessageBoxを表示するだけしてそのまま処理を素通しさせてしまうと、
アプリケーションは以下のようなダイアログを出します。

いかにも「異常終了しました」という印象を与えてしまいます。

これを防ぐために、イベント内で、

  • Application.Current.Shutdown
  • Environment.Exit

のどちらかを呼んであげる必要があります。
できれば、前者のApplication.Current.Shutdownの方が、
落ちる際に色々できるのでお勧めです。

いずれかのメソッドを呼ぶと、アプリケーションはプログラムの処理によって
終了したとみなされるため、
上図のようなOSが出す標準エラーダイアログのようなものは出なくなります。

例外を握りつぶす

さて、場合によっては

  • その例外は原因が分かりきっていて、かつ致命的でもないので処理を続行させたい
  • 致命的かもしれないけどいきなり落とすのは心情的に嫌

みたいなこともあるかもしれません。

そんなとき、以下のようにコードを書くと、
起きたすべての例外を握りつぶすことができます。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        Application.Current.DispatcherUnhandledException += OnDispatcherUnhandledException;
    }

    private void OnDispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
    {
        // 狂気の全例外握りつぶし
        e.Handled = true;
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        string a = null;
        MessageBox.Show(a.ToString());
    }
}

いくら例外が起きても決してアプリケーションが落ちることはなくなりました。

なお、例外を握りつぶしてもたいていは問題の先送りにしかならず、
どんどん取り返しのつかない状態になっていくので絶対にやめましょう。
特に情報の永続保存を行う、編集ツールなどでは御法度です。
最悪、不正データの発生・データ破壊などが起きます

影響範囲の限られている、
特定の例外にのみ、この対応を使うことがあります。

基本的に、例外が起きてしまったらもうどうしようもない、と思った方が良いですね。
不具合修正に必要な情報を出力したら、
素直にアプリケーションを落としましょう。

まとめ

まとめです。

  • WPFにおけるUIスレッドの未処理例外捕捉には、
    DispatcherUnhandledExceptionイベントを購読
  • 例外内容を出力して、不具合修正の情報として扱うのが主な用途
  • 例外の握りつぶしも可能だが最終手段

未処理例外はなるべく捕捉して情報を出すようにし、
迅速な不具合修正ができるようにしておきましょう!

最後までお読みいただき、ありがとうございました。

コメント

タイトルとURLをコピーしました