devxlogo

Implementing Two-Way Control Binding for Web Forms

Implementing Two-Way Control Binding for Web Forms

SP.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.

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.

You can bind data to controls, but there’s no automatic way to unbind it back to the data source.

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 DataSetwith data from a Load() method that internally populates a DataSetand DataRowmember (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:

       

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.

Subclass 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 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.

Adding Controls to the Form
The first step is to add the wwWebDataControls.dll to the Toolbar by following these steps:

  1. Select the Toolbox.
  2. Right-click and select Add New Tab.
  3. Type the name for this tab (e.g., West Wind Web Controls).
  4. Select the new tab.
  5. Right-click again and select Add/Remove Items.
  6. Browse for the installation directory for the samples and select the data binding/wwWebDataControls/bin/Debug/wwWebDataControls.dll.
  7. 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.
  8. 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:

   

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.

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 IwwWebDataControlinterface, as defined in Table 1.

Table 1: Definitions for the control properties and methods of the IwwWebDataControl interface.

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. It can use syntax like Customer.DataRow.

BindingSourceProperty

The property or field that the data is bound to.

BindingProperty

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 two 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 an 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 does the data binding.

The class wwWebDataHelperhas 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 BindingSourceObjectand 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 ListBoxor 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 Rowswrite to the Fieldcollection, 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 IwwWebDataControlinterface. 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 IwwWebDataControlinterface and then call the BindData()method. Listing 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 IwwWebControlinterface. 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 UnbindDatamethod 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 BindingErrorobjects 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.

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 from within , 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.

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 InputMasksthat 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.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist

©2024 Copyright DevX - All Rights Reserved. Registration or use of this site constitutes acceptance of our Terms of Service and Privacy Policy.