Two-Way Databinding

Oct 4, 11:00 pm

Article Author: Bill Xie
.NET 3.5 Books

Introduction


Databinding is an essential functionality in ASP.NET, but ASP.NET 1.1 only facilitates binding data from business object to web form through databinding expressions. Databinding in the reverse direction (ie. updating the database to match the data that has been edited on the web form) typically involves tedious and lengthy assignment operations along with type conversion and special handling such as formatting, empty string checking and database null value checking. The resultant code can be error-prone and hard to maintain. In this article I will present a new method called two-way databinding that supports bidirectional databinding between web forms and business objects


The following code illustrates what the new databinding logic looks like.


Suppose you have a simple online insurance application web form and data is stored in a table called policy. So you have a business object called Policy. The data presented to the user for viewing and editing will include client name, policy number, policy effective date and policy expired date. And suppose you want all fields to be editable in the web form.


Without two-way databinding, the code might look something like this:



//Binding data from business object to web
// tbxClientName, tbxPolicyNo, tbxPolicyEffectiveDate and
// tbxPolicyExpiredDate are server controls in web form.
tbxClientName.Text = Policy.ClientName;
tbxPolicyNo.Text = Convert.ToString(Policy.PolicyNo);
tbxPolicyEffectiveDate.Text= String.Format("{0:MM/dd/yyyy}", Policy.PolicyEffectiveDate);
tbxPolicyExpiredDate.Text = String.Format("{0:MM/dd/yyyy}", Policy.PolicyExpiredDate);
//Binding data from web to business object
Policy. ClientName = tbxClientName.Text.Trim();
Policy.PolicyNo = Convert.ToInt32(tbxPolicyNo.Text.Trim());
Policy.PolicyEffectiveDate = Convert.ToDateTime(tbxPolicyEffectiveDate.Text.Trim());
Policy.PolicyExpiredDate = Convert.ToDateTime(tbxPolicyExpiredDate.Text.Trim());


With two-way databinding the code might look as follows



//Binding data from business object to web
// pnlPolicy is a panel that holds relevant web form fields
ArrayList arr = FindFormFields(pnlPolicy);
BindFromBizObject2Web(Policy,arr);
//Binding data from web to business object
ArrayList arr = FindFormFields(pnlPolicy);
BindFromWeb2BizObject(arr, Policy);


You can see that using two-way databinding significantly simplifies the databinding logic.There is no need for you to deal with type conversion, string formatting, database null value checking, etc. You will further find out that the databinding logic above will not change even if you want to change handling of some binding fields.


Now suppose after you are done with above coding, your boss tells you that the following changes must be applied as soon as possible.


  1. The policy number can not be editable,

  2. The client name should be checked to see if there is duplication

  3. An extra text field called PolicyReference should be added.

With two-way databinding you need only make the following changes


  1. Change PolicyNo from a TextBox to a Label

  2. For checking the client name, there are several ways you can achieve this: You can add name validation logic before databinding function BindFromWeb2BizObject(). Alternatively, you can set one of tbxClientName s extended properties, BindingDirection, to be FromBizObject2Web in the web form; this means that BindFromWeb2BizObject() will skip this field, and then add your name duplication logic the function call. This way, you have to bind the client name to the business object manually. Notice that since you encapsulate name duplication logic within the Policy object, the databinding logic will not change.

  3. In Policy object you need to add a new property called PolicyReference.and at the web form add an extra textbox field, tbxPolicyReference

The new databinding technique makes it straightforward to make those kinds of changes. More specificily,the new technique has thes advantages:


  • It reduces databinding logic to one or two lines of code, but it still allows for special handling of specific fields if necessary.

It handles type conversions, string formatting and null value checking automatically. Declarative syntax allows developers to change data binding properties with ease.


Code logic becomes more readable and thus easy to maintain. For example, the technique will deal with the business object and the list of server controls as a whole. As such code readability is improved.


What this Article Covers


This article is structured as follows:


First I ll present the foundations for realizing the two-way databinding: Identifying the list of server controls that will participate in the data binding, mapping each server control to a property of the business object, coding the common interface for server control, and coding the common interface for the business object.


Next, I ll show you how to use compositional inheritance to extend the server control and also show you how to use these extended server controls in web form. I follow up with a detailed implementation of the common interface for server controls.


Then I ll give you several implementations of the business object followed with a sample web application. In the sample application I ll show you in detail how to apply two-way databinding to your applications.


System Requirements


The implementation of two-way databinding presented in this article is based on .NET version 1.1 and IIS 5.0. But it should in principle be easy to port it to ASP.NET 2.0 for developers using .NET 2.0. Besides ASP.NET 1.1, you’ll also need


  • SQL Server 2000 with the Northwind database.

  • The Data Application Block, version 2.0

