Implementing two-way Data Binding for ASP.NET

How it works (contd)

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.

You might also like...

Comments

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Owning a computer without programming is like having a kitchen and using only the microwave oven” - Charles Petzold