メニュー

技術ブログ

Denso IT Lab.社員による技術情報の紹介

Denso IT Laboratory researcher's blog sites

数式

RSS

ページトップへ

[MVVM] UIスレッドへの通知を適切なコンテキストで行うBindableBase

 

[MVVM] 便利で間違えのないプロパティ変更通知を行うBindableBaseの実装

で、表記を短く、タイプセーフに、ポータブルに書く方法を紹介しました。

ところで、重いワーカースレッドを持つような処理だと、そもそもプロパティ変更通知自体がスレッド越えをしないといけないシーンが増えます。

特にモデルから直接ビューへデータバインディングをしているような場合は、適切なスレッド管理をするのは難しそうです。

これに透過的に対処するBindableBaseというのはどのようになるのでしょうか?

 

ヘルパーライブラリなどには例が無いようですが、ネット上にいくつかこれにトライしたものがあります。

DispatcherをBindableBase内部で用いるパターン

A better BindableBase – ZAG LOG

Zag Studioのブログに書かれている例ですが、CoreWindowなどからゲットしたUIスレッドのディスパッチャーをもってこれを直接呼び出します。

   1: protected void OnPropertyChanged([CallerMemberName]

   2:     string propertyName = null)

   3: {

   4:     if (_dispatcher == null || _dispatcher.HasThreadAccess)

   5:     {

   6:         var eventHandler = this.PropertyChanged;

   7:         if (eventHandler != null)

   8:         {

   9:             eventHandler(this,

  10:                 new PropertyChangedEventArgs(propertyName));

  11:         }

  12:     }

  13:     else

  14:     {

  15:         IAsyncAction doNotAwait =

  16:             _dispatcher.RunAsync(CoreDispatcherPriority.Normal,

  17:             () => OnPropertyChanged(propertyName));

  18:     }

  19: }

 

コードビハインドなどで書いている場合はこれで十分ですね。

Making sure OnPropertyChanged() is called on UI thread in MVVM WPF app – stackoverflow

ただこれはUIElementなどに近い場所で使うにはいいのですが、Modelの奥深くでこれをやるのはどうかなと思ったりします。

 

SynchronizationContextを利用したPortable Class Library対応パターン

またPortable Class LibraryではDispatcherクラスは扱えないので、SynchronizationContextを使った実装をしてみました。

   1: public event PropertyChangedEventHandler PropertyChanged;

   2: protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)

   3: {

   4:     var handler = PropertyChanged;

   5:     if (handler == null) return;

   6:  

   7:     var context = SynchronizationContext.Current;

   8:     if (context == null)

   9:     {

  10:         handler(this, new PropertyChangedEventArgs(propertyName));

  11:     }

  12:     else

  13:     {

  14:         context.Post((obj) =>

  15:         {

  16:             handler(this, new PropertyChangedEventArgs(propertyName));

  17:         }, null);

  18:     }

  19: }

事前にSynchonizationContext.Currentに、Viewからもらったコンテキストを入れておく必要がありますが、それだけです。

contextが存在しない場合は元々UIスレッドで、実行されているという制御をやるべきなのですが、そこはまだできていません。

こういうデータバインディングで勝手にスレッド超えをしてしまう場合の対処を、ベースクラスに任せてしまうのは、エレガントといえるとは思いますが、見えないオーバーヘッドを多数作ってしまうので、使用には気を付けたいですね。

 

ObservableCollectionをラップしてスレッド越え対応にするパターン

この目的でいくと当然ながらObservableCollectionについても対応しないといけません。

これも同様の対応でいけると思いますが、ラップするのは少々大変です。

かずきさんがこれをトライされています。

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

   1: if (IsValidAccess())

   2: {

   3:     // UIスレッドならそのまま実行

   4:     base.OnCollectionChanged(e);

   5: }

   6: else

   7: {

   8:     // UIスレッドじゃなかったらDispatcherにお願いする

   9:     Action<NotifyCollectionChangedEventArgs> changed = OnCollectionChanged;

  10:     this.EventDispatcher.Invoke(changed, e);

  11: }

こんな感じですが、isValidAccessというメソッドが味噌ですね。

これで通常のオブジェクトとコレクションオブジェクトのUIスレッド、ワーカースレッド間の安全なデータバインディングが実現されました。


このエントリーをはてなブックマークに追加