Using Reflection to Bind Business Objects to ASP.NET Form Controls

 

John Dyer
Dallas Theological Seminary

September 2004

Applies to:
   Microsoft Visual Studio 2005 and previous versions
   ASP.NET 1.1
   C# programming language
   Visual Basic programming language

Summary: Use Reflection to bind your Business objects to ASP.NET Web Forms with a single line of code, resulting in decreased complexity and fewer errors. (12 printed pages)

Download the MSDFormBinding.msi sample file.

Contents

Introduction
Simplifying and Shortening Form Code
Getting Started: Retrieving a List of Properties from Reflection
Binding Object Property Values to Controls
Setting Values of Unknown Controls with Known Properties
Reversing the Process: BindControlsToObject
Performance and Extending the FormBinding scheme
Conclusion

Introduction

One of the most common tasks Web developers do over and over is to build a simple form that updates a database table. We create a list page that displays the records in a table and a form page with appropriate form controls for each database field. Many developers also use business objects that represent their database tables to organize their code into a multitiered design. If the database table (Documents) is represented by a business object (Document), many of our forms look something like the code below:

<script runat="server">
protected void Page_Load(Object Src, EventArgs E) {
if (!IsPostBack) {
   Document document = 
        Documents.GetDocument(Request.QueryString["DocumentID"]);

   Title.Text = document.Title;
   Active.Checked = document.Active;
   CreatedDate.Text = document.CreatedDate.ToString();
   AuthorID.FindByValue(document.AuthorID.ToString()).Selected = 
        true;
   // ... and so on
   HtmlBody.Text = document.HtmlBody;
}
}
protected void SaveButton_Click(Object Src, EventArgs E) {
   Document document = 
        Documents.GetDocument(Request.QueryString["DocumentID"]);

   document.Title = Title.Text;
   document.Active = Active.Checked;
   document.CreatedDate = Convert.ToDateTime(CreatedDate.Text);
   document.AuthorID = Convert.ToInt32(AuthorID.SelectedItem.Value);
   // ... and so on
   document.HtmlBody = HtmlBody.Text;

   Documents.Update(document);
}
</script>

Simplifying and Shortening Form Code

