メニュー

技術ブログ

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スレッド、ワーカースレッド間の安全なデータバインディングが実現されました。

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

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

MVVMを使って実装しているとデータバインディングのために、ViewModelでのプロパティ通知の実装が煩雑でいやになります。微妙に異なる、色々な記法やヘルパーがあるので、今更ですが、まとめておきます。

ヘルパーなし(.NET 3.0)

実際INotifyPropertyChangedインターフェイスのPropertyChangedを呼び出すだけのコードなのですが、ヘルパーなどを使わない場合は、以下のように実装します:

 

方法 : INotifyPropertyChanged インターフェイスを実装する – MSDN .NET Framework 3.0

.NET 3.0でのイベントハンドラ
  1. public event PropertyChangedEventHandler PropertyChanged;
  2. protected virtual void OnPropertyChanged(string name)
  3. {
  4.     if (PropertyChanged == null) return;
  5.     PropertyChanged(this, new PropertyChangedEventArgs(name));
  6. }

これを利用して変更通知可能なプロパティを実装するにはMSDNでは、以下のようになります。

.NET 3.0でのプロパティ実装
  1. public class DemoCustomer : INotifyPropertyChanged
  2. {
  3.     public string CompanyName
  4.     {
  5.         get {return this.companyNameValue;}       
  6.         set
  7.         {
  8.             if (value != this.companyNameValue)
  9.             {
  10.                 this.companyNameValue = value;
  11.                 NotifyPropertyChanged("CompanyName");
  12.             }
  13.         }
  14.     }
  15. }

 

ヘルパーなし(.NET 4.5)

MSDNの.NET 4.5のサンプルでは以下のように表記が楽になります。

方法 : INotifyPropertyChanged インターフェイスを実装する – MSDN .NET Framework 4.5

.NET 4.5でのイベントハンドラ
  1. public event PropertyChangedEventHandler PropertyChanged;
  2. private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
  3. {
  4.     if (PropertyChanged != null)
  5.     {
  6.         PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
  7.     }
  8. }

これを利用して変更通知可能なプロパティを実装するにはMSDNでは、以下のようになります。

.NET 4.5でのプロパティ実装
  1. public class DemoCustomer : INotifyPropertyChanged
  2. {
  3.     public string CompanyName
  4.     {
  5.         get {return this.companyNameValue;}       
  6.         set
  7.         {
  8.             if (value != this.companyNameValue)
  9.             {
  10.                 this.companyNameValue = value;
  11.                 NotifyPropertyChanged();
  12.             }
  13.         }
  14.     }
  15. }

これは表記が短くなるだけでなく、毎回コピペで実装するこのプロパティ実装で文字列の指定によるタイプミスや変更忘れをなくす効果があります。

また、リファクタリングでプロパティ名を自動的に変更しても大丈夫です。

これでかなり楽になりましたね。

ただ、何をやっているかを理解するにはいいですが、プロパティ変化通知可能なクラスを作るたびにコピーするのは煩雑です。通常は自分でベースクラスを作るか、ヘルパーライブラリを用います。

 

MVVMLight

最もポピュラーなサードパーティMVVMヘルパーライブラリであるMVVMLightではViewModelBaseというベースクラスが用意されています。メソッド名がNotifyPropertyChanged()からRaiseProperyChanged()に代わりますが、基本的には同じものです。

MVVMLight でのプロパティ実装
  1. public class DemoCustomer : ViewModelBase
  2. {
  3.     private string companyNameValue = String.Empty;
  4.     public string CompanyName
  5.     {
  6.         get {return this.companyNameValue;}       
  7.         set
  8.         {
  9.             if (value != this.companyNameValue)
  10.             {
  11.                 this.companyNameValue = value;
  12.                 RaisePropertyChanged();
  13.             }
  14.         }
  15.     }
  16. }

プロパティ名は省略した記法が用意されています。ただし、MVVMLightの定義には以下のように、省略できるのはC#5 VB11以降のみと注意書きがあります。

  1. protected virtual void RaisePropertyChanged(string propertyName = "A property name must be specified if not using C# 5/VB11");

 