Installing and Compiling the Sample Code


The code for this article contains two projects:


  • FormField is a library that implements two-way databinding

  • TwoWayDataBindingTest is a web application that is used to test the databinding project.

To run the testing code, you need to create an IIS virtual directory called TwoWayDataBindingTest, and map it to the project folder TwoWayDataBindingTest contains the sample web project. Second, you need to modify the connection string (server, uid, and pwd) in the web.config file to connect to the Northwind database on your system. Finally, you should run the SQL script Twowaydatabinding.sql to install some stored procedures that are used by the sample code (I ll talk about those later in the article). The sample code uses the Microsoft Data Application Block version 2.0 and includes it in TwoWayDataBindingTest directory.


Common Interfaces for Databinding


In this section I’ll go over the procedures involved in implementing the two-way databinding technique. In particular you need to consider:


  • Identifying the list of server controls participating data binding

  • Mapping the server control to the business object

  • Coding the common databinding interface for server control

  • Coding a common databinding interface for business object

Server Controls Participating in Data Binding


Many ASP.NET server controls can serve as a data container or form field. In other words, they can participate in data binding activity. For this application you ll use an enumeration called FormFieldType to represent these server controls.



public enum FormFieldType : int {Literal, Label, TextBox, CheckBox, 
  RadioButton, DropDownList, SingleMappingRadioButtonList, 
  MultiMappingRadioButtonList, CheckBoxList, SingleMappingListBox, 
  MultiMappingListBox}
public enum BindingDirectionType : int {None, FromWeb2BizObject, FromBizObject2Web, BiDirection}


Here you ll only consider the ASP.NET built-in server controls listed in the enumeration, but in the end of this article I ll show you how to extend this list by adding more controls. Basically there are two types of server controls: one type maps to a single property of the business object. These controls include Literal, Label, TextBox, CheckBox, RadioButton, DropDownList, SingleMappingRadioButtonList, SingleMappingListBox; the other type maps to multiple properties of one business object; these controls include MultiMappingRadioButtonList, CheckBoxList and MultiMappingListBox. RadioButtonList and ListBox might fall into either of the two categories.


The BindingDirectionType enumeration defines 4 exclusive options to specify how a server control will participate in the databinding: never (None), from web to business object (FromWeb2BizObject), from business object to web (FromBizObject2Web) and both direction (BiDirection).


Mapping the Server Control to the Business Object


We use the business object as data source. So two-way databinding requires a mechanism to deduce from a server control which property of the business object it represents. For single mapping server controls, the Control ID property can serve our purpose: ID will essentially be the same as the property name of business object but with a slight difference. For multiple mapping controls, the value of each item (ListItem) will map to the property of business object. Since the list item can either be selected or not selected it will make sense to restrict the corresponding property of business object to a boolean value plus the database null value. MultiRadioButtonList allows at most one of the business object properties could be true while SingleMappingRadioButtonList allows that the property of business object can have different values, which are not limited to true or false; this applies too to SingleMappingListBox and MultiMappingListBox.


Multiple server controls in a web form can not have the same IDs. However, for two-way databinding to work, you may need to map different server controls to the same property of business object. To work around this, I use a simple naming technique: a control ID that consists of three parts: prefix, underscore and property name of business object. A function, ResolveID()extracts the property name from the control ID.



private static string ResolveID(string id)
{ return id.Substring(id.IndexOf("_") + 1);
}


The Common Interface for Server Controls


All server controls are inherited from Control. However, this common base class is not advantageous to databinding since it lacks properties and functions to support databinding, such as formatting and type conversion, etc. To support this, you ll define a common interface for databinding called IFormField as shown in the following code snippet



interface IFormField{
   string GroupID {get; set;}
   string FormatString {get; set;}
   string ValueIfNull{get;set;}
   string NullIfValuePostback{get; set;}
   BindingDirectionType  BindingDirection {get; set;}
   void BindFromControl2BizObject(IBizObject obj);
   void BindFromBizObject2Control(IBizObject obj);
}


The properties and methods of this interface have the following purposes:


  • GroupID distinguishes different groups of server controls which map to the same business object. Typically GroupID might be the value of primary key of a table record.

  • FormatString provides formatting when needed. This field only makes sense for Literal, Label and TextBox. For other controls it will be ignored.

  • ValueIfNull provides a replacement value when the property of the business object field is the database null value. It applies to all items for multiple mapping server controls.

  • NullIfValuePostback is used when value of the server control is NullIfValuePostback and the property of business object will be set to database null value.

  • BindingDirection specifies how the server control will participate in databinding. The default value is BiDirection.

  • BindFromControl2BizObject() binds value of control to the property of business object.

  • BindFromBizObject2Control() binds the property of business object to the server control.

The Common Interface for the Business Objects


