Windows 8.1の新機能、asyncメソッドからの例外トラップ機能を使うには?[Windows 8.1ストアアプリ開発]WinRT/Metro TIPS

Windows 8.1ストア・アプリではトップレベルでまとめて例外をトラップできるようになった。その実装方法を解説する。

» 2014年01月16日 17時50分 公開
WinRT/Metro TIPS
業務アプリInsider/Insider.NET

powered by Insider.NET

「WinRT/Metro TIPS」のインデックス

連載目次

 async/awaitキーワード*1によって非同期処理が簡単に記述できるのは素晴らしいのだが、Windows 8(以降、Win 8)用のWindowsストアアプリ(以降、Win 8アプリ)では1つだけ困ったことがあった。アプリのトップレベルでまとめて例外をトラップできなかったのである。Windows 8.1(以降、Win 8.1)ではそれが改善されている。そこで本稿では、アプリのトップレベルでまとめて例外をトラップする方法を解説する。なお、本稿のサンプルは「Windows Store app samples:MetroTips #61(Windows 8/8.1版)」からダウンロードできる。

事前準備

 Win 8.1用のWindowsストアアプリ(以降、Win 8.1アプリ)を開発するには、Win 8.1とVisual Studio 2013(以降、VS 2013)が必要である。本稿ではOracle VM VirtualBox上で64bit版Windows 8.1 Pro(日本語版)とVisual Studio Express 2013 for Windows(日本語版)*2を使用してプログラミングしている。また、本稿の前半ではWin 8アプリの動作を確認するが、それにはWin 8とVisual Studio 2012(以降、VS 2012)が必要である。

*1 C#では「async/await」であり、VBでは「Async/Await」であるが、小文字のみの表記とさせていただく。VBでは適宜読み替えていただきたい。

*2 マイクロソフト公式ダウンロードセンターの「Microsoft Visual Studio Express 2013 for Windows」から無償で入手できる。


asyncメソッドの憂鬱(Win 8)

 Windowsストアアプリでは、非同期処理を多用する。Visual Studio 2012で導入されたasync/awaitキーワードによって非同期処理はごく簡潔に記述できるようになったので、多用するからといって手間が掛かるわけでもないし、コードの見通しが悪くなることもない*3。このようにasync/awaitキーワードは素晴らしいものだが、アプリのトップレベルで例外をトラップできないという問題があった。

*3 async/awaitキーワードについて詳しくは次の記事を参照してほしい。「連載:C# 5.0&VB 11.0新機能「async/await非同期メソッド」入門


 まず、その問題点をWin 8アプリのコードを書いて確認しておこう。

 例外が発生したその場で対処しきれる場合はよいのだが、そうではなくアプリのトップレベルで対処したい場合がある。例えば、例外に応じて他の画面に遷移させる場合、末端のメソッドレベルで例外に対処するとメンテナンスが厄介になりかねないので、トップレベルで一括して対処したい、あるいは想定外の例外が出たときの対処も、やはり末端のメソッドレベルにいちいち例外を処理するコードを記述するのではなく、まとめ て1カ所で済ませたい、また、例外をログとして送信する場合に1カ所にまとめて書きたい、といった場合だ。

 そこで、Applicationクラス(Windows.UI.Xaml名前空間)のUnhandledExceptionイベントを利用して、「App.xaml.cs」ファイルに次のようなコードを書く。

public App()
{
  this.InitializeComponent();
  this.Suspending += OnSuspending;

  this.UnhandledException += App_UnhandledException;
  // 注:デバッグビルドでは
  //     DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
  //     を定義しないとトラップできない
}

async void App_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
  e.Handled = true; // 多くの場合、これで実行を継続できる(終了してしまう場合もある)

  Exception ex = e.Exception;
  await (new Windows.UI.Popups.MessageDialog(
                ex.ToString() + "¥n¥n" + e.Message,
                "例外が発生しました")
        )
        .ShowAsync();
  // 注:実際には、ここで例外を検査して、対応するエラー表示画面に遷移させたり、
  //     現在のFrameを破棄して最初から構築し直したりする
}

Public Sub New()
  InitializeComponent()

  AddHandler Me.UnhandledException, AddressOf App_UnhandledException
  ' 注:デバッグビルドでは
  '     DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION
  '     を定義しないとトラップできない
End Sub

