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

中断してしまったのでおさらいになるが、5/13の日記ではリスト形式の項目をバインドするカスタム属性を用意したが、宿題が残っていた。

どうせ.NET Framework2.0を使用しているのであればやはりBindingSourceクラスを使わない手は無いと思うので、今後はBindingSourceを上手く使う方向で考えていく予定。

という訳で、カスタム属性によるデータバインディングと.NET2.0固有のデータバインディング用クラスであるBindingSourceとを組み合わせてみることにする。BindingSourceクラスに関してはここで書くと長くなるので割愛するが、一言で書くとデータバインディングにおける中間層を提供するクラスである。通常のデータバインディングの場合、

データソースのプロパティ <- バインド -> コントロールのプロパティ

という図式になるのだが、BindingSourceを使う場合

データソース(プロパティ) <- バインド -> BindingSource <- バインド -> コントロールのプロパティ

という関係になるため、データソースから見たバインディング側、コントロールから見たバインディング側が仮想化されるのがミソだ。(詳しくはMSDN等を参照のこと)

さて、BindingSourceを組み込むといっても大して難しいことは無い。このシリーズエントリで書いたメソッドの一部を変更して、BindingSourceをコントロールのBindingに追加するだけ。

private void DataBindingCustomAttribute()
{
    PropertyInfo infos = Array.FindAll(
        this.GetType().GetProperties(BindingFlags.Public |
            BindingFlags.Instance | BindingFlags.FlattenHierarchy),
        delegate(PropertyInfo info)
        {
            return info.IsDefined(typeof(DataBindingsAttribute), true);
        });
    foreach(PropertyInfo info in infos)
    {
        DataBindingsAttribute attr = Attribute.GetCustomAttribute(info, 
                typeof(DataBindingsAttribute), true) as DataBindingsAttribute; 
        if ( !String.IsNullOrEmpty(attr.ControlName))
        {
            Control controls = this.context.BindForm.Controls.Find(attr.ControlName, true);
            foreach (Control control in controls)
            {
                if (!String.IsNullOrEmpty(attr.DataMember))
                {
                    control.DataBindings.Add(new Binding(
                        attr.PropertyName, 
                        GetBindingSource(this), 
                        attr.DataMember, 
                        attr.FormattingEnabled, attr.DataSourceUpdateMode));                }
                else
                {
                    control.DataBindings.Add(new Binding(
                        attr.PropertyName, 
                        GetBindingSource(this), 
                        info.Name, attr.FormattingEnabled, attr.DataSourceUpdateMode));                
                }
                〜 以下、リストコントロール対応用の処理等があるが長くなるんで略 〜
            }
        }
    }
    if ( infos.Length > 0 )
    {
        BindingSource source = GetBindingSource(this);
        if ( source != null ) source.DataSource = this;
    }
}

コードには二つポイントがある。一つは

control.DataBindings.Add(new Binding(attr.PropertyName, GetBindingSource(this), info.Name, attr.FormattingEnabled, attr.DataSourceUpdateMode));

前回のエントリでは、二つ目のパラメタには直接エンティティクラス(以降、5/10の日記で紹介したPersonクラス等、なんらかのデータの入れ物として定義したクラスと読み替えて欲しい)のインスタンスをセットしていたのだが、今回はBindingSourceのインスタンスが必要なため、新たに追加する静的なメソッドGetBindingSource(this)で、新たにBindingSourceのインスタンスを返すようにする。

Visual Studio 2005のデータ、データソース関連の機能をフルに利用してデータバインディングを設定する場合、まずデータソースを決定して、次に対象のFormにドロップすることで、BindingSourceコンポーネントは自動的に生成される。このとき同じデータソースを複数回ドロップしてもBindingSourceは複数回生成されることは無いので、コードによる自動化の場合も、対象の型が決まった時に、その型に対応したBindingSourceクラスのインスタンスを生成するようにしよう。

サンプルでは対象のエンティティの型毎にBindingSourceのインスタンスを管理している。また、生成したBindingSourceの後始末をどうするかはここでは触れていない。

private static IDictionary bindingSources =
    new Dictionary();

public static BindingSource GetBindingSource(AbstractEntity entity)
{
    Type type = entity.GetType();

    BindingSource source = null;
    if ( bindingSources.ContainsKey(type) )
    {
        source = bindingSources[type];
    }
    else
    {
        lock (bindingSources)
        {
            if ( bindingSources.ContainsKey(type) )
            {
                source = bindingSources[type];
            }
            else
            {
                source = new BindingSource();
                bindingSources.Add(type, source);
            }
        }
    }
    source.DataSource = type;
    return source;
}

Generics DictionaryはHashtableクラス等のSynchronizedメソッドによる、同期ビューが存在していないので、排他はダブルチェッキングロックを使っている。Atomicityを考えると良い例とはいえないが、これしか書き方を知らない。(Generics Dictionaryで使える、もっとスマートな排他制御の実装をご存知の方は是非教えていただきたい。)

もう一つのポイントはメソッドの最後ブロック。

    if ( infos.Length > 0 )
    {
        BindingSource source = GetBindingSource(this);
        if ( source != null ) source.DataSource = this;
    }

これで、この型で使えるBindingSourceのインスタンスが生成されて、エンティティとコントロールに拘束されたので、以下のコードでデータのインスタンスをBindingSourceのデータソースとして指定することにより、データの相互通信が開始される。
マスタメンテナンス、データエントリ画面のように複数のデータを扱うのであれば、

    if ( infos.Length > 0 )
    {
        BindingSource source = GetBindingSource(this);
        if (source != null) source.Add(this); 
    }

等として、内部リストにエンティティを追加してやれば、その後BindingSourceのメソッド(MoveFirst, MoveLast, MoveNext, MovePrevious, etc)でデータのナビゲーションが可能になる。また、AddNewメソッド、CancelEdit、EndEditメソッド等と組み合わせることで追加されるエンティティの簡易なトランザクション制御(コミット/ロールバック)が出来るようになる。

WindowsFormsアプリケーションにおいて、Form(と貼り付けられたControl群)をViewとして扱う場合、大抵はなんらかのDTO(DataTransferObject)を用意して、LogicとViewとを完全に分離したいと思うことが多いだろう。
通常はViewとDTOとの間にはデータ転記のためのコードが必ず必要になるのだが、今回実現したカスタム属性によるデータバインディングの自動化や、元々あるVisualStudioのIDEの機能を利用することにより、DTOとView、互いのデータ転記のコードをほぼ0行に減じることが出来る(つまりはObserverパターンの実装を省くことができる)のは非常に強力だと思う。