読者です 読者をやめる 読者になる 読者になる

銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

テンプレートプロジェクトでの画面遷移とasync voidの美味しくない、かもしれない関係

C# WindowsStroeApp

Windows Store App Advent Calendar 2013 - Qiita [キータ] の2日目のエントリです。

ストアアプリ開発(C#)といえばasyncメソッド!というくらい、ストアアプリをC#で開発していると非同期メソッドが大量に出てきます。時間のかかる可能性のあるAPIはすべて非同期APIのみ、ということなので仕方のないことです。で、asyncメソッドといえば、async voidは避けるべき100億の理由があると弊社では言い伝えられています。参照: neue cc - asyncの落とし穴Part3, async voidを避けるべき100億の理由

が、Visual Studio 2013でストアアプリのGridアプリケーションなどのテンプレートを使うと、画面遷移部分でasync voidが使われています。イベントハンドラ部分なのでやむを得ないような気もするものの、これってどうなるの?を調べたのが今回のブログのネタです。

長くなりそうなのでいったんまとめを。

  • テンプレートで使われているasync void は処理の結果を待機しないという使い道なので、すぐに問題があるというわけではない
  • ただし、アプリケーション全体での例外ハンドラは書かないといけない
  • 非同期処理の部分に別の処理を追加する場合は、「処理の結果を待機しない」ということに注意しないといけない

テンプレートで使われている async void

実際にasync void が使われているのは、たとえばこんなところです。

public GroupedItemsPage()
{
    this.InitializeComponent();
    this.navigationHelper = new NavigationHelper(this);
    this.navigationHelper.LoadState += navigationHelper_LoadState;
}

private async void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var sampleDataGroups = await SampleDataSource.GetGroupsAsync();
    this.DefaultViewModel["Groups"] = sampleDataGroups;
}

NavigationHelperというクラスを介して、画面遷移して来た時にデータを取得する処理を書いています。テンプレートの場合データの取得は、SampleDataSourceクラスでJSONデータを読み込んでいます。

f:id:tanaka733:20131201191604p:plain

こんな感じでメソッドが呼ばれています。

待機しない async void

最初に紹介したのいえせんせーの記事の通り、async void は 処理の完了を待機しません。まずそれを確かめてみましょう。データを取得するときに10秒待つようにしてみます。

private async Task GetSampleDataAsync()
{
    if (this._groups.Count != 0)
        return;
    await Task.Delay(10000);
    Uri dataUri = new Uri("ms-appx:///DataModel/SampleData.json");
    //以下略
}

これで実行すると、画面遷移自体はすぐに完了しますが、データが表示されるのは10秒後になるはずです。

f:id:tanaka733:20131201200033p:plain

f:id:tanaka733:20131201200040p:plain

UIに動きとしては、画面遷移はすぐに完了し、時間のかかる可能性のあるデータ取得は待機せずに実行するのは理に適っています。ただ、async void は待機しない、ということを知っておかないとここに処理を追加するときに困ったことになるかもしれません。 また、処理自体は待機せずに実行したい、だけどその処理の結果を画面遷移完了後に利用したい、という場合にはTaskをフィールドで持つようにするといいようです。

private Task<int> task;

private async void navigationHelper_LoadState(object sender, LoadStateEventArgs e)
{
    // TODO: 問題のドメインでサンプル データを置き換えるのに適したデータ モデルを作成します
    var sampleDataGroups = await SampleDataSource.GetGroupsAsync();
    this.DefaultViewModel["Groups"] = sampleDataGroups;
    // 非同期メソッドを実行するが、待機しない
    task = GetValueAsync();
}

private async Task<int> GetValueAsync()
{
    return await Task.FromResult(1);
}

//画面処理完了後に呼ばれるメソッド。非同期処理の結果を利用する
//非同期処理が完了していればすぐ結果を取得できる。まだ実行中であれば、完了するまで待機する。
private async Task Hoge()
{
    var result = await task;
}

捕捉されない例外

async void のもう一つの問題は例外を捕捉できないということです。実際に動くサンプルを作るのは難しいのですが(非UIスレッドで例外をスローしないといけないので)、注意しないといけないのは、必ず ApplicationのUnhandledException イベントハンドラを登録しておくことです。これがないとasync void内で投げられた例外が突き抜けてアプリを落としてしまいます。

public App()
{
    this.InitializeComponent();
    this.Suspending += OnSuspending;
    UnhandledException += (s, e) => { Debug.WriteLine("最後の例外捕捉"); };
}

ただ、async void の場合、呼び出した側での例外が捕捉されないため、呼び出し側で例外処理を行うことができなくなります。その場合は、テンプレートで用意されているクラスを変更する必要があるでしょう。

まとめ

結果としては、async void が気になって追いかけたものの通常ではそこまで問題ないかなという感じです。(個人的には気になりますが、書き換えるコストも大きいし、というぐぬぬな感じ) ただ、async voidは突然の死を招きかねないので、画面遷移まわりをいじるときは気を付けましょう。