The common interface for the business objects has already been used in the databinding functions in IFormField. Any business object participating two-way databinding should implement this interface, which is defined as follows:



public interface IBizObject{
   object GetByName(string name);
   void SetByName(string name, string strValue, string nullIfValue);
}


In this interface, GetByName() returns the value of a property through the property name. Note that the returned value is of generic type object. SetByName() is responsible for converting a string value to the property of a business object. SetByName()sets a property to the database null value if the passed value strValue contains the same string as nullIfValue. For the purposes of this article, a business object is an object that implements the IbizObject interface. It could be a customized class corresponding to a table record in the database or a simple wrapper of DataRow which provides an implementation of this interface. I ll present several sample implementations later.


Extending Web Server Controls


Using Compositional Inheritance to Extend Server Control


To realize two-way databinding you need to extend all server controls to support IFormField. An straightforward approach is direct inheritance: create a new class, such as TextBoxEx, inherit from IFormField and implement all properties and functions defined in IFormField. In this article I ll introduce you to another approach to achieve this, called compositional inheritance. The advantage of compositional inheritance is that it will centralize all implementations in one place. On the other hand it will still supply all server controls a common interface as we need.


The code below shows how compositional inheritance works.



public class FormFieldImp : IFormField
{ FormFieldType fType; Control con; StateBag viewState; public FormField(Control c, FieldType ft, StateBag sb) { this.con = c; this.fType = ft; this.viewState = sb }
public interface IFormFieldWrapper { IFormField FF{get; } }
public class LiteralEx : IFormFieldWrapper
{ private IFormField _ff; public Literal():base() { this._ff = new FormField(this, FieldType.Literal, this.ViewState) } public FF { get{return this._ff;} }
}


The code contains a class called FormField, which contains implementations of all server controls. All necessary data (the field type, the control itself, and the view state) are passed in through its constructor. In an extended server control LiteralEx a compositional variable _ff of type IFormField is defined. And _ff is given an instance of FormField in the constructor of LiteralEx. Further you define a new interface called IFormFieldWrapper, which contains only one read-only property FF of type IFormField. IFormFieldWrapper not only supplies a common interface to all extended server controls but also provides a chance to access all functionalities in IFormField. The implementation of FF in LiteralEx is just one line of code. All other server controls will be extended in a similar way except for possible differences in the parameters supplied to each constructor. Any later changes to the implementation should take place in the FormField class while all extended server controls remains untouched.


Using the Extended Server Controls in a Web Form


To use the extended controls in web form you can take advantage of declarative syntax in ASP.NET web forms: When ASP.NET encounter a declarative statement of the form xxx="yyy" in the markup, it will automatically use the public field or property named xxx of that control and convert the string value yyy to the type expected by that property, and assign the value to that property. For a more complex object, the corresponding syntax is objectName-propertyName="yyy". Good examples include Font: Font-Size, Font-Color, etc. For the application the syntax is eg. FF-FormatString, FF-ValueIfNull, etc. The following code illustrates this usage.



<xzb:SingleMappingRadioButtonListEx id="xRBL" FF-BindingDirection="Both" 
  FF-ValueIfNull="True" Runat="server">
   <asp:ListItem Selected="False" Value="Cat">Cat</asp:ListItem>
   <asp:ListItem Selected="True" Value="Dog">Dog</asp:ListItem>
   <asp:ListItem Selected="False" Value="Bird">Bird</asp:ListItem>
</xzb:SingleMappingRadioButtonListEx>


Implementing the Common Interface


I’ll now walk you through the implementation of FormField, which inherits from the common interface IFormField. For our needs FormField will use Control, ViewState and FormFieldType. All these variables are passed in through its constructor.


Implementing Common Properties


The implementation of properties in IFormField will follow the pattern of getting and setting values from the ViewState of the control Pay attention to the default values of properties. GroupID must be set; otherwise it will throw an exception. BindingDirection will default to BindingDirectionType.BiDirection. ValueIfNull will default to empty string, but for controls whose value is a boolean, such as RadioButton and MultiMappingRadioButtonList an empty value means false. FormatString will default to null. For controls other than Literal, Label and TextBox, FormatString is ignored. NullIfValuePostback will default to null



//GroupID must be set before its use.
public string GroupID
{ get { object o = this.viewState["GroupID"]; if(o null) throw new ApplicationException("GroupID is not set yet!"); else return (string)(o); } set { this.viewState["GroupID"] = value; } } //Default to null public string FormatString { get { object o = this.viewState["FormatString"]; if(o null) return null else return (string)(o); } set { this.viewState["FormatString"] = value; }
}
//Default to empty string
public string ValueIfNull
{ get { object o = this.viewState["ValueIfNull"]; if(o null) return string.Empty; else return (string)(o); } set { this.viewState["ValueIfNull"] = value; } } //Default to null public string NullIfValuePostback { get { object o = this.viewState["NullIfValuePostback"]; if(o null) return null; else return (string)(o); } set { this.viewState["NullIfValuePostback"] = value; }
}
//Default to BiDirection
public BindingDirectionType BindingDirection
{ get { object o = this.viewState["BindingDirection"]; if(o null) return BindingDirectionType.Both; else return (BindingDirectionType)(o); } set { this.viewState["BindingDirectionType"] = value; } }

