非同期処理をおこなうための方法はいくつかありますが、C# 5.0 以降では async/await 演算子を用いることで非同期処理に関する記述が簡潔になりました。
ここでは async/await 演算子の基本的な使い方について紹介します。
サンプルプロジェクトはここからダウンロードできます。
ページ内リンク
概要
ここでは次のような UI のアプリケーションを作成します。
このアプリケーションは、ボタンを押すと重たい処理を実行しますが、
この処理は非同期処理されるため、
UI がフリーズすることなく動作するようになっています。
したがって、ボタンを押した直後も下図のようにチェックボックス等の操作ができます。
ただし、連続して処理が開始されないように、
処理中はボタン操作できないようにします。
サンプル UI の作成
上記のサンプル UI の XAML は次のようになります。
<Window x:Class="AsyncSample.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainView" Height="300" Width="300">
<StackPanel>
<Button Content="Click me!" Command="{Binding ButtonCommand}" />
<TextBlock Text="{Binding Result}" />
<TextBox Text="テキスト入力" />
<CheckBox Content="Check me!" />
</StackPanel>
</Window>
Code 1 : サンプル UI の XAML コード
StackPanel コントロールで各コントロールを並べているだけです。
ボタンを押したときに処理を開始させたいので、
Button コントロールには ButtonCommand プロパティを、
処理が実行されているかどうかをわかるようにしたいので、
TextBlcok コントロールには Result プロパティをそれぞれデータバインドしています。
詳細は次項で説明します。
ViewModel の作成 (同期処理として動作)
上記のサンプル UI でデータバインド設定した ButtonCommand プロパティおよび
Result プロパティを実装した ViewModel を次のように定義します。
Result プロパティは重たい処理の前後に書き換えられ、
現在の状態をテキストで表示させます。
IsBusy プロパティは重たい処理実行中に true になるようにします。
そして ButtonCommand プロパティでボタンが押されたときの処理およびボタンの有効/無効判定をおこないます。
ここでは重たい処理を HeavyWork() メソッドで表現しています。
このメソッドでは重たい処理を仮想的に表現するために Sleep() メソッドで 3[s] 間待機するようにしています。
また、その前後で Result プロパティおよび IsBusy プロパティを変更することで、
重たい処理を実行しているときとそうでないときを区別しようとしています。
しかし、このままでは重たい処理は同期的に処理されるため、ボタンが押されるとこの処理が終わるまで応答が返ってこなくなってしまいます。
出力ウィンドウにも
と表示され、すべて順番に実行されている様子がわかります。
また、重たい処理の前後で Result プロパティや IsBusy プロパティを変更していますが、
その変化は UI に反映されていません。
これもやはり重たい処理の部分でプログラムが返ってこないためです。
そして、プログラムが返ってきたときにはもう Result プロパティは "終了"、
IsBusy プロパティは false に戻ってしまうため、
UI の見た目には実行中の動作は現れてきません。
非同期処理に書き直した ViewModel
それでは上記のコードを非同期処理に書き直します。
先ほどの HeavyWork() メソッドを HeavyWorkAsync() メソッドからコールする形にします。
await 演算子を付けてコールすることで、
そのメソッドを非同期処理することを意味します。
また、await 演算子を使っているということを宣言するために、
HeavyWorkAsync() メソッドに async 修飾子を付けています。
await 演算子を使ってコールされるメソッドは必ず System.Threading.Tasks.Task クラスを返す必要があります。
これは、await 演算子によってメソッドを実行するのではなくタスクを実行するためです。
このため、HeavyWork() メソッドの戻り値を Task クラスとし、
その中身は Task.Run() メソッドを使って、
スレッドプール上に配置するタスクのタスクハンドルを返すようにします。
実際の処理は Run() メソッドの中にデリゲートの形で記述します。
最後に、HeavyWorkAsync() メソッドを呼ぶように ButtonCommand プロパティの中身を書き換えます。
以上の変更によって、Fig.2 に示したように重たい処理が非同期処理されるようになります。
また、出力ウィンドウへの出力は
となり、重たい処理をコールし終わったメッセージが、
重たい処理が終了する前に出力されている様子がわかります。
また、スレッドの ID が非同期処理をコールした側と、
実際に重たい処理を実行している側で異なっていることが確認できます。
上記で示しているコードの中のコメントにも書いてあるように、
async/await による非同期処理をおこなった場合、
それぞれのスレッドで実行されるコードの順番が少し複雑になります。
ここで紹介しているコードと実行結果のスレッド ID を照らし合わせながら
順に追って把握しておくことをお勧めします。