65.9K
CodeProject 正在变化。 阅读更多。
Home

自动绑定面板控件

starIconstarIconemptyStarIconemptyStarIconemptyStarIcon

2.00/5 (1投票)

2007 年 6 月 28 日

CPOL

7分钟阅读

viewsIcon

43241

downloadIcon

478

一篇关于自动数据绑定面板控件的文章。

引言

在我浏览自己一些旧项目时,我发现其中一个项目涉及到了 .NET Framework 中的一个有趣主题。这个主题就是“数据绑定”。

Using the Code

你们中的许多人都知道,数据绑定的过程很复杂,并且与 Web 应用程序和 Windows 应用程序不同,它为它们各自进行了优化。从我的角度来看,数据绑定可以分为两个分支:简单数据绑定和复杂数据绑定。当我们只需要将单个值绑定到控件的某个属性时,就可以称之为简单数据绑定。简单数据绑定操作使用 <%# %> 来执行。数据绑定标签之间的表达式仅在控件的数据绑定方法被调用时才会被计算。

简单数据绑定仅适用于字符串和整数等标量值。复杂数据绑定则是另一回事。它适用于实现 IEnumerable 接口的任何数据类型。我们的控件(BindingPanelBindingTextBox)将利用这种类型的数据绑定。我将首先实现两个辅助接口,我们将在代码中使用它们。第一个是 IDataBound。在这个接口中,我们定义了将绑定到我们控件的值的数据类型,用于绑定过程的列名的 BoundColumn,以及绑定过程后控件的新值的 BoundValue。最后是 IDataBoundInfo 接口,它包含数据绑定中使用的表名。

数据类型定义

namespace MyControls
{
    public enum DataTypes
    {
        Default,
        String,
        Integer,
        DateTime,
        Double
    }
}

IDataBound 接口定义

namespace MyControls
{
    public interface IDataBound
    {
        DataTypes DataType { get; set; }
        string BoundColumn { get; set; }
        object BoundValue { get; set; }
        bool SingleBind { get; set; }
    }
}

IDataBoundInfo 接口定义

namespace MyControls
{
    public interface IDataBoundInfo : IDataBound
    {
        string TableName { get; set; }
    }
}

BindingTextBox 控件

在简要介绍了这三个辅助接口之后,让我们开始实际操作。这是 BindingTextBox 控件的代码。

using System;
using System.Web.UI.WebControls;

/// <summary>
/// Summary description for BindingTextBox
/// </summary>
namespace MyControls
{
    public class BindingTextBox : TextBox, IDataBoundInfo
    {
        /// <summary>
        /// IDataBound members.
        /// </summary>
        private DataTypes _datatype = DataTypes.Default;
        private string _boundcolumn;
        private bool _singlebind;

        /// <summary>
        /// IDataBoundInfo members.
        /// </summary>
        private string _tablename;

        public DataTypes DataType
        {
            get { return _datatype; }
            set { _datatype = value; }
        }

        public string BoundColumn
        {
            get { return _boundcolumn; }
            set { _boundcolumn = value; }
        }

        public virtual object BoundValue
        {
            get { return ControlHelper.ConvertValue(_datatype, this.Text); }
            set
            {
                if (value is DBNull)
                    this.Text = "";
                else
                    this.Text = value.ToString();
            }
        }

        public bool SingleBind
        {
            get { return _singlebind; }
            set { _singlebind = value; }
        }

        public string TableName
        {
            get { return _tablename; }
            set { _tablename = value; }
        }

    }
}

此时,请忽略 SingleBid 属性。我曾用它来处理其他目的(多值数据绑定)。

ControlHelper 类

我忘了提 ControlHelper 类。它只包含一个用于转换的方法。这是 ControlHelper 类的代码。

using System;

/// <summary>
/// Summary description for ControlHelper
/// </summary>
namespace MyControls
{
    public class ControlHelper
    {
        public static object ConvertValue(DataTypes toType, object value)
        {
            try
            {
                switch (toType)
                {
                    case DataTypes.String: return Convert.ToString(value);
                    case DataTypes.Integer: return Convert.ToInt32(value);
                    case DataTypes.DateTime: return Convert.ToDateTime(value);
                    case DataTypes.Double: return Convert.ToDouble(value);
                    case DataTypes.Default: return value;
                }
            }
            catch
            {
                return null;
            }

            return null;
        }
    }
}

我认为这两段代码(BindingTextBoxControlHelper)都很直接、简单,并且是自解释的。

BindingPanel 控件

