[WPF/MVVM] INotifyPropertyChanged の実装がメンドイので
みなさん、あけましておめでとうございます。
WPFとMVVMに向きあう今日この頃。表題の件、ちょっと考えて次のように書けるようにしてみた。
class MyViewModel : ViewModelBase // 事前に用意したベースクラスを継承 { public int Hoge { get { return base.Properties.Get<int>(); } set { base.Properties.Set( value ); } // PropertyChanged イベントが発行される } }
既に多くの偉大なる先人たちが取り組んでいる古典的な問題っぽいので今更感はありそうだけど、ともかく恥を偲んで紹介してみよう。晒してこそ勉強になるはず。
良いと思うところ。
- get も set も実装は1行で済んでいる。
- プロパティ用のフィールド(int _hoge; とか)の宣言が不要。
- プロパティ名の文字列を使用していない。
イマイチなところ、気になるところ。
- ベースクラスの継承を強要する方式はやっぱりイマイチかな・・・。
- プロパティ名の文字列が省略できるカラクリは、StackTrace で呼び出し元を取得することで実現している。とりあえず動いたけど、ギミックがトリッキー過ぎる気がするし、なんか落とし穴はないのかな、この方式。ちょっと不安。
- パフォーマンスは悪いかも・・・。
では順に説明してみる。ViewModelBase は次のように定義した。
public class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged( string name ) { ... } protected readonly PropertyContainer Properties; public ViewModelBase() { this.Properties = new PropertyContainer( name => this.RaisePropertyChanged( name ) ); } }
見ての通り INotifyPropertyChanged の実装部分に新しいところはなく、後半で宣言している PropertyContainer というクラスがキモとなる。このクラスは、コンストラクタ引数で PropertyChanged イベントを発行する Action を受け取るように定義している。
public class PropertyContainer { readonly Action<string> _notifyPropertyChanged; public PropertyContainer( Action<string> notifyPropertyChanged ) { _notifyPropertyChanged = notifyPropertyChanged; } ... }
次が PropertyContainer の核心部分となる。
public class PropertyContainer { ... // プロパティの名前と値を Dictionary で管理 readonly Dictionary<string, object> _properties = new Dictionary<string, object>(); // ディクショナリから値を検索して返す public T Get<T>( string name ) { object x = null; return _properties.TryGetValue( name, out x ) ? (T)x : default( T ); } // ディクショナリに値をセットしてイベントを発行 public bool Set<T>( string name, T value ) { T curr = this.Get<T>( name ); if ( !object.Equals( curr, value ) ) { _properties[name] = value; _notifyPropertyChanged( name ); return true; } return false; } ... }
さらに Get/Set をラップして、プロパティ名を StackTrace から取得するようにする。これによりプロパティ名を文字列で明示的に指定する必要がなくなる。
using System.Diagnostics; public class PropertyContainer { ... public T Get<T>() { try { var name = new StackTrace().GetFrame( 1 ).GetMethod().Name; return name.StartsWith( "get_" ) ? this.Get<T>( name.Substring( 4 ) ) : default( T ); } catch { return default( T ); } } public bool Set<T>( T value ) { try { var name = new StackTrace().GetFrame( 1 ).GetMethod().Name; return name.StartsWith( "set_" ) ? this.Set<T>( name.Substring( 4 ), value ) : false; } catch { return false; } } }
ここがヤバイよーとか、もっといい方法あるよーとか、あったら教えて~。