- プログラミング
[MVVM] UIスレッドへの通知を適切なコンテキストで行うBindableBase
[MVVM] UIスレッドへの通知を適切なコンテキストで行うBindableBase
[MVVM] 便利で間違えのないプロパティ変更通知を行うBindableBaseの実装
で、表記を短く、タイプセーフに、ポータブルに書く方法を紹介しました。
ところで、重いワーカースレッドを持つような処理だと、そもそもプロパティ変更通知自体がスレッド越えをしないといけないシーンが増えます。
特にモデルから直接ビューへデータバインディングをしているような場合は、適切なスレッド管理をするのは難しそうです。
これに透過的に対処する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の奥深くでこれをやるのはどうかなと思ったりします。
また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についても対応しないといけません。
これも同様の対応でいけると思いますが、ラップするのは少々大変です。
かずきさんがこれをトライされています。
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スレッド、ワーカースレッド間の安全なデータバインディングが実現されました。