BindingPanel 的代码更复杂一些,所以,我将先展示代码,然后一步一步地进行讲解。

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Web.UI;
using System.Web.UI.WebControls;

/// <summary>
/// Summary description for BindingPanel
/// </summary>
namespace MyControls
{
    public class BindingPanel : Panel
    {
        private string _data_member;
        private object _datasource;

        #region public string DataMember
        <Browsable(false)>
        public string DataMember
        {
            get { return _data_member; }
            set { _data_member = value; }
        }
        #endregion

        #region public object DataSource
        <Browsable(false)>
        public object DataSource
        {
            get { return _datasource; }
            set
            {
                if ((value == null) || (value is IListSource) || 
                    (value is IEnumerable))
                {
                    _datasource = value;
                }
                else
                    throw new ArgumentException(@"Invalid object. Object must " + 
                       @"implement IListSource or IEnumerable", 
                       "DataSource");
            }
        }
        #endregion

        #region private void UpdateFromControlsRecursive(Control control, object row)
        private void updateFromControlsRecursive(Control control, object row)
        {
            foreach (Control ctrl in control.Controls)
            {
                if (ctrl is IDataBound)
                {
                    IDataBound idbc = (IDataBound)ctrl;
                    string boundField = idbc.BoundColumn;
                    object _old_value = null;

                    if (boundField.Length > 0)
                    {
                        if (row is DataRow)
                            _old_value = ((DataRow)row)<boundField>;

                        if (_old_value != idbc.BoundValue)
                        {
                            if (row is DataRow)
                            {
                                if (idbc.BoundValue != null)
                                    ((DataRow)row)<boundField> = idbc.BoundValue;
                                else
                                    ((DataRow)row)<boundField> = DBNull.Value;
                            }
                        }
                       
                    }
                }
            }
        }
        #endregion

        #region private void BindControlsRecursive(Control control, object row)
        private void bindControlsRecursive(Control control, object row)
        {
            foreach (Control ctrl in control.Controls)
            {
                if (ctrl is IDataBound)
                {
                    IDataBound idbc = (IDataBound)ctrl;
                    string boundField = idbc.BoundColumn;

                    if (boundField != null && boundField.Length > 0)
                    {
                        if (row is DataRow)
                            idbc.BoundValue = ((DataRow)row)<boundField>;

                     }
                }
            }
        }
        #endregion

        #region private void clearControlsRecursive(Control control)
        private void clearControlsRecursive(Control control)
        {
            foreach (Control ctrl in control.Controls)
            {
                if (ctrl is IDataBound)
                {
                    IDataBound idbc = (IDataBound)ctrl;
                    string boundField = idbc.BoundColumn;

                    if (boundField != null && boundField.Length > 0)
                        idbc.BoundValue = DBNull.Value;
                }
            }
        }
        #endregion

        #region private PropertyDescriptor[] GetColumnPropertyDescriptors(object dataItem)
        private PropertyDescriptor[] GetColumnPropertyDescriptors(object dataItem)
        {
            ArrayList props = new ArrayList();
            PropertyDescriptorCollection propDescps = 
                              TypeDescriptor.GetProperties(dataItem);
            foreach (PropertyDescriptor pd in propDescps)
            {
                Type propType = pd.PropertyType;
                TypeConverter converter = TypeDescriptor.GetConverter(propType);

                if ((converter != null) && converter.CanConvertTo(typeof(string)))
                    props.Add(pd);
            }
            props.Sort(new PropertyDescriptorComparer());
            PropertyDescriptor[] columns = new PropertyDescriptor[props.Count];
            props.CopyTo(columns, 0);
         return columns;
        }
        #endregion
        
        #region protected virtual IEnumerable GetDataSource()
        protected virtual IEnumerable GetDataSource()
        {
            if (_datasource == null)
                return null;
            IEnumerable resolvedDataSource = _datasource as IEnumerable;
            if (resolvedDataSource != null)
                return resolvedDataSource;
            IListSource listDataSource = _datasource as IListSource;
            if (listDataSource != null)
            {
                IList listMember = listDataSource.GetList();
                if (listDataSource.ContainsListCollection == false)
                    return (IEnumerable)listMember;
                ITypedList typedListMember = listMember as ITypedList;
                if (typedListMember != null)
                {
                    PropertyDescriptorCollection propDescps =
                        typedListMember.GetItemProperties(null);
                    PropertyDescriptor propertyMember = null;
                    if ((propDescps != null) && (propDescps.Count != 0))
                    {
                        string dataMember = DataMember;
                        if (dataMember != null)
                        {
                            if (dataMember.Length == 0)
                                propertyMember = propDescps[0];
                            else
                                propertyMember = propDescps.Find(dataMember, true);
                            if (propertyMember != null)
                            {
                                object listRow = listMember[0];
                                object list = propertyMember.GetValue(listRow);
                                if (list is IEnumerable)
                                    return (IEnumerable)list;
                            }
                        }
                        throw new Exception("A list that coresponds " + 
                              "to the selected DataMember can not be found.");
                    }
                    throw new Exception("The DataSource does not " + 
                              "contains any data members to bind to.");
                }
            }
            return null;
        }
        #endregion
        
        #region public void BindControls(DataRow row)
        public void BindControls(DataRow row)
        {
            bindControlsRecursive(this, row);
        }
        #endregion
        
        #region public void BindControls(object datasource)
        public void BindControls(object datasource)
        {
            bindControlsRecursive(this, datasource);
        }
        #endregion
        
        #region public void ClearControls()
        public void ClearControls()
        {
            clearControlsRecursive(this);
        }
        #endregion
        
        #region public void UpdateFromControls(DataRow row)
        public void UpdateFromControls(DataRow row)
        {
            updateFromControlsRecursive(this, row);
        }
        #endregion
        
        #region public void UpdateFromControls(object datasource)
        public void UpdateFromControls(object datasource)
        {
            updateFromControlsRecursive(this, datasource);
        }
        #endregion
        
        #region public override void DataBind()
        public override void DataBind()
        {
            IEnumerable dataSource = null;
            base.OnDataBinding(EventArgs.Empty);
            dataSource = GetDataSource();
            if (dataSource != null)
            {
                PropertyDescriptor[] properties = null;
                foreach (Control ctrl in this.Controls)
                {
                    if (ctrl is IDataBound)
                    {
                        IDataBound idbc = (IDataBound)ctrl;
                        string boundField = idbc.BoundColumn;
                        if (boundField.Length > 0)
                        {
                            foreach (object dataItem in dataSource)
                            {
                                properties = GetColumnPropertyDescriptors(dataItem);
                                for (int i = 0; i < properties.Length; i++)
                                {
                                    PropertyDescriptor pd = properties[i];
                                    if (boundField.CompareTo(pd.Name) == 0)
                                    {
                                        object ctlValue = pd.GetValue(dataItem);
                                        idbc.BoundValue = 
                                          pd.Converter.ConvertTo(ctlValue, typeof(string));
                                    }
                                }
                                if (idbc.SingleBind)
                                    break;
                            }
                        }
                    }
                }
            }
        }
        #endregion
        
        #region NESTED CLASSES
        #region private sealed class PropertyDescriptorComparer : IComparer
        private sealed class PropertyDescriptorComparer : IComparer
        {
            public int Compare(object objectA, object objectB)
            {
                PropertyDescriptor pd1 = (PropertyDescriptor)objectA;
                PropertyDescriptor pd2 = (PropertyDescriptor)objectB;
                return String.Compare(pd1.Name, pd2.Name);
            }
        }
        #endregion
        #endregion
    }
}

DataMember 属性是面板将要绑定的数据成员名称。在我们的例子中,它将是数据集中的一个表名。DataSource 属性用于设置或获取参与绑定过程的数据源。我们将使用一个从测试数据库填充的数据集。我们的数据集将包含一个作为数据成员的表。您会注意到,数据源必须继承 IList 接口或 IEnumerable 接口,否则会抛出异常。让我们来看看 GetDataSource() 方法。它返回一个 IEnumerable,这就是我们的数据源对象。正如我之前提到的,在数据绑定过程中,我们可以使用任何实现 IEnumerable 接口的对象作为数据源。基于这个规则,我们需要检查我们的数据源对象类型是否为 IEnumerable(或 IList,它继承自 IEnumerable)。

if (_datasource == null)
    return null;

IEnumerable resolvedDataSource = _datasource as IEnumerable;

if (resolvedDataSource != null)
    return resolvedDataSource;

IListSource listDataSource = _datasource as IListSource;

if (listDataSource != null)
     {.....}

IListSource 不过是一个接口,它为对象提供了返回可绑定到数据源的列表的功能。它还公开了一个 ContainsListCollection 属性,指示集合是否是 IList 对象集合。

IList listMember = listDataSource.GetList();

if (listDataSource.ContainsListCollection == false)
    return (IEnumerable)listMember;

我们使用 GetList() 方法设置 listMember 变量的值。之后,检查 listDataSource 对象是否是 IList 对象集合。如果不是,那么 listMember 必须是 IEnumerable 类型,进行强制类型转换,然后返回退出方法。

ITypedList typedListMember = listMember as ITypedList;

if (typedListMember != null)
{
    PropertyDescriptorCollection propDescps =
        typedListMember.GetItemProperties(null)
    if ((propDescps != null) && (propDescps.Count != 0))
    {
        string dataMember = DataMember;

        if (dataMember != null)
        {
            if (dataMember.Length == 0)
                propertyMember = propDescps[0];
            else
                propertyMember = propDescps.Find(dataMember, true);

            if (propertyMember != null)
            {
                object listRow = listMember[0];
                object list = propertyMember.GetValue(listRow);

                if (list is IEnumerable)
                    return (IEnumerable)list;
            }
        }

        throw new Exception("A list that coresponds to the " + 
                            "selected DataMember can not be found.");
    }

    throw new Exception("The DataSource does not contains any data members to bind to.");
}

如果数据源是 IList 对象集合,则获取我们可绑定列表对象的架构,并将其保存在 typedListMember 中。MSDN 对 ITypedList 提供了以下描述:

“提供用于发现可绑定列表的架构的功能,其中可用于绑定的属性与要绑定的对象的公共属性不同。”

如果存在这样的架构,则获取代表用于绑定数据的每个项的属性的 PropertyDescriptorCollection。我们通过使用 typedListMember 对象公开的 GetItemProperties() 方法来做到这一点。此方法需要一个 PropertyDescriptor 对象数组作为参数,或者您可以传递一个 null 引用。PropertyDescriptor 数组用于从集合中查找可绑定对象。现在,创建一个名为 propertyMemberPropertyDescriptor 对象,并将其初始化为 null。如果 propDescps 集合不为 null,或者至少包含一个项,则使用 DataMember 属性中的值设置 dataMember 字符串。如果 dataMember 为空,我们将从 propDescps 集合中获取第一个 PropertyDescriptor 对象。如果不为空,则使用 propDescps 公开的 Find(string name, bool ignoreCase) 方法。此方法将返回具有指定名称的 PropertyDescriptor 对象。布尔参数(ignoreCase)指示是否忽略名称的大小写。获得 propertyMember 后,我们将从 listMember 中获取第一个对象,并将其作为参数传递给 propertyMember 公开的 GetValue(object component) 方法。此方法将返回指定组件的属性值。最后要做的就是检查返回的值是否为 IEnumerable 类型;如果不是,则抛出一个异常,通知找不到指定的成员。

private PropertyDescriptor[] GetColumnPropertyDescriptors(object dataItem)
{
    ArrayList props = new ArrayList();
    PropertyDescriptorCollection propDescps = TypeDescriptor.GetProperties(dataItem);

    foreach (PropertyDescriptor pd in propDescps)
    {
        Type propType = pd.PropertyType;
        TypeConverter converter = TypeDescriptor.GetConverter(propType);

        if ((converter != null) && converter.CanConvertTo(typeof(string)))
            props.Add(pd);
    }

    props.Sort(new PropertyDescriptorComparer());
    PropertyDescriptor[] columns = new PropertyDescriptor[props.Count];
    props.CopyTo(columns, 0);

    return columns;
}

总的来说,对于 GetColumnPropertyDescriptors(object dataItem)(它返回一个 PropertyDescriptor 数组),解释是相同的。我们获取对应于每个列的 PropertyDescriptor,获取 TypeConverter,并检查属性类型是否可以转换为 string,将它们添加到 ArrayList 中,并使用 PropertyDescriptorComparer 作为参数进行排序,最后将它们插入到 PropertyDescriptor 数组(列)中。

最后,我们到达了数据绑定过程中最知名的方法,DataBind()。这是此方法的代码。

public override void DataBind()
{
    IEnumerable dataSource = null;

    base.OnDataBinding(EventArgs.Empty);

    dataSource = GetDataSource();

    if (dataSource != null)
    {
        PropertyDescriptor[] properties = null;

        foreach (Control ctrl in this.Controls)
        {
            if (ctrl is IDataBound)
            {
                IDataBound idbc = (IDataBound)ctrl;
                string boundField = idbc.BoundColumn;

                if (boundField.Length > 0)
                {
                    foreach (object dataItem in dataSource)
                    {
                        properties = GetColumnPropertyDescriptors(dataItem);

                        for (int i = 0; i < properties.Length; i++)
                        {
                            PropertyDescriptor pd = properties[i];
                            if (boundField.CompareTo(pd.Name) == 0)
                            {
                                object ctlValue = pd.GetValue(dataItem);
                                idbc.BoundValue = 
                                  pd.Converter.ConvertTo(ctlValue, typeof(string));
                            }
                        }

                        if (idbc.SingleBind)
                            break;
                    }
                }
            }
        }
    }
}

执行的步骤如下:

  • 调用基类的 OnDataBinding(EventArgs e) 方法。
  • 获取将用于数据绑定的数据源。
  • dataSource = GetDataSource()
  • 遍历此面板中包含的控件。
  • foreach (Control ctrl in this.Controls)
  • 检查当前控件是否实现 IDataBound 接口;如果实现,则将其强制转换并获取 BoundColumn 属性。
  • if (ctrl is IDataBound)
    {
        IDataBound idbc = (IDataBound)ctrl;
        string boundField = idbc.BoundColumn;
        ..................
    }
  • 遍历数据源数据项对象,并使用 GetColumnPropertyDescriptors 方法获取每个数据项对象的属性。
  • foreach (object dataItem in dataSource)
    {
        properties = GetColumnPropertyDescriptors(dataItem);
        ..................
    }
  • 对于每个属性,获取相应的属性描述符,并将其名称与 BoundColumn 值(boundField)进行比较。如果匹配,则获取属性描述符的值,将其转换为 string,并将其赋值给当前控件的 BoundValue 属性。
  • for (int i = 0; i < properties.Length; i++)
    {
        PropertyDescriptor pd = properties[i];
        if (boundField.CompareTo(pd.Name) == 0)
        {
           object ctlValue = pd.GetValue(dataItem);
           idbc.BoundValue =    pd.Converter.ConvertTo(ctlValue, typeof(string));
        }
    }

这些方法构成了我们绑定面板控件的骨干。BindingPanel 控件还包含三个重要方法:bindControlsRecursiveupdateFromControlsRecursiveclearControlsRecursive。我将从 bindControlsRecursive 方法开始。

private void bindControlsRecursive(Control control, object row)
{
    foreach (Control ctrl in control.Controls)
    {
        if (ctrl is IDataBound)
        {
            IDataBound idbc = (IDataBound)ctrl;
            string boundField = idbc.BoundColumn;

            if (boundField != null && boundField.Length > 0)
            {
                if (row is DataRow)
                idbc.BoundValue = ((DataRow)row);
            }
        }
    }
}

此方法需要两个参数:一个 Control,它是将参与绑定过程的子控件的容器,以及数据对象(行)。该方法遍历所有子控件,并检查它们是否实现 IDataBound 接口。如果它们实现了,它将根据 BoundColumn 属性获取相应的列值,并用数据填充我们的控件。

private void updateFromControlsRecursive(Control control, object row)
{
    foreach (Control ctrl in control.Controls)
    {
        if (ctrl is IDataBound)
        {
            IDataBound idbc = (IDataBound)ctrl;
            string boundField = idbc.BoundColumn;
            object _old_value = null;

            if (boundField.Length > 0)
            {
                if (row is DataRow)
                    _old_value = ((DataRow)row)<boundField>;

                if (_old_value != idbc.BoundValue)
                {
                   if (row is DataRow)
                   {
                      if (idbc.BoundValue != null)
                         ((DataRow)row)<boundField> = idbc.BoundValue;
                      else
                         ((DataRow)row)<boundField> = DBNull.Value;
                   }
                }
            }
        }
    }
}

updateFromControlsRecursive 方法执行的操作与 bindControlsRecursive 方法相同,但逻辑相反。它遍历所有子控件,并检查它们是否实现 IDataBound 接口。如果它们实现了,它将获取当前控件的值,并根据 BoundColumn 属性用它来填充数据对象(行)中相应的列。

private void clearControlsRecursive(Control control)
{
    foreach (Control ctrl in control.Controls)
    {
        if (ctrl is IDataBound)
        {
            IDataBound idbc = (IDataBound)ctrl;
            string boundField = idbc.BoundColumn;

            if (boundField != null && boundField.Length > 0)
                        idbc.BoundValue = DBNull.Value;
        }
    }
}

为了清理控件值,我们使用 clearControlsRecursive 方法。它只是遍历所有子控件,并检查它们是否实现 IDataBound 接口。如果它们实现了,它将清除它们的值(控件的值将被设置为 DBNull.Value)。

好了,这些就是关于这个控件最重要的事情。

结论

如果您愿意,可以扩展这里介绍的所有功能。zip 文件包含完整的项目以及一个使用示例。您需要做的就是为此创建一个测试数据库。玩得开心!

© . All rights reserved.