White Papers                  Home |  White Papers  |  Message Board |  Search |  Products |  Purchase | News | Web Log | 
 
 

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

 

If you find this article useful, consider making a small donation to show your support  for this Web site and its content.

 

 

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:

 

  1. Create a form and optionally subclass it from wwWebDataForm
  2. Add the wwWebDataControls.dll to your Toolbox in VS.Net
  3. Use the controls on your form
  4. Set the Binding properties to bind to data, objects or properties
  5. 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:

 

  1. Select the toolbox.
  2. Right click and select Add New Tab
  3. Type the name for this tab (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 /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.
  7. 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.

 

Listing 1 – The Inventory Form’s key action methods

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

 

Listing 2 – TextBox implementation of the IwwWebDataControl interface

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

 

Listing 3 – Binding a control with data from a datasource

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.

Listing 4 – Unbinding data from the control back into the data source.

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.

 

Listing 5 – Binding all controls on a form

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.

 

Listing 6 – Unbinding all controls into their datasource

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):

 

Listing 7 – Assigning binding error messages when unbinding

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

 

 

If you find this article useful, consider making a small donation to show your support  for this Web site and its content.

 

By Rick Strahl

 

Rick Strahl is president of West Wind Technologies on Maui, Hawaii. The company specializes in Web and distributed application development and tools with focus on Windows Server Products, .NET, Visual Studio and Visual FoxPro. Rick is author of West Wind Web Connection, a powerful and widely used Web application framework for Visual FoxPro and West Wind HTML Help Builder. He's also a Microsoft Most Valuable Professional, and a frequent contributor to magazines and books. He is co-publisher and co-editor of CoDe magazine, and his book, "Internet Applications with Visual FoxPro 6.0", is published by Hentzenwerke Publishing. For more information please visit: http://www.west-wind.com/ or contact Rick at rstrahl@west-wind.com.

 

 

 

 

 

 

 

 

 

 

 

 

 

 in .Net

 is not one of .Net’s strong features either in Web Forms or Windows Forms. Although Windows Forms provides a ton of flexibility and power for   the implementation of the actual code in the final form can be messy and code intensive. Web Forms on the other hand are much less complete in their   in that they provide only one-way binding. Both mechanisms can easily be extended by subclassing the existing form controls and adding the required functionality with relatively little amounts of code. For Windows forms this means little more than wrapping the existing functionality with some higher level wrappers at the control level that allow control level properties to be set. In Web Forms the binding is much more primitive and requires digging deeper by using Reflection to read and write values from the controls into the data source and vice versa.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Interfaces

Interfaces are a great mechanism for identifying a group of objects that all implement common functionality. Interfaces enforce a specific property/method/event/field interface that any class that implements it must implement at the compiler level. In addition, interfaces are great if you need to reference a specific type ‘generically’ as you can cast an object this generic interface reference. You can then easily determine whether an object is part of a specific group. This article uses one interface implementation extensively to describe the custom interfaces built ontop of the existing user interface controls and the interface is used extensively to differentiate standard controls from the custom controls.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Reflection

Reflection is .Net’s type discovery and access mechanism and it’s the key to making the custom binding and unbinding described in this article work. Reflection allows you to ‘evaluate’ string based property and method names and run the code necessary dynamically by discovering the names of methods and properties and evaluating them dynamically at runtime. This allows the ability to store the property and field names as strings and then dynamically bind and unbind data from the control in question. Reflection can be quite unwieldy, especially if your objects accessed are complex and multiple levels deep. This article includes a helper class (wwUtils) that greatly simplifies common Reflection tasks such as retrieving and setting properties and fields and drilling down multiple levels in an object’s internal hierarchy.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Format Strings

The ability to write out data in common display formats is something that almost every application needs to do and  should provide this functionality natively. .Net supports simple formatting via the String.Format() method which is used behind the scenes for many formatting operations. Whenever you see syntax like {0:c} or {0:d} you’re likely looking at some functionality that behind the scenes is using String.Format(). This syntax means actually that the first parameter (parameter 0) is supposed to be converted into a specific format. The full syntax really is: String.Format("{0:c}",curValue);. This means that a format string often can be embellished with additional characters like this: String.Format("{0:c}% pure power",curValue); which is perfectly legal. The reverse of Format is the Parse method available on many of the type objects (like Int32, Decimal, Currence, DateTime etc.) which can take standard format expressions and parse them back into their underlying types. For example, Decimal.Parse("$3,000.12") understands the US currency syntax (if it’s running in the US version of Windows) and sets the value to 3000.12.

 

 

 
  White Papers                  Home |  White Papers  |  Message Board |  Search |  Products |  Purchase | News |