この記事は、「C# Advent Calendar 2014」の21日目の記事です。
WPFやWindows Phoneやストアアプリでは、Windows Formsのような「コードビハインド」による実装の他に、「データバインディング」を積極的に使う実装を行う事が出来ます。というよりも、データバインディングという手法を使わないと、複雑なUIを表現するのが困難です。この記事では、その辺りと非同期がどう関係するのかを書いてみたいと思います。
(なお、例はWPFですが、ストアアプリでもほぼ同様です)
データバインディングって何?
そもそもデータバインディングとは、どういう手法でしょうか?
Windows Formsの時代、データバインディングと言うと「DataSet」クラスの内容を、「DataGridView」コントロールを使って表示させるという用途が一般的でした。
詳しい解説は「Windowsフォームにおける「データ・バインディング」」を参考にして下さい。
この手法の問題点として:
- DataSetやその周りのクラス(DataTableなど)と、List等の限定されたクラスがターゲット。
実際には、IListインターフェイスやIBindingSourceインターフェイスを実装したクラスであれば受け入れる事が出来ますが、殆どの場合はこれらのインターフェイスを独自実装しないで、単にDataSetクラスを使用していたと思います。
- DataGridViewのセル内に、更に別の(コレクション)要素を配置するようなリッチな表現を、統一された方法で実現するのが困難。
「DataGridViewのセル内に「コンボボックス」を配置するのはどうしたら良いのか?」と言うのは、定期的に発生するFAQであったと思いますが、このような拡張がスムーズに行えないのは、グリッドの表現をDataGridViewが独自に制御していたため、応用性が無く、学習効果が得られないからと思われます。
- バインディング対象のインスタンスがコンポーネントとしてフォームに設定される必要があり、柔軟性に欠ける。
DataGridView以外のコントロールでも、バインディング機能を使う事は出来ました。但し、バインディングしたいインスタンス(モデルクラスやコレクションなど)は、コンポーネントとしてフォームに配置される必要があったため、フォームとデータが密結合する事になり、データバインディングする事の意義が見出しにくい状態でした。また、コントロールへの直接アクセスの手法があまりに一般に広まったため、そもそもこの手法が利用可能である事も周知されませんでした。
「DataSetを用意して、単純にDataGridViewに表形式で表示」させるのであれば十分なのですが、それ以上のUI表現に踏み込むと途端に難易度が高くなり、インターフェイスが複雑化してしまいます。
また、DataSet自体がデータベースのテーブルを模倣したものであったため、階層化されたデータを容易に扱えないという問題もあります。Visual Studioのデザイナー機能によって「型付きDataSet」を生成出来るようになりましたが、結局(生の)DataSetをラップしたものに過ぎず、本来コード上でモデル型として表現されている構造から乖離してしまう事は避けられませんでした。
.NET 3.0でWPFが発表され、ここの部分に大きなメスが入りました。DataGridViewのような、特殊なクラスの特殊な機能によってバインディングを実現するのではなく、そもそもWPFの枠組みでこれを実現し、どのようなコントロールでも統一されたバインディングを使えるようにし、UIとデータを完全に分離可能にした… それが今のデータバインディングです。
図のように、「DataGridコントロール(WPF)」と「TextBoxコントロール(WPF)」は、両方ともモデルクラスとのデータの送受を「バインディング式(Binding expression)」で実現しています。この手法には全く違いは無く、更にDataGridコントロールの場合は、階層化されたモデルクラスのインスタンス群をどのようにDataGridで表現するのかまでを、全てバインディング式で記述する事が出来ます。
DataGridコントロールだけではなく、リスト形式で要素を表示する「ListBoxコントロール」や、コントロール要素の配置を制御する「Panelクラス」を継承したコントロール群「DockPanel・StackPanel・Grid等」も、ほぼ同様の手法でデータとの関連付けが可能です。
そして、これらのコントロールを階層化して組み合わせた複雑なUIは、データバインディングの規則に従っている限り、技術的な制限なく実装する事が出来ます。
WPFでも、従来のWindows Formsのようにコントロールに直接アクセスして、ほぼ全てを制御する事も出来ますが、データバインディングを使えるようになると、かなり面倒に感じられるはずです。また、ちょっとUIが複雑化するだけで実現難易度が高くなるため、単調なUIで実現する事しか選択の余地が無くなってしまいます。
(Windows Formsが楽と言う意味ではなく、むしろ複雑なUIを実現する場合、Windows Formsでは更に難易度が高い)
データバインディングのキモ
データバインディングのキモは、コントロールの公開するプロパティと、モデルクラスの公開するプロパティの値を自動転送するところです。以下にTextBoxの例を示します。
これはXAMLの定義です:
<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="525"
Height="100">
<TextBox x:Name="sampleTextBox" />
</Window>
対応するコードビハインド:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// TextBoxに初期文字列を設定
sampleTextBox.Text = "ABC";
}
}
TextBoxのTextプロパティは、テキストボックスに入力された文字列をやり取りするプロパティです。あらかじめこのプロパティに値を設定すれば、テキストボックスの初期表示文字列となり、ユーザーが文字列を入力したり編集したりすれば、それがこのプロパティから読み取れます。ここまではWindows Formsと同様です。
ここで、Textプロパティにバインディング式を記述します。バインディングするためには、相方のプロパティが必要です。とりあえず、MainWindowクラスに直接実装しましょう。
public partial class MainWindow : Window
{
// バインディングされる値を保持するフィールド
private string sampleText_ = "Bound!";
public MainWindow()
{
InitializeComponent();
// バインディング対象のインスタンスを、このクラス自身に設定
this.DataContext = this;
}
// バインディングされるプロパティ
public string SampleText
{
get
{
return sampleText_;
}
set
{
sampleText_ = value;
}
}
}
XAMLにはこのようにバインディング式を書きます:
<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="525"
Height="100">
<!-- TextプロパティとSampleTextプロパティをバインディングする式 -->
<TextBox Text="{Binding SampleText}" />
</Window>
XAMLのコードについて、重要な部分が二カ所あります。一つはバインディング式を記述した事です。TextBoxにはTextプロパティがあるのですが、そのプロパティに対して「{Binding SampleText}」と記述します。この中括弧+Bindingで始まる指定がバインディング式です。「SampleText」と言うのは、バインディングさせたい対象のプロパティ名です。ここではMainWindowクラスに定義した「SampleText」プロパティを指定しています。
もう一つの重要な点は、もはや「x:Name」による、コントロールへの命名が不要になっている点です。この点についてはまた後で説明します。
MainWindowクラスへの変更も説明をしておきます。MainWindowクラスにも重要な二カ所の変更点があります。一つはバインディング対象のプロパティ「SampleText」を追加した事です。これに合わせて、値を保存しておくためのフィールドも追加してあります(熱心なC#信者なら、自動実装プロパティを使う事が出来ると思うかもしれません。デバッガで動きを追えるようにするためにわざと手動で実装しています)。
もう一つはコンストラクタに追加した、「DataContext」プロパティにthisを代入するという式です。このプロパティは、XAML上でバインディング式を実行する際に、デフォルトのバインディング対象のインスタンスを特定するために使われます。ここでthisを代入しているという事は、デフォルトのバインディング対象はMainWindowクラスのインスタンスとなる、と言う事です。そして、XAML上に記述したバインディング式が「{Binding SampleText}」となっているので、結局「MainWidnowクラスのインスタンス」の「SampleTextプロパティ」が、TextBoxのTextプロパティにバインディングされる、と言う事になります。
さて、このコードを実行すると、望みの通り、バインディングが実現し、初期状態としてフィールドの値が表示されます。しかし、テキストボックスの文字列をタイプして変更しても、SampleTextプロパティのsetterに遷移しません(ブレークポイントを設定して確かめて下さい)。
これは、バインディング粒度のデフォルトが、コントロールからフォーカスが外れた時、となっているためで、サンプルではTextBoxが一つしかないためにそのタイミングが無い事によります。ここでは単純に、「一文字でも変化があれば」、と変えておきます。この指定もバインディング式だけで実現出来ます。
<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="525"
Height="100">
<TextBox Text="{Binding SampleText, UpdateSourceTrigger=PropertyChanged}" />
</Window>
XAMLのバインディング式に「UpdateSourceTrigger=PropertyChanged」と付け加えます。すると、バインディング処理の実行が、Textプロパティに変化があった時という条件となります。これで、setterにブレークポイントを張っておいて、テキストボックスで「A」をタイプするとこのようになり、バインディング式によって自動的に値が転送されて来る事が分かります。
上記のデータバインディング例を図示すると、このようになります。従来のWindows Formsのように、イベントハンドラのフックやTextBoxのTextプロパティへの直接アクセスをする事無く、値の受け渡しが出来ている事になります。おっと、忘れる所でした。これで「x:Name」によるコントロールの命名が不要になっている理由も分かりますね。プロパティ同士がバインディング式で直接転送されるので、「どのコントロールに対する操作なのか」の明示は不要となっているのです。
(ここで解説しませんが、能動的にSampleTextの値が変化する場合は、MainWindowクラスにINotifyPropertyChangedインターフェイスを実装して、プロパティの値が変化したことをWPFに通知する必要があります)
ここまで、Windows Formsっぽい手法からスタートして説明するため、バインディング対象のインスタンスをDataContextプロパティにthisを代入する事で「MainWindow」そのものにしましたが、更にコードの責務を分離するために、バインディングするクラスを独自のクラスに完全に分離することも出来ます。例えば「MainWindowViewModel」クラスを作り、DataContextにこのクラスのインスタンスを代入する事で、バインディング対象をMainWindowから切り離す事が出来ます。
// MainWindowクラスから完全に独立したクラス(ビューモデル)
public sealed class MainWindowViewModel
{
// バインディングされる値を保持するフィールド
private string sampleText_ = "Bound!";
// バインディング対象のプロパティ
public string SampleText
{
get
{
return sampleText_;
}
set
{
sampleText_ = value;
}
}
}
// コードビハインドが殆ど存在しないMainWindowクラス(ビュー)
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
// バインディング対象のインスタンスを、MainWindowViewModelに設定
this.DataContext = new MainWindowViewModel();
}
}
これが「MVVM(Model-View-ViewModel)」パターンの入り口です。
データバインディングと非同期
長い前フリでしたが (;´Д`) 本題に入ります。ストアアプリに導入された新しいWinRT APIのセットは、原則として外部リソースアクセスは全て非同期で実現しなければならないようになっています。ブログの記事や勉強会でも説明した通り、これらはC#で言う所の「async/await」を使って実現します(より深くはTaskクラスやIAsyncInfoインターフェイスを使う事ですが、本題と関係ないので省略)。
「非同期処理」と「ユーザーインターフェイス」とはどう絡むのかが本題です。この問題は古典的でもあって、一般的なウェブブラウザのフォームでも同じような問題を抱えていました。例えば、「購入」ボタンを何度もクリックされると困るので、クリック後はボタンを無効化して押せなくする等の処置は、(ブラウザとは無関係に)ウェブサーバー側で購入処理が非同期で実行されるから発生する問題でした。
あるいはもっと良い方法もあります。何度「購入」ボタンが押されても問題ないように、同じセッションが維持されていると分かる場合は、単一の要求と見なすなどです。
どちらの手法を取るにしても、「非同期処理は何も対策しないで実現することは無理」と言う事です。async/awaitのサポートによって、非同期処理の実装難易度は格段に改善されたのですが、この部分をどうするのかは、仕様や実装を行う者に任されています。
二つのパターンの背景を説明しましたが、非同期処理をユーザーインターフェイスを工夫する事によって担保する方法について、データバインディングと絡めて考えてみます。
非同期処理中にユーザーインターフェイスをロックダウンする、と言うのは、もっとも単純で効果的な方法です。真っ先に思いつくのは、Windowsクラス全体のIsEnabledをfalseに設定する事でしょう。これをいつ行えばよいでしょうか?というよりも、非同期処理をどこで開始するのか?と言う事です。
例えば、UIにボタンが配置されており、ボタンのクリックと同時に非同期処理が開始されるとします。データバインディングを使うのであれば、「ICommandインターフェイス」を実装したクラスを使って、ボタンクリックのイベントを「バインディング」するでしょう。移譲されたイベントハンドラは、以下のような実装となります。
// (これは擬似コードのため動きません。INotifyPropertyChangedの実装やCommandクラスの実装が必要です)
public sealed class MainWindowViewModel
{
public MainWindowViewModel()
{
// ICommandインターフェイスを実装したクラスを使って、バインディング可能なイベントハンドラを公開する
this.FireStart = new Command(this.OnFireStart);
}
// バインディング可能なイベントハンドラ(ボタンのCommandプロパティにバインディングする)
public ICommand FireStart
{
get;
private set;
}
// ウインドウ全体の有効・無効を制御する(MainWindowのIsEnabledプロパティにバインディングする)
public bool IsEnabled
{
get;
private set;
}
// Commandクラスによって、転送されるイベントの実処理
private async void OnFireStart(object parameter)
{
// ユーザーインターフェイスを無効化する
this.IsEnabled = false;
try
{
// 非同期処理...
using (var stream = await httpClient.GetStreamAsync("..."))
{ ... }
}
finally
{
// ユーザーインターフェイスを有効化する
this.IsEnabled = true;
}
}
}
この例では、ボタンのクリックと言う「アクション」を引き金に動作するので、比較的分かりやすいと言えます。非同期プログラミングのベストプラクティスによれば、シグネチャとして「async void」と定義するのはイベントハンドラの場合だけだと述べられています。この例は本物(CLR)のイベントハンドラではありませんが、ICommandによって間接的に呼び出されるコールバックとして、広義において同じと見なしても良いと思います。
では、値を転送するデータバインディングそのものが引き金になる場合はどうでしょうか?前フリのような、テキストボックスに対する編集操作で、非同期的に処理を実行し、結果を反映する場合です。例えば、Visual Studioのインテリセンスがシンボル名の絞り込みを行う過程や、ウェブで言うなら「googleサジェスト」のような機能です。
この場合、バインディングされたプロパティに、一文字変化がある度に値が転送されてきます。転送はsetterのコールによって実行されるため、非同期処理はsetterから始める必要があります。
<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="525"
Height="200"
IsEnabled="{Binding IsEnabled}">
<DockPanel>
<!-- Textプロパティ同士をバインディングし、テキストボックスへの文字の入力を検知可能にする -->
<TextBox DockPanel.Dock="Top" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />
<!-- 結果を表示するリストボックス -->
<ListBox ItemsSource="{Binding ResultItems}">
<ListBox.ItemTemplate>
<DataTemplate>
<!-- 結果を表示するテンプレート -->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>
// (これも同様に擬似コードです)
public sealed class MainWindowViewModel
{
private string text_;
public MainWindowViewModel()
{
this.ResultItems = new ObservableCollection<ResultModel>();
}
// TextBox.Textにバインディング(UpdateSourceTrigger=PropertyChangedとする)
public string Text
{
get
{
return text_;
}
set
{
// ユーザーインターフェイスを無効化する
this.IsEnabled = false;
try
{
// 非同期処理...
using (var stream = await httpClient.GetStreamAsync("...?q=" + text_))
{ ... }
}
finally
{
// ユーザーインターフェイスを有効化する
this.IsEnabled = true;
}
}
}
public bool IsEnabled
{
get;
private set;
}
public ObservableCollection<ResultModel> ResultItems
{
get;
private set;
}
}
このコードには仕様的にも実装的にも問題があります。仕様面では、一文字入力する度にWindowが無効化される(しかも非同期処理が完了するまで解除されない)ため、恐らくユーザー体験は最悪の物となる事です。実装的な問題としては、setter内ではawait出来ない事です。これはsetterにはasyncを指定出来ない事から来ています。
まず、実装的な問題は、実処理をメソッド抽出すれば解決します。
public sealed class MainWindowViewModel
{
private string text_;
public MainWindowViewModel()
{
}
// TextBox.Textにバインディング(UpdateSourceTrigger=PropertyChangedとする)
public string Text
{
get
{
return text_;
}
set
{
// 非同期メソッドを呼び出すが、待機は出来ない。
// →Taskを返し、メソッド名に「Async」と付ける事で、setter呼び出し後は、非同期処理が継続する事がはっきりする
var task = this.ProceedTextAsync(value);
}
}
// 一般的な非同期処理のシグネチャを順守
private async Task ProceedTextAsync(string value)
{
// ユーザーインターフェイスを無効化する
this.IsEnabled = false;
try
{
// 非同期処理...
using (var stream = await httpClient.GetStreamAsync("...?q=" + value))
{ ... }
}
finally
{
// ユーザーインターフェイスを有効化する
this.IsEnabled = true;
}
}
public bool IsEnabled
{
get;
private set;
}
}
実処理を「ProceedText」としてasync voidとする事も出来ますが、ここではより汎用的になるように(そしてベストプラクティスに従うように)、Taskクラスを返却するようにしました。setterで戻り値のTaskを無視すると、C#コンパイラが警告を発します。そのため、ダミーの変数で受けるように書いています。わざわざTaskクラスを返す事で、別の仕様要件でProceedTextAsyncを呼び出さなければならない場合にも、非同期処理の待機が可能になる余地を残す事が出来ます。
残る仕様的な問題ですが、ウインドウ全体を無効化する必要が無いのであれば、特定のコントロールに絞って無効化を実施するという方法が考えられます。例えば、検索結果に応じてリストボックスの内容を変化させる場合、テキストボックスは有効としたまま、リストボックスだけを無効化するようにすれば良いのです。その場合、ロジックの変更は必要ありません。XAMLのバインディング式をListBoxに適用します。
<Window x:Class="CenterCLR.AsyncBindings.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"
Width="525"
Height="200">
<DockPanel>
<TextBox DockPanel.Dock="Top" Text="{Binding Text, UpdateSourceTrigger=PropertyChanged}" />
<!-- IsEnabledをListBoxにバインディング -->
<ListBox ItemsSource="{Binding ResultItems}" IsEnabled="{Binding IsEnabled}">
<ListBox.ItemTemplate>
<DataTemplate>
<!-- 結果を表示するテンプレート -->
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Window>
非同期処理のエラーの対処
ここまでで、データバインディングで値が転送された場合の非同期処理は対処可能になりました。では、非同期処理中に発生するエラーはどのように対処すれば良いでしょうか?
そもそも、仕様的な面で非同期処理中のエラーにどう対処するかを考える必要があります。最後の例では、ユーザーがテキストボックスに何かタイプすると、非同期で検索処理を行い、結果がリストボックスに(非同期的に)表示されます。一文字タイプし、その事が原因で何らかのエラーが発生したとしても、ユーザーはもう次の文字をタイプしているかも知れませんし、ウインドウ上の別の機能を実行しているかも知れませんし、ウインドウを閉じているかも知れません。
- エラーが発生したら、その場でメッセージボックスを表示する。
単純なユーザーインターフェイスであれば、これでも良いと思われます。但し、複数の非同期処理を実行している可能性があり、それらが一斉に失敗すると、メッセージボックスが大量にスクリーンに表示される可能性があります。また、忘れた頃にいきなり表示される可能性もあります。
- ログ出力のような表示領域に表示させる。
過去に発生した内容を表示できるので、より望ましいと言えます。但し、どのメッセージが何の操作に紐づいていたのかは分かりにくくなるかも知れません。
どのような方法でも、握りつぶすのでなければ、対処方法は同じでしょう。非同期処理の実質的なコードが例外をスローする場合は、単にtry-catchで例外を補足し、エラー処理を行います。
private async Task ProceedTextAsync(string value)
{
// ユーザーインターフェイスを無効化する
this.IsEnabled = false;
try
{
try
{
// 非同期処理...
using (var stream = await httpClient.GetStreamAsync("...?q=" + value))
{ ... }
}
catch (Exception ex)
{
// メッセージボックスを表示する
MessageBox.Show(ex.Message, "Error occurred!", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
finally
{
// ユーザーインターフェイスを有効化する
this.IsEnabled = true;
}
}
注意点としては、C# 5.0ではcatch-finallyブロック内でawait出来ない事です。WPFの場合はMessageBoxの内部でメッセージポンプが実行される(Win32メッセージループが実行される)ので良いのですが、ストアアプリではawaitで待機する必要があります。そのため、多少泥臭いコードを書く必要があります(エラーを保存して一旦catchブロックから抜けてから表示するなど)。
エラー処理のコード例をお見せしましたが、結局のところ、一般的なコードと何も変わらないことが分かると思います。
ビジネスロジックの分離
仕様がより高度になるにつれ、データバインディングを実装しているクラスを分割したくなります。特にユーザーインターフェイスに対応するための固有のコードと、いわゆるビジネスロジックの分離です。リファクタリングの作法に従って、責務によってクラスを分割して行くと、MVVMで言う所のビューモデルとモデルに分割されます(うまくやればね :)。この時、前述の例で示したHttpClientによる検索処理などのビジネスロジックは、ビューモデル(元のクラス)からモデルに移動する事になります。
実装を移動する際、HttpClient.GetStreamAsyncは非同期メソッドなので、await可能でなければなりません。と言う事は、移動先のメソッドにはasyncが適用されなければならず、ベストプラクティスに従うならTaskクラスを返却するメソッドでなければなりません。結局のところ、「ProceedTextAsync」メソッドはほぼそのままの形でモデルクラスに移動する事になります。
ProceedTextAsyncがビューモデルに依存している箇所は、それぞれを以下のように対処します。
- 結果をどう反映するか : モデルはthis.ResultItemsにはアクセス出来ない。したがって、ProceedTextAsyncは結果をまとめて(コレクションに入れるなどして)返却する。
- IsEnabledへのアクセス : モデルは直接UIとの関係を持たないと考えると、IsEnabledの操作はビューモデルに残しておく。
- メッセージボックスへのアクセス : モデルは直接UIとの関係を持たないと考えると、メッセージボックスの操作(例外の処理)はビューモデルに残しておく。
以下の例では、モデルクラス自身をコレクション(List)とし、UpdateAsyncメソッドでここに結果を保持するようにします。モデルの存在意義を考えてこのようにしましたが、実際の所はメソッドを単なる移譲として分離(戻り値にコレクションを返すスタティックなメソッド)した方が良かったかも知れません。
// ビジネスロジックを含むモデル(コレクション)
public sealed class BusinessLogicModel : List<ResultItem>
{
// コレクションを更新する
public async Task UpdateAsync(string value)
{
// 非同期処理...
using (var stream = await httpClient.GetStreamAsync("...?q=" + value).
ConfigureAwait(false)) // 以降の処理をワーカースレッドで実行
{
var document = SgmlReader.Parse(stream);
// ここではUIを直接操作出来ない(ワーカースレッドなので)
this.Clear();
this.AddRange(
from html in document.Elements("html")
from body in html.Elements("body")
// ...
select new ResultItem { ... });
}
}
}
このように分離すれば、あとはビューモデルから呼び出すだけです。
private async Task ProceedTextAsync(string value)
{
// ユーザーインターフェイスを無効化する
this.IsEnabled = false;
try
{
try
{
// モデルを用意して結果をフィルする
var model = new BusinessLogicModel();
await model.UpdateAsync(value);
// バインディングする
// (awaitでメインスレッドにマーシャリングされているので、UIを操作しても問題ない)
this.ResultItems = model;
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "Error occurred!",
MessageBoxButton.OK, MessageBoxImage.Error);
}
}
finally
{
// ユーザーインターフェイスを有効化する
this.IsEnabled = true;
}
}
更なる発展
コレクションは段階的に反映したいですね。結果が認識されるたびに、1件ずつリストボックスに反映されたりすると、ユーザーインターフェイス的にとても良いと思います。
このような実装にするには、結果が1件取得される度に、バインディングしているコレクションに反映して、それがUIにも反映される必要があります。コレクションへの反映はINotifyCollectionChangedインターフェイスで通知することが出来ます。このインターフェイスはObservableCollectionクラスが実装しているので、Listの代わりにこのクラスを使うと良いでしょう。
注意点として、コレクションへの追加処理は、UIスレッド(WPFでは恐らくメインスレッド・ストアアプリではビュー毎に割り当てられているスレッド)で実行する必要があるという事です。ここにトレードオフがあります。
CPU依存性処理をワーカースレッドで効率よく処理させるために、「ConfigureAwait」メソッドを使用したくなるかもしれません。これにより、awaitの度にUIスレッドにマーシャリングされなくなるため、パフォーマンスが向上します。しかし、当然の事ながらUIスレッドにマーシャリング出来ないので、(メソッドを抜けるまでは)UIに絡む処理は実行出来ません。
ObservableCollectionを使用した場合、要素の変更時に「CollectionChanged」イベントを発火します。このイベントの発火はUIスレッドで実行しなければならないため、ObservableCollectionの操作は暗黙にUIスレッドで実行する必要があります。
前述のコード例でConfigureAwaitを使用しているのは、結果を一旦Listに保存しているので、直接的にも間接的にもUIに依存していない事が分かっているためです。このように、呼び出し元のスレッドがどこまで有効に作用するのかを考えて、ロジックを分離すると良いでしょう。
この辺りを掘り下げた資料は、以前の勉強会で発表した資料があるので、参考にどうぞ。
今年もあと少し、今日は寒くて堪りませんでした。明日はmoririringさんです。よろしく!