Implementing two-way Data Binding
for ASP.Net Web Forms
By Rick Strahl
www.west-wind.com
rstrahl@west-wind.com
Last Updated: July 23rd, 2004
An updated version specific to ASP.NET 2.0 and using an
Extender Control:
http://msdn.microsoft.com/msdnmag/issues/06/12/ExtendASPNET/default.aspx
Source Code for this article:
http://www.west-wind.com/presentations/ASPNET/ASPNET.zip
ASP.Net has raised the bar for Web development considerably with
very rich developer functionality built into a flexible and highly extensible
object model. For developers who come from a background of hand coding ASP or
other scripting or CGI style technology .Net seems almost too good to be true as
it reduces a lot of redundant code and simplifies the development process
significantly. But one area – simple data binding for controls like textboxes,
checkboxes, radio buttons and so on leave a lot to be desired both in terms of
ease of use for binding the data as well as providing the ability to read the
data back into the data source. In this article Rick examines what’s wrong with
simple data binding and provides a set of subclasses that make data binding a
lot quicker requiring much less manual code.
Data binding tends to be one of the tasks that most
developers deal with on a daily basis. Most applications are data centric and
whenever you create user interface code that relates to the data you’ll find
that if you use the default mechanisms of the .Net Framework that you’ll do the
same things over and over again. Not only that, but .Net really doesn’t make data
binding as easy as it should be either in Windows Forms or in ASP. Net. In this
article, I’ll describe briefly how data binding in ASP. Net works and then
offer a solution to make the process of data binding much easier by using
subclassed controls that handle the repetitive tasks. In a future article I’ll
then discuss the same issues in Windows Forms.
If you’re coming from an ASP or other Web Development
background, you’re probably thinking “what is Rick talking about?” Data binding
in ASP.Net is a huge improvement over whatever I had to do previously in Web
forms. After all we do have a form of data binding (actually several forms) in
.Net and we have various forms of state management that automatically assign
our control values back to the controls so we’re free of having manually
populate fields with data. Big improvement for sure.
What’s wrong with Data Binding in ASP. Net?
Unfortunately, I personally think that data binding in
ASP.Net doesn’t go nearly far enough. First the process of assigning data
sources is cumbersome at best using either a slow and work extensive designer
or alternately by having to embed yet another script based markup tag (<%# %>)
into source code. Both are way too cumbersome if you’re dealing with a lot of data
on a regular basis.
But more importantly data binding in ASP.Net is one way
only. You can only bind data to data but there’s no mechanism to unbind
the data from the control back into its underlying datasource. It’s
hard to really call ASP.Net’s mechanism data binding because really what it is
is a data display mechanism.
To clarify though, there are really two types of data
binding in ASP.Net. There’s list based data binding of the type that you use to
bind data to a ListBox or a DataGrid. This mechanism actually works very well
and provides a good amount of flexibility. This is also primarily a display
mechanism – you tend to display data or lists with this type of binding.
Then there is simple Control Data binding which basically
binds to a single value to a property of a control. A text box binding to a
field of the database is an example of simple data binding. This also tends to
be the most common data binding that most people do during data entry and the
one that is the most time consuming. And here is where the problem lies – the
control binding here is one way and involves some convoluted syntax that isn’t
even property based.
The fact that data binding is one-way in ASP.Net is actually
not all that surprising. The reason is that it’s not easy to automatically bind
back in Web applications, because it’s very difficult in this stateless
environment to tell exactly when data should be bound back to the
underlying data source. After all on a Web page a lot of things need to happen
in a specific order to re-establish state and there’s no easy way to
automatically know when a datasource is ready to receive the data without some
logic as part of the application.
As you’ll see in a minute my implementation skirts this
particular issue by having the bind back operation occur manually through a
call to a helper method or form method (if using an optional custom WebForm subclass).
How things work now
Let me give you an example to put the current process into
perspective. Assume that I have a business form and want to display and edit
some customer information. So I use my business object to load up a dataset
with data from a Load() method which internally populates a DataSet and DataRow
member (you could also do this manually in your code of course). I now have a
Dataset that I can bind to the various controls. This is easily done by using
the control’s data binding options in the property sheet or by manually
assigning the value using the ASP.Net data binding scripting syntax (yes
another variation of <% %> syntax using <%# %>). What’s
interesting is that the binding syntax is not property based but generates a
chunk of ASP.Net script code that gets embedded into the HTML. The following
binds to Company field of my DataRow for example:
<asp:TextBox id="txtCompany" runat="server" Width="285px"
Text='<%#
Customer.DataRow["company"] %>'></asp:TextBox>
You can enter that expression manually into the ASP.Net HTML
document or you can use the builder that generates this expression
automatically as shown in figure 1. This syntax is not exactly intuitive and
obviously requires the ASP.Net script parser to parse the string first before
ever assigning the data binding expression to be evaluated.

Figure 1 – Data binding dialog in ASP.Net lets
you use custom expressions (shown here) or by selecting datasource from
contained components that support the data binding interfaces.
Once bound to the data with this mechanism I can now display
data in my Web Form. Then I want to edit the data in the various input controls
and ASP.Net provides perfect state for doing the control based editing and
posting. So if an error occurs I can display an error message or use an error
provider to display the message without loosing my data for example. So far so
good.
The next step is to save the data. When I click the save
button some code is fired in the Web Form to save the data from the Web Form
into my datasource, and then eventually back to the database. Although I’ve
already told the control what data I want to bind to (how could you forget <%#
Customer.DataRow["company"] %> after all), there’s no automated
way to bind the data back into the control. The reason for this should be clear
– ASP.Net generated an actual value into the Text property, but really it never
‘bound’ anything to the form.
So instead of simply unbinding the data I now have to write
code like this for my business object or data source to handle the bind back:
Customer.DataRow["Company"] =
this.txtCompany.Text;
Customer.DataRow["Address"] =
this.txtAddress.Text;
Just about every data entry form
will need this sort of code to bind data back. If the form has only a handful of
controls this is no big deal, but if you have a heavy data entry form and many
of these forms this gets to be a hassle. It’s also a maintenance nightmare –
every time you add a new control to the form you have to also add the code to
post back the data – you now have to keep track of this in two separate places.
It gets worse when you need to bind back non-String data as you have to do type
coercion and error handling:
try {
Customer.DataRow["CustomerLevel"] =
Int32.Parse(this.txtCustomerLevel.Text");
}
catch(Exception) {
this.ErrorMsg = this.ErrorMsg + "Invalid Customer
Level";
}
I don’t know about you, but this is a lot of repetitive work
that I certainly don’t want to do every time I bind back a form. There has to
be a better way and there is.
Subclassing the Web controls for better data binding
As you can see by the process above, there are a number of
shortcomings in the data binding process. The process is quite repetitive and
if we can delegate some of this functionality into the control itself or some
helper class we could make life on us a whole lot easier.
My solution to this problem is to subclass the various Web
form controls and add functionality to them natively. The classes
I’ll describe now provide the following functionality:
- Simple
property based control binding
Instead of the embedded script code that performs binding I added three
properties to each control: BindingSource, BindingSourceProperty and
Binding Property. The control source is an object on the Web Form – it
could be a plain object
- Two way
binding
Controls can be bound one way, two way or not at all. The binding
process is controlled with a method call on a generic helper method or if
you use a custom WebForm subclass a method call to the form (DataBind(),
UnbindData()).
- Error
Handling
When forms are rendered an error message is automatically set into the
form property to let you see that the control could not be bound. When you
perform unbinding any bind back errors are automatically handled and
dumped into an error list that can easily be parsed and generated into an
error message to display in your Web Form. I’ll also show a simple way to
display error hints on the control themselves.
- Basic Display
Formatting
You can apply basic .Net display formatting to controls as they are
rendered. Using standard format expressions ( such as {0:c} for currency
or {0:f2} for fixed ) etc.
The implementation of this mechanism is based on a Strategy
pattern where the actual controls have only small wrapper methods that call
back to a worker class that actually performs the , unbinding for
both the controls individually and the form as a whole.
The key classes involved in this solution are:
- IwwWebDataControl
This is the interface that each of the controls must
implement it includes the BindingSourceObject, BindingSourceProperty,
BindingProperty.
- wwWeb<Control>
subclasses
All of the controls that are to be databound are subclassed from the
standard control classes and implement the IwwWebDataControl interface. In
the project provided TextBox, CheckBox, RadiobuttonGroup, Listbox and
DropDownList are subclassed since these are the most common ones you do
two-way data binding with.
- wwWebDataHelper
This is the strategy class that handles all of the dirty work of
actually binding the controls both individually and for the entire form.
All the methods on this class are static and they receive parameters of
the Web Page object and the controls they are binding.

Figure 2 – The custom data binding scheme uses
custom subclasses of each of the form controls and optionally the Web Page
itself. These controls implement the IwwWebDataControl interface the methods of
which call out to the wwWebDataControlHelper class to perform the actual
binding work.
Figure 2 shows the relationship between these classes. The
concept is straight forward: each control is subclassed and implements the
IwwWebDataControl interface. This interface serves two purposes: It provides
the properties needed to handle the data binding in an easy property sheet based
input mechanism using plain properties. It also serves as an identifier for the
controls we want to bind when binding all controls of a Web form. The interface
also has BindData() and UnbindData() methods which typically do nothing more
than forward their property values to the wwWebDataHelper class and its
methods. For example, an implementation of the BindData method in wwWebTextBox
looks like this:
public void BindData(Page WebForm)
{
wwWebDataHelper.ControlBindData(WebForm,this);
}
Listing 2 (a little further down) shows a full
implementation of the wwWebTextBox class so you can get an idea of what goes
into a full class that implements this interface. The idea is that the
wwWebDataHelper class does all the logic for data binding rather than the
control methods themselves, so that we can reuse the same code for binding. All
the controls including list controls like the ListbBox and DropDownList can use
the same mechanism for binding. Controls like the ListBox and DropDownList
continue to work with list based binding as well as implementing the updated
functionality so you can bind value based data to them as well.
You can optionally use the wwWebForm class which implements
BindData() and UnbindData() and also overrides DataBind() to automatically call
BindData(). Using this class is optional though as you can directly call the
wwDataHelper.FormBindData() or wwDataHelper.FormUnbindData() methods.
The process to use these controls then is:
- Create a form
and optionally subclass it from wwWebDataForm
- Add the
wwWebDataControls.dll to your Toolbox in VS.Net
- Use the
controls on your form
- Set the
Binding properties to bind to data, objects or properties
- Call the
appropriate form level binding methods
Figure 3 shows an example of a simple data entry form that
displays Inventory data and allows editing of that data. It contains data that
is retrieved from a SQL Server database using a business object which loads
data into an internally exposed DataSet and DataRo member. These are then bound
to the data.

Figure 3 – The sample form demonstrates the base
elements of . Each of the controls on the form is bound to data. To
bind or unbind the controls only a single method call each is required to the
form’s BindData and UnbindData methods respectively.
Adding Controls to the form
The first step before we get started is to add the
wwWebDataControls.dll to the Toolbar. To do so follow these steps:
- Select the
toolbox.
- Right click
and select Add New Tab
- Type the name
for this tab (West Wind Web Controls)
- Select the new
tab
- Right click
again and select Add/Remove Items
- Browse for the
Installation directory for the samples and select the
/wwWebDataControls/bin/Debug/wwWebDataControls.dll
(for now) – later you’ll probably want to install this to the GAC. For now
it’s handy to use the debug version so you can change the controls.
- Drag and drop
the controls onto the form.
The controls use a default script tag prefix of ww which is
registered against the DLL. This looks like this in script code for the header:
<%@ Register TagPrefix="ww"
Namespace="Westwind.Web.Data"
Assembly="wwWebDataControls"%>
And like this for a control:
<ww:wwwebtextbox id="txtPrice" runat="server" size="20"
BindingSourceObject="Inventory.DataRow"
BindingSourceProperty="Price" UserFieldName="Price"
DisplayFormat="{0:c}">
</ww:wwwebtextbox>
If you use the toolbar though you won’t have to do any of
this manually. What’s nice about this form is that other than the property
assignments which are made in the property sheet (Figure 4) there’s no code
involved in the . It’s very quick and easy to create the
s in this fashion.

Figure 4 – All settings for data binding are made
in one place in the property sheet.
Note that the data binding in this example binds against the
retrieved DataRow of the business object, which is just a plain DataRow object.
The BindingSourceProperty in this case references a field. But you can also
bind to a DataSet and specify the TableName.Field syntax that you traditionally
use. In this case the binding occurs against the first row of the table. The
same is true if you use a DataTable or DataView as your binding source.
In addition you can also bind to objects or properties of the form. You can set
the BindingSourceObject to this or me for example, and then bind against a
property you’ve exposed on the form. If you have an object and properties you’d
like to bind to you can do:
BindingSourceObject: Customer.Address
BindingSourceProperty: Street
Notice that you can step down the object hierarchy which
implicitly starts at the form level (so this is actually
this.Customer.Address). This is flexible if you use business objects that don’t
expose the underlying data directly or if you need to bind against objects that
simply don’t map to data (like configuration object or wizards etc.).
In addition notice that you can also specify a format flag.
If you look at Figure 3 again you’ll see for example that the Price is
displayed with the $ in front of it. This field is using the {0:c} format flag
to format currency, and when you save the data in this format the controls
automatically allow the conversion back into the numeric value using Parsing.
If an error occurs during the bindback the field is flagged – I’ll come back to
that a little later.
Binding the entire form
Besides setting the properties of the controls in the
property sheet there are only two things that you need to do in code: Call the
appropriate method to bind and then unbind the data when you save. Listing 1
shows the key elements of the Inventory Form.
private void Page_Load(object
sender, System.EventArgs e)
{
if (!this.IsPostBack) {
Inventory = new
busInventory();
// *** Get Item list
into a table TItemList
Inventory.GetItemList("sku,descript");
// *** Do standard
for the list
this.txtSearchSku.DataSource
= Inventory.DataSet.Tables["TItemList"];
this.txtSearchSku.DataBind();
}
}
private void btnSearch_Click(object
sender, System.EventArgs e)
{
string Sku = this.txtSearchSku.SelectedValue;
if (Sku == null || Sku.Length < 1)
{
this.ShowErrorMessage("Invalid Sku selected...");
return;
}
Inventory = new
busInventory();
// *** Load the item - will
load Item.DataSet and Item.DataRow
if (
!Inventory.LoadBySku(Sku) )
{
this.ShowErrorMessage(Inventory.ErrorMsg);
return;
}
// Save the current Sku so
we can retrieve it
// later when we save
ViewState.Add("Sku",Sku);
InvTable = Inventory.DataSet.Tables["wws_items_Record"];
// *** Now bind to data
wwWebDataHelper.FormBindData(this);
}
private void btnSubmit_Click(object
sender, System.EventArgs e)
{
Inventory = new
busInventory();
string Sku =(string) ViewState["Sku"];
if (Sku == null) {
this.ShowErrorMessage("Can't save this item - invalid Sku");
return;
}
// Load the existing item
// Creates Inventory.DataRow
member that the form is bound to
Inventory.LoadBySku(Sku);
// Now Update the bound
fields (DataRow) from the form
wwWebDataHelper.FormUnbindData(this);
if (!Inventory.Save())
{
this.ShowErrorMessage(Inventory.ErrorMsg);
return;
}
// *** Rebind with
the new values as saved
wwWebDataHelper.FormBindData(this);
this.ShowErrorMessage("Inventory Item Saved...");
//Response.Redirect("Itemlist.aspx");
}
Page_Load does traditional list based binding for the dropdown list against
a DataTable. This mechanism actually uses the built-in data binding which is
automatically inherited and still works as you would expect normally. But that
control can also act as a simple data binding control against the SelectedValue.
When the form first loads the drop down is loaded up, but the item below is
left blank until a selection is made.
The selection is handled by the btnSearch_Click method,
which instantiates a business object and based on the selection in the list
retrieves the required item. The result of this operation is the
Inventory.DataSet and Inventory.DataRow are set and this is going to be the
target of our .
When the data has loaded the data binding is activated with a
call to:
wwWebDataHelper.FormBindData(this)
That’s it. If you use wwWebForm then you can also call
this.DataBind() to accomplish the same thing but also cause any standard data
binding to occur.
The process to get the data back out is not any more
complicated and shown in btnSubmit_Click. Here the business object is loaded up
with the sku again (this time retrieved from Viewstate as an extra check to
make sure an item is actually selected). Once the item is loaded there is again
an Inventory.DataSet and Inventory.DataRow object in place which matches what
the form controls are bound to. Now a call is made to:
wwWebDataHelper.FormUnbindData(this);
And the data is bound back into the underlying DataSet.
After that a simple call to the business object’s Save() method causes the data
to actually be written to the database.
Couldn’t be easier, right? There’s very little UI code in
this block, and because of the business object there’s no SQL code splattered
over this WebForm code either. And that really is the concept behind this
process – you don’t ever have to write bind back code manually again as this
takes care of it automatically. Not only that, this process also handled the
data conversions and error handling (which I’ll describe shortly).
The combination of using even this simple business object
(the source code for this sample busSimpleBusObj class is provided with the
samples) and this data binding mechanism reduce the amount of code that has to
happen for form management logic drastically.
How it works
To make this simplified data binding code happen requires a
little work, and the main concept behind this is subclassing and then
delegation to worker classes that do the dirty work. The hardest part to using
this stuff most likely will be to remember to use these subclassed controls
rather than the built in ones.
To see how this works let’s start by looking at the
wwWebTextBox class and how it subclasses the standard Web TextBox. The code for
this is shown in Listing 2. Note that there is a little bit of code omitted
here that deals with a few non-related issues such as password value
assignments and rendering error messages. What you see in Figure 2 is the core
code needed to implement a two-way data binding control (you can review the
samples code for the full source).
[ToolboxBitmap(typeof(TextBox)),
DefaultProperty("Text"),
ToolboxData("<{0}:wwWebTextBox
runat='server' size='20'></{0}:wwWebTextBox>")]
public class wwWebTextBox :
WebControls.TextBox,IwwWebDataControl
{
[Category("Data"),
Description("The
object that the control is bound to (DsCustomers or Customer.DataRow or
Customer)"),
DefaultValue("")]
public string BindingSourceObject
{
get { return this.cBindingSourceObject;
}
set { this.cBindingSourceObject = value;
}
}
string
cBindingSourceObject = "";
[Category("Data"),
Description("The
property of the object that the control is bound to ("),
DefaultValue("")]
public string BindingSourceProperty
{
get { return this.cBindingSourceProperty;
}
set { this.cBindingSourceProperty = value; }
}
string
cBindingSourceProperty = "";
[Category("Data"),
DefaultValue("Text")]
public string BindingProperty
{
get { return this.cBindingProperty;
}
set { this.cBindingProperty = value;
}
}
string cBindingProperty = "Text";
/// <summary>
/// Error message set when a data unbinding error occurs.
/// Optional - Default message: 'Invalid Data Format for 'control name'
/// </summary>
[Category("Data"),
DefaultValue("")]
public string BindingErrorMessage
{
get { return this.cBindingErrorMessage;
}
set { this.cBindingErrorMessage = value;
}
}
string
cBindingErrorMessage;
/// <summary>
/// the format string used to format this field when binding
/// </summary>
[Category("Data"),
Description("The
format string used to format this field when binding"),
DefaultValue("")]
public string DisplayFormat
{
get { return this.cDisplayFormat;
}
set { this.cDisplayFormat = value;
}
}
string cDisplayFormat = "";
/// <summary>
/// The format string used to format this field when binding.
/// Defaults to fieldname with
txtStripped.
/// </summary>
[Category("Data"),
Description("The
format string used to format this field when binding."),
DefaultValue("")]
public string UserFieldName
{
get
{
if (this.cUserFieldName == String.Empty)
this.UserFieldName
= this.ID.Replace("txt","");
return this.cUserFieldName;
}
set {this.cUserFieldName = value;}
}
string cUserFieldName = "";
public void BindData(Page WebForm)
{
wwWebDataHelper.ControlBindData(WebForm,this);
}
public void UnbindData(Page WebForm)
{
wwWebDataHelper.ControlUnbindData(WebForm,this);
}
}
The key here is the implementation of the properties and
methods of the IwwWebDataControl interface which is defined as follows:
|
IwwWebDataControl Property
|
Description
|
|
BindingSourceObject
|
The object that the control is bound to. This will be a
DataSet, DataRow, DataTable/View or it could be a custom object on the form.
Syntax can use . syntax like: Customer.DataRow.
|
|
BindingSourceProperty
|
This is the property or field that the data is bound to.
|
|
BindingProperty
|
This is the property of the control that the binding
occurs against.
|
|
DisplayFormat
|
A format string that is compatible with String.Format()
for the specified type. Example: {0c} for currency or {0:f2} for fixed 2
decimals
|
|
UserFieldName
|
Descriptive name of the field. Used if an error occurs to
provide an error message.
|
|
BindingErrorMessage
|
Internally used value that gets set if a unbinding error
occurs. Controls that have this set can optionally generate error information
next to them.
|
|
IwwWebDataControl Method
|
Description
|
|
BindData()
|
Binds data to the control from the BindingSource
|
|
UnbindData()
|
Unbinds data back into the BindingSource
|
If you look at the code for wwWebTextBox you’ll see that
there really is nothing there except forwarding calls to wwWebDataHelper, which
actually performs all the hard work of doing the .
wwWebDataHelper is a class with all static members. The
class works essentially by using Reflection to evaluate the value in the data
source and in the control and then assigning the value into one or the other
depending on whether you are binding or unbinding. To help with the Reflection
tasks there’s another helper class – wwUtils – which includes wrapper methods
that do things like GetProperty, GetPropertyEx, SetProperty and SetPropertyEx.
These methods use the PropertyInfo (or FieldInfo) classes to retrieve the
values. The Ex versions provide a little more flexibility by allowing you to
walk an object hierarchy and by retrieving and setting value further down the
object chain. For example you can do:
wwUtils.SetProperty(this,"Customer.Address.Street","32
Kaiea")
which is lot more friendly than the 3 long Reflection calls
you’d have manually write to get there. Let’s start with Control binding and
unbinding which is shown in Listing 3.
public static void
ControlBindData(Page WebPage,
IwwWebDataControl ActiveControl) {
string
BindingSourceObject = ActiveControl.BindingSourceObject;
string
BindingSourceProperty = ActiveControl.BindingSourceProperty;
string BindingProperty
= ActiveControl.BindingProperty;
try
{
if
(BindingSourceObject == null ||
BindingSourceObject.Length == 0 ||
BindingSourceProperty == null || BindingSourceProperty.Length == 0)
return;
// *** Get a reference
to the actual control source object
object
loBindingSource = null;
loBindingSource = wwUtils.GetPropertyEx(WebPage,BindingSourceObject);
if
(loBindingSource == null)
return;
// *** Retrieve the
control source value
object loValue;
if
(loBindingSource is System.Data.DataSet)
{
string
lcTable = BindingSourceProperty.Substring(0,
BindingSourceProperty.IndexOf("."));
string
lcColumn = BindingSourceProperty.Substring(
BindingSourceProperty.IndexOf(".")+1);
DataSet Ds = (DataSet) loBindingSource;
loValue =
Ds.Tables[lcTable].Rows[0][lcColumn];
}
else if(loBindingSource is
System.Data.DataRow)
{
DataRow Dr = (DataRow) loBindingSource;
loValue = Dr[BindingSourceProperty];
}
… DataTable, DataView omitted
else // we have a
property
loValue =
wwUtils.GetPropertyEx(loBindingSource,
BindingSourceProperty);
/// *** Figure out the type of the control we're binding to
object
loBindValue = wwUtils.GetProperty(ActiveControl,
BindingProperty);
string
lcBindingSourceType = loBindValue.GetType().Name;
if (loValue == null || loValue == DBNull.Value)
if
(lcBindingSourceType == "String")
wwUtils.SetProperty(ActiveControl,BindingProperty,"");
else if (lcBindingSourceType == "Boolean")
wwUtils.SetProperty(ActiveControl,BindingProperty,false);
else
wwUtils.SetProperty(ActiveControl,BindingProperty,"");
else
{
if
(lcBindingSourceType == "Boolean")
wwUtils.SetProperty(ActiveControl,BindingProperty,loValue);
else
{
if
(wwUtils.Empty(ActiveControl.DisplayFormat))
wwUtils.SetProperty(ActiveControl,BindingProperty,
loValue.ToString());
else
wwUtils.SetProperty(ActiveControl,BindingProperty,
String.Format(ActiveControl.DisplayFormat,
loValue));
}
}
}
catch(Exception ex)
{
string
lcException = ex.Message;
throw(new Exception("Can't
bind " + ((Control) ActiveControl).ID );
}
}
The code starts by retrieving the BindingSourceObject and
tries to get a reference to the object. If that works it retrieves the property
string. At this point a check is performed on what type of object is being
bound against, which determines where the data comes from. If it’s a DataSet –
use the field of the first row of the table specified in the property string.
If it’s DataRow use the field. If it’s an object use Reflection to retrieve the
actual value.
Once we have a value we can then try and assign that value
to the property specified in the BindingProperty. But before we can do that a
few checks need to be made for the type of the property as well as checks for
null values which would crash the controls if bound to. Yup this code actually
automatically handles nulls by assigning empty values to display. The
assignment of the value is done using Reflection again by using SetProperty().
Note that if a format string is provided the format is applied to the string as
it’s written out.
The process of Unbinding a control is very similar – the
same process in reverse as shown in Listing 4.
public static void
ControlUnbindData(Page WebPage,
IwwWebDataControl
ActiveControl) {
string BindingSourceObject
= ActiveControl.BindingSourceObject;
string
BindingSourceProperty = ActiveControl.BindingSourceProperty;
string BindingProperty =
ActiveControl.BindingProperty;
if (BindingSourceObject ==
null || BindingSourceObject.Length == 0 ||
BindingSourceProperty == null
|| BindingSourceProperty.Length == 0)
return;
object loBindingSource = null;
if (BindingSourceObject ==
"this" ||
BindingSourceObject.ToLower() == "me")
loBindingSource = WebPage;
else
loBindingSource =
wwUtils.GetPropertyEx(WebPage,BindingSourceObject);
if (loBindingSource == null)
throw(new Exception("Invalid
BindingSource"));
// Retrieve the new value from
the control
object loValue =
wwUtils.GetPropertyEx(ActiveControl,BindingProperty);
// Try to retrieve the type of
the BindingSourceProperty
string
lcBindingSourceType;
string lcDataColumn = null;
string lcDataTable = null;
// *** figure out the type of
the binding source by reading the value
if (loBindingSource is System.Data.DataSet) {
// *** Split out the
datatable and column names
int lnAt =
BindingSourceProperty.IndexOf(".");
lcDataTable = BindingSourceProperty.Substring(0,lnAt);
lcDataColumn =
BindingSourceProperty.Substring(lnAt+1);
DataSet Ds = (DataSet) loBindingSource;
lcBindingSourceType =
Ds.Tables[lcDataTable].Columns[lcDataColumn].DataType.Name;
}
else if(loBindingSource is
System.Data.DataRow) {
DataRow Dr = (DataRow) loBindingSource;
lcBindingSourceType = Dr.Table.Columns[BindingSourceProperty].DataType.Name;
}
else if (loBindingSource is
System.Data.DataTable) {
DataTable dt = (DataTable) loBindingSource;
lcBindingSourceType =
dt.Columns[BindingSourceProperty].DataType.Name;
}
else {
// *** It's an object property
or field - get it
MemberInfo[] loInfo =
loBindingSource.GetType().GetMember(BindingSourceProperty,
wwUtils.MemberAccess);
if
(loInfo[0].MemberType == MemberTypes.Field) {
FieldInfo loField = (FieldInfo) loInfo[0];
lcBindingSourceType = loField.FieldType.Name;
}
else {
PropertyInfo loField = (PropertyInfo) loInfo[0];
lcBindingSourceType = loField.PropertyType.Name;
}
}
// *** Convert the control value
to the proper type
object loAssignedValue;
if ( lcBindingSourceType
== "String")
loAssignedValue = loValue;
else if (lcBindingSourceType == "Int16")
loAssignedValue = Int16.Parse( (string) loValue, NumberStyles.Integer );
else if
(lcBindingSourceType == "Int32")
loAssignedValue = Int32.Parse( (string) loValue, NumberStyles.Integer );
else if
(lcBindingSourceType == "Int64")
loAssignedValue = Int32.Parse ( (string) loValue, NumberStyles.Integer)
else if (lcBindingSourceType == "Byte")
loAssignedValue = Convert.ToByte(loValue);
else if (lcBindingSourceType == "Decimal")
loAssignedValue = Decimal.Parse( (string) loValue,NumberStyles.Any);
else if (lcBindingSourceType == "Double")
loAssignedValue = Double.Parse( (string) loValue,NumberStyles.Any);
else if (lcBindingSourceType == "Boolean") {
loAssignedValue = loValue;
else if (lcBindingSourceType == "DateTime")
loAssignedValue = Convert.ToDateTime(loValue);
else // Not HANDLED!!!
throw(new Exception("Field
Type not Handled by Data unbinding"));
/// Write the value back to the underlying object/data item
if (loBindingSource is System.Data.DataSet) {
DataSet Ds = (DataSet) loBindingSource;
Ds.Tables[lcDataTable].Rows[0][lcDataColumn] =
loAssignedValue;
}
else if(loBindingSource is
System.Data.DataRow) {
DataRow Dr = (DataRow) loBindingSource;
Dr[BindingSourceProperty] = loAssignedValue;
}
else if(loBindingSource is
System.Data.DataTable) {
DataTable dt = (DataTable) loBindingSource;
dt.Rows[0][BindingSourceProperty] = loAssignedValue;
}
else if(loBindingSource is
System.Data.DataView) {
DataView dv = (DataView) loBindingSource;
dv[0][BindingSourceProperty] = loAssignedValue;
}
else
wwUtils.SetPropertyEx(loBindingSource,BindingSourceProperty,loAssignedValue);
}
This code starts by retrieving the Control Source object and
the value contained in the control held by the BindingProperty field. This is
most likely the Text field, but could be anything the user specified, such as
Checked for a CheckBox or SelectedValue for a ListBox or DropDownList. The
ControlSource is also queried for its type by retrieving the current value. The
type is needed so we can properly convert the type back into the type that the
control source expects. This involves String to type conversion including the proper
type parsing so you can use things like currency symbols for decimal values
etc. The Parse method is quite powerful for this sort of stuff. Finally once
the value has been converted Reflection is used one more time to set the value
into the binding source field based on the type of object we’re dealing with.
DataSets,Tables and Rows write to the Field collection, while objects and
properties are written natively to the appropriate member.
These two methods are the core of the binding operations and
they are fully self contained to bind back controls. This process lets us bind
individual controls. These methods are then called by each control’s BindData()
and UnbindData() methods respectively as shown in Listing 2.
The next thing we need to do is bind all the controls on a
form so we don’t have to individually bind them. This is pretty easy in
concept. We know all of our controls implement the IwwWebDataControl interface,
so it’s fairly easy to walk the Web form’s Controls collection (and child
collections) and look for any controls that implement the IwwWebDataControl
interface and then call the BindData() method. Listings 5 and 6 show the
FormBindData() and FormUnbindData() methods that do just that.
static void FormBindData(Control Container, Page WebForm) {
// *** Drill through each
control on the form
foreach( Control
loControl in Container.Controls) {
// ** Recursively call
down into any containers
if
(loControl.Controls.Count > 0)
wwWebDataHelper.FormBindData(loControl,
WebForm);
// ** only work on
those that support interface
if (loControl is IwwWebDataControl ) {
IwwWebDataControl control =
(IwwWebDataControl) loControl;
try {
//*** Call
the BindData method on the control
control.GetType().GetMethod("BindData",
wwUtils.MemberAccess).Invoke(control,
new object[1] { WebForm } );
}
catch(Exception)
{
// ***
Display Error info
try {
control.Text = "** Field binding Error **";
}
catch(Exception)
{;}
}
}
}
}
As you can see FormBindData() runs through the controls
collection and checks for the IwwWebControl interface. Note that this method is
recursive and calls itself if it finds a container and drills into them. This
makes sure the entire form databinds. When a control is found the BindData()
method of the control is called dynamically using Reflection.
When an error occurs the Text of the control is set to Field
binding error so you can immediately see the error without throwing an
exception on the page. This is handy as you don’t get errors individually. This
is likely to be a developer error – not a runtime error so this handling is
actually preferable.
The unbinding works in a similar fashion as shown in Figure
6.
public static BindingError[] FormUnbindData(Page WebForm)
{
BindingError[] Errors = null;
FormUnbindData(WebForm,WebForm,ref Errors);
return Errors;
}
static BindingError[]
FormUnbindData(Control Container, Page WebForm,
ref BindingError[] Errors) {
// *** Drill through each of
the controls
foreach( Control
loControl in Container.Controls) {
// ** Recursively call
down into containers
if
(loControl.Controls.Count > 0)
FormUnbindData(loControl, WebForm,ref Errors);
if (loControl is IwwWebDataControl ) {
IwwWebDataControl control =
(IwwWebDataControl) loControl;
try {
// ***
Call the UnbindData method on the control
control.GetType().GetMethod("UnBindData",
wwUtils.MemberAccess).Invoke(control,
new
object[1] { WebForm } );
}
catch(Exception
ex) {
// ***
Display Error info
try
{
BindingError loError = new BindingError();
control.BindingErrorMessage =
loError.Message;
// … more error handling code
here
if
(Errors == null) {
Errors = new BindingError[1];
Errors[0] = loError;
}
else {
//
*** Resize the array and assign Error
int
lnSize = Errors.GetLength(0);
Array loTemp =
Array.CreateInstance(typeof(BindingError),
lnSize + 1);
Errors.CopyTo(loTemp,0);
loTemp.SetValue(loError,lnSize);
Errors =
(BindingError[]) loTemp;
}
}
catch(Exception)
{;} // ignore additional exceptions
}
}
}
return Errors;
}
This code is very similar to the FormBindData() method. The
difference here is that we call the UnbindData method and that we deal with
errors on unbinding differently. It’s much more likely that something goes
wrong with binding back then binding as users can enter just about anything
into a textbox like characters for numeric data or non data formats for date
fields. This scenario throws an exception in the control’s bindback code which
has handled here.
Error Display
This method creates an array of BindingError objects which
contains information about the error. You can configure custom binding error
messages by setting a binding error message on the control (see Figure 4).
Otherwise the following code assigns a generic error message to the property
with this code (omitted in Figure 6):
BindingError loError = new
BindingError();
if
(wwUtils.Empty(control.BindingErrorMessage))
{
if (
control.UserFieldName != "")
loError.Message = "Invalid
format for " + control.UserFieldName;
else
loError.Message = "Invalid
format for " + loControl.ID.Replace("txt","");
}
else
loError.Message = control.BindingErrorMessage;
// *** Assign the error message
to the control
// *** this will cause the
control to render it
control.BindingErrorMessage = loError.Message;
loError.ErrorMsg = ex.Message;
loError.Source = ex.Source;
loError.StackTrace = ex.StackTrace;
loError.ObjectName = loControl.ID;
if (Errors == null)
{
Errors = new
BindingError[1];
Errors[0] = loError;
}
else
{
// *** Resize the array and
assign Error
int lnSize =
Errors.GetLength(0);
Array loTemp = Array.CreateInstance(typeof(BindingError),lnSize + 1);
Errors.CopyTo(loTemp,0);
loTemp.SetValue(loError,lnSize);
Errors = (BindingError[]) loTemp;
}
This array of binding errors if any is returned from the
Unbind operation. A couple of helper methods exist to turn the array into HTML.
The code for the Inventory example we saw earlier then looks something like
this:
…
BindingError[] Errors = wwWebDataHelper.FormUnbindData(this);
if (Errors != null)
{
this.ShowErrorMessage(
wwWebDataHelper.BindingErrorsToHtml(Errors) );
return;
}
if (!Inventory.Save())
…
In addition each of the control contains some custom code to
display error information as shown in Figure 5.

Figure 5 –
Binding errors can be automatically flagged and converted into an HTML display
(top).
The code that accomplishes that has a few dependencies that
I’ve not had time to abstract away at this point so some of this is hardcoded
into the control:
protected override void
Render(HtmlTextWriter writer)
{
// *** Write out the existing control code
base.Render (writer);
// *** now append an error icon and ‘tooltip’
if (this.BindingErrorMessage != null
&& this.BindingErrorMessage != "" )
writer.Write("
<img src='images/warning.gif' alt='" +
this.BindingErrorMessage
+ "')'>");
}
As you can see it’s quite easy to add additional output to
controls. This extensibility model is just very flexible and easy to work with.
A few more odds and ends
While in the process of subclassing and dealing with data
binding it’s also useful to address some things that just don’t quite seem
to work right in ASP.Net. For example, listboxes do not persist their
SelectedValue unless you use ViewState, which is very annoying if you don’t
want to ship the content of your lists over the wire each time. This is
actually quite easy to fix with
override protected void
OnLoad(EventArgs e)
{
base.OnLoad(e);
/// *** Handle auto-assigning of SelectedValue
/// *** so we don't need Viewstate to make this happen
if (!this.EnableViewState && this.Page.IsPostBack)
{
string lcValue = this.Page.Request.Form[this.ID];
if (lcValue != null)
this.SelectedValue
= lcValue;
}
}
Voila, you no longer need Viewstate to postback the selected
value.
Another problem I ran into on several admin forms is that
Passwords in text boxes are not posted back to forms. This is possibly not a
bad idea, but a problem when you really need to post a password back for admin
purposes and you don’t want to have people keep retyping the password each
time.
override protected void
OnLoad(EventArgs e)
{
base.OnLoad(e);
// *** Post back password
values as well - you can always clear manually
if (this.TextMode == TextBoxMode.Password)
this.Attributes.Add("value", this.Text);
}
A few limitations
Ok, all of this stuff probably sounds pretty good to you
right about now. But be aware that there are a few limitations to what I’ve
shown you so far.
- Binding
doesn’t work against indexed objects or properties
You can’t bind against collections or arrays or any member that
resolves through collections or arrays. For example, you can bind to a
DataRow if you have a simple property that points at this DataRow (such as
the Customer.DataRow in my examples), but you cannot bind to it with
Customer.DataSet.Tables["wws_Item"].Rows[0]. All resolving will
fail if an enumerated type is encountered. This can be fixed with some
changes to the Reflection wrappers, but I haven’t time to look into this.
Although this seems like a big deal you can always work around this by
using wrapper properties either on your form or your objects. If you look
at the sample code I expose an InvTable property on the form to bind
against the Table for example. The code simply sets this property when the
table is loaded.
- Binding to
Private members is not possible
Because all binding occurs inside of an external class Private members
are not accessible to Reflection. This means any objects you bind to must
be protected or public.
- Subclassed
controls don’t work well with child templates
If you subclass controls like the ListBox or DropDownList and manually
assign values in the HTML template, you’ll find that because of the type
prefix for the control standard template expressions don’t show
Intellisense. So although you can continue to use <asp:ListItem>
from within <ww:wwWebDropDownList> you will not get Intellisense. On
the other hand if you do a lot of stuff with templates manually you
probably don’t need data binding anyway – in that case just use the stock
controls.
None of these are show stoppers, but they are things you
should be aware of before you take off on this path.
Summing up
Although it’s such a downer that ASP.Net doesn’t include
better data binding support natively, it also say a lot for the architecture
that you can extend controls easily enough to provide this functionality with a
relatively little amount of code. I suspect most serious developers end up subclassing the stock controls anyway and so adding this stuff in is only a
small step anyway.
There’s a lot more that can be done with the basic
extensions I’ve built here. For example it’d be real nice to build better input
formatting into this stuff, providing things like InputMasks that could be
handled client side. ASP.Net provides Validation controls, but again the design
is generally more work than it needs to be. A single validation property would
be very cool. In any case there are many extensions that would be useful, but I
hope you find this base useful and something you can extend. If you end up
enhancing this stuff please send me a line so I can check it out.
Next time around I’ll take a look at Windows Forms and how
we can build simple data binding controls in much the same way as we did here to
simplify behind in rich client applications.
As always you can reach me via email at
rstrahl@west-wind.com or even better on
our Message Board at
http://www.west-wind.com/wwThreads/Default.asp?Forum=Code+Magazine.
Source Code for this article:
http://www.west-wind.com/presentations/ASPNET/ASPNET.zip
|