Windows 8.1ストア・アプリではトップレベルでまとめて例外をトラップできるようになった。その実装方法を解説する。
powered by Insider.NET
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」から無償で入手できる。
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
それでは、例外が発生するコードを書いて試してみよう。
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
これで実行してボタンをタップしてみると、「例外を出すメソッド」で例外が発生し、それが先述したAppクラスのUnhandledExceptionイベントハンドラーでトラップされて、次の画像のようにメッセージダイアログが表示される。
このように、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
これを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
このコードはasync/awaitキーワードを使っていないが、AppクラスのUnhandledExceptionイベントではトラップできず、しかもアプリは継続してしまう。例外はスレッドをまたいで伝播しないからである。
なお、「別スレッドで例外を出すメソッド」の末尾にコメントアウトしてある「task.Wait()」を追加すると、そのWaitメソッド内で別スレッドの例外をリスローしてくれるので、トラップできるようになる。
Win8.1では、AppクラスのUnhandledExceptionイベントが改良され、async/awaitキーワードを使っているメソッドで発生した例外もトラップされるようになった。
コードは何も変更せず、そのままWin8.1で実行してみよう*4。async付きのメソッドから呼び出すパターン(=2番目のボタン)でも、Win 8.1では例外がトラップされる(次の画像)。Win 8ではトラップできず、アプリは終了させられていたものだ。
このようにWin8.1ではAppクラスのUnhandledExceptionイベントで例外をトラップできるようになったので、活用していってほしい。
*4 筆者はWin 8.1の環境にVS 2012も入れてあるので、Win 8の環境からソースコードを持ってきてそのままビルドした。VS 2013の用意しかないときは、VS 2013で新しくプロジェクトを作って、上と同様なコードを記述してほしい。
さて、便利になった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
アプリの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イベントではアンマネージコードで発生した例外も含めてトラップできる。なお、アンマネージコードで発生した例外は、トラップしてもアプリが終了してしまうこともあるので注意が必要だ。
Copyright© Digital Advantage Corp. All Rights Reserved.