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;
namespace MyControls
{
public class BindingTextBox : TextBox, IDataBoundInfo
{
private DataTypes _datatype = DataTypes.Default;
private string _boundcolumn;
private bool _singlebind;
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;
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;
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!