Prism

Microsoftが提供するMVVMヘルパーライブラリであるPrismでも、当然ながら同様の仕組みがNotificationObjectというベースクラスで用意されています。

Prismではもう少し進んだ記法が用意されています。RaisePropertyChaged()は以下のようにExpression型でプロパティ記述を指定できます。ラムダ式などでクラスを指定した形でプロパティ名を通知できます。

  1. protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)

以下のように、ラムダ式でCompanyNameを指定できるので、文字列の表記間違いは回避できます。

Prismでのプロパティ実装
  1. public class DemoCustomer : NotificationObject
  2. {
  3.     private string companyNameValue = String.Empty;
  4.     public string CompanyName
  5.     {
  6.         get {return this.companyNameValue;}       
  7.         set
  8.         {
  9.             if (value != this.companyNameValue)
  10.             {
  11.                 this.companyNameValue = value;
  12.                 RaisePropertyChanged( ( ) = > CompanyName);
  13.         }
  14.     }
  15. }

だいぶ楽になるとは思いますが、表記上の煩雑さはあまり改善されていませんね。もうちょっと何とかならないかと思っていました。

 

Common.BindableBase

Windows8ストアアプリの実行環境である、WinRTにはCommon.BindableBaseなるベースクラスが用意されています。

これにはSetPropertyというsetterを簡単に書けるメソッドが用意されており、以下のような表記で済ますことができます。

Common.BindableBaseによるプロパティ実装
  1. public class DemoCustomer : BindableBase
  2. {
  3.     private string companyNameValue = String.Empty;
  4.     public string CompanyName
  5.     {
  6.         get {return this.companyNameValue;}
  7.         set { SetProperty(ref companyNameValue, value);}
  8.     }
  9. }

これは、かなり楽になりますね。なんで今まで無かったのでしょうか?

ただし、このクラスどうも現在のWindows 8.1のライブラリには存在しないように見えます。

またPortableClassLibraryではCommonネームスペース自体が見えません。

 

自作BindableBase

上記の便利なプロパティ設定を行うだけのベースクラスは以下のような定義で簡単に実装できます。

わけあって、ヘルパーを使えない場合などよろしいですね。

自作BindableBase
  1. abstract public class BindableBase : ICleanup, INotifyPropertyChanged
  2. {
  3.     protected BindableBase(){}
  4.  
  5.     public event PropertyChangedEventHandler PropertyChanged;
  6.     protected virtual void RaisePropertyChanged([CallerMemberName] string propertyName = null)
  7.     {
  8.         var handler = PropertyChanged;
  9.         if (handler != null)
  10.         {
  11.             handler(this, new PropertyChangedEventArgs(propertyName));
  12.         }
  13.     }
  14.  
  15.     protected bool SetProperty<T>(ref T storage, T value, [CallerMemberName] String propertyName = null)
  16.     {
  17.         if (object.Equals(storage, value)) return false;
  18.         storage = value;
  19.         this.RaisePropertyChanged(propertyName);
  20.         return true;
  21.     }
  22. }

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

XamarinがVisual StudioでのPortable Class Library開発に対応

C#でクロスプラットフォームというとMonoが有名ですがその開発元であるXamarinがPortable Class Libraryに正式対応したようです。
どういうことかというと、これまでのPCLが.NET, Silverlight, Windows PhoneなどのMicrosoftの製品群での共用のみだったのが、ぐっと範囲を広げて、Xamarin.iOS/Xamarin.Androidまでを参照元として使えるということです。
Visual Studio 2013のローンチイベントで同時に発表されたようです。

Visual Studio 2013 Launch – ++C++; // 未確認飛行 C ブログ

にあるようにこれまでクローズドだったPCLの仕様がオープンになったことで、サードパーティーの.NET互換環境などと共用も可能になったということで、Visual StudioでiOS, Androidのクロスプラットフォームができるようになった、と言っても良い状況です。

ただ、PCLに共通ですが、画面回りは当然ながら書き直す必要がありますし、デバッグなどはできないので、ロジック部分のみの共用となります(元々ビューに関わるライブラリは使えないですから。)

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