ASP.NET has considerably raised the bar for Web development with very rich developer functionality built into a flexible and highly extensible object model.
If you have a background of hand-coding ASP or other scripting or CGI-style technology, .NET's redundant code reduction and development process simplification seems almost too good to be true. But data binding for controls leaves a lot to be desired in terms of ease-of-use and reading the data back into the data source. This article examines what's wrong with simple data binding and provides a set of subclasses, making data binding a lot quicker and requiring much less manual code.
Data binding is a task that most developers deal with on a daily basis. Most applications are data-centric and whenever you create UI code that relates to the data, you'll find that, using the default mechanisms of the .NET Framework, 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. This article describes briefly how data binding in ASP.NET works, and then offers a solution using subclassed controls to handle the repetitive tasks. A future article will discuss the same issues in Windows Forms.
You can bind data to controls, but there's no automatic way to unbind it back to the data source.
If you're coming from an ASP or other Web development background, you're probably thinking, "What are you talking about?" Data binding in ASP.NET is a huge improvement over whatever you had to do previously in Web Forms. After all, there are forms of data binding and various forms of state management in .NET that automatically assign control values back to the controls, so you're free of manually populating fields with data. That's a big improvement, for sure.
What's Wrong with Data Binding in ASP.NET?
I think data binding in ASP.NET doesn't go nearly far enough. For one thing, the process of assigning data sources is cumbersome, using either a slow and work-intensive designer or by having to embed yet another script-based markup tag (<%# %>) into source code. Both methods 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 goes only one way. You can bind data to controls but there's no mechanism to unbind the data from the control back into its underlying data source. It's hard to really call ASP.NET's mechanism data binding because it is little more than a data display mechanism.
To clarify though, there are two types of data binding in ASP.NET. First, there's the list-based type you use to bind data to a ListBox or a DataGrid. This mechanism actually works very well and provides a good amount of flexibility. It 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 binds a single value to a property of a control (such as text box binding to a field of the database). This is also the most common data binding to do during data entry, and the one that is the most time-consuming. Here is where the problem lies: the Control binding is one-way and involves some convoluted syntax that isn't even property-based.
That data binding is one-way in ASP.NET is not all that surprising. It's not easy to automatically bind back in Web applications because it's difficult to tell exactly when data should be bound back to the underlying data source in this stateless environment. 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 know automatically when a data source 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 a 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. I use my business object to load up a DataSet with data from a Load() method that internally populates a DataSet and DataRow member (you could also do this manually, 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 code binds to the 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 requires the ASP.NET script parser to parse the string first before ever assigning the data binding expression to be evaluated.
Once bound to the data with this mechanism, I can display data in my Web Form. Then I want to edit the data in the various input controls, and ASP.NET provides a perfect state for doing the control-based editing and posting. If an error occurs, I can display an error message or use an error provider to display the message without losing my data. So far, so good.
The next step is to save the data. When I click the Save button, code is fired in the Web Form to save the data from the Web Form into my data source, 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.
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 needs 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-duty data entry form and if there are many of these forms, it's 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?and you have to keep track of these in two separate places. It gets worse when you need to bind back non-string data, as you have to do the type-coercion and error-handling that goes along with that:
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 don't want to do every time I bind back a form. There has to be a better way.
Subclassing the Web Controls for Better Data Binding
As you can see, there are a number of shortcomings in the data binding process. The process is quite repetitive and, if you can delegate some of this functionality into the control itself or some helper class, you could make life a whole lot easier.
Subclassing provides the power to inherit all existing functionality and extend it with flexible additional functionality.
My solution to this problem is to subclass the various Web Form controls and add data binding functionality to them natively. The classes I'll describe next provide the following functionality:
- Simple property-based control binding: Instead of the embedded script code that performs binding, I add three properties to each control: BindingSource, BindingSourceProperty, and BindingProperty. The control source is an object on the Web Form; it could be a plain object or a DataSet, DataRow, DataTable, or DataView.
- 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.
- 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 calling back to a worker class that actually performs the data binding, unbinding for both the controls individually and for the form as a whole.
The key classes involved in this solution are:
- IwwWebDataControl: This is the data binding interface that each of the controls must implement. It includes the BindingSourceObject, BindingSourceProperty, and BindingProperty.
- wwWeb<Control> subclasses: All of the controls that are to be data bound are subclassed from the standard control classes and implement the IwwWebDataControl interface. In the project, TextBox, CheckBox, RadioButtonGroup, Listbox, and DropDownList are subclassed because these are the most common ones with which to do two-way data binding.
- 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 receive parameters of the Web page object and the controls they are binding.
Figure 2 shows the relationship between these classes. The concept is straightforward: 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. And it serves as an identifier for the controls you 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 1 shows a full implementation of the wwWebTextBox class. The wwWebDataHelper class does all the logic for data binding rather than using the control methods, so you can reuse the same code for binding. All the controls, including list controls like the ListBox and DropDownList, can use the same mechanism for binding.
You can use the wwWebForm class, which implements BindData() and UnbindData() and overrides DataBind(), to call BindData() automatically. Using this class is optional, as you can directly call the wwDataHelper.FormBindData() or wwDataHelper.FormUnbindData() methods.
The process to use these controls 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 Data, Objects, or Properties.
- Call the appropriate form-level binding methods.
Figure 3 shows a data entry form displaying Inventory data and allowing editing. The form contains data retrieved from a SQL Server database using a business object that loads data into an internally exposed DataSet and DataRow member. These are then bound to the data.
Adding Controls to the Form
The first step is to add the wwWebDataControls.dll to the Toolbar by following these steps:
- Select the Toolbox.
- Right-click and select Add New Tab.
- Type the name for this tab (e.g., 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 data binding/wwWebDataControls/bin/Debug/wwWebDataControls.dll. 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. Here's the script code for the header:
<%@ Register TagPrefix="ww"
Namespace="Westwind.Web.Data"
Assembly="wwWebDataControls"%>
And 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, you won't have to do any of this manually. What's nice about this form is that other than the property assignments that are made in the property sheet (Figure 4), there's no code involved in the data binding. It's quick and easy to create the data bindings in this fashion.
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 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.
You can also bind to objects or properties of the form. You can set the BindingSourceObject to this 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 this:
BindingSourceObject: Customer.Address
BindingSourceProperty: Street
Notice that you can step down the object hierarchy that implicitly starts at the form level (making it this.Customer.Address). Stepping down 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 objects or wizards, etc.).
You can also specify a format flag. If you look at Figure 3 again, you'll see that the Price is displayed with the $ in front of it. This field uses 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 bind back, the field is flagged.
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 2 shows the key elements of the Inventory Form.
Page_Load makes a traditional list based on the drop-down list against a DataTable. This mechanism uses the built-in data binding that is automatically inherited and works as you would expect. 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 that the Inventory.DataSet and Inventory.DataRow are set, which is the target of our data binding.
Binding and unbinding form data should take no more than a single line of code.
When the data has loaded, the data binding is activated with a call to:
wwWebDataHelper.FormBindData(this)
That's it. If you use wwWebForm, you can also call this.DataBind() to accomplish the same thing and cause any standard data binding to occur as well.
The process to get the data back out is not any more complicated and is shown in btnSubmit_Click. Here the business object is loaded up with the SKU again (retrieved from ViewState as an extra check to make sure an item is actually selected). Once the item is loaded, there are Inventory.DataSet and Inventory.DataRow objects in place that match what the form controls are bound to. Now a call is made to:
wwWebDataHelper.FormUnbindData(this);
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 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 Web Form code either. That really is the concept behind this process: you don't ever have to write bind back code manually again. Not only that, this process also handled the data conversions and error handling (which I'll describe shortly).
Using this simple business object (the source code for this busSimpleBusObj class is provided with the samples) and this data binding mechanism together drastically reduces the amount of code that has to happen for form management logic.
How It Works
To make this simplified data binding code happen requires a little work. The main concept behind it is subclassing and then delegation to worker classes that do the dirty work. The hardest part to using this stuff is to remember to use these subclassed controls rather than the built-in ones.
To see how this simplified data binding code works, let's look at the wwWebTextBox class and see how it subclasses the standard Web TextBox. The code is shown in Listing 1. (There is a little bit of code omitted in Listing 1 that deals with a few unrelated issues, such as password value assignments and rendering error messages. What you see in Listing 1 is the core code needed to implement a two-way data binding control. You can review the sample code for the complete source code).
The key here is the implementation of the properties and methods of the IwwWebDataControl interface, as defined in Table 1.
If you look at the code for wwWebTextBox, you'll see that there really is nothing there except forwarding calls to wwWebDataHelper, which actually does the data binding.
The class wwWebDataHelper has all static members. It works by using reflection to evaluate the value in the data source and the control and then by assigning the value to 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 to 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 navigate through an object hierarchy and by retrieving and setting values further down the object chain. For example, you can use:
wwUtils.SetProperty(this,
"Customer.Address.Street",
"32 Kaiea")
This is a lot more friendly than the three longer reflection calls you'd have to make to get there.
Let's start with Control binding and unbinding (see Listing 3).
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, determining 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 you have a value, you can assign that value to the property specified in the BindingProperty. But before you can do that, a few checks need to be made for the type of the property and checks for null values that can crash the controls if bound to. Yes, this code actually handles nulls by assigning empty values to display automatically. The assignment of the value is done by reflection using SetProperty(). If a format string is provided, the format is applied to the string as it's written out.
Setting properties in the Property Sheet is much quicker than using a Builder or writing script expressions inside of ASP.NET HTML.
The process of unbinding a control is very similar; it's the same process in reverse, as shown in Listing 4.
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 specifies, such as checked for a CheckBox, or SelectedValue for a ListBox or DropDownList. The BindingSource is also queried for its type by retrieving the current value. The type is needed so you can properly convert it back into the type that the control source expects. This involves the string to type conversion, including the proper type parsing, so you can use things like currency symbols for decimal values, and so on. The Parse method is quite powerful for this sort of stuff. Once the value has been converted, reflection is used to set the value into the binding source field based on the type of object you're dealing with. DataSets, Tables, and Rows write to the Field collection, and objects and properties are written natively to the appropriate member.
These two methods are the core of the binding operations and are fully self-contained to bind back controls. This process lets you bind individual controls and the methods are then called by each control's BindData() and UnbindData() methods respectively, as shown in Listing 1.
The next thing you need to do is bind all the controls on a form so you don't have to bind them individually. This is an easy concept. You know that all of your controls implement the IwwWebDataControl interface. So it's fairly easy to navigate through 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 Listing 6 show the FormBindData() and FormUnbindData() methods that do just that.
As you can see, FormBindData() runs through the controls collection and checks for the IwwWebControl interface. This method is recursive and calls itself if it finds a container and drills into it. It makes sure that the entire form data binds. 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 a 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. The error is most likely to be a developer error, not a runtime error, so this handling is actually preferable.
Unbinding works in a similar fashion as that shown in Listing 6.
The code in Listing 6 is very similar to the FormBindData() method. The difference here is that you call the UnbindData method and that you deal with errors on unbinding differently. It's much more likely that something will go wrong with binding back then with binding, as users can enter just about anything into a text box, like characters instead of numeric data or non-date formats in date fields. These user errors throw an exception in the control's bind back code, and are handled there.
Error Display
This next method creates an array of BindingError objects containing 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 code in Listing 7 assigns a generic error message to the property.
Reflection makes it possible to dynamically read and assign property values that were assigned at design time. You can think of it as a simple evaluation mechanism.
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 you saw earlier 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 controls contains some custom code to display error information, as shown in Figure 5.
I've not had time to design these error display code controls so that they are completely abstract, and there are still hard-coded dependencies:
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 very flexible and easy to work with.
A Few More Odds and Ends
During 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 this bit of code:
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;
}
}
You no longer need ViewState to post back the selected value.
Another problem I ran into on several administrative forms is that Passwords in text boxes are not posted back to forms. This is possibly not a bad idea, but can be a problem when you really need to post a password back for administrative purposes and you don't want users to retype the password each time. Try this:
override protected void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Post back password values as well
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 there isn't room to cover that here. 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 an external class, private members are not accessible for 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 showstoppers, but they are things you should be aware of before you take this path.
Summing Up
Although it's disappointing that ASP.NET doesn't include better data binding support natively, it also says a lot for the architecture that you can extend controls easily enough to provide this functionality with relatively little code. Most serious developers will end up subclassing the stock controls anyway, and adding this stuff in is only a small additional step.
You can do a lot more with the basic extensions I've built here. For example, I think you could build better input formatting into this stuff, providing things like InputMasks that you could handle client-side. ASP.NET provides Validation controls, but again, the design is generally more work than it needs to be. I'd like a single Validation property. In any case, there are many other extensions that would be useful, but I hope you use this base and extend it. If you end up enhancing this stuff, please drop me a line so I can check it out.
You can reach me via email at rstrahl@west-wind.com or even better, on the 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/ASPNETDataBinding/ASPNETDataBinding.zip