Automatic Data Binding Controls

Introduction

While I was browsing some old projects, I discovered that in one of them I have approached an interesting topic of .NET framework. That topic was "Data Binding."

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 splinted in two branches: simple data binding and complex data binding.

You can speak about simple data binding when you are in the situation of binding a single value to a property of a control. Simple data binding operation is performed using <%# %>. The expression between data binding tags is evaluated only when the control's data binding method is invoked. Here is an example of simple data binding:

Customer: <%# custID %>

where "Customer" is a label and "custID" is a public property.

Simple data binding works only with scalar values like strings and integers. Complex data binding is a different story. It works with any data type that implements IEnumerable interface. At the end of this article, you should have a panel that will act as a container for the child custom controls and it will automatically fill them with the corresponding values from its data source. For this to work, you need to create two custom interfaces. The first of them is IDataBound. The IDataBound interface defines the data types (DataTypes) of values that will be bounded to your control, the "BoundColumn" property, which will keep the name of the column used in binding process. The BoundValue property is used to store the new value of the control after the binding process. And finally, the IDataBoundInfo interface that contains the name of the table used in data binding. Please note that DataTypes enum contains at this time only data types that are found on System.Type. The reason I have added it to the code is that I might want to use a custom object as a data type for the data binding in the future.

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 those two helper interfaces, it's time to get down to business. I will create a text box custom control that will be called "BindingTextBox". Here is the code for this 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) that I will not describe in this article.

ControlHelper Class

I have forgotten to mention about the ControlHelper class. If you remember, I made an observation regarding the DataTypes enum that at this time contains only data types that are found on System.Type, and I have give you an explanation also. The same thing goes for the ControlHelper class. It contains only one method used for conversion, but if you want some custom object, you could implement different methods for 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).

Automatic Data Binding Controls

BindingPanel Control

The BindingPanel code is more complicated, so first I'll show you the code. 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 cannot be
                                       found.");
               }
               throw new Exception("The DataSource does not contain
                                    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 DataMember property is the data member name with which the panel will be bound. In this 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. You will use a data set filled from a NorthWind database. Your data set will contain one table that will be the data member. You can observe that the data source must inherit the IList interface or IEnumerable interface; otherwise, an exception will be thrown.

Automatic Data Binding Controls

Take a look at GetDataSource() method. It returns a IEnumerable that is your data source object. As I mentioned earlier, in the data binding process you can use as a data source any object that implements IEnumerable. Because of this rule, you have to check that your 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 bound to a data source. It also exposes the ContainsListCollection property that indicates whether the collection is a collection of IList objects.

IList listMember = listDataSource.GetList();

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

You set the value of listMember variable using the GetList() method. After this, check to see whether the listDataSource object is a collection of IList objects. If it's not, 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 your bindable list object and save it in typedListMember. MSDN offers the following description for ITypedList: "Provides functionality to discover the schema for a bondable 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. You do this by using the GetItemProperties() method that is exposed by a typedListMember object. This method requires an array of the PropertyDescriptor object as a parameter, or you can pass a null reference. The PropertyDescriptor array is used to find the bondable objects from the collection. Now, create a PropertyDescriptor object called propertyMember and initialize it with null. If propDescps collection is not null or it contains at least one item, set the value for dataMember string from DataMember property. If dataMember is empty, you will get the first PropertyDescriptor object from the propDescps collection.

If it's not empty, use the Find(string name, bool ignoreCase) method that 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 you have obtained the propertyMember, you will take the first object from listMember that will be passed as a paramater to the GetValue(object component) method exposed by propertyMember. This method will return the value of a property for the specified component. Now, the last thing to do is to check whether the returned value is of type IEnumerable; if it's not, throw an exception to notify you that the specified data member cannot 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 the GetColumnPropertyDescriptors(object dataItem) that returns a PropertyDescriptor array. You get the PropertyDescriptor that corresponds to each column, get the TypeConverter, check whether the property type can be converted to string, add them to an array list, sort them using PropertyDescriptorComparer as parameter, and finally insert them into the PropertyDescriptor array (columns).

Finally, you 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:

  1. Call the OnDataBinding(EventArgs e) method of the base class.
  2. Get the data source that will be used in data binding.
  3. dataSource = GetDataSource()
  4. Iterate through the controls contained in this panel.
  5. foreach (Control ctrl in this.Controls)
  6. Check whether the current control implements IDataBound interface. If it does, cast it to it and get the BoundColumn property.
  7. if (ctrl is IDataBound)
    {
       IDataBound idbc = (IDataBound)ctrl;
       string boundField = idbc.BoundColumn;
    }
    
  8. Iterate through data source data item objects and get the properties for each data item object by using the GetColumnPropertyDescriptors method.
  9. foreach (object dataItem in dataSource)
    {
       properties = GetColumnPropertyDescriptors(dataItem);
    }
    
  10. 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 a string, and assign it to the current control BoundValue property.
  11. 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 backbone of your binding panel control. The BindingPanel control also contains three other important methods: bindControlsRecursive, updateFromControlsRecursive, and clearControlsRecursive. I will start the presentation 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)[boundField];
         }
      }
   }
}

This method requires two parameters: a Control that 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, checks whether they implement the IDataBound interface. If they do implement it, it gets the corresponding column value based on BoundColumn property and populates your 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 whether they implement IDataBound interface. If they do implement it, 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 controls values, you use the clear ControlsRecursive method. It simply iterates through all child controls and checks whether they implement IDataBound interface. If they do implement it, it clears their values (the control value will be DBNull.Value).

These are the most important things about this control.

Conclusion

As you can see, for the data binding process you have used reflection. The idea is simple. Say, for example, that your custom text box control has the BoundColumn property value "ContactName" and the TableName property value "customers". After you set the DataMember and DataSource properties of BindingPanel, when DataBind() method is called, the panel will iterate thru its child controls and for each child control will also iterate thru its DataMember columns. When a match is found between the BoundColumn property of the child control and a column name from the DataMember, the control's value will be updated with the value of the corresponding column.

I want to make an observation that, in the example project, I'm using only the first row from the DataMember because the panel control is not iterating thru through DataMember's rows. If you want to iterate through all rows, use a repeater, make a template for it, and include the custom panel in it.

If you want, you can extend all the functionality presented here. The zip file contains the entire project and also an usage example. As the database, I have used NorthWind; just change the connection string if required.



About the Author

Michael Heliso

I work as a software developer for about 3 years. Until these days I have worked for different international software companies using different technologies and programming languages like: C/C++, lotus script, lotus API, C#, ASP.NET, MS-SQL, Oracle, Domino Server. My main interest, at this time, is focused on .NET technology (http://dotnetcaffe.net).

Downloads

Comments

  • There are no comments yet. Be the first to comment!

Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

Most Popular Programming Stories

More for Developers

RSS Feeds

Thanks for your registration, follow us on our social networks to keep up-to-date