かずきのBlog@hatena

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

MVVMパターンでViewModelを楽に作る方法

MSDNマガジンの英語版の最新のを見てたら、こんな以下の記事を見つけた。
http://msdn.microsoft.com/ja-jp/magazine/ff798279(en-us).aspx

まだ全部見てないけど、ここにViewModelを楽に作る方法が書いてあっておぉ〜っと思ったので紹介します。
このテクニックが使えるプラットフォームは.NET Framework 4です。

ViewModelは、作るとViewとModelを切り離せるけど、Modelをラップするようなプロパティを大量に作らないといけません。
例えば、以下のようなModelがあったとして

public class Person
{
  public string FullName { get; set; }
}

以下のようなViewModelを作ります。

// ViewModelBaseはINotifyPropertyChangedの実装をしている
public class PersonViewModel : ViewModelBase
{
  private Person innerModel;
  public PersonViewModel(Person innerModel)
  {
    this.innerModel = innerModel;
  }
  
  // こんな風にModelをラッピングするプロパティを定義する
  public string FullName
  {
    get 
    { 
      return innerModel.FullName; 
    }
    set 
    {
      innerModel.FullName = value;
      OnPropertyChanged("FullName");
    }
  }
}

今回の例のように1つしかModelにプロパティが無い場合はいいとして、10個20個プロパティがあったりすると途端に苦行になります。
この問題を解決するエレガントな方法として動的オブジェクトを使う方法が紹介されていました。System.Dynamic.DynamicObjectというものが存在していて、Rubyなどの動的な型の言語のようなオブジェクトを模倣してくれます。
このオブジェクトを継承して、内部に持ってるModelクラスのプロパティから値をとってくるように細工することで、いちいち自分でModelのプロパティをラップするコードを書かなくてよくなります。
実装すると、以下のような感じになります。TryGetMemberとTrySetMemberがポイントです。

using System.ComponentModel;
using System.Dynamic;

namespace WpfApplication3
{
    public class DynamicViewModelBase<T> : DynamicObject, INotifyPropertyChanged
        where T : class
    {
        // Modelクラスのインスタンス
        private T innerModel;

        protected T InnerModel
        {
            get { return innerModel; }
        }

        public DynamicViewModelBase(T innerModel)
        {
            this.innerModel = innerModel;
        }

        // INotifyPropertyChangedの実装
        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string name)
        {
            var h = PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(name));
            }
        }

        // プロパティの取得
        public override bool TryGetMember(GetMemberBinder binder, out object result)
        {
            var propertyName = binder.Name;
            var property = innerModel.GetType().GetProperty(propertyName);
            if (property == null || !property.CanRead)
            {
                // プロパティが存在しないか読み取りが出来ないので値の取得に失敗
                result = null;
                return false;
            }

            // プロパティから値を取得する
            result = property.GetValue(innerModel, null);
            return true;
        }

        // プロパティの設定
        public override bool TrySetMember(SetMemberBinder binder, object value)
        {
            var propertyName = binder.Name;
            var property = innerModel.GetType().GetProperty(propertyName);
            if (property == null || !property.CanWrite)
            {
                return false;
            }

            // プロパティの値をセットしてイベントを発行する
            property.SetValue(innerModel, value, null);
            OnPropertyChanged(propertyName);
            return true;
        }
    }
}

このクラスを利用するとさっきのPersonクラスに対するPersonViewModelの定義は以下のようにシンプルになります。

public class PersonViewModel : DynamicViewModelBase<Person>
{
    public PersonViewModel(Person p) : base(p)
    {
    }
}

このViewModelをDataContextに入れてText={Binding Path=FullName}とやるだけでバインドが出来てしまいます。
これは、面白い!!と思ったので紹介しました。