かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

マルチスレッド環境下でのコレクションの操作

WPFでM-V-VMパターンでアプリ組んでた時に、ちょっと悩んでしまったのでメモ。
サービスの呼び出しや、TCP/IPでの通信とかはUIをブロックしないように別スレッドでやるのがセオリーだけど、結果をViewModelのプロパティに反映する際に、普通のプロパティに書き換えは平気だけど、コレクションの操作は例外が起きてしまうといった現象に遭遇しました。
普通のBinidingされただけのプロパティは、例外が出ないので裏でよきにはからってくれてるのかな?(未確認)なんですが、ObservableCollectionにバインドしたListBoxやItemsControl(正確には、その間に入ってる人がイヤイヤしてるかな)がある状態で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とはかけ離れた存在になってしまうんだろうなぁ。でも、仕方ないか。楽なほうがいいし。