DataBidingsAttributeによる双方向データバインディングの自動化

Form(とControl)をViewに見立てて、データやロジックと分離するプログラミングをする場合、データをView、つまりForm(とControl)に転記する、逆にViewからデータに値を書き戻すという処理を書くのは単純で面倒だ。WindowsFormsの双方向データバインディングはそのようなコーディングを省いてくれるので、もっと積極的に使ったほうが良いと思うんだけど、ASP.NET(+ADO.NET)のデータバインディングに比べると、積極的に使っているという話はあまり聞いたことが無い。(Visual Studio 2005のデータ関連のツールを使う例は結構見かけるが)
先日のエントリで言及していた、WindowsFormsにおけるコントロールとオブジェクトの相互データバインドの省力化だが、カスタム属性とリフレクションだけで簡単に実現できそうだったのでやってみた。
対象となるオブジェクトは以下のクラスのインスタンスとする。

namespace DataBind {
    public class Person {
        protected string name = String.Empty;
        protected string address = String.Empty;
        protected string job = String.Empty;
        protected Form bindForm;

        public Person(): base() {}
        
        [Binding(BindingType = BindingType.MAY, Value = "Form1")]
        public virtual Form BindForm {
            get { return this.bindForm; }
            set { this.bindForm = value; }
        } 
        public virtual string Name {
            get { return this.name; }
            set { this.name = value; }
        }
        public virtual string Address {
            get { return this.address; }
            set { this.address = value; }
        }
        public virtual string Job {
            get { return this.job; }
            set { this.job = value; }
        }
    }
}

プロパティ"BindForm"に宣言されているBindingカスタム属性は、以前に紹介したとおりSeasar2と同様の実装による、アノテーション・プロパティインジェクションを期待する宣言である。(期待と書いたのは"BindingType.MAY"を指定しているからだ)
このオブジェクトをバインドする対象のControl(System.Windows.Forms.Controls)だが、以下のようにFormクラスに定義された3つのテキストボックスとしよう。

public class Form1 : System.Windows.Forms.Form {
    public TextBox edtName;
    public TextBox edtAddress;
    public TextBox edtJob;

    public Form1 () {
        InitializeComponent();
    }
}

このフォームのControlに、Personクラスのオブジェクトを実行時にバインドするには、以下のようなコードを書く必要があったはずだ。

Form1 form1 = new Form1();
Person person = new Person();

form1.edtName.DataBindings.Add("Text", person, "Name");
form1.edtAddress.DataBindings.Add("Text", person, "Address");
form1.edtJob.DataBindings.Add("Text", person, "Job");
:

(.NET2.0では新たに導入されたBindingSourceクラス(コンポーネント)によって、もっとスマートなデータバインディングが可能になっているが、ここでは元々ある単純な双方向のデータバインディングに絞っている。)
では、このコードを、DIコンテナとカスタム属性を使用して不要にしてしまおう。そのために新しく用意したカスタム属性クラス、DataBindingsAttributeは以下のとおりである。

namespace DataBind {
    [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)]
    public class DataBindingsAttribute : Attribute {
        private string controlName;
        private string propertyName = "Text";
        private string dataMember;
        private bool formattingEnabled = false;
        private DataSourceUpdateMode dataSourceUpdateMode = DataSourceUpdateMode.OnValidation;
        public string ControlName {
            get { return this.controlName; }
            set { this.controlName = value; }
        }
        public string PropertyName {
            get { return this.propertyName; }
            set { this.propertyName = value; }
        }
        public string DataMember {
            get { return this.dataMember; }
            set { this.dataMember = value; }
        }
        public bool FormattingEnabled {
            get { return this.formattingEnabled; }
            set { this.formattingEnabled = value; }
        }
        public DataSourceUpdateMode DataSourceUpdateMode {
            get { return this.dataSourceUpdateMode; }
            set { this.dataSourceUpdateMode = value; }
        }
    }
}

DataBindingsAttributeクラスは、カスタム属性を宣言する際に必要なプロパティを格納する単純なものであり、特に説明は要らないだろう。フィールドで初期値が設定されているものは、対応するプロパティが明示的に指定されなかった際の既定値となる。例えば、バインド対象のコントロールのプロパティ名を指定するpropertyNameフィールドには"Text"がセットされているが、これは何も指定されなかった場合は、バインドの対象がTextプロパティであることを示すことになる。

これで準備は完了。最後にPersonクラスにデータバインディングを指示するカスタム属性を宣言しておく。これを書かないと始まらない。

        〜
        [DataBindings(ControlName = "edtName")]
        public virtual string Name {
            get { return this.name; }
            set { this.name = value; }
        }
        [DataBindings(ControlName = "edtAddress")]
        public virtual string Address {
            get { return this.address; }
            set { this.address = value; }
        }
        [DataBindings(ControlName = "edtJob")]
        public virtual string Job {
            get { return this.job; }
            set { this.job = value; }
        }

実行時にはDIコンテナでPersonクラスとFormクラスをsingletonで実体化し、PersonオブジェクトにFormオブジェクトがインジェクションされたときに、このカスタム属性を取り出してデータバインディングを実施するだけである。以下が、そのコード。

   〜
    [Binding(BindingType = BindingType.MAY, Value = "form1")]
    public virtual Form BindForm {
        get { return this.bindForm; }
        set { 
            if (value != null && value != this.bindForm) {
                this.bindForm = value;
                if (this.bindForm != null)
                {
                    //カスタム属性からデータバインデイングを設定
                    this.DataBindingCustomAttribute();
                }
            }
        }
    } 
    
    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 = info.GetCustomAttributes(
                typeof(DataBindingsAttribute), true)[0] as DataBindingsAttribute;
            if ( attr.ControlName != null && attr.ControlName.Length > 0  ) {
                Control controls = this.context.BindForm.Controls.Find(attr.ControlName, true);
                foreach (Control control in controls) {
                    if (attr.DataMember != null && attr.DataMember.Length > 0 ) {
                        control.DataBindings.Add(
                            attr.PropertyName, this, attr.DataMember, attr.FormattingEnabled, attr.DataSourceUpdateMode);
                    } else {
                        control.DataBindings.Add(
                            attr.PropertyName, this, info.Name, attr.FormattingEnabled, attr.DataSourceUpdateMode);
                    }
                }
            }
        }
    }

Personクラスだけに限定すれば、使用するプロパティは固定できるのだが、プロパティ名に依存しない作りにするために、このDataBindingCustomAttributeメソッドのように、対象の型に定義されている、尚且つDataBindingsAttributeが宣言されている全てのプロパティを抽出して、一つずつ処理していく必要がある。

これで、Personクラスのインスタンスを取り出したときにForm1クラスのインジェクションに成功していれば、何もせずともPersonオブジェクトとForm1上の全てのTextboxが同期するようになっているはずだ。

IDIContainer container = DIContainer.Create();
Form1 form1 = container.GetComponent("form1");
Person person = container.GetComponent("person");

ちなみに、今回はテキストボックスと文字列の双方向バインディングしか試していない。
今回は具象クラスに直接カスタム属性を取得する処理を書いたが、実際にはこのような書き方はせず、エンティティの基本的なデータと振る舞いを抽象クラスにカプセル化して、エンティティのテンプレートとし、面倒な処理は全てそちらに記述したほうが良いだろう。

追記:
散々便利になるようなことを書いているが、省力化という観点であれば、Visual Studio 2005のデータ、データソース関連のツール、ウィザードの使い方に詳しければ、そちらの方が余程効果が高いだろうとは思う。私はウィザードの類があまり好きではないこと、ウィザードを使ってもコードが発生することは同じであることから、今回のエントリを書いている。(更に悪いことに、私のプロジェクトは新規で型(オブジェクト)からデータソースを追加しようとするとエラーが発生してしまうのだ)