カスタム属性を用いたデータバインディングの省力化(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属性をつける必要がある。

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

DynamicProxyとリフレクション

拙作の.NET Framework2.0用DIコンテナ(Seasar2.4クローン)のAOP機能でも採用しており、ここの日記でも何度か紹介しているDynamicProxy(Castle.DynamicProxy)だが、DynamicProxyにより拡張された型から、リフレクションによりメンバ情報を取り出すときには注意が必要である。

以下のような、オブジェクトに対して任意の名前のプロパティ情報を取得するメソッドがあったとしよう。

public PropertyInfo GetPropertyInfo(object target, string propName)
{
    return target.GetType().GetProperty(propName);
}

.NETの一般的な型(クラス)であれば、なんてことの無い処理だがDynamicProxyで拡張した型から生成したインスタンスをメソッドに食わせると、正しい(存在するはずの)プロパティ名使用しても、以下の例外が発生する場合がある。

System.Reflection.AmbiguousMatchException 
Message: あいまいな一致が見つかりました。
Source: mscorlib
   場所 System.RuntimeType.GetPropertyImpl(String name, BindingFlags bindingAttr, Binder binder, Type returnType, Type types, ParameterModifier modifiers)
   場所 System.Type.GetProperty(String name) ...

この例外の原因は、DynamicProxyが型を拡張するときに、元の型のプロパティの中からプロキシを介する対象となるプロパティを再公開するために、同一のプロパティ名を持つプロパティが型情報に再定義されているからだ。
この問題を回避するには、拡張前の型を退避しておき、その型からプロパティ情報を取得するか、又は以下のようにプロパティ情報を取得する際に、継承元をたどらず対象の型で定義、公開されているプロパティだけを対象にする必要がある。

public PropertyInfo GetPropertyInfo(object target, string propName)
{
    PropertyInfo info = target.GetType().GetProperty(propName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
}

.NETで利用できるプロキシにはいくつかのタイプがあるが、DynamicProxyのように型を拡張するタイプはリフレクション情報が変わることを考慮する必要があるだろう。

追記:
以上のことから、以前にこの日記で公開した二つのクラス
Seasar.Framework.Aop.DynamicProxy.DynamicAopProxy.cs
Seasar.Framework.Aop.DynamicProxy.DynamicProxyMethodInvocation.cs
これらは、インターセプタに処理を移す際に拡張する前の型を退避しておく必要があるだろう。従ってDynamicProxyMethodInvocationクラスのコンストラクタの引数を一つ追加し、拡張前の型をtargetTypeとして渡すように修正した。(これはあくまで拡張前の型を退避、利用したい場合の策。元の型拡張前の型を使わないのであれば、修正する必要は無い)

プラネットアース

プラネットアース/NHKスペシャル

5/7にNHKで放映された同名の番組のハイビジョン特集を見た。最近のTVで本当に凄いと感じる映像など、まずお目にかかることなど無かったのだが、正直この番組には驚いた。なにに驚いたかって、高空から衛星、又は飛行機、その両方で撮ったと思われる地球の自然を収めた、今まで見たことの無いクォリティの映像にだ。本当に凄い。
その映像は、一般のVTRのそれと比べて、解像度が明らかに桁違いであり、かなりの高度と思われるところから撮影されているにも関わらず、動物の細かいディテールが明確に判別できるのである。なんというか、最初みた時はあまりに鮮明だったので、ピクサー(PIXAR)のCG映像かと思ったくらいだ。
番組の後半に、アフリカのボツワナにあるオカバンゴというデルタ地帯が水に満たされて、そこをたくさんのアフリカ象が歩いていくシーンがあるのだが、かなりの引きで撮っているにも関わらず、その細かさと迫力が両立している映像には圧倒されてしまった。CGでも似たような構図の映像はあるが、本物と違い、最初は凄いなと思ってもすぐに見飽きてしまう。本物は穴が開くほど見ても同じパターン等なく、どれだけ見ても飽きないのだ。
エンドロールで、映像提供にJAXANASAがあったのだがなるほど納得だ。