WPFでM-V-VMパターンでアプリ組んでた時に、ちょっと悩んでしまったのでメモ。
サービスの呼び出しや、TCP/IPでの通信とかはUIをブロックしないように別スレッドでやるのがセオリーだけど、結果をViewModelのプロパティに反映する際に、普通のプロパティに書き換えは平気だけど、コレクションの操作は例外が起きてしまうといった現象に遭遇しました。
普通のBinidingされただけのプロパティは、例外が出ないので裏でよきにはからってくれてるのかな?(未確認)なんですが、ObservableCollection
ということで、コレクションの操作はDispatcher経由でやるのがいいということになるのですが、いちいち
// UI操作するのに使えるディスパッチャをどうにかしてゲット Dispatcher d = UIを操作できるDispatcher; // コレクションの操作をDelegateに入れて Action action = () => collection.Add(item); d.Invoke(action); // UIスレッドで合法的に操作
って書くのはだるいです。要は、コレクションを変更した通知処理がUIとは別のスレッドで動いていることが問題なので、そこの部分をうまくやってくれるコレクションクラスを作って、それを使えばいい!!ってことで以下のようなものを作りました。
public class DispatchObservableCollection<T> : ObservableCollection<T> { // CollectionChangedイベントを発行するときに使用するディスパッチャ public Dispatcher EventDispatcher { get; set; } #region コンストラクタ public DispatchObservableCollection() { InitializeEventDispatcher(); } public DispatchObservableCollection(IEnumerable<T> collection) : base(collection) { InitializeEventDispatcher(); } public DispatchObservableCollection(List<T> list) : base(list) { InitializeEventDispatcher(); } private void InitializeEventDispatcher() { // インスタンスが作られた時のDispatcherを取得 this.EventDispatcher = Dispatcher.CurrentDispatcher; } #endregion protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { if (IsValidAccess()) { // UIスレッドならそのまま実行 base.OnCollectionChanged(e); } else { // UIスレッドじゃなかったらDispatcherにお願いする Action<NotifyCollectionChangedEventArgs> changed = OnCollectionChanged; this.EventDispatcher.Invoke(changed, e); } } // UIスレッドからのアクセスかどうかを判定する private bool IsValidAccess() { // Dispatcherが設定されていないときは、どうしようもないのでOKにしとく // Dispatcherが設定されていたら、今のスレッドとDispatcherのスレッドを見比べる return EventDispatcher == null || EventDispatcher.Thread == Thread.CurrentThread; } }
こいつをObservableCollection
*1
*1:こういうのが増えていくとViewModelはPOCOとはかけ離れた存在になってしまうんだろうなぁ。でも、仕方ないか。楽なほうがいいし。