Implementing Data Binding Functions

Implementations of BindFromControl2BizObject() and BindFromBizObject2Control() will iterate through all server controls using the switch statement. But they will first check whether this control is to participate in databinding of the specified direction. If not, each function will return. Two private properties are used: ExistsFormatString will return true if FormatString is not null and DefaultBooleanValueIfNull will return true if ValueIfNull is the empty string or false. Here's the relevent code:

private bool ExistFormatString
{
  get{ return FormatString != null; }
}
private bool DefaultBooleanValueIfNull
{
  get{ 
    if(ValueIfNull.Length0)
      return true;
    else
      return Convert.ToBoolean(ValueIfNull);
  }  
}
public void BindFromControl2BizObject(IBizObject obj)
{ if(this.BindingDirection BindingDirectionType.None) return; if(this.BindingDirection BindingDirectionType.BizObject2Web) return; string propertyName = ResolveID(this.con.ID); switch(this.fieldType) { case FormFieldType.Literal: obj.SetByName(propertyName, ((Literal) con).Text.Trim(), NullIfValuePostback); return; case FormFieldType.Label: obj.SetByName(propertyName, ((Label) con).Text.Trim(), NullIfValuePostback); return; case FormFieldType.TextBox: obj.SetByName(propertyName, ((TextBox) con).Text.Trim(), NullIfValuePostback); return; case FormFieldType.CheckBox: obj.SetByName(propertyName, Convert.ToString(((CheckBox)con).Checked), NullIfValuePostback); return; case FormFieldType.RadioButton: obj.SetByName(propertyName, Convert.ToString(((RadioButton)con).Checked), NullIfValuePostback); return; case FormFieldType.DropDownList: obj.SetByName(propertyName, ((DropDownList)con).SelectedValue, NullIfValuePostback); return; case FormFieldType.SingleMappingRadioButtonList: obj.SetByName(propertyName, ((RadioButtonList)con).SelectedValue, NullIfValuePostback); return; case FormFieldType.MultiMappingRadioButtonList: foreach(ListItem li in ((RadioButtonList)con).Items) { propertyName = ResolveID(li.Text.Trim()); obj.SetByName(propertyName, li.Value, NullIfValuePostback); } return; case FormFieldType.CheckBoxList: foreach(ListItem li in ((CheckBoxList)con).Items) { propertyName = ResolveID(li.Text.Trim()); obj.SetByName(propertyName, li.Value, NullIfValuePostback); } return; case FormFieldType.SingleMappingListBox: obj.SetByName(propertyName, ((ListBox)con).SelectedValue, NullIfValuePostback); return; case FormFieldType.MultiMappingListBox: foreach(ListItem li in ((ListBox)con).Items) { propertyName = ResolveID(li.Text.Trim()); obj.SetByName(propertyName, li.Value, NullIfValuePostback); } return; default: throw new ApplicationException( "BindFromControl2BizObject Not implemented for " + this.con.ID); }
}
public void BindFromBizObject2Control(IBizObject obj)
{ if(this.BindingDirection BindingDirectionType.None) return; if(this.BindingDirection BindingDirectionType.Web2BizObject) return; string propertyName = ResolveID(this.con.ID); object val; switch(this.fieldType) { case FormFieldType.Literal: val = obj.GetByName(propertyName); Literal lit = ((Literal) con); if(Convert.IsDBNull(val)) lit.Text = this.ValueIfNull; else if (ExistFormatString) lit.Text = string.Format(FormatString, val); else lit.Text = Convert.ToString(val); return; case FormFieldType.Label: val = obj.GetByName(propertyName); Label lbl = ((Label) con); if(Convert.IsDBNull(val)) lbl.Text = this.ValueIfNull; else if (ExistFormatString) lbl.Text = string.Format(FormatString, val); else lbl.Text = Convert.ToString(val); return; case FormFieldType.TextBox: val = obj.GetByName(propertyName); TextBox tbx = ((TextBox) con); if(Convert.IsDBNull(val)) tbx.Text = this.ValueIfNull; else if (ExistFormatString) tbx.Text = string.Format(FormatString, val); else tbx.Text = Convert.ToString(val); return; case FormFieldType.CheckBox: val = obj.GetByName(propertyName); CheckBox chk = ((CheckBox) con); if(Convert.IsDBNull(val)) chk.Checked = DefaultBooleanValueIfNull; else chk.Checked = Convert.ToBoolean(val); return; case FormFieldType.RadioButton: val = obj.GetByName(propertyName); RadioButton rb = ((RadioButton) con); if(Convert.IsDBNull(val)) rb.Checked = DefaultBooleanValueIfNull; else rb.Checked = Convert.ToBoolean(val); return; case FormFieldType.DropDownList: val = obj.GetByName(propertyName); DropDownList ddl = ((DropDownList) con); if(Convert.IsDBNull(val)) ddl.SelectedValue = ValueIfNull; else ddl.SelectedValue = Convert.ToString(val); return; case FormFieldType.SingleMappingRadioButtonList: val = obj.GetByName(propertyName); RadioButtonList srbl = ((RadioButtonList) con); if(Convert.IsDBNull(val)) srbl.SelectedValue = ValueIfNull; else srbl.SelectedValue = Convert.ToString(val); return; case FormFieldType.MultiMappingRadioButtonList: //In fact all fields are of type boolean and only one will be true foreach(ListItem li in ((RadioButtonList)con).Items) { propertyName = ResolveID(li.Value); val = obj.GetByName(propertyName); if(Convert.IsDBNull(val)) li.Selected = false; else li.Selected = Convert.ToBoolean(val); } return; case FormFieldType.CheckBoxList: //all fields are boolean type foreach(ListItem li in ((CheckBoxList)con).Items) { propertyName = ResolveID(li.Value); val = obj.GetByName(propertyName); if(Convert.IsDBNull(val)) li.Selected = false; else li.Selected = Convert.ToBoolean(val); } return; case FormFieldType.SingleMappingListBox: val = obj.GetByName(propertyName); ListBox slbx = ((ListBox) con); if(Convert.IsDBNull(val)) slbx.SelectedValue = ValueIfNull; else slbx.SelectedValue = Convert.ToString(val); return; case FormFieldType.MultiMappingListBox: //all fields are boolean type foreach(ListItem li in ((ListBox)con).Items) { propertyName = ResolveID(li.Value); val = obj.GetByName(propertyName); if(Convert.IsDBNull(val)) li.Selected = false; else li.Selected = Convert.ToBoolean(val); } return; default: throw new ApplicationException( "BindFromBizObject2Control Not implemented for " + this.con.ID); }
}


Limitations of Compositional Inheritance


Compositional inheritance allows the implementation to be centralized in one place but it has some restrictions. First it requires that the common interface applies to all server controls. In this case only FormatString and NullIfValue have some slight variations in application, so it works fine. Second, you may note the number of ooptions in the switch statements in BindingFromControl2BizObject() and BindingFromBizObject2Control(). Direct inheritance can avoid those kinds of large switch statements.


Adding a New Server Control


Adding a new server control is very straight forward if the properties and functions of IFormField apply to that control. For example, ASP.NET 2.0 introduces a new server control called HiddenField or you could create a similar control in ASP.NET 1.x. This is a good example of another control you might wish to include in two-way databinding.


Implementing the Two-way DataBinding


Given a business object as data container and a list of server controls that will participate in two-way databinding, two simple static functions, BindFromBizObject2Web() and BindFromWeb2BizObject() will achieve this:



public static void BindFromBizObject2Web(
  IBizObject bo, ArrayList arrExtendedControls)
{ foreach(IFormFieldWrapper iff in arrExtendedControls) { iff.FF.BindFromBizObject2Control(bo); }
}
public static void BindFromWeb2BizObject( ArrayList arrExtendedControls, IBizObject bo)
{ foreach(IFormFieldWrapper iff in arrExtendedControls) { iff.FF.BindFromControl2BizObject(bo); }
}


Several supporting functions are provided in the sample to facilitate extracting the GroupID from a single control or an array of controls and identifiying all extended server controls from a container.


  • string GetGroupID(Control extendedControl)

  • string GetGroupID(ArrayList arrExtendedControl)

  • string GetGroupIDForChildControls(Control container)

  • ArrayList FindChildFormFields(Control container)

  • ArrayList FindChildFormFields(Control container, string groupID)

  • Control FindSingleFormField(string propertyName, ArrayList arr)

Building the Business Object


Before illustrating how to use two-way databinding I will show you how to build the business object. As I stated before, the concept of the business object is wide, the restriction being that it implements IBizObject and implements two functions: GetByName() and SetByName(). I’ll consider three examples of business object: DataRow, SqlParameters, and a customized entity.


DataRow as a Business Object


The code snippet below illustrates how to wrap a DataRow into a business object. When a DataTable binds to an iterative control such as a Repeater, the DataItem you get in the DataItemBound event will actually be a DataRowView. Both are implemented here.



public class DataRowBizObj : IBizObject
{ private DataRow dataRow; public DataRowBizObj(DataRow dr) { this.dataRow = dr; } public DataRow Row { get{return this.dataRow;} } public object GetByName(string realName) { return dataRow[realName]; } public void SetByName(string realName, string val, string nullIfValue) { if((val null && nullIfValuenull) ||(val==nullIfValue) ) { dataRow[realName] = System.DBNull.Value; } else { dataRow[realName] = val; //automatic type converstion } }
}
public class DataRowViewBizObj : IBizObject
{ private DataRowView dataRowView; public DataRowViewBizObj(DataRowView dr) { this.dataRowView = dr; } public DataRowView Row { get{return this.dataRowView;} } public object GetByName(string realName) { return dataRowView[realName]; } public void SetByName(string realName, string val, string nullIfValue) { if((val null && nullIfValuenull) ||(val==nullIfValue) ) { dataRowView[realName] = System.DBNull.Value; } else { //automatic type converstion dataRowView[realName] = val; } }
}


SqlParameter[] as a Business Object


An array of SqlParameter won’t often be used to represent a business object, but it can happen: For example, you may need to bind values of server controls to an array of SqlParameter, as seen in the sample application. The following code shows you how to make SqlParameter[] a business object.



public class SqlParamBizObj : IBizObject
{ private SqlParameter[] sparams; public SqlParamBizObj(SqlParameter[] sps) { this.sparams = sps; } public SqlParameter[] Parameters { get{return this.sparams;} } public object GetByName(string name) { foreach(SqlParameter sp in sparams) { if(sp.ParameterName.Substring(1).Equals(name)) { return sp.Value; } } throw new ApplicationException(name + " is not found in GetByName"); } public void SetByName(string name, string val, string nullIfValue) { foreach(SqlParameter sp in sparams) { if(sp.ParameterName.Substring(1).Equals(name)) { if((val null && nullIfValuenull) ||(val==nullIfValue) ) { sp.Value = System.DBNull.Value; } else { //automatic type converstion sp.Value = val; } return; } } throw new ApplicationException(name + " is not found in SetByName"); }
}


A Custom Entity as a Business Object


For our purposes a custom entity is a class used to represent a record set of a data table. DataRow and SqlParameter will support null values. However, the lack of support for database null values in .NET 1.1 built-in types (int, string, bool, to name a few) makes it hard to support database null value in a custom entity. The .NET 2.0 Framework overcomes this defect. A workaround for people using .NET 1.1 is to avoid using null values. With this workaround, in the backend table no column would allow null, and all columns have a default value. When a web form is posted back all server controls will get a value. With two-way databinding, you just leave out NullIfValuePostback and you don t need to worry about handling null value when binding from web to business object. I’ll use reflection to implement the IBizObject interface. The following code illustrates a custom entity called Order that is used in the sample application.



public class Order : BizSoftZen.IBizObject
  {
    public Order(){}
  #region private members
    private int mOrderID;
    private string mCustomerID;
    private string mCustomerName;
    private int mEmployeeID;
    private string mEmployeeName;
    private DateTime mOrderDate;
    private DateTime mShippedDate;
    private int mShipVia;
    private string mShipperName;
    private string mShipAddress;
    public int OrderID
    {
      get{return this.mOrderID;}
      set{this.mOrderID = value;}
    }
    public string CustomerID
    {
      get{return this.mCustomerID;}
      set{this.mCustomerID = value;}
    }
    public string CustomerName
    {
      get{return this.mCustomerName;}
      set{this.mCustomerName = value;}
    }
    public int EmployeeID
    {
      get{return this.mEmployeeID;}
      set{this.mEmployeeID = value;}
    }
    public string EmployeeName
    {
      get{return this.mEmployeeName;}
      set{this.mEmployeeName = value;}
    }
    public DateTime OrderDate
    {
      get{return this.mOrderDate;}
      set{this.mOrderDate = value;}
    }
    public DateTime ShippedDate
    {
      get{return this.mShippedDate;}
      set{this.mShippedDate = value;}
    }
    public int ShipVia
    {
      get{return this.mShipVia;}
      set{this.mShipVia = value;}
    }
    public string ShipperName
    {
      get{return this.mShipperName;}
      set{this.mShipperName = value;}
    }
    public string ShipAddress
    {
      get{return this.mShipAddress;}
      set{this.mShipAddress = value;}
    }
    public object GetByName(string name)
    {
      System.Reflection.PropertyInfo prop;
      prop = this.GetType().GetProperty(name);
      return prop.GetValue(this,null);
    }
    public void SetByName(string name, string val, string nullIfValue)
    {
      System.Reflection.PropertyInfo prop;
      prop = this.GetType().GetProperty(name);
      prop.SetValue(this, val, null);
    }
    public void SetByName(string name, object val)
    {
      System.Reflection.PropertyInfo prop;
      prop = this.GetType().GetProperty(name);
      prop.SetValue(this, val, null);
    }
    }


Using Two-way Databinding in a Web Page


The following steps give you a summary on how to use the two-way databinding technique.


  • Create the business object if necessary. You have to create the business object when you use a customized entity. If you use DataRow in DataTable you simply use the wrapped business object as shown above.

  • Use the extended web server controls in your web form instead of the ASP.NET built-in server controls if you want these controls to participate in the data binding. The ID for single maping controls and the Value for multiple mapping controls should follow naming mechanism stated before. If the set of server controls is not within a single container, you should give them a common parent containing control such as Panel or PlaceHolder. If the list of server controls is contained in items of an iterative control such as Repeater, the item (RepeaterItem) will serve as the containing control for the list of server controls.

  • Find out the list of server controls and the business object and call BindFromWeb2BizObject() or BindFromBizObject2Web(). You will have to put this logic into the correct function where data binding happens. The function might be a data bound event handler or any function where data binding can happen such as Page_Load(), Render(), etc.

Don t be intimidated by the above steps. Almost all of them are intuitive and you’ll use a sample application, TwoWayDataBindingTest, to illustrate this. TwoWayDataBindingTest has a web form called NorthwindOrders.aspx. It connects to the Northwind Database and then retrieves two lists of orders. The lists are two Repeater controls. Basically the two lists contain the same data except that the upper list is for view only and will be refreshed any time the page is post back while the lower one allows editing and updating. Figure 1 shows the image of the two lists. The following ASP.NET markup shows how the extended server controls are used in the bottom repeater.



<asp:Repeater ID="rptOrdersEdit" Runat="server">
  <HeaderTemplate>
    <table border="0" cellpadding="2" cellspacing="0" 
      width="100%" style="font-size:xx-small; font-family:verdana; ">
      <tr style="font-weight:bold;" bgcolor="DarkGray" align="center">
        <td>Order ID</td>
        <td>Customer Name</td>
        <td>Employee Name</td>
        <td>Order Date</td>
        <td>Shipped Date</td>
        <td>Shipper Name</td>
        <td>Ship Address</td>
      </tr>
  </HeaderTemplate>
  <ItemTemplate>
      <tr align="center">
        <td>
          <xzb:LiteralEx id="b_OrderID" runat="server"></xzb:LiteralEx></td>
        <td>
          <xzb:DropDownListEx id="b_CustomerID"runat="Server">
          </xzb:DropDownListEx></td>
        <td>
          <xzb:DropDownListEx id="b_EmployeeID" runat="server">
          </xzb:DropDownListEx></td>
        <td>
          <xzb:TextBoxEx id="b_OrderDate"
            runat="Server" FF-FormatString="{0:MM/dd/yyyy}">
          </xzb:TextBoxEx></td>
        <td>
          <xzb:TextBoxEx id="b_ShippedDate"
            runat="Server" FF-FormatString="{0:MM/dd/yyyy}">
          </xzb:TextBoxEx></td>
        <td>
          <xzb:DropDownListEx id="b_ShipVia" runat="server">
          </xzb:DropDownListEx></td>
        <td>
          <xzb:TextBoxEx id="b_ShipAddress" runat="server">
          </xzb:TextBoxEx>
        </td>
      </tr>
  </ItemTemplate>
  <AlternatingItemTemplate>
      <tr align="center" bgcolor="#cccccc">
        <td>
          <xzb:LiteralEx id="ab_OrderID" runat="server">
          </xzb:LiteralEx>
        </td>
        <td>
          <xzb:DropDownListEx id="ab_CustomerID" runat="Server">
          </xzb:DropDownListEx></td>
        <td>
          <xzb:DropDownListEx id="ab_EmployeeID" runat="server">
          </xzb:DropDownListEx></td>
        <td>
          <xzb:TextBoxEx id="ab_OrderDate"
            runat="Server" FF-FormatString="{0:MM/dd/yyyy}">
          </xzb:TextBoxEx></td>
        <td>
          <xzb:TextBoxEx id="ab_ShippedDate"
             runat="Server" FF-FormatString="{0:MM/dd/yyyy}">
           </xzb:TextBoxEx>
        </td>
        <td>
          <xzb:DropDownListEx id="ab_ShipVia" runat="server">
          </xzb:DropDownListEx>
        </td>
        <td>
          <xzb:TextBoxEx id="ab_ShipAddress" runat="server">
          </xzb:TextBoxEx>
      </td>
    </tr>
  </AlternatingItemTemplate>
  <FooterTemplate>
    </table>
  </FooterTemplate>
</asp:Repeater>



NOT VALID: ImageTooWide: The NorthwindOrders.aspx page of the sample code
This figure has been reduced in size to fit in the text. To view the full image Click here


Binding Data from the Business Object to the Web Form


When the DataBind() function is called to bind data from the data source to the repeater, the ItemDataBound event handler rptOrdersEdit_ItemDataBound() will execute. Note that for the bottom repeater all drop down lists should be populated first. The business object is extracted from e.Item.DataItem. For the top repeater the DataItem is of type DataRowView while the custom entity Order is used in the bottom repeater. Doing this demonstrates how different types of business object can be used.



private void rptOrdersTop_ItemDataBound(object sender, RepeaterItemEventArgs e)
{ if(e.Item.ItemType==System.Web.UI.WebControls.ListItemType.Item || e.Item.ItemType==System.Web.UI.WebControls.ListItemType.AlternatingItem) { ArrayList arr = DataBindingHelper.FindChildFormFields(e.Item); DataRowViewBizObj obj = new DataRowViewBizObj((DataRowView)(e.Item.DataItem)); DataBindingHelper.BindFromBizObject2Web(obj,arr); }
}
private void rptOrdersEdit_ItemDataBound(object sender, RepeaterItemEventArgs e)
{ if(e.Item.ItemType==System.Web.UI.WebControls.ListItemType.Item || e.Item.ItemType==System.Web.UI.WebControls.ListItemType.AlternatingItem) { ArrayList arr = DataBindingHelper.FindChildFormFields(e.Item); //Bind customer List DropDownList ddl = (DropDownList)(DataBindingHelper.FindSingleFormField("CustomerID",arr)); this.BindCustomers(ddl); ddl = (DropDownList)(DataBindingHelper.FindSingleFormField("EmployeeID",arr)); this.BindEmployees(ddl); ddl = (DropDownList)(DataBindingHelper.FindSingleFormField("ShipVia",arr)); this.BindShipVias(ddl); Order order = (Order)(e.Item.DataItem); DataBindingHelper.BindFromBizObject2Web(order,arr); }
}


Binding Data from the Web Form to the Business Object


When you click the Update button in NorthwindOrders.aspx, all changes to items in the lower list will be persisted into the database. So in the button event handler btnUpdate_Click() you simply iterate all items in the Repeater control, find out all the controls, bind controls to the business object SqlParamBizObj and finally update. You can also associate a button with each Repeater item and then you need to put the data binding logic and update logic in the ItemComannd event handler. In this way only one item needs updating each time you submit the page.



private void btnUpdate_Click(object sender, System.EventArgs e)
{ foreach(RepeaterItem rpt in this.rptOrdersEdit.Items) { Order order = new Order(); ArrayList arr = DataBindingHelper.FindChildFormFields(rpt); const string SP = "sp_Orders_Update"; SqlParamBizObj spb; spb = new SqlParamBizObj(
SqlHelperParameterCache.GetSpParameterSet(this.NorthWindConnString,
SP,
false)); DataBindingHelper.BindFromWeb2BizObject(arr, spb); using(SqlConnection conn = new SqlConnection(this.NorthWindConnString)) { SqlHelper.ExecuteNonQuery( conn, CommandType.StoredProcedure, SP, spb.Parameters); } } }


In the sample application several simple stored procedures are used. You need to create them in the database before running the sample.


  • Sp_Orders_List retrieves orders according to an range of OrderID.

  • Sp_Orders_Select retrieves one record from the Orders table and populates the Order business object

  • sp_Orders_Update updates an Order.

Comparison with Two-way Databinding in ASP.NET 2.0


Before concluding this article I’d like to make a comparison between the two-way databinding implemented in this article and that available in ASP.NET. Though our code is based in .NET 1.1 Framework it can be easily ported to ASP.NET 2.0 since I’ve not used anything specific to .NET 1.1. The two-way databinding in ASP.NET 2.0 takes more advantage of declarative syntax as you can connect data source such as business object, sql data source directly to thos server controls that support two-way databinding: GridView, DetailsView and FormView. By contrast the technique introduced here requires some coding to hook up the databinding logic. The goal set in this article is essentially to simplify that logic. However, this technique can be used with more controls including Repeater, DataList and DataGrid. You can further customize server controls to your needs as I did with SingleMappingRadioButtonList and MultiMappingRadioButtonList. To that extent, the technique I presented is more flexible and powerful than what is available out of the box in ASP.NET 2.0.


Conclusion


In this article I introduced and demonstrated a new two-way databinding method to facilitate binding data from business object to web form and vice versa. This method will make your data binding logic simpler, less error prone, more self-documenting, and easyier to maintain, Compared with two-way databinding in ASP2.0 it extends the two-way databinding functionalities to the server controls including Repeater, DataList and DataGrid. You can follow the sample application to use this technique in your own applications.

Founders at Work

Commenting is closed for this article.