In the above code, each control is explicitly cast and set to the correct property of the form control. Depending on the number of properties and form controls, this part of the code can become long and troublesome to manage. The code should also contain error correction for type conversion and ListControls, creating further complexities. Even if the form is created by a code generation tool (such as Eric J. Smith's excellent CodeSmith), it is easy to introduce errors if any custom logic is needed.

With reflection, it is possible to bind all the properties of our business objects to corresponding form controls using only a single line of code, resulting in fewer lines of code and increased readability. When we are finished building the reflection system, the above code will be reduced to:

protected void Page_Load(Object Src, EventArgs E) {
   if (!IsPostBack) {
      Document document = 
     Documents.GetDocument(Request.QueryString["DocumentID"]);

      FormBinding.BindObjectToControls(document);
   }
}
protected void Save_Click(Object Src, EventArgs E) {
   Document document = 
        Documents.GetDocument(Request.QueryString["DocumentID"]);

   FormBinding.BindControlsToObject(document);

   Documents.Update(document);
}

This code will work for all standard ASP.NET Controls (TextBox, DropDownList, CheckBox, etc.) and many third-party controls (such as Free TextBox and Calendar Popup). Regardless of the number of business object properties and form controls, the same single line of code will handle all the necessary functionality, provided the form control ID matches the business object property name.

Getting Started: Retrieving a List of Properties from Reflection

The first thing we need to do is examine the properties of our business object and look for ASP.NET controls that have the same ID as the business object property name. The code below forms the basis of the binding lookup:

public class FormBinding {
   public static void BindObjectToControls(object obj, 
        Control container) {
      if (obj == null) return;
      Type objType = obj.GetType();
      PropertyInfo[] objPropertiesArray = 
              objType.GetProperties();

      foreach (PropertyInfo objProperty in objPropertiesArray) {

         Control control = 
                     container.FindControl(objProperty.Name);
         if (control != null) {
             // process Control ...
         }
      }
   }
}

In the above code, the method BindObjectsToControls accepts the business object obj and a container control. The container control will typically be the Page object of current WebForm. If you are using an implementation of ASP.NET 1.x MasterPages that changes the nesting order of controls at runtime you will need to specify the Content control in which the form controls reside. This is due to the way the FindControl method works with nested controls and naming containers in ASP.NET 1.x.

In the code above, we get the Type of our business object and use that Type to get an array of PropertyInfo objects. Each PropertyInfo object contains information about our business object properties as well as the ability to get and set values from the business object. Using a foreach loop, we check the container for ASP.NET controls that have an ID property that corresponds to the business object property name (PropertyInfo.Name). If we find a control, we will attempt to bind the property value to the control.

Binding Object Property Values to Controls

Most of our processing will take place at this stage. We need to fill the Control we found with the value of our object's property. One way to do this would be to create if ... else statements for each control type. All the controls that derive from ListControl (DropDownLists, RadioButtonLists, CheckBoxLists and ListBoxes) can be grouped together since they have a common interface that can be uniformly accessed. If the control we found is a ListControl, we can cast it as a ListControl and then set the selected item:

Control control = container.FindControl(objProperty.Name);
if (control != null) {
   if (control is ListControl) {
      ListControl listControl = (ListControl) control;
      string propertyValue = objProperty.GetValue(obj, 
              null).ToString();
      ListItem listItem = 
             listControl.Items.FindByValue(propertyValue);
      if (listItem != null) listItem.Selected = true;
   } else {
      // handle other control types
   }
}

Unfortunately for us, other control types do not derive from parent class. Several common controls (TextBox, Literal, and Label) all have a .Text string property, but because property is not derived from a common parent class, we need to cast each control type individually. We also need to cast other control types such as the Calendar control in order to use the appropriate property (in the Calendar's case, the SelectedDate property). It does not require an excessive number of lines of code to include all the standard ASP.NET form controls and access the correct property of the form control.

if (control is ListControl) {
   ListControl listControl = (ListControl) control;
   string propertyValue = objProperty.GetValue(obj, 
        null).ToString();
   ListItem listItem = listControl.Items.FindByValue(propertyValue);
   if (listItem != null) listItem.Selected = true;
} else if (control is CheckBox) {
   if (objProperty.PropertyType == typeof(bool))
      ((CheckBox) control).Checked = (bool) 
              objProperty.GetValue(obj, null);
} else if (control is Calendar) {
   if (objProperty.PropertyType == typeof(DateTime))
      ((Calendar) control).SelectedDate = (DateTime) 
               objProperty.GetValue(obj, null);
} else if (control is TextBox) {
   ((TextBox) control).Text = objProperty.GetValue(obj, 
        null).ToString();
} else if (control is Literal)(
   //... and so on for, Labels, etc.
}

This approach nicely covers the standard ASP.NET 1.x controls and at this point we have a fully functional BindObjectToControls method. But while this approach does work, it is limited in application because it only considers built-in ASP.NET 1.x controls. If we want to support new ASP.NET 2.0 controls or use any third-party controls, we have to reference the control's assembly in the FormBinding project and add the control type to the if ... else list.

The solution to this problem is to use a second round of Reflection to look at the properties of each control and find out if the control has a property type that corresponds to the type of our business object's property.

Setting Values of Unknown Controls with Known Properties

As mentioned above, several controls share the String property .Text and most form controls use this property in essentially the same way. It is used to get and set data that the user inputs. There are other common properties and property types used by a variety of controls. Some of these properties include a DateTime property called .SelectedDate used in many calendar and date-picker controls, a bool property called .Checked used in Boolean controls, and a string property called .Value that is common in hidden controls. These four properties (string Text, string Value, bool Checked, and DateTime SelectedDate) are the most common control properties. If we can design our system to bind to these properties regardless of the control type, then our Binding method will apply to any control that uses those four properties.

In the code below, we use reflection a second time (this time on the form control instead of the business object) to determine if it has any of our commonly used properties. If so, we will attempt to set our business object's property value to the control's property. As an example, we'll iterate through the PropertyInfo array and look for a String property called .Text. If the control has that property, we'll send the data from our business object to that control's property.

if (control is ListControl) {
   // ...
} else {
   // get the type and properties of the Control
   //
   Type controlType = control.GetType();
   PropertyInfo[] controlPropertiesArray = 
        controlType.GetProperties();

   // look for .Text property
   //
   foreach (PropertyInfo controlProperty 
        in controlPropertiesArray) {
      if (controlPropertiesArray.Name == "Text" && 
              controlPropertiesArray.PropertyType == typeof(String)) {
         // set the Control's .Text property
         //
         controlProperty.SetValue(control, 
                   (String) objProperty.GetValue(obj, null), null);

      }
   }

}

If .Text is found we use the PropertyInfo class's GetValue method to retrieve the value from our business object's property. We then use the SetValue method of Control's .Text property. Here, we also Type check the control's property as typeof(String) and explicitly cast the value from the property using the (String) notation.

To make the BindObjectToControls method complete, we also need to handle the other common properties, .Checked, .SelectedDate, and .Value. In the code below, we will wrap up the control property search into a helper method called FindAndSetControlProperty to simplify the code.

if (control is ListControl) {
   // ...
} else {
   // get the properties of the control
   //
   Type controlType = control.GetType();
   PropertyInfo[] controlPropertiesArray = 
        controlType.GetProperties();

   bool success = false;
   success = FindAndSetControlProperty(obj, 
        objProperty, control, controlPropertiesArray, 
        "Checked", typeof(bool) );

   if (!success)
      success = FindAndSetControlProperty(obj, 
              objProperty, control, controlPropertiesArray, 
              "SelectedDate", typeof(DateTime) );

   if (!success)
      success = FindAndSetControlProperty(obj, 
              objProperty, control, controlPropertiesArray, 
              "Value", typeof(String) );

   if (!success)
      success = FindAndSetControlProperty(obj, 
              objProperty, control, controlPropertiesArray, 
              "Text", typeof(String) );

}

private static void FindAndSetControlProperty(object obj, 
  PropertyInfo objProperty, Control control, 
  PropertyInfo[] controlPropertiesArray, string propertyName, 
  Type type) {
   // iterate through control properties

   foreach (PropertyInfo controlProperty in 
        controlPropertiesArray) {
      // check for matching name and type
      if (controlPropertiesArray.Name == "Text" && 
              controlPropertiesArray.PropertyType == typeof(String)) {
         // set the control's property to the 
                  // business object property value
         controlProperty.SetValue(control, 
                   Convert.ChangeType( 
                   objProperty.GetValue(obj, null), type) , null);
         return true;
      }
   }
   return false;
}

The order of the above property checks is important because some controls have more than one of the above properties, but only one that we want to set. For example, the CheckBox control has both a .Text and .Checked property. In this case, we want to use the .Checked property and not the .Text property, so we place .Checked first in the order of the property search. In each case, if we find a control property of the correct name and type, we attempt to set the control's property to the value of the business object property.

At this point, we have a fully functional BindObjectToControls method that we can call anywhere on an ASPX form, with any class and any combination of controls and it will "just work." Now we need to create a method that will do the reverse when the form is submitted. Instead of setting the value of control properties to that of our business object, we need to retrieve the new values from the controls that represent the user's input.

Reversing the Process: BindControlsToObject

In the BindControlsToObject method, we will start out the same way, retrieving a list of properties from our business object and using the FindControl method to find controls with IDs that match our object's properties. If we find a control, we'll retrieve the value and return it to our business object. This section will also contain separate code for ListControls since they have a common interface. We'll use another helper method to search for and retrieve the value in the control and return it to the business object.

public static void BindControlsToObject(object obj, 
  Control container) {
   Type objType = obj.GetType();
   PropertyInfo[] objPropertiesArray = objType.GetProperties();

   foreach (PropertyInfo objProperty in objPropertiesArray) {

      if (control is ListControl) {
         ListControl listControl = (ListControl) control;
         if (listControl.SelectedItem != null)
            objProperty.SetValue(obj, 
                          Convert.ChangeType(list.SelectedItem.Value,
                          objProperty.PropertyType), null);

      } else {
         // get the properties of the control
         //
         Type controlType = control.GetType();
         PropertyInfo[] controlPropertiesArray = 
                     controlType.GetProperties();

         bool success = false;
         success = FindAndGetControlProperty(obj, 
                    objProperty, control, controlPropertiesArray, 
                    "Checked", typeof(bool) );

         if (!success)
            success = FindAndGetControlProperty(obj, 
                         objProperty, control, controlPropertiesArray, 
                         "SelectedDate", typeof(DateTime) );

         if (!success)
            success = FindAndGetControlProperty(obj, 
                          objProperty, control, controlPropertiesArray, 
                          "Value", typeof(String) );

         if (!success)
            success = FindAndGetControlProperty(obj, 
                         objProperty, control, controlPropertiesArray, 
                         "Text", typeof(String) );

      }
   }
}

private static void FindAndGetControlProperty(object obj, 
  PropertyInfo objProperty, Control control, PropertyInfo[] 
  controlPropertiesArray, string propertyName, Type type) {
   // iterate through control properties
   foreach (PropertyInfo controlProperty in 
        controlPropertiesArray) {
      // check for matching name and type
      if (controlPropertiesArray.Name == "Text" && 
              controlPropertiesArray.PropertyType == typeof(String)) {
         // set the control's property to the 
                  // business object property value
         try {
            objProperty.SetValue(obj, 
                          Convert.ChangeType( 
                          controlProperty.GetValue(control, null), 
                          objProperty.PropertyType) , null);
            return true;
         } catch {
            // the data from the form control 
                        // could not be converted to 
                        // objProperty.PropertyType
            return false;
         }
      }
   }
   return true;
}

With these two methods complete, our form syntax becomes as simple as described above in Simplifying and Shortening Form Code. Type conversion and error correction for every property and control are automatically handled. The two methods, BindObjectToControls and BindControlsToObject, allow the developer quite a bit of flexibility in form creation. Some common scenarios that are also handled are:

  • If a new property is added to the business object and that new property needs to be accessed on the form, the developer only needs to add a control to the page with its ID set to the name of the new property and the FormBinding methods will handle the rest.
  • If a developer needs to change the type of control used for a particular property, such as from a TextBox to a third-party HTML editor control, he or she simply needs to ensure that the new control has one of the above properties (such as .Text) and the form will work exactly as it did before.
  • Forms can also be quickly generated using all TextBox controls and the input will still be converted to the correct type for the business object property. For example, a TextBox control could be used in place of a Calendar control or third-party date-picker control. As long as the user inputs a value DateTime string, the value in the TextBox's .Text property will be converted to DateTime, just as if it were a .SelectedDate property on a calendar control. If the TextBox is changedl later to a date-picker control, the logic will remain the same.
  • A developer could also quickly create a "View" page by changing all the controls to Literal controls. The .Text property of the Literal would be set to the value of the business object property just as if it were a TextBox.
  • In real world scenarios, forms also contain other data types and custom configurations. The code to handle these special operations can be placed after the calls to BindObjectToControls and BindControlsToObject.

Performance and Extending the FormBinding scheme

Some developers may wonder if the performance hit that comes with using reflection is worth the price. In my tests using an object with seven properties (int DocumentID, bool Active, DateTime Created, int CategoryID, String Title, string Author, and String htmlText), BindObjectToControls took approximately one-third of a millisecond and BindControlsToObject took approximately 1 millisecond. These values were found by running the BindObjectToControls and BindControlsToObject methods in a loop 1000 times. For common Add and Edit form scenarios, this performance should not pose any significant problems and is surely worth the development speed and flexibility.

Although this method will work in almost every form, there are some cases where the above code may need to be modified. In some scenarios, a developer may want to use a control that does not use one of the above properties as its primary interface. In that case, the FormBinding methods will need to be updated to include that property and type.

Conclusion

The two FormBinding methods, BindObjectToControls and BindControlsToObject, can be used to vastly simplify form code and allow maximum flexibility when developing ASP.NET forms. I hope it helps your team as much as it has helped mine.

 

About the author

John Dyer is the lead Web developer at Dallas Theological Seminary overseeing their award-winning online education program built on Telligent Systems' Community Server. He's also the author of the widely used FreeTextBox ASP.NET HTML Editor—the control from which everyone but him makes money. In his spare time he is working a Masters of Theology at DTS and is planning a wedding for January 1, 2005.

© Microsoft Corporation. All rights reserved.