GUIとスレッド
まず、WPF関係ない話ですが、一般にGUIはシングルスレッドモデルを前提にしてます。つまり、UIコントロールの生成、変更、削除は単一のスレッドからのみに限定されてます。これは主に性能のためです。ので、UIコントロールを触るためのスレッドをUIスレッドとか言ったりします。
WPFも当然ながらこの制約を受けます。じゃあWPFだとマルチスレッド無理なの?という話ではなく、あくまでUIに関する部分のみが単一スレッドなだけです。ので、重い処理とかは別スレッドでやって、処理が終わったらUIスレッドに戻ってUIを更新、みたいな感じになります。Webで言うところのWeb Workersみたいな感じです。余談ですが、GUIアプリとかで固まってUIの応答さえしなくなるものがあったりしますが、あれはUIスレッドで重い処理をしちゃってるからです。
さて、単一のUIスレッド的なものをグローバルに持っておいて、UI更新時にはそれ使ってね、でも良いんですが、より汎用的なモデルとしてWPFではDispatcherというものが間に入ります。
Dispatcher
WPFでは全てのUIコントロールはDisptacherObject
クラスを継承します。このクラス自身はWPFとは直接関係ないもので、「生成時のスレッドで処理を行う必要がある者」が継承するために存在します。で、Dispatcher
というプロパティが用意されています。
このDispatcher
というものが何なのかは、まあ、ひとまず置いておくとして、Dispatcher.Invoke
を介して操作すると、そのオブジェクトの生成スレッドで処理を行ってくれます。わあ便利。
という訳でWPFではViewModelでUIの操作が必要になった場合、そのUIのDispatcherを使ってやる必要があります。まあ、とは言っても今までUIコントロールを生で触ってこなかったので特に何の問題もなかったわけですが。
さて、サンプルコードです。ViewでTextBlockを<TextBlock x:Name="textBlock"/>
みたいな感じで定義してみてください。あ、x:Name
というのはコントロールのインスタンスをコードビハインドで使用できるようにするためのものです。似たようなものにx:Key
がありますが、これはリソースで使われます。MVVM、つまり見た目とロジックを分離するよう務めるならx:Name
はあまり積極的に使用すべきではないでしょう。x:Key
は普通に使いまくります。
で、Viewのコードビハインド、つまりView.xaml.csのコンストラクタでTask.Run(() => textBlock.Text = "hogepiyo");
と書いてみてください。はい、落ちましたね?
「このオブジェクトは別のスレッドに所有されているため、呼び出しスレッドはこのオブジェクトにアクセスできません。」というメッセージと共にInvalidOperationExceptionが投げられたかと思います。まあ、これがGUIの単一スレッドモデルというやつです。
で、これをTask.Run(() => textBlock.Dispatcher.Invoke(() => textBlock.Text = "hogepiyo"));
に書き換えると動きます。別スレッドの中で更にUIスレッドに移動してるわけですね、ややこしい。
データバインディング
さて、今までデータバインディングを使ってきたわけですが、ひょっとしなくてもこれは裏でUIコントロールを更新しています。が、実はデータバインディングは特別扱いを受けているのでUIスレッド以外から更新しても上手くいきます。試してみたい場合はデータバインディングを定義し、Task.Run
内でプロパティを更新してみてください。
が、残念ながらコレクションに関しては自動ではやってくれません。
早速試してみましょう。Viewに<ListView ItemsSource="{Binding Names}"/>
とでもしておいて、ViewModelを以下のようにします。
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Livet;
public class MainWindowViewModel : ViewModel
{
readonly ObservableCollection<string> names = new ObservableCollection<string>();
public ObservableCollection<string> Names { get { return names; } }
public void Initialize()
{
Task.Run(() => Names.Add("Kokudori"));
}
}
「この型の CollectionView は、Dispatcher スレッドとは異なるスレッドからその SourceCollection への変更をサポートしません。」というメッセージで今度はNotSupportedExceptionが投げられました。まあ、サポートしてませんよと。
実際に別スレッドでバインディングされたコレクションにAddすることは全然あり得るのでこれはちょっと困ります。ので、Livetには自動でDispatcherに対応している通知コレクション、DispatcherCollection<T>
があります。
using System.Threading.Tasks;
using Livet;
public class MainWindowViewModel : ViewModel
{
readonly DispatcherCollection<string> names = new DispatcherCollection<string>(DispatcherHelper.UIDispatcher);
public DispatcherCollection<string> Names { get { return names; } }
public void Initialize()
{
Task.Run(() => Names.Add("Kokudori"));
}
}
あ、Viewは前のままです。DispatcherHelper.UIDispatcher
というのは名前の通り、UIスレッドのDispatcherです。これはApp.xaml.csで確保されています。
さて、これで問題なくコレクションを別スレッドから操作することが出来るようになりました。
BindingOperations.EnableCollectionSynchronization
(追記)
WPF4.5からBindingOperations.EnableCollectionSynchronization
メソッドによって通知コレクションを複数のスレッドにまたがって通知することが可能になったようです。
BindingOperations.EnableCollectionSynchronization(collection, lockobj);
長い。
lockobj
は同期を取るためのオブジェクトで、lock
文で必要になるのと同じやつです。フィールドにobject
型で確保して置くと良いでしょう。
オーバーロードでコールバックも登録できるようです。が、独自デリゲートです。何故なのか。
https://msdn.microsoft.com/ja-jp/library/hh198845%28v=vs.110%29.aspx
まとめ
UIコントロールをViewModelで生成して云々みたいなアプリでも作らない限り、基本的にはデータバインディングだけでなんとかなると思います。その場合、普通のデータバインディングは何も気にせずとも使えます。通知コレクションは罠っぽい感じですが、DispatcherCollection<T>
を使えばこれまた何も気にせずとも使えるのでまあ良いでしょう。
あ、ちなみにViewModelでUIコントロール(もっと言うとDispatcherObject
)を生成する場合、UIスレッド以外で生成するとメモリリークが発生するようです。
バックグラウンド スレッドで UI 要素を作るとメモリリークする (WPF)
バックグラウンドスレッドでUI要素を作るともっと問題は深刻かもしれない。(WPF)