Private Async Sub App_UnhandledException(sender As Object, e As UnhandledExceptionEventArgs)
  e.Handled = True  ' 多くの場合、これで実行を継続できる(アプリが終了してしまう場合もある)

  Dim ex As Exception = e.Exception
  Await (New Windows.UI.Popups.MessageDialog(
               ex.ToString() + vbLf + vbLf + e.Message,
               "例外が発生しました")
        ) _
        .ShowAsync()
  ' 注:実際には、ここで例外を検査して、対応するエラー表示画面に遷移させたり、
  '     現在のFrameを破棄して最初から構築し直したりする
End Sub

アプリのトップレベルで例外をトラップするコード(上:C#、下:VB)
VS 2012で、Appクラスに太字の部分を追加する。
また、デバッグビルドでトラップするには、C#では、プロジェクトのプロパティの[ビルド]タブにある[条件付きコンパイル シンボル]に、VBではプロジェクトのプロパティの[コンパイル]タブの[詳細コンパイル オプション]の[カスタム定数]に、「, DISABLE_XAML_GENERATED_BREAK_ON_UNHANDLED_EXCEPTION」を追記する必要がある(リリースビルドでは追記不要)。

 それでは、例外が発生するコードを書いて試してみよう。

作成した画面(Win 8) 作成した画面(Win 8)▼▽画面にはボタンを3つ配置する。順に「Button1」/「Button2」/「Button3」と名前を付け、それぞれにClickイベントハンドラーを用意する。

 1つ目のボタンのイベントハンドラーには、例外が発生するメソッドを呼び出すコードをasyncキーワードなしで記述してみる(次のコード)。

// 1つ目のボタンのClickイベントハンドラー
private void Button1_Click(object sender, RoutedEventArgs e)
{
  例外を出すメソッド("asyncの付いていないメソッドから呼び出し");
}

private void 例外を出すメソッド(string errMsg)
{
  throw new InvalidOperationException(errMsg);
}

' 1つ目のボタンのClickイベントハンドラー
Private Sub Button1_Click(sender As Object, e As RoutedEventArgs)
  例外を出すメソッド("Asyncの付いていないメソッドから呼び出し")
End Sub

Private Sub 例外を出すメソッド(errMsg As String)
  Throw New InvalidOperationException(errMsg)
End Sub

例外が発生するメソッドをasync無しで呼び出す(上:C#、下:VB)

 これで実行してボタンをタップしてみると、「例外を出すメソッド」で例外が発生し、それが先述したAppクラスのUnhandledExceptionイベントハンドラーでトラップされて、次の画像のようにメッセージダイアログが表示される。

Appクラスで例外がトラップされたところ(Win 8) Appクラスで例外がトラップされたところ(Win 8)▼▽デバッグ実行では、例外が発生した時点でいったんブレークしてしまうが、そのまま実行を継続させる。

 このように、AppクラスのUnhandledExceptionイベントを利用すれば、トップレベルで例外をハンドリングできる。

 ところがWin 8で実行すると、async/awaitキーワードを使ったメソッドで発生した例外は、この方法ではトラップできないのだ。それも、メソッドが呼び出される過程のどこかでasync/awaitキーワードが使われていたらだめなのである。それを次で確認する。

 2つ目のボタンのイベントハンドラーには、asyncキーワードを付けて試してみよう。それ以外は、先ほどと同じコードを書く(次のコード)。

// 2つ目のボタンのClickイベントハンドラー
private async void Button2_Click(object sender, RoutedEventArgs e)
// 先ほどとの違いは、シグネチャにasyncを付けただけ
{
  例外を出すメソッド("async付きのメソッドから呼び出し");
}

' 2つ目のボタンのClickイベントハンドラー
Private Async Sub Button2_Click(sender As Object, e As RoutedEventArgs)
  ' 先ほどとの違いは、シグネチャにasyncを付けただけ
  例外を出すメソッド("Async付きのメソッドから呼び出し")
End Sub

例外が発生するメソッドをasync付きのメソッドから呼び出す(上:C#、下:VB)
awaitしているコードがないので警告が出るが、試しにやってみているだけなので無視してほしい。

 これをWin 8上で実行してみると、例外をトラップできず、アプリは終了してしまう。逆に、イベントハンドラーの「async」を削り、「例外を出すメソッド」の方にasyncキーワードを付けてみても、やはり例外をトラップできずにアプリは終了してしまう。実行経路上のどこかにasyncキーワード付きのメソッドがあると、Win 8ではAppクラスのUnhandledExceptionイベントで例外をトラップできないのだ。

 Windowsストアアプリは非同期処理を多用するので、実行経路のどこにもasync/awaitが無いという方が珍しいだろう。すなわち、せっかくAppクラスのUnhandledExceptionイベントがあっても、Win 8では使えないに等しかったのだ。

 なお、別スレッドで例外が発生した場合は、asyncキーワードの有無に関わらず、トラップできない。例外が発生したスレッドで例外をトラップし、元のスレッドに戻ってからリスロー(例外を投げ直す)しなければならない。これも確かめておこう(次のコード)。

// 3つ目のボタンのClickイベントハンドラー
private void Button3_Click(object sender, RoutedEventArgs e)
{
  別スレッドで例外を出すメソッド();
}

private void 別スレッドで例外を出すメソッド()
{
  var task =  System.Threading.Tasks.Task
          .Run(() =>
          {
            throw new InvalidOperationException("別スレッドで例外");
          });
  // 別スレッドの例外は、そのままではメインスレッドではトラップされない

  // ↓スレッドの完了待ちをすれば、例外がリスローされる
  // task.Wait();
}

'3つ目のボタンのClickイベントハンドラー
Private Sub Button3_Click(sender As Object, e As RoutedEventArgs)
  別スレッドで例外を出すメソッド()
End Sub

Private Sub 別スレッドで例外を出すメソッド()
  Dim task = System.Threading.Tasks.Task _
            .Run(
              Sub()
                Throw New InvalidOperationException("別スレッドで例外")
              End Sub)
  ' 別スレッドの例外は、そのままではメインスレッドではトラップされない

  ' ↓スレッドの完了待ちをすれば、例外がリスローされる
  'task.Wait()
End Sub

別スレッドで発生した例外はUnhandledExceptionでトラップできない(上:C#、下:VB)
例外が発生しているにも関わらず、メインスレッドでは例外をトラップできないし、アプリも終了しない。また、例外が発生したスレッドは、そこで打ち切られ、消滅する。

 このコードはasync/awaitキーワードを使っていないが、AppクラスのUnhandledExceptionイベントではトラップできず、しかもアプリは継続してしまう。例外はスレッドをまたいで伝播しないからである。

 なお、「別スレッドで例外を出すメソッド」の末尾にコメントアウトしてある「task.Wait()」を追加すると、そのWaitメソッド内で別スレッドの例外をリスローしてくれるので、トラップできるようになる。

改良されたUnhandledExceptionイベント(Win8.1)

 Win8.1では、AppクラスのUnhandledExceptionイベントが改良され、async/awaitキーワードを使っているメソッドで発生した例外もトラップされるようになった。

 コードは何も変更せず、そのままWin8.1で実行してみよう*4。async付きのメソッドから呼び出すパターン(=2番目のボタン)でも、Win 8.1では例外がトラップされる(次の画像)。Win 8ではトラップできず、アプリは終了させられていたものだ。

async付きのメソッドで発生した例外もトラップできる(Win 8.1) async付きのメソッドで発生した例外もトラップできる(Win 8.1)

 このようにWin8.1ではAppクラスのUnhandledExceptionイベントで例外をトラップできるようになったので、活用していってほしい。

*4 筆者はWin 8.1の環境にVS 2012も入れてあるので、Win 8の環境からソースコードを持ってきてそのままビルドした。VS 2013の用意しかないときは、VS 2013で新しくプロジェクトを作って、上と同様なコードを記述してほしい。


アンマネージコードの例外もトラップできるUnhandledErrorDetectedイベント(Win8.1)

 さて、便利になったAppクラスのUnhandledExceptionイベントにも欠点がある。マネージコードで発生した例外しかトラップできないのだ。従って、DirectXなどのアンマネージコードを使うクラスライブラリで発生した例外はトラップできない。

 Win 8.1では、CoreApplicationクラス(Windows.ApplicationModel.Core名前空間)にUnhandledErrorDetectedイベントが新設された。こちらは、アンマネージコードで発生した例外もトラップできる。

 UnhandledErrorDetectedイベントで例外をトラップするには、VS 2013でAppクラスに次のようなコードを記述すればよい。

public App()
{
  this.InitializeComponent();
  this.Suspending += OnSuspending;

  // Win8.1で新設された機能(アンマネージコードで発生した例外もトラップできる)
  Windows.ApplicationModel.Core.CoreApplication.UnhandledErrorDetected
    += CoreApplication_UnhandledErrorDetected;
}

async void CoreApplication_UnhandledErrorDetected(object sender,
            Windows.ApplicationModel.Core.UnhandledErrorDetectedEventArgs e)
{
  Exception ex = null;
  try
  {
    e.UnhandledError.Propagate();
  }
  catch (Exception exception)
  { // この時点でe.UnhandledError.Handledはtrueに変わっている

    ex = exception;
  }
  if (ex == null)
    return;

  await (new Windows.UI.Popups.MessageDialog(
               ex.ToString() + "¥n¥n" + ex.Message,
               "例外が発生しました (UnhandledErrorDetectedでキャッチ)")
        )
        .ShowAsync();
}

Public Sub New()
  InitializeComponent()

  ' Win8.1で新設された機能(アンマネージコードで発生した例外もトラップできる)
  AddHandler Windows.ApplicationModel.Core.CoreApplication.UnhandledErrorDetected, _
              AddressOf CoreApplication_UnhandledErrorDetected
End Sub

Private Async Sub CoreApplication_UnhandledErrorDetected(sender As Object, _
                  e As Core.UnhandledErrorDetectedEventArgs)
  Dim ex As Exception = Nothing
  Try
    e.UnhandledError.Propagate()
  Catch exception As Exception
    ' この時点でe.UnhandledError.Handledはtrueに変わっている

    ex = exception
  End Try

  If (ex Is Nothing) Then
    Return
  End If

  Await (New Windows.UI.Popups.MessageDialog( _
               ex.ToString() + vbLf + vbLf + ex.Message, _
               "例外が発生しました (UnhandledErrorDetectedでキャッチ)")
        ) _
        .ShowAsync()
End Sub

Win 8.1で新設されたUnhandledErrorDetectedイベントを利用するコード(上:C#、下:VB)
VS 2013で、Appクラスに太字の部分を追加する。
なお、アプリを終了させないためには、MSDNのドキュメントにはe.UnhandledError.Handledにtrue/Trueを代入するように書かれているが、間違いである(セッターが無いので代入できない)。Propagateメソッドを実行して例外を取り出した時点で、e.UnhandledError.Handledはtrue/Trueに変わっている。

 アプリのUIとボタンのイベントハンドラーは、前述したWin 8のものと全く同じに記述する。実行してみて、asyncメソッドで発生した例外もトラップできることを確かめてほしい。また、アンマネージコードを含むクラスライブラリを書いて、その中で発生した例外もトラップできることを確認したいところだが、長くなるので本稿では割愛させていただく。

 なお、UnhandledErrorDetectedイベントを使うとアンマネージコードで発生した例外もトラップできるようになるのであって、別スレッドや別プロセスで発生した例外はやはりトラップできないので注意してほしい。例えば、WebViewコントロールで発生するJavaScriptのエラー*5は、別プロセスで発生しているものなのでトラップできない。

 このCoreApplicationクラスのUnhandledErrorDetectedイベントは、前述したAppクラスのUnhandledExceptionイベントと比べると、ちょっと書き方が面倒だ。必要に応じて使い分けていただきたい。

*5 WebViewコントロールで発生するJavaScriptのエラーについては次の記事を参照してほしい。「WinRT/Metro TIPS:WebViewコントロールで簡易Webブラウザを作るには?[Windows 8.1ストアアプリ開発]」の「Just-In-Timeデバッガのエラー・ダイアログについて」


まとめ

 Win 8.1では、asyncメソッドで発生した例外もトップレベルで一括してトラップできるようになった。AppクラスのUnhandledExceptionイベントではマネージコードで発生した例外だけが、CoreApplicationクラスのUnhandledErrorDetectedイベントではアンマネージコードで発生した例外も含めてトラップできる。なお、アンマネージコードで発生した例外は、トラップしてもアプリが終了してしまうこともあるので注意が必要だ。

「WinRT/Metro TIPS」のインデックス

WinRT/Metro TIPS

Copyright© Digital Advantage Corp. All Rights Reserved.

RSSについて

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

メールマガジン登録

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