連載
» 2015年12月16日 05時00分 UPDATE

.NET TIPS:WPF:例外をまとめてトラップするには?[C#/VB]

例外を1カ所でまとめて処理したくなることがよくある。これを行う四つの方法を本稿では紹介する。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
.NET TIPS
Insider.NET

 

「.NET TIPS」のインデックス

連載目次

対象:.NET 4.0以降


 例外処理をまとめて1カ所に記述できたらよいのにと思ったことはないだろうか? 例えば、発生した例外を全てログに書き出したいときである。過去に筆者は、「try〜catchしてロギングするコードを全てのメソッドに書け」というコーディングルールに遭遇したことがある。そんな面倒なことをしなければならないのだろうか? また、例えば、処理されなかった例外をまとめてトラップし、可能ならばその例外を無視してプログラムの実行を継続したい、継続が無理ならばユーザーフレンドリーなメッセージを出してからプログラムを終了したい、ということもあるだろう。

 処理されなかった例外をまとめてトラップする方法について、.NET 2.0/Windowsフォームの場合は「.NET TIPS:適切に処理されなかった例外をキャッチするには?」をご覧いただきたい。

 本稿では、.NET 4.0以降の新機能も使ってWPFで例外をまとめてトラップする方法を整理して紹介する。なお、UIスレッドで発生する例外の話を除いて、コンソールプログラムやWebアプリケーションなどでも同様である。

 また、本稿のサンプルは「Windows desktop code samples:.NET Tips #1122」からダウンロードできる。次の画像は、その実行例である。

別途公開のサンプルを実行している様子(Windows 10での実行) 別途公開のサンプルを実行している様子(Windows 10での実行)
このサンプルをビルドするは、Visual Studio 2012以降が必要である。

事前準備:バックグラウンドタスクで発生した例外をトラップ可能にするには?

 本題に入る前に、.NET 4.5での仕様変更の話をしておかなくてはならない。

 非同期処理の記述は、.NET 4.0で導入されたタスク並列ライブラリ(TPL)の利用が今や一般的になってきた(「.NET TIPS:WPF/Windowsフォーム:時間のかかる処理をバックグラウンドで実行するには?(async/await編)」)。ところが、その例外処理に関しては、.NET 4.5で大きな仕様変更があったのだ。

 バックグラウンドタスクの完了を待機する(awaitキーワードやSystem.Threading.Tasks名前空間のTaskクラスのWaitメソッドを使う)場合には、バックグラウンドタスクで発生した例外が呼び出し元のスレッドに送られる。これには変更がない。

 バックグラウンドタスクの完了を待機しない場合には、次のように仕様が変わっている。

  • .NET 4.0: トラップされなかった場合は、プログラムが終了する
  • .NET 4.5: トラップされなかった場合は、その例外は失われる(既定値)

 この.NET 4.5の動作は、.NET 4.0と同じ動作をするように変更できる。本稿では、.NET 4.0と同じ動作に変更しておく。それには環境変数やレジストリを設定する方法もあるが、ここでは動作環境によらずアプリごとに設定する方法を用いる。.NET 4.5のプロジェクトの場合は、App.configファイルでThrowUnobservedTaskExceptionsをtrueに設定しておいてもらいたい(次のコード)。

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  ……省略(既存の記述)……

  <runtime>
    <ThrowUnobservedTaskExceptions enabled="true"/>
  </runtime>
</configuration>

バックグラウンドタスクで発生した例外を必ず受け取れるようにする設定(.NET 4.5)
.NET 4.5のプロジェクトでは、そのApp.configファイルに太字の部分を追加する。
これにより、.NET 4.0と同じ動作になる。すなわち、バックグラウンドタスクで発生した例外を処理しなかった場合にプログラムが終了するようになる。

例外が発生した瞬間にまとめてトラップするには?

 AppDomainクラス(System名前空間)のFirstChanceExceptionイベントを使えばよい。

 これは.NET 4.0の新機能である。try〜catch構文で例外がキャッチされるよりも前に、例外が発生したという通知を受け取れるのだ。ただし、その例外を処理することはできない。また、マネージコードで発生した例外のみである。発生した全ての例外をロギングするといった用途に利用できる(膨大な量になるので開発中に限定した方がよい)。

 このFirstChanceExceptionイベントを使ってメッセージボックスを出すコード例は次のようになる。

public partial class App : Application
{
  public App()
  {
    // マネージコード内で例外がスローされると最初に必ず発生する(.NET 4.0より)
    AppDomain.CurrentDomain.FirstChanceException
      += CurrentDomain_FirstChanceException;
  }

  private void CurrentDomain_FirstChanceException(
                  object sender, 
                  System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e)
  {
    string errorMember =  e.Exception.TargetSite.Name;
    string errorMessage = e.Exception.Message;
    string message = string.Format(
@"例外が{0}で発生。プログラムは継続します。
エラーメッセージ:{1}"
                              errorMember, errorMessage);
    MessageBox.Show(message, "FirstChanceException",
                    MessageBoxButton.OK,MessageBoxImage.Information);
  }
}

Class Application
  Public Sub New()
    ' マネージコード内で例外がスローされると最初に必ず発生する(.NET 4.0より)
    AddHandler AppDomain.CurrentDomain.FirstChanceException,
      AddressOf CurrentDomain_FirstChanceException
  End Sub

  Private Sub CurrentDomain_FirstChanceException(
                sender As Object,
                e As Runtime.ExceptionServices.FirstChanceExceptionEventArgs)

    Dim errorMember As String = e.Exception.TargetSite.Name
    Dim errorMessage As String = e.Exception.Message
    Dim message As String = String.Format(
"例外が{0}で発生。プログラムは継続します。" + vbCrLf _
+ "エラーメッセージ:{1}",
                              errorMember, errorMessage)
    MessageBox.Show(message, "FirstChanceException",
                    MessageBoxButton.OK, MessageBoxImage.Information)
  End Sub
End Class

FirstChanceExceptionイベントを使うコード例(上:C#、下:VB)
本稿では、無償のVisual Studio Express 2012 for Windows Desktopを使用している。
WPFのプロジェクトを作成し、App.xaml.csファイルのAppクラス(VBでは、Application.xaml.vbファイルのApplicationクラス)をこのように編集する。

WPFのUIスレッドで発生した未処理例外をまとめてハンドリングするには?

 Applicationクラス(System.Windows名前空間)のDispatcherUnhandledExceptionイベントを使えばよい。

 これはWPF登場時からの機能である。WindowsフォームのApplicationクラス(System.Windows.Forms名前空間)のThreadExceptionイベントに相当するものだ。UIスレッドで発生した例外が処理されなかった場合に、このイベントが発生する。

 DispatcherUnhandledExceptionイベントのハンドラーメソッドでは、例外を処理済みにすることができる(try〜catchブロックで例外をキャッチしてリスローしないことに相当)。それには、引数のDispatcherUnhandledExceptionEventArgsオブジェクト(System.Windows.Threading名前空間)のHandledプロパティにtrueを設定する。

 このDispatcherUnhandledExceptionイベントを使ってメッセージボックスを出すコード例は次のようになる。ここでは、メッセージボックスで[はい]ボタンをクリックすると例外を処理済みにしてプログラムを継続するようにしてある。

public partial class App : Application
{
  public App()
  {
    // UIスレッドで実行されているコードで処理されなかったら発生する(.NET 3.0より)
    this.DispatcherUnhandledException += App_DispatcherUnhandledException;
  }

  private void App_DispatcherUnhandledException(
                  object sender, 
                  System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
  {
    string errorMember = e.Exception.TargetSite.Name;
    string errorMessage = e.Exception.Message;
    string message = string.Format(
@"例外が{0}で発生。プログラムを継続しますか?
エラーメッセージ:{1}",
                              errorMember, errorMessage);
    MessageBoxResult result
      = MessageBox.Show(message, "DispatcherUnhandledException",
                        MessageBoxButton.YesNo, MessageBoxImage.Warning);
    if(result == MessageBoxResult.Yes)
      e.Handled = true;
  }
}

Class Application
  Public Sub New()
    ' UIスレッドで実行されているコードで処理されなかったら発生する(.NET 3.0より)
    AddHandler Me.DispatcherUnhandledException,
      AddressOf App_DispatcherUnhandledException
  End Sub

  Private Sub App_DispatcherUnhandledException(
              sender As Object,
              e As System.Windows.Threading.DispatcherUnhandledExceptionEventArgs)

    Dim errorMember As String = e.Exception.TargetSite.Name
    Dim errorMessage As String = e.Exception.Message
    Dim message As String = String.Format(
"例外が{0}で発生。プログラムを継続しますか?" + vbCrLf _
+ "エラーメッセージ:{1}",
                              errorMember, errorMessage)
    Dim result As MessageBoxResult _
      = MessageBox.Show(message, "DispatcherUnhandledException",
                          MessageBoxButton.YesNo, MessageBoxImage.Warning)
    If (result = MessageBoxResult.Yes) Then
      e.Handled = True
    End If
  End Sub
End Class

DispatcherUnhandledExceptionイベントを使うコード例(上:C#、下:VB)
WPFのプロジェクトで、App.xaml.csファイルのAppクラス(VBでは、Application.xaml.vbファイルのApplicationクラス)をこのように編集する。

バックグラウンドタスクで発生した未処理例外をまとめてハンドリングするには?

 TaskSchedulerクラス(System.Threading.Tasks名前空間)のUnobservedTaskExceptionイベントを使えばよい。

 これは.NET 4.0の新機能である。バックグラウンドタスクで発生した例外が処理されなかった場合に、このイベントハンドラーで例外を処理できる(.NET 4.5でも前述した設定に関係なく処理できる。前述した設定は、このイベントハンドラーで例外を処理済みとしなかった場合にプログラムが終了するかどうかという違いになる)。ただし、このイベントが呼び出されるのは、バックグラウンドタスクのインスタンスが破棄されるときである。通常はシステムのガベージコレクタに破棄を任せているため、呼び出されるタイミングは不定である。アプリケーション自体が先に終了してしまい、イベントハンドラーが呼び出されないままになる可能性もあるので注意してほしい。

 UnobservedTaskExceptionイベントのハンドラーメソッドでは、例外を処理済みとすることができる。それには、引数のUnobservedTaskExceptionEventArgsオブジェクト(System.Threading.Tasks名前空間)のSetObservedメソッドを呼び出せばよい。

 このUnobservedTaskExceptionイベントを使ってメッセージボックスを出すコード例は次のようになる。ここでは、メッセージボックスで[はい]ボタンをクリックすると例外を処理済みにしてプログラムを継続するようにしてある。

public partial class App : Application
{
  public App()
  {
    // バックグラウンドタスク内で処理されなかったら発生する(.NET 4.0より)
    TaskScheduler.UnobservedTaskException
      += TaskScheduler_UnobservedTaskException;
  }

  private void TaskScheduler_UnobservedTaskException(
                  object sender,
                  UnobservedTaskExceptionEventArgs e)
  {
    string errorMember = e.Exception.InnerException.TargetSite.Name;
    string errorMessage = e.Exception.InnerException.Message;
    string message = string.Format(
@"例外がバックグラウンドタスクの{0}で発生。プログラムを継続しますか?
エラーメッセージ:{1}",
                        errorMember, errorMessage);
    MessageBoxResult result
      = MessageBox.Show(message, "UnobservedTaskException",
                        MessageBoxButton.YesNo, MessageBoxImage.Warning);
    if (result == MessageBoxResult.Yes)
      e.SetObserved();
  }
}

Class Application
  Public Sub New()
    ' バックグラウンドタスク内で処理されなかったら発生する(.NET 4.0より)
    AddHandler TaskScheduler.UnobservedTaskException,
      AddressOf TaskScheduler_UnobservedTaskException
  End Sub

  Private Sub TaskScheduler_UnobservedTaskException(
                sender As Object,
                e As UnobservedTaskExceptionEventArgs)

    Dim errorMember As String = e.Exception.InnerException.TargetSite.Name
    Dim errorMessage As String = e.Exception.InnerException.Message
    Dim message As String = String.Format(
"例外がバックグラウンドタスクの{0}で発生。プログラムを継続しますか?" + vbCrLf _
+ "エラーメッセージ:{1}",
                        errorMember, errorMessage)
    Dim result As MessageBoxResult _
      = MessageBox.Show(message, "UnobservedTaskException",
                          MessageBoxButton.YesNo, MessageBoxImage.Warning)
    If (result = MessageBoxResult.Yes) Then
      e.SetObserved()
    End If
  End Sub
End Class

UnobservedTaskExceptionイベントを使うコード例(上:C#、下:VB)
WPFのプロジェクトで、App.xaml.csファイルのAppクラス(VBでは、Application.xaml.vbファイルのApplicationクラス)をこのように編集する。

全ての未処理例外を最後にまとめてトラップするには?

 AppDomainクラスのUnhandledExceptionイベントを使えばよい。

 これは、.NET Frameworkに最初からある機能だ。処理されなかった例外がある場合に、本稿で紹介した四つのイベントの中で、最後に通知される。ただし、FirstChanceExceptionと同様に、その例外を処理することはできない。ほとんどの場合、このイベントハンドラーから抜けた時点でプログラムは終了してしまう。

 このUnhandledExceptionイベントを使ってメッセージボックスを出すコード例は次のようになる。

public partial class App : Application
{
  public App()
  {
    // 例外が処理されなかったら発生する(.NET 1.0より)
    AppDomain.CurrentDomain.UnhandledException
      += CurrentDomain_UnhandledException;
  }

  private void CurrentDomain_UnhandledException(
                  object sender,
                  UnhandledExceptionEventArgs e)
  {
    var exception = e.ExceptionObject as Exception;
    if (exception == null)
    {
      MessageBox.Show("System.Exceptionとして扱えない例外");
      return;
    }

    string errorMember =  exception.TargetSite.Name;
    string errorMessage = exception.Message;
    string message = string.Format(
@"例外が{0}で発生。プログラムは終了します。
エラーメッセージ:{1}",
                                errorMember, errorMessage);
    MessageBox.Show(message, "UnhandledException",
                    MessageBoxButton.OK, MessageBoxImage.Stop);
    Environment.Exit(0);
  }
}

Class Application
  Public Sub New()
    ' 例外が処理されなかったら発生する(.NET 1.0より)
    AddHandler AppDomain.CurrentDomain.UnhandledException,
      AddressOf CurrentDomain_UnhandledException
  End Sub

  Private Sub CurrentDomain_UnhandledException(
                sender As Object,
                e As UnhandledExceptionEventArgs)

    Dim exception = TryCast(e.ExceptionObject, Exception)
    If (exception Is Nothing) Then
      MessageBox.Show("System.Exceptionとして扱えない例外")
      Return
    End If

    Dim errorMember As String = exception.TargetSite.Name
    Dim errorMessage As String = exception.Message
    Dim message As String = String.Format(
"例外が{0}で発生。プログラムは終了します。" + vbCrLf _
+ "エラーメッセージ:{1}",
                                errorMember, errorMessage)
    MessageBox.Show(message, "UnhandledException",
                    MessageBoxButton.OK, MessageBoxImage.Stop)
    Environment.Exit(0)
  End Sub
End Class

UnhandledExceptionイベントを使うコード例(上:C#、下:VB)
WPFのプロジェクトで、App.xaml.csファイルのAppクラス(VBでは、Application.xaml.vbファイルのApplicationクラス)をこのように編集する。

まとめ

 例外をまとめて扱う手段が.NET 4.0で追加されている。本稿では四つの方法を紹介した。

 例外をログに書き込むには、例外発生時のFirstChanceExceptionイベントか、アプリケーション終了直前のUnhandledExceptionイベントが適している(この二つは例外発生を知ることができるだけで、例外を処理済みにすることはできない)。

 未処理例外をトラップした上でリカバリ処理を試みて、それが成功したときにプログラムを継続させるにはDispatcherUnhandledExceptionイベントやUnobservedTaskExceptionイベントが利用できる。

利用可能バージョン:.NET Framework 4.0以降
カテゴリ:WPF 処理対象:例外
使用ライブラリ:AppDomainクラス(System名前空間)
使用ライブラリ:Applicationクラス(System.Windows名前空間)
使用ライブラリ:TaskSchedulerクラス(System.Threading.Tasks名前空間)
関連TIPS:適切に処理されなかった例外をキャッチするには?
関連TIPS:WPF/Windowsフォーム:時間のかかる処理をバックグラウンドで実行するには?


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。