Got something to write about? Check out our Article Builder.

Implementing two-way Data Binding
for ASP.Net Web Forms - Page 2

By Rick Strahl
West Wind Technologies
rstrahl@west-wind.com

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.

http://www.programmersheaven.com/articles/rickshaw/aspnetdatabinding/image005.gif 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.

About the Author

This article was written by By Rick Strahl from www.west-wind.com.
He can be contacted by email at rstrahl@west-wind.com

Previous Page






 

Other Views

corner
Popular resources and forums for programmers on Programmersheaven.com
Assembly, Basic, C, C#, C++, Delphi, Java, JavaScript, Pascal, Perl, PHP, Python, Ruby, Visual Basic
© Copyright 2009 Programmersheaven.com - All rights reserved.
Reproduction in whole or in part, in any form or medium without express written permission is prohibited.
Violators of this policy may be subject to legal action. Please read our Terms Of Use and Privacy Statement for more information.
Publisher: Lars Hagelin. Read the latest words from the publisher here.
Be the first to sign up for Lars Hagelin’s In-depth Outsourcing Newsletter here.
bootstrapLabs Logo A bootstrapLabs project.