Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Auto data binding panel controls

0.00/5 (No votes)
28 Jun 2007 1  
An article on auto data binding panel controls.

Introduction

While I was browsing some old projects of mine, I discovered that in one of them, I had approached an interesting topic of the .NET Framework. And that topic is "Data Binding".

Using the Code

Many of you know that the process of data binding is a complex one, and differs from web application to Windows application, being optimized for each of them. From my point of view, data binding can be split in two branches: simple data binding and complex data binding. We can speak about simple data binding when you are in the situation of binding a single value to a property of a control. A simple data binding operation is performed using <%# %>. The expression between the data binding tags is evaluated only when the control's data binding method is invoked.

Simple data binding works only with scalar values like string and integers. Complex data binding is a different story. It works with any data type that implements the IEnumerable interface. Our controls (BindingPanel, BindingTextBox) will make use of this type of data binding. I will start by implementing two supplementary interfaces which we will use in our code. First of them is IDataBound. In this interface, we define the data types of values that will be bound to our control, the BoundColumn which will keep the name of the column used in the binding process, and BoundValue which is the new value of the control after the binding process. And finally, the IDataBoundInfo interface which contains the name of the table used in data binding.

DataTypes Definition

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

IDataBound Interface Definition

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

IDataBoundInfo Interface Definition

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

BindingTextBox Control

After this short presentation of the three helper interfaces, let's get to business. Here is the code of the BindingTextBox control:

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; }
        }

    }
}

At this time, please ignore the SingleBid property. I have used it for other purposes (data binding with multiple values).

ControlHelper Class

I forgot to mention about the ControlHelper class. It contains only one method used for the conversion. Here is the code for the ControlHelper class:

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;
        }
    }
}

I consider that both pieces of code are straight, simple, and self-explanatory (BindingTextBox and ControlHelper).

BindingPanel Control

The BindingPanel code is more complicated, so first, I'll show you the code, and after that, I'll present it step by step.

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
    }
}

The property DataMember is the data member name with which the panel will be bound. In our case, it will be a table name from a data set. The DataSource property is used to set or get the data source that will be involved in the binding process. We will use a data set filled from a test database. Our data set will contain a table that will be the data member. You can observe that the data source must inherit the IList interface or the IEnumerable interface, else an exception will be thrown. Let's take a look at the GetDataSource() method. It returns an IEnumerable which is our data source object. As I mentioned earlier, in the data binding process, we can use as a data source any object which implements the IEnumerable interface. Because of this rule, we have to check that our data source object type is IEnumerable (or IList, which inherits from 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 is nothing more than an interface that provides the functionality for an object to return a list that can be bound to a data source. It also exposes a ContainsListCollection property that indicates if the collection is a collection of IList objects.

IList listMember = listDataSource.GetList();

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

We set the value of the listMember variable using the GetList() method. After this, check to see if the listDataSource object is a collection of IList objects. If it's not, then listMember must be of type IEnumerable, make a cast, and exit from the method by returning it.

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.");
}

If the data source is a collection of IList objects, get the schema of our bindable list object and save it in typedListMember. MSDN offers the following description for ITypedList:

"Provides functionality to discover the schema for a bindable list, where the properties available for binding differ from the public properties of the object to bind to."

If there is such a schema, get the PropertyDescriptorCollection that represents the properties on each item used to bind data. We do this by using the GetItemProperties() method that is exposed by the typedListMember object. This method requires an array of PropertyDescriptor objects as parameter, or you can pass a null reference. The PropertyDescriptor array is used to find the bindable objects from the collection. Now, create a PropertyDescriptor object called propertyMember and initialize it to null. If the propDescps collection is not null, or it contains at least one item, then set the value for the dataMember string from the DataMember property. If dataMember is empty, we will get the first PropertyDescriptor object from the propDescps collection. If it's not empty, then use the Find(string name, bool ignoreCase) method which is exposed by propDescps. This method will return the PropertyDescriptor object with the specified name. The boolean parameter (ignoreCase) indicates whether to ignore the name case. After we have obtained propertyMember, we will take the first object from listMember which will be passed as a parameter to the GetValue(object component) method exposed by propertyMember. This method will return the value of a property for the specified component. The last thing to do is to check if the returned value is of type IEnumerable; if it's not, throw an exception to notify that the specified data member can not be found.

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;
}

In general, the same explanation goes for GetColumnPropertyDescriptors(object dataItem) which returns a PropertyDescriptor array. We get the PropertyDescriptor that corresponds to each column, get the TypeConverter, and check if the property type can be converted to string, add them to an array list, and sort them using PropertyDescriptorComparer as parameter, and finally insert them into the PropertyDescriptor array (columns).

Finally, we have reached the most known method involved in data binding process, DataBind(). Here is the code of this method:

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;
                    }
                }
            }
        }
    }
}

Here are the steps performed:

  • Call the OnDataBinding(EventArgs e) method of the base class.
  • Get the data source that will be used in data binding.
  • dataSource = GetDataSource()
  • Iterate through the controls contained in this panel.
  • foreach (Control ctrl in this.Controls)
  • Check if the current control implements the IDataBound interface; if it does, cast it and get the BoundColumn property.
  • if (ctrl is IDataBound)
    {
        IDataBound idbc = (IDataBound)ctrl;
        string boundField = idbc.BoundColumn;
        ..................
    }
  • Iterate through the data source data item objects, and get the properties for each data item object using the GetColumnPropertyDescriptors method.
  • foreach (object dataItem in dataSource)
    {
        properties = GetColumnPropertyDescriptors(dataItem);
        ..................
    }
  • For each property, get the corresponding property descriptor, and compare its name with the BoundColumn value (boundField). If they match, get the value of the property descriptor, convert it to string, and assign it to the current control BoundValue property.
  • 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));
        }
    }

These methods represent the back bone of our binding panel control. The BindingPanel control also contains three important methods: bindControlsRecursive, updateFromControlsRecursive, and clearControlsRecursive. I will start with the bindControlsRecursive method.

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);
            }
        }
    }
}

This method requires two parameters: a Control which is the container for the child controls that will be involved in the binding process, and the data object (row). The method iterates through all the child controls and checks if they implement the IDataBound interface. If they do implement it, it gets the corresponding column value based on the BoundColumn property and populates our control with data.

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;
                   }
                }
            }
        }
    }
}

The method updateFromControlsRecursive performs the same operations as the bindControlsRecursive method, but with the logic reversed. It iterates through all the child controls and checks if they implement the IDataBound interface. If they do, it gets the current control value and populates the corresponding column of the data object (row) based on the BoundColumn property.

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;
        }
    }
}

For cleaning up the control values, we use the clearControlsRecursive method. It simply iterates through all the child controls and checks if they implement the IDataBound interface. If they do, it clears their values (the control value will be set to DBNull.Value).

Well, these are the most important things about this control.

Conclusion

If you want, you can extend all the functionality presented here. The zip file contains the complete project and also a usage example. The only thing that you must do is create a test database for it. Have fun!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here