カスタム属性を用いたデータバインディングの省力化(INotifyPropertyChanged実装編)

カスタム属性を用いたデータバインディングの省力化に関しては、期待した実装、つまりはユーザが定義したオブジェクトとコントロールの双方向のバインディングを全て実現できたのだが、実は今までのエントリでは敢えて触れていない部分がある。.NET Framework 2.0のデータバインディングに詳しい方であれば、どうして今までのコードだけでオブジェクトとコントロールの間で双方向バンディングが出来ていたか、不思議だったに違いない。実のところ、今まで解説してきたカスタム属性によるデータバィンディングの実装だけでは

コントロールのプロパティ変更 -> バインドされているオブジェクトのプロパティ変更

は自動化されたが、

オブジェクトのプロパティ変更 -> バインドされているコントロールのプロパティ変更

は手動のままなのである。これを自動化(コードレス化)するためには、対象のオブジェクトがSystem.ComponentModel.INotifyPropertyChangedインタフェースを実装して、プロパティの変更時にイベントを起動しなくてはならないのである。
実際のINotifyPropertyChangedインタフェースは以下のように、イベントを公開するだけのシンプルなものだ。

public interface INotifyPropertyChanged {
    event PropertyChangedEventHandler PropertyChanged;
}

実はこの公開されるイベントというのがミソで、.NET Framework2.0のデータバインディングは、自身に格納されたオブジェクトがINotifyPropertyChangedインタフェースを実装していることを検知すると、このイベントハンドラによりバインドされているコントロールのプロパティに変更にされた値を反映してくれるのである。(BindingSourceの無い、.NET1.1ではプロパティ名+Changedという名前のイベントをプロパティ個々に用意する必要があった)
従って、双方向のデータバインディング対象のオブジェクトは、以下に用意したクラスのように全ての、個々のプロパティの変更を検出してPropertyChangedEventHandlerのイベントを起動するコーディングをする必要がある。これらをコードスニペットに登録する手もあるが、そもそもフレームワークが必要としているだけのコードであり、プロパティが増えるとともに面倒な作業だ。

public interface IEntity : INotifyPropertyChanged {
   void NotifyPropertyChanged(String propertyName);
}
public class Person : IEntity {
    private string lastName;
    public event PropertyChangedEventHandler PropertyChanged;
    public void NotifyPropertyChanged(String propertyName)
    {
        if (PropertyChanged != null)
        {
            //PropertyChangedデリゲートはBindingSource内のイベントにこっそりアタッチされる(みたい)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
    public virtual string LastName
    {
       get { return this.lastName;}
       set
       {
          if (value != this.lastName)
          {
              this.NotifyPropertyChanged("LastName");
          }
          this.lastName = value;
       }
    }
}

今回の省力化は、この色がついた部分DIコンテナと、そのAOP機能を使ってコーディングすること無しに実現してしまおうという試みである。(今までの分にも実はこの実装は入っていたのだが、最後に紹介したかったために今まで触れなかったのである)

以下、順を追って作業した内容を書いてみる。

  • プロパティ変更通知用インターセプタの用意

Seasar2.xと同様、インタセプタ抽象クラスAbstractInterceptorから継承したNotifyPropertyChangedInterceptorというクラスを用意した。

public class NotifyPropertyChangedInterceptor : AbstractInterceptor {
    public override object Invoke(IMethodInvocation invocation) {
        MethodBase method = invocation.Method;
        if (method.Name.StartsWith("set_")) { //プロパティのセッターだけを対象に
            string propName = method.Name.Split(new string[] { "set_" }, StringSplitOptions.None)[1];
            IEntity entity = invocation.Target as IEntity;
            if (entity != null) {
                PropertyInfo info = invocation.Target.GetType().GetProperty(propName,
                    BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
                if (info != null) {
                    //アクセサ実行前の値を退避
                    object preValue = info.GetValue(invocation.Target, null);
                    //アクセサを実行
                    Object ret = invocation.Proceed();
                    //アクセサ実行後の値を退避
                    object postValue = info.GetValue(invocation.Target, null);
                    if (preValue == null) {
                        if ( postValue != null ) {
                            entity.NotifyPropertyChanged(propName);
                        }
                    } else {
                        if (! preValue.Equals(postValue)) {
                            entity.NotifyPropertyChanged(propName);
                        }
                    }
                    return ret;
                }
            }
        }
        return invocation.Proceed();
    }
}

実際の運用ではAspectタグやアノテーションでPointcutを指定するので、ここでプロパティのセッターをフィルタする必要は無いのだが、解り易いように入れてみた。.NETはプロパティのアクセサは自動的に生成されるため、実際のコードは隠されている。しかし、アクセサの命名規約はゲッタであれば"get_プロパティ名"、セッタであれば"set_プロパティ名"であるので、このようにセッタだけを対象にしている。今後、プロパティのセッタのプレフィクスが変わる可能性があると心配な場合は、ここに"set_"などとハードコードするのは止めて、外部からプレフィクスを取得するなどの工夫が必要だろう。
処理自体は単純で、プロパティのセッタの前後でプロパティの値を比較して、変更されていればイベントをレイズするためのメソッドを呼び出しているだけである。

  • DIコンテナから生成されることを前提に、カスタム属性でAspectを指定する

DIコンテナにより、Aspectを織り込み済みのインスタンスを取得できるように、クラスに対してAspectアノテーションを記述する

[Aspect(Pointcut="set_.*", Interceptor="Framework.Entity.NotifyPropertyChangedInterceptor")]
public class Person : IEntity { 〜

これでこのクラスのcomponent記述を明示的に行うか、クラスを自動生成の対象にして、DIコンテナによりインスタンスを生成することにより個々のプロパティに対して必要だった、変更通知のためのコーディングが完全に不要になる。
なお、DIコンテナを前提とできない場合は、以下のようなファクトリを介すことでしか、Aspectを織り込んだインスタンスを返す術は無い。(残念だが、コンストラクタを乗っ取る術が無いので仕方が無い)

public static IEntity GetChangedNotifiableEntity(IEntity entity) {
    IAspect aspect = new AspectImpl(new NotifyPropertyChangedInterceptor()
        , new PointcutImpl(new string { "set_.*" }));
    DynamicAopProxy proxy = new DynamicAopProxy(typeof(entity), new IAspect { aspect });
    return proxy.Create() as IEntity;
}

なお、AOP用のプロキシは過去に何度か紹介した、Castle.DyanmicProxyを内部で利用したDyanamicAopProxyクラスを使用しているため、プロパティの基底のアクセサはVirtual属性をつける必要がある。

これで本当にオブジェクトとコントロールの間に双方向のバインディングを確立することができる。