DataBidingsAttributeによる双方向データバインディング(ListBoxとComboBox)

先日はカスタム属性"DataBidingsAttribute"により、オブジェクトとTextBoxクラスのデータバインドを自動化したが、今回はListBoxとComBoxに関してもデータバインドしてみよう。
前回のTextBoxとの違いは、想定しているバインディング対象のオブジェクトの型が単項目ではなくリスト(IList)を実装する、複数項目であるということだ。そのため、ListBoxとComboBoxクラスのデータバインディングの対象プロパティはTextプロパティではなくDataSourceプロパティである。
前回書いたコードのDataBindingCustomAttributeメソッドを修正し、DataBidingsAttributeのPropertyNameプロパティに"DataSource"を指定された場合は、対象のコントロールのDataSourceプロパティを直接書き換えてしまうコードにしてみた。

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; //複数指定されていないはず。序数1以降は無視
        if ( StrUtil.IsNotEmpty(attr.ControlName)){
            Control controls = this.context.BindForm.Controls.Find(attr.ControlName, true);
            foreach (Control control in controls) {
                if (attr.PropertyName.Equals("DataSource"))
                {
                    PropertyInfo dataSourcePropInfo = control.GetType().GetProperty(attr.PropertyName);
                    if (dataSourcePropInfo != null) {
                        dataSourcePropInfo.SetValue(control, info.GetValue(this, null), null);
                    }
                } else {
                    Binding binding = control.DataBindings[attr.PropertyName];
                    if (binding != null) {
                        control.DataBindings.Remove(binding);
                    }
                    if (StrUtil.IsNotEmpty(attr.DataMember)) {
                        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);
                    }
                }
            }
        }
    }
}

ベタな方法ではあるが、これでとりあえずは動く。変更したのは以下の部分だ

                if (attr.PropertyName.Equals("DataSource")) {
                    PropertyInfo dataSourcePropInfo = control.GetType().GetProperty(attr.PropertyName);
                    if (dataSourcePropInfo != null) {
                        dataSourcePropInfo.SetValue(control, info.GetValue(this, null), null);
                    }
                }

ところで、リスト型のオブジェクトをバインドする場合に考慮しなければならないことがある、それはリストに格納されているオブジェクトの型は定かではないということだ。リストに格納されているのがstringであればこのコードに問題は無いが、複数のデータメンバを持つ型のオブジェクトをバインドする場合は、これに加えて明示的にデータメンバを指定する必要がある。

指定するデータメンバはデータバインディングの規則に沿うため、型が公開するプロパティとして定義されている必要がある。また、ListBoxとComboBoxは表示するデータと実際に格納するデータを分離してバインドすること可能であり、それぞれ"DisplayMember"と"ValueMember"プロパティにデータメンバの名前(プロパティ名)を指定する必要がある。DataBidingsAttributeにはこの二つのプロパティを指定することはできないため、リスト項目をバインドするための新たなカスタム属性を用意する必要があるだろう。それに関しては次回のエントリで。

追記:
DataSourceプロパティに対してデータバインディング対象のオブジェクトを指定する場合、IListインタフェースの実装クラスであれば可能だという印象を最初は持っていたのだが、実際に、例えばList型のオブジェクトをバインドしても、リストの内に格納した文字列が即座にListBoxコントロールに反映される訳ではないようだ。TextBoxと同様にオブジェクトの変更がバインドしたコントロールに対して即座に反映されるようにするには、IBindingListインタフェースの実装が必要。そのためにBindingListというGenerics型が用意されている。