Skip to article
Architecture Data Web Development (General) Quick ID: 0705081

Overcome the Barriers Around Using the Custom Class in ASP.NET

In .NET Web applications you can find that in many places custom classes and collections are better choices than the DataSet or DataTable. The custom classes or the custom class collections, which are truly object oriented, allow developers to employ all object-oriented programming techniques.

Updated

In .NET Web applications you can find that in many places custom classes and collections are better choices than the DataSet or DataTable.

The custom classes or the custom class collections, which are truly object oriented, allow developers to employ all object-oriented programming techniques.

Unfortunately, .NET Framework and Visual Studio 2005 don’t provide good support of the custom class. As a result, developers often encounter the following three barriers around using the custom class.

DataSet and DataTable are Microsoft’s solutions to these problems. Microsoft provides lots of built-in support of DataSet and DataTable in their development tools and in the .NET Framework library. But the DataSet and DataTable are far from object-oriented. Besides, they have lots of overhead.
  1. Data access. Though Microsoft makes data access with ADO.NET very straightforward and easy to use, developers still have to spend a considerable amount of time to develop the code connecting a custom class to the database. If you take a look at the Pet Shop reference project and the famous CSLA.NET framework you will find that this is never an easy job. The major difficulties come from filling the custom class with data from DataReader and building the SqlParameters, and they prevent further encapsulation of the data access into a truly object-oriented component.
  2. Data binding. Conceptually you can map a custom class or business object to a group of server controls in a Web Form. Two-way data binding and the ObjectDataSource introduced in ASP.NET 2.0 are designed to resolve this problem, but this approach has many restrictions and thus limits the developers’ programmatic control of data source.
  3. Business object template. Here the template refers to the well-known template pattern (References, [1]), not the code generation template. It turns out that abstracting common functionalities into a business object template is very hard or even impossible for the custom class. Developers typically use code generation as a remedy to this problem, but code generation may be a big concern for later upgrade and maintenance as functionalities and code size of the application increases.

The DataSet and DataTable are Microsoft’s solutions to these problems. Microsoft provides lots of built-in support of DataSet and DataTable in their development tools and in the .NET Framework library. But the DataSet and DataTable are far from object-oriented. Besides, they have lots of overhead.

Personally I think these three barriers are due to three more fundamental inadequacies in the .NET Framework:

  • Type mismatch between SQL and .NET Framework. There is no strict 1:1 mapping between the SQL data types and the .NET data types. When you convert a SQL data type to a .NET data type, the data loses its SQL type information. This leads to the big problem with custom class data access.
  • Run-time identity loss of the custom class property. In other words, the business object does not support getting or setting the value of its properties through the property name. In contrast, you can identify data in SQL and the Web Form through its identity (table column name and control unique ID). You can use reflection to remedy this problem but you’ll sacrifice some performance.
  • Lack of a common interface for the .NET data types and objects. A common interface should contain a common set of properties and methods. For example, the data types in the Web application require the support of nullability, state tracking, string formatting and parsing, strong typing, etc. System.Object is too general to meet this requirement. In terms of composite design pattern (References, [2]), this can be a very serious problem.

In this article I will present a solution to complement these three fundamental inadequacies. Consequently, the solution can completely overcome the barriers around the custom class data access and data binding. My solution also allows you to easily abstract common functionalities into business object templates.

The solution consists of two common interfaces called xType and xObject for the data type and the custom class respectively. xType always keeps its SQL type information. xObject supports the manipulation of the properties of the custom class through their names. The common set of properties and methods for xType include strong typing, nullability, state tracking, string formatting and parsing, clone, etc. xObject includes two common operations: retrieve the custom class property through its name and resolve the real name of the property. I’ll refer the real name to the corresponding table column name in the database. These two operations suffice to fix the second inadequacy above. You can augment xObject to include other common operations such as iteration, serialization, and so on. Before I jump to the detailed implementation of the common interfaces, here’s an early look at how xType and xObject can overcome the three barriers.

  • Data access. Reduce filling the custom class with data from DataReader and building the SqlParameters to one line of code as shown below. This means that you can build a truly object-oriented data access component to support the custom class:
void FillBizObject(xObject obj, IDataReader dr);
    
IList<xObject> FillBizObjectCollection(xObject 
objSeed, IDataReader dr);
    
SqlParameter[] BuildSqlInputParameters(xObject 
obj, params string[] objPropertyNames);
    
SqlParameter[] BuildSqlOutputParameters(xObject 
obj, params string[] objPropertyNames);
  • Data binding. Establish the mapping between a group of Web Form fields and a custom class with one line of code, but we need to provide all related server controls a common interface with respect to data binding. These controls are called xControl in this article:
static void BindDataFromBizObjectToWeb(xObject 
obj, params xControls[] arrxControls);
    
static void BindDataFromWebToBizObject(xObject 
obj, params xControls[] arrxControls);
    
static void BindDataFromBizObjectToWeb(xObject 
obj, Control container, params string[] 
arrControlIDs);
    
static void BindDataFromWebToBizObject(xObject 
obj, Control container, params string[] 
arrControlIDs);
    
  • Create reusable business object templates. The common interfaces make it possible to abstract the common functionalities into the business object templates. The template classes do not only centralize your code, but they also allow you the flexibility to override the default functionalities with your own. In this article I will illustrate this flexibility by wrapping the Active Record and the Dynamic Object into the business object templates.

In the rest of the article I will introduce xType and xObject and their implementations. Then I will illustrate how to use xType and xObject to achieve the goals above. I will conclude with examining some representative code from the Microsoft .NET Pet Shop project (References, [6]) and discuss how you can use xType and xObject in practical Web applications. You will see the benefits that xType and xObject offer.

The Common Interface: xType and xObject

The common interface xType looks like the following:

public interface xType
{
    xType Clone();
    xType NewInstance();
        
    bool IsNull{ get; set;}
    void SetNull();
        
    object BizValue{ get;set;}
        
    bool ValueChanged { get;}
    void ResetChange();
  
    string ToString(string valIfNull, 
                    string format);
    void Parse(string val, string valIfNull);
    
    SqlDbType SqlDbType { get;}
}
The solution can completely overcome the barriers around the custom class data access and data binding. It also allows you to easily abstract common functionalities into business object templates.
  • The xType interface above is far from complete yet. For example, you can incorporate many operations available in SqlTypes to xType. xType presents lots of desirable traits.
  • xType is an interface with a common set of operations for all subtypes. All subtypes such as xSqlInt32, xSqlBoolean, xSqlString, xSqlDateTime, and xImage will implement this interface.
  • All subtypes are strongly typed intrinsically. Though the property BizValue returns the generic type object, the data value is represented internally using a strongly typed variable, so you can safely cast object to specific types only if the data is not null. For convenience, all subtypes have a property called Value to get and set the internal value as strongly typed.
  • xType keeps its SQL type information through a property called SqlDbType. Since you can also deduce its .NET type from its SQL type, you can avoid using reflection to get the type information.
  • xType contains some of the most common operations, for example, nullability support, string formatting and parsing, state tracking, cloning, etc. Some essential operations such as equality checking and type conversion are not provided yet, but you can delegate them to the Value or BizValue property.
  • You could add another property called Name to xType. Name is related to how a business object names its data. I will not deal with this issue in this article.

SQL has 24 types. Accordingly you need to provide 24 corresponding subtypes in .NET as listed in Table 1. I’ve implemented the xTypes marked with a red asterisk in the sample code in this article. Since lots of SQL types map to the same .NET type, the workload is not as big as it looks.

The xObject common interface looks like:

public interface xObject
{
    xType GetValueByName(string realName);
    string ResolveRealName(string name);
}

In this article, xObject contains only two functions: ResolveRealName and GetValueByName. Function GetValueByName allows you to retrieve the property value through the real name and the returned type is xType. Since the identity of a variable can differ in SQL, a business object, and a Web Form, you can use the ResolveRealName function to get the real name of the property. I assume that the table column name of the data in the SQL code is the real name. There are many ways to implement function GetValueByName. In this article I use reflection to implement GetValueByName for simplicity. I also use a simple naming mechanism: the name of the property in the custom class and the control ID in the Web Form are the real name prefixed with a dash plus some other characters such as m_. Thus resolving the real name is just the removal of the prefix. You can also employ the property attributes and external XML configuration file to establish the name mapping, but this technique requires more coding.

Implementation of xTypes

Implementation of these derived types is fairly self-explanatory. I will only show two representative implementations in Listing 1: xInt32 and xImage. Note these three things:

  • You must ensure that Value is not null when you access the BizValue or Value properties; otherwise an exception will be thrown.
  • If the internal value is represented using reference type with the exception of string, the implementation of state change tracking will only track whether its internal reference has been changed.
  • If an operation does not make sense, either an exception will be thrown or a trivial message will be returned, for example, ToString for xImage will simply return a message.

For simplicity, all derived types inherit from an abstract class, xTypeBase, which provides partial implementation of xType.

Custom Class Data Access

The most troublesome issues in custom data access come from populating the custom class with data from DataReader into the custom class or the custom class collections and building the SqlParameters. xType and xObject allow you to wrap these functionalities into static functions. The following code snippet depicts how you can reduce populating the custom class with data from DataReader into the custom class or the custom class collections to one function call:

public static void FillBizObject(xObject xbo, 
                                 SqlDataReader dr)
{
    int cnt = dr.FieldCount;
    for (int kk = 0; kk < cnt; kk++)
    {
        string realName = dr.GetName(kk);
        xbo.GetValueByName(realName).BizValue =
                             dr.GetValue(kk);
               
     }
}
    
public static xObject[] FillBizObjectCollection(
            xObject objectType, SqlDataReader dr)
{
    ArrayList arr = new ArrayList();
    while(dr.Read())
    {
       xObject xbo = objectType.NewInstance();
       FillBizObject(xbo, dr);
       arr.Add(xbo);
    }
    return (xObject[])arr.ToArray();
}

The function below shows how to build the SqlParameters. Note that the parameter names are passed in as the function parameter. The purpose of doing so is twofold: improve the code readability and inform the function what kind of custom class properties it is operating on. In later sections I will use this technique to deal with generic two-way data binding and create the business object templates:

public static SqlParameter[]     
BuildSqlInputParameters(xObject xbo, 
                        params string[] arrNames)
{
    SqlParameter[] arr = 
                new SqlParameter[arrNames.Length];
    
    for(int kk=0; kk < arrNames.Length; kk++)
    {
       string realName = 
                    ResolveRealName(arrNames[kk]);
       xType xt = xbo.GetValueByName(realName);
       arr[kk] = new SqlParameter("@" + realName,
                                  xt.BizValue);
    }
    return arr;
}

In these functions, xObject serves as the data container passing data back and forth. This is the pattern called Data Transfer Object (References, [3]). Note that you may have other ways or even better ways to write these functions. Here I only want to illustrate how you can wrap these functionalities into a static function with xType and xObject.

With the two hardest problems resolved, you can write a truly object-oriented, data-access component. You can use these two functions to replace tens of lines of data-access-related code in real applications.

Generic Two-way Data Binding in ASP.NET

The basic functionality of a Web Form is to present the data from the data source for viewing and editing. From the perspective of object-oriented thinking, you can map all or part of the properties in a custom class to a group of Web Form server controls (form fields). Practically, because of nullability checking and string formatting and parsing, the mapping requires lots of coding to handle these issues. ASP.NET 2.0 comes with a declarative approach to bind data to and from different data sources, but this method is only available to some ASP.NET 2.0-specific server controls and cannot fully reflect the mapping between the custom class and the group of Web Form fields. The method I will implement can overcome this restriction and establish mapping between a business object and a group of related server controls in the Web Form through data-binding functions below:

static void     
BindDataFromBizObjectToWeb(xObject 
obj, params xControls[] arrxControls);
    
static void BindDataFromWebToBizObject(xObject 
obj, params xControls[] arrxControls);
    
static void BindDataFromBizObjectToWeb(xObject 
obj, Control container, params string[] 
arrControlIDs);
    
static void BindDataFromWebToBizObject(xObject 
obj, Control container, params string[] 
arrControlIDs);

Implementation of this technique requires three steps:

Identify a list of controls where you can bind the data to the properties of a business object. Typically the controls participating in data binding include Literal, Label, TextBox, RadioButton, CheckBox, DropDownList, RadioButtonList, CheckBoxList, ListBox, HiddenField, et al. You can expand this list according to your needs.

Extend the list of the server controls with a common interface called xControl as shown below. All controls must implement the common interface xControl:

public interface xControl
{
 void BindDataFromBizObjectToControl(xObject xbo);
 void BindDataFromControlToBizObject(xObject xbo);
}

Add the extended properties to the controls. Different controls may have different requirements when participating in data binding. These requirements are reflected through the value of the extended properties. For example, when binding data from a business object to Literal, Label, and TextBox, you may need string formatting. For all controls, if data is null, you should specify the replacement value. For the list controls CheckBoxList, RadioButtonList and ListBox, you can map their items to a single property (single mapping) or a group of properties of Boolean type (multiple mapping). For single mapping, Control ID corresponds to the property name of the business object. For multiple mapping, each ListItem Value will map to a property of the custom class. If you want to use null instead of some string value when the data is posted back, NullIfValuePostBack can meet your needs, but very few of the controls will need this property. I’ve summarized all these requirements in Table 2. For purposes of clarification, I simply split the list control into two types of controls: single mapping and multiple mapping. Note that ID or ListItem Value may not be the same as the property name of the business object. A supportive function ResolveRealName will resolve the real name from ID or ListItem Value.

After you resolve these thorny issues, implementing xControl becomes a fairly easy job. Listing 2 depicts the sample implementation of xTextBox. Listing 3 gives the implementation of the four data-binding functions. It also demonstrates how important it is to define string conversion functions in xType. In Listing 4 I present a different approach to implement this generic two-way data binding.

A business object template is superior to the code generation template in that code generation results in multiple copies of the code template and thus is hard to maintain and scale the application.

For example, to use this technique in your application, consider two-way data binding with Repeater. RepeaterItem will act as a control container, and in the Repeater, the data-binding event handler code looks like the following:

BindDataFromBizObjectToWeb((xObject)dataItem,
    repeaterItem, "txt_FirstName", "txt_LastName",
   "ddl_StateID", ......);

You can also take advantage of declarative syntax in ASPX code. Any public properties in a Control can be assigned value using markup code. The following code illustrates this usage:

<ccl:TextBoxEx    id="txt_BirthDate" 
                  Runat="Server" 
                  FormatString="{0:d}" 
                  ReplacementIfNull="">
</ccl:TextBox>

Creating Reusable Business Object Templates

I often encounter some common functionalities in the business object. For purposes of reusability, I want to abstract these common functionalities into a business object template. For example, I often encounter data patterns (Active Record, data mappers, etc), iteration, and XML serialization in data-driven Web applications. The current solution for developers is to create code-generation templates and use code-generation tools such as CodeSmith to generate the custom class as needed. But with xType and xObject, you can easily abstract these data patterns into the base template classes with default implementations. In addition, you can augment these two interfaces to support other functionalities such as iteration, XML serialization, object factory, events, etc.

A business object template is superior to the code generation template in that code generation results in multiple copies of the code template and thus is hard to maintain and scale the application.

In this article I will illustrate how to create the business object template by abstracting the Active Record and Dynamic Object into template classes.

Active Record

Active Record is an extremely useful enterprise application pattern (References, [5]). Typically developers use it to mimic a table record with four basic data access operations: Create, Retrieve, Update, and Delete, commonly known as CRUD. Listing 4 depicts the template class called ActiveRecordBase for the Active Record pattern. One feature that makes this implementation distinct from others is that it can update only changed fields by building a dynamically updated SQL statement, showcasing state tracking in xType. You can choose SQL text or a stored procedure to accomplish CRUD. However, dynamic updating only works with SQL text. I assume that the database table uses an identity column as its primary keys. For simplicity, I only show two static supportive functions: BuildTableUpdateString and UpdateDataRecord. I use BuildTableUpdateString to build dynamic SQL update text and UpdateDataRecord to execute the updated SQL text. In ActiveRecordBase, you will find a sample implementation of ResolveRealName and GetValueByName. To create an Active Record, you only need to extend ActiveRecordBase and supply all class properties, and then an Active Record is ready for use.

Dynamic Business Object

When developing Web applications using the custom class, I often find that I need a way to quickly create and instantiate a custom class object, which I refer to as a dynamic object. A Hashtable or ArrayList can serve this purpose. The property name and type information are passed in through the constructor of the dynamic object. A dynamic object implemented with xType has two advantages: the data is internally strongly typed all the time and the number of properties may vary. You can also create a dynamic object according to some external configuration setting. You can find applications of the dynamic object in the places where performance is not a critical issue, for example, your Web Forms dealing with application configuration settings. The following code snippet illustrates two types of dynamic objects: one has a varying number of properties and the other does not:

public abstract class VaringDynamicObjectBase :
 xObject
{
   Hashtable hashTable = new Hashtable();
    
   public xType GetValueByName(string realName)
   {
      return (xType) hashTable[realName];
   }
        
   public string ResolveRealName(string name)
   {
      return name;
   }
}
    
public abstract class QuickDynamicObjectBase : 
 xObject
{
   Hashtable hashTable = new Hashtable();
    
   public xType GetValueByName(string realName)
   {
      return (xType)hashTable[realName];
   }
    
   public string ResolveRealName(string name)
   {
      return name;
   }
        
   public QuickDynamicObjectBase(
string[] propNames, SqlDbType[] propTypes)
   {
      for(int kk = 0; kk< propTypes.Length; kk++)
      {
          hashTable.Add(propNames[kk],
    xTypeHelp.CreatexTypeInstance(propTypes[kk]));
      }
   }
}

Use the Techniques in Your Application

With the two hardest problems resolved, you can write a truly object-oriented, data-access component. You can use these two functions to replace tens of lines of data-access-related code in real applications.

You can easily adapt xType, xObject, and all techniques in this article to your applications. What you are required to do is to change a data type to a specific xType and have your business object inherit from xObject or the business object template. Then you can enjoy the neat SqlParameter building, populating a data business object or business object collection from SQL, active record, dynamic object, and generic two-way data binding. The implementation of xObject in the article uses reflection and is not optimal. You can replace them with your own. The only inconvenience is that accessing the value of a business object property value needs a little more typing.

To better understand the advantages of xType and xObject, I’ll look at some code in the real application. Listing 6 shows some code excerpted from the Pet Shop project. Instead of modifying the code, I will point out how you can simplify the code with the use of xType and xObject.

Listing 5 depicts two functions: Insert and GetOrder excerpted from the Order.cs in the SQLServerDAL project of the Pet Shop. Look at the bulky code dealing with creating SQL parameters and populating data into the custom class. If you change the data type in the custom classes OrderInfo and AddressInfo to xType in the Model project, obviously you can reduce the tens of lines of code into a couple lines of code. You can go further to inherit the custom classes from the active record. Note that Pet Shop emphasizes using the stored procedures. It is not hard to create a stored-procedure based, active-record template. However, this requires you to restructure the code a little bit. I believe the code size will be further drastically reduced because of the built-in support of CRUD in the active record.

Next, look at the data-binding code in the user control AddressForm.ascx.cs in Listing 6. Actually you can replace the chunk of code with two data-binding function calls. You need to use xControls instead of those plain server controls.

Conclusion

At this point you probably get the concept of xType and xObject. In this article I can not talk about all issues around using xType and xObject. But you can see how you will benefit from them from the Pet Shop project In a word, xType and xObject are deliberately contrived to overcome the barriers resulted from SQL and .NET type mismatch and business object run-time identity lost. They connect the business object to the underlying data access, Web Form data binding, and application-level design patterns. You can use them to create reusable business object templates and avoid using or lessen the dependency on code generation. xType can make your application more compact, more readable, and easy to change and maintain.

References

[1] Gamma, Erich, Richard Helm, Ralph Johhnson, and John Vlissides. 1995. Design Patterns: Elements of Resuable Object-oriented Software, P325.

[2] Gamma, Erich, Richard Helm, Ralph Johhnson, and John Vlissides. 1995. Design Patterns: Elements of Resuable Object-oriented Software, P163

[3] Martin Flower. 2002. Patterns of Enterprise Application Architecture, P486

[4] Bill Xie. 2006. “Two-way Databinding”, http://www.asptoday.com/Content.aspx?id=2437">http://www.asptoday.com/Content.aspx?id=2437

[5] Martin Flower. 2002. Patterns of Enterprise Application Architecture, P160

[6] Microsoft .NET Pet Shop 4. http://msdn2.microsoft.com/en-us/library/aa479070.aspx">http://msdn2.microsoft.com/en-us/library/aa479070.aspx

Listing 1: Implemenations of xInt32 and xImage

// xInt32 is a reference implementation of value type while       
// xImage is a reference implementation of reference type.       
// Similarly, you can implement all the remaining types.      
      
[Serializable]
public abstract class xTypeBase : xType
{      
   public abstract xType Clone();
   public abstract xType NewInstance();
    
   protected bool _isNull;
   public bool IsNull { get { return this._isNull; } }
   public void SetNull()
   {
      this._isNull = true;
      _valueChanged = false;
   }
   protected void FlipNull()
   {
      this._isNull = false;
   }
      
   protected bool _valueChanged = false;
   public bool ValueChanged { get { return this._valueChanged; } }
   public void ResetChange() { this._valueChanged = false; }
      
   public abstract object BizValue { get; set;}
      
   public string ToString(string valIfNull, string format)
   {
      if (this.IsNull)
          return valIfNull;
      if (format != null)
          return string.Format(format, this.BizValue);
      else
          return Convert.ToString(this.BizValue);
   }
      
   public abstract void Parse(string val, string valIfNull);
      
   public override string ToString()
   {
      return ToString(string.Empty, null);
   }
   
   public abstract SqlDbType SqlDbType { get;}
}
      
      
[Serializable]
public sealed class xInt32 : xTypeBase
{
   private int _value;
   public xInt32() { this._isNull = true; }
   public xInt32(int i) { this._value = i; FlipNull(); }
      
   public override xType Clone()
   {
       xInt32 xt = new xInt32();
       if (!this.IsNull)
       {
          xt._value = this.Value;
       }
      
       return xt;
   }
      
   public override xType NewInstance()
   {
       return new xInt32();
   }
      
   public override SqlDbType SqlDbType 
   { 
       get { return SqlDbType.Int; } 
   }
   public override object BizValue
   {
       get
       {
           xTypeHelp.ExceptionIfNull(this);
           return this._value;
       }
       set
       {
           if (value == null || System.Convert.IsDBNull(value))
           {
              this._valueChanged = !_isNull;
              this.SetNull();
           }
           else
           {
              int val = System.Convert.ToInt32(value);
              if (this._isNull)
              {
                  this._valueChanged = true;
              }
              else
              {
                  this._valueChanged = (this._value != val);
              }
              this._value = val;
              this.FlipNull(); //do not forget
           }
        }
   }
        
   public int Value
   {
        get { return Convert.ToInt32(BizValue); }
        set
        {
            if (this._isNull)
            {
               this._valueChanged = true;
            }
            else
            {
               this._valueChanged = (this._value != value);
            }
            this._value = value;
            this.FlipNull();
        }
   }
      
      
   public override void Parse(string val, string valIfNull)
   {
        if (val == null || val == valIfNull)
        {
             this._valueChanged = !this._isNull;
             this.SetNull();
             return;
        }
        this.Value = System.Convert.ToInt32(val);
   }
}
      
      
[Serializable]
public sealed class xImage : xTypeBase
{
   private byte[] _value;
   public xImage() { SetNull(); }
        
   public xImage(byte[] arr)
   {
       if (arr == null)
       {
           SetNull(); return;
       }
       this._value = arr; //shallow copy 
       FlipNull();
   }
      
   public override xType Clone()
   {
       if (this.IsNull)
       {
           return new xImage();
       }
       else
       {
           xImage xt = new xImage();
           byte[] newArr = new byte[this._value.Length];
           _value.CopyTo(newArr, 0);
           xt.Value = newArr;
           return xt;
       }
   }
      
   public override xType NewInstance()
   {
       return new xImage();
   }
      
   public override SqlDbType SqlDbType 
   { 
       get { return SqlDbType.Image; } 
   }
      
   public override object BizValue
   {
       get
       {
            xTypeHelp.ExceptionIfNull(this);
            return this._value;
       }
       set
       {
            if (value == null || System.Convert.IsDBNull(value))
            {
                 this._valueChanged = !_isNull;
                 this.SetNull();
            }
            else
            {
                 byte[] val = (byte[])value;
                 this._valueChanged = true;
                 this._value = val;
                 this.FlipNull(); //do not forget
            }
        }
   }
      
        
   public byte[] Value
   {
        get
        {
            xTypeHelp.ExceptionIfNull(this);
            return this._value;
        }
        set
        {
            if (value == null)
            {
                 this._valueChanged = !_isNull;
                 this.SetNull(); return;
            }
            this._valueChanged = true;
            this._value = value;
            this.FlipNull(); //do not forget
        }
   }
      
   public override void Parse(string strValue, 
                               string dBNullIfString)
   {
         throw new ApplicationException("xImage does not support" +
                     "method FromWebString!");
   }
}
      

Listing 2: Implementation of the extended server control

// xTextBox serves as a reference implementation. Similarly,       
// you can implement other controls.      
public class TextBoxEx : TextBox, xControl
{
   public string FormatString
   {
      get
      {
         object obj = ViewState["FormatString"];
         return obj == null ? null: (string)obj;
      }
      set
      {
         ViewState["FormatString"] = value;
      }
   }
      
   //default is null
   public string NullIfValuePostBack
   {
       get
       {
          object obj = ViewState["NullIfValuePostBack"];
          return obj == null ? null : (string)obj;
       }
       set
       {
          ViewState["NullIfValuePostBack"] = value;
       }
   }
      
   //default is empty string
   public string ReplacementIfNull
   {
       get
       {
          object obj = ViewState["ReplacementIfNull"];
          return obj == null ? string.Empty : (string)obj;
       }
       set
       {
          ViewState["ReplacementIfNull"] = value;
       }
   }
      
   public void BindDataFromBizObjectToControl(xObject xbo)
   {
        this.Text = xbo.GetValueByName(Util.
        DefaultResolveRealName(this.ID))
        .ToString(this.ReplacementIfNull, 
        FormatString);
   }
      
   public void BindDataFromControlToBizObject(xObject xbo)
   {
xbo.GetValueByName(Util.DefaultResolveRealName(this.ID)).Parse(
          this.Text.Trim(), this.NullIfValuePostBack);
   }
}
        

Listing 3: Implementation of the two-way, data-binding functions

// You can replace these simple functions with your own if       
// you need special controls of data binding.      
public static void BindDataFromBizObjectToWeb(xObject xbo, params
xControl[] arrxControls)
{
   foreach (xControl xc in arrxControls)
   {
       xc.BindDataFromBizObjectToControl(xbo);
   }
}
      
public static void BindDataFromWebToBizObject(xObject xbo, params
xControl[] arrxControls)
{
   foreach (xControl xc in arrxControls)
   {
       xc.BindDataFromControlToBizObject(xbo);
   }
}
      
public static xControl FindxControl(Control container, string 
realName)
{
   foreach (Control con in container.Controls)
   {
      if (ResolveRealName(con.ID) == realName)
         return con as xControl;
   }
      
   throw new ApplicationException(realName + " cannot be found!");
}
      
public static void BindDataFromBizObjectToWeb(xObject xbo, 
Control container, params string[] arrControlIDs)
{
   foreach (string name in arrControlIDs)
   {
      string realName = ResolveRealName(name);
      FindxControl(container, realName).BindDataFromBizObjectToControl(xbo);
   }
}
      
public static void BindDataFromWebToBizObject(xObject xbo, 
Control container, params string[] arrControlIDs)
{
   foreach (string name in arrControlIDs)
   {
       string realName = ResolveRealName(name);
       FindxControl(container, 
       realName).BindDataFromControlToBizObject(xbo);
   }
}

Listing 4: Implementation of the active record template

// Here you will see that xType and xObject allow you to easily      
// wrap those common functionalities found in the business       
// object into the reusable business object template.      
      
public static string BuildTableUpdateString(string tableName,
string[] keyNames, string[] arrFields)
{
   StringBuilder sb = new StringBuilder();
      
   sb.Append("UPDATE ");
   sb.Append(tableName);
   sb.AppendLine();
   sb.Append("SET");
   sb.AppendLine();
   for (int kk = 0; kk < arrFields.Length - 1; kk++)
   {
       sb.Append(arrFields[kk]);
       sb.Append("=@");
       sb.Append(arrFields[kk]);
       sb.Append(",");
       sb.AppendLine();
    }
    sb.Append(arrFields[arrFields.Length - 1]);
    sb.Append("=@");
    sb.Append(arrFields[arrFields.Length - 1]);
    sb.AppendLine();
      
    sb.Append("WHERE ");
    for (int kk = 0; kk < keyNames.Length - 1; kk++)
    {
        sb.Append(keyNames[kk]);
        sb.Append("=@");
        sb.Append(keyNames[kk]);
        sb.Append(" AND ");
    }
    sb.Append(keyNames[keyNames.Length - 1]);
    sb.Append("=@");
    sb.Append(keyNames[keyNames.Length - 1]);
    return sb.ToString();
}
      
public static void UpdateTableRecord(string connString, xObject 
xbo, string tableName, string[] keyNames, string[] arrFields)
{
    string sqltext = BuildTableUpdateString(tableName, keyNames, 
    arrFields);
    using (SqlConnection conn = new SqlConnection(connString))
    {
       SqlCommand cmd = conn.CreateCommand();
       cmd.CommandType = CommandType.Text;
       cmd.CommandText = sqltext;
       cmd.Parameters.AddRange(BuildSqlInputParameters(xbo, 
       keyNames));
       cmd.ExecuteNonQuery();
    }
}
      
      
public abstract class ActiveRecordBase : xObject
{
    private string mTableName;
    private string mConnString;
      
    public ActiveRecordBase(string connString, string tableName)
    {
        mConnString = connString;
        this.mTableName = tableName;
    }
      
    public virtual void Create(string[] arrFields, string 
identityFieldName)
    {
        InsertTableRecord(mConnString, this, this.mTableName, 
GetRealNames(arrFields), ResolveRealName(identityFieldName));
    }
             
    public virtual void Retrieve(string[] arrKeys, string[] 
arrFields)
    {
        RetrieveTableRecord(mConnString, this, mTableName, 
GetRealNames(arrKeys), GetRealNames(arrFields));
    }
      
    public virtual void Update(string[] arrKeys, string[] 
arrFields)
    {
        UpdateTableRecord(mConnString, this, mTableName, 
GetRealNames(arrKeys), GetRealNames(arrFields));
    }
      
    public virtual void Delete(string[] arrKeys, string[] 
arrFields)
    {
        DeleteTableRecord(mConnString, this, mTableName, 
GetRealNames(arrKeys), GetRealNames(arrFields));
    }
      
    private string[] GetRealNames(string[] arrNames)
    {
        string[] arr = new string[arrNames.Length];
        for (int kk = 0; kk < arrNames.Length; kk++)
            arr[kk] = this.ResolveRealName(arrNames[kk]);
        return arr;
    }
      
      
    public xType GetValueByName(string realName)
    {
        PropertyInfo[] arrProps = 
this.GetType().GetProperties(BindingFlags.Public | 
BindingFlags.GetProperty);
      
        foreach (PropertyInfo prop in arrProps)
        {
           if (ResolveRealName(prop.Name).
           ToLower().Equals(realName.ToLower()))
               return (xType)(prop.GetValue(this, null));
        }
      
        throw new ApplicationException(realName + " is not 
        found!");
    }
      
    public string  ResolveRealName(string name)
    {
        return xDataBinder.ResolveRealName(name);
    }
}

Listing 5: Data access code excepted from Order.cs in the Pet Shop project

      
public void Insert(OrderInfo order) {
            StringBuilder strSQL = new StringBuilder();
      
            // Get each command's parameter arrays
            SqlParameter[] orderParms = GetOrderParameters();
      
            SqlCommand cmd = new SqlCommand();
      
            // Set up the parameters
            orderParms[0].Value = order.UserId;
            orderParms[1].Value = order.Date;
            orderParms[2].Value = order.ShippingAddress.Address1;
            orderParms[3].Value = order.ShippingAddress.Address2;
            orderParms[4].Value = order.ShippingAddress.City;
            orderParms[5].Value = order.ShippingAddress.State;
            orderParms[6].Value = order.ShippingAddress.Zip;
            orderParms[7].Value = order.ShippingAddress.Country;
            orderParms[8].Value = order.BillingAddress.Address1;
            orderParms[9].Value = order.BillingAddress.Address2;
            orderParms[10].Value = order.BillingAddress.City;
            orderParms[11].Value = order.BillingAddress.State;
            orderParms[12].Value = order.BillingAddress.Zip;
            orderParms[13].Value = order.BillingAddress.Country;
            orderParms[14].Value = order.OrderTotal;
            orderParms[15].Value = order.BillingAddress.FirstName;
            orderParms[16].Value = order.BillingAddress.LastName;
            orderParms[17].Value = order.ShippingAddress.FirstName;
            orderParms[18].Value = order.ShippingAddress.LastName;
      orderParms[19].Value = order.AuthorizationNumber.Value;
      
            foreach (SqlParameter parm in orderParms)
                cmd.Parameters.Add(parm);
      
            // Create the connection to the database
            using (SqlConnection conn = new 
            SqlConnection(SqlHelper.
            ConnectionStringOrderDistributedTransaction)) {
      
                // Insert the order status
                strSQL.Append(SQL_INSERT_ORDER);
                SqlParameter[] itemParms;
                // For each line item, insert an orderline record
                int i = 0;
                foreach (LineItemInfo item in order.LineItems) {
                    strSQL.Append(SQL_INSERT_ITEM).Append(" @ID")
                    .Append(", @LineNumber").Append(i).Append(", 
                     @ItemId").Append(i).Append(", @Quantity")
                     .Append(i).Append(", 
                     @Price").Append(i).Append("); 
                     SELECT @ERR=@ERR+@@ERROR;");
      
                    //Get the cached parameters
                    itemParms = GetItemParameters(i);
      
                    itemParms[0].Value = item.Line;
                    itemParms[1].Value = item.ItemId;
                    itemParms[2].Value = item.Quantity;
                    itemParms[3].Value = item.Price;
                    //Bind each parameter
                    foreach (SqlParameter parm in itemParms)
                        cmd.Parameters.Add(parm);
                    i++;
                }
      
                conn.Open();
                cmd.Connection = conn;
                cmd.CommandType = CommandType.Text;
                cmd.CommandText = strSQL.Append("SELECT @ID, 
                @ERR").ToString();
      
                // Read the output of the query, should return 
                // error count
                using (SqlDataReader rdr = 
                cmd.ExecuteReader(CommandBehavior.
                CloseConnection)) {
                    // Read the returned @ERR
                    rdr.Read();
// If the error count is not zero throw an exception      
                    if (rdr.GetInt32(1) != 0)
                        throw new ApplicationException("DATA 
                        INTEGRITY ERROR ON ORDER INSERT 
                        - ROLLBACK ISSUED");
                }
                //Clear the parameters
                cmd.Parameters.Clear();
            }
        }
      
        /// Read an order from the database
        /// <param name="orderId">Order Id</param>
        /// <returns>All information about the order</returns>
        public OrderInfo GetOrder(int orderId) {
      
            OrderInfo order = new OrderInfo();
      
            //Create a parameter
            SqlParameter parm = new SqlParameter(PARM_ORDER_ID, 
            SqlDbType.Int);
            parm.Value = orderId;
      
            //Execute a query to read the order
            using (SqlDataReader rdr = 
            SqlHelper.ExecuteReader(SqlHelper.
            ConnectionStringOrderDistributedTransaction, 
            CommandType.Text, SQL_SELECT_ORDER, parm)) {
      
                if (rdr.Read()) {
      
                    //Generate an order header from the first row
                    AddressInfo billingAddress = new 
                    AddressInfo(rdr.GetString(5), 
                    rdr.GetString(6), rdr.GetString(7), 
                    rdr.GetString(8), rdr.GetString(9), 
                    rdr.GetString(10), rdr.GetString(11), 
                    rdr.GetString(12), null, "email");
      
                    AddressInfo shippingAddress = new 
                    AddressInfo(rdr.GetString(13), 
                    rdr.GetString(14), rdr.GetString(15), 
                    rdr.GetString(16), rdr.GetString(17), 
                    rdr.GetString(18), rdr.GetString(19), 
                    rdr.GetString(20), null, "email");
      
                    order = new OrderInfo(orderId, 
                    rdr.GetDateTime(0), rdr.GetString(1), 
                    null, billingAddress, shippingAddress, 
                    rdr.GetDecimal(21), null, null);
      
                    IList<LineItemInfo> lineItems = new 
                    List<LineItemInfo>();
                    LineItemInfo item = null;
      
                    //Create the lineitems from the first row 
                    //and subsequent rows
                    do {
                        item = new LineItemInfo(rdr.GetString(22),
                        string.Empty, rdr.GetInt32(23), 
                        rdr.GetInt32(24), rdr.GetDecimal(25));
                        lineItems.Add(item);
                    } while (rdr.Read());
      
                    order.LineItems = new 
                    LineItemInfo[lineItems.Count];
                    lineItems.CopyTo(order.LineItems, 0);
                }
            }
      
            return order;
        }

Listing 6: Data-binding access code excepted from AddressForm.ascx.cs in the Pet Shop project.

// You can replace them with one databinding function call.      
public AddressInfo Address {
         get {
      
                // Return null if control is empty
                if (string.IsNullOrEmpty(txtFirstName.Text) && 
                   string.IsNullOrEmpty(txtLastName.Text) && 
                   string.IsNullOrEmpty(txtAddress1.Text) && 
                   string.IsNullOrEmpty(txtAddress2.Text) && 
                   string.IsNullOrEmpty(txtCity.Text) && 
                   string.IsNullOrEmpty(txtZip.Text) && 
                   string.IsNullOrEmpty(txtEmail.Text) && 
                   string.IsNullOrEmpty(txtPhone.Text))
                    return null;
      
                  // Make sure you clean up the input
                  string firstName = 
                  WebUtility.InputText(txtFirstName.Text, 50);
                  string lastName = 
                  WebUtility.InputText(txtLastName.Text, 50);
                  string address1 = 
                  WebUtility.InputText(txtAddress1.Text, 50);
                  string address2 = 
                  WebUtility.InputText(txtAddress2.Text, 50);
                   string city = 
                   WebUtility.InputText(txtCity.Text, 50);
                   string zip = 
                   WebUtility.InputText(txtZip.Text, 10);
                   string phone = 
                   WebUtility.InputText(WebUtility.
                   CleanNonWord(txtPhone.Text), 10);
                       string email = 
                       WebUtility.InputText(txtEmail.Text, 80); 
                       string state = 
                       WebUtility.InputText(listState.
                       SelectedItem.Value, 2);
                       string country = 
                       WebUtility.InputText(listCountry.
                       SelectedItem.Value, 50);
                
                       return new AddressInfo(firstName, 
                       lastName, address1, address2, city, state, 
                       zip, country, phone, email);
                  }
                  set {
                        if(value != null) {
                        if(!string.IsNullOrEmpty(value.FirstName))
                           txtFirstName.Text = value.FirstName;
                        if(!string.IsNullOrEmpty(value.LastName))
                           txtLastName.Text = value.LastName;
                        if(!string.IsNullOrEmpty(value.Address1))
                           txtAddress1.Text = value.Address1;
                        if(!string.IsNullOrEmpty(value.Address2))
                           txtAddress2.Text = value.Address2;
                        if(!string.IsNullOrEmpty(value.City))
                           txtCity.Text = value.City;
                        if(!string.IsNullOrEmpty(value.Zip))
                           txtZip.Text = value.Zip;
                        if(!string.IsNullOrEmpty(value.Phone))
                           txtPhone.Text = value.Phone;
                        if(!string.IsNullOrEmpty(value.Email))
                           txtEmail.Text = value.Email;
                        if(!string.IsNullOrEmpty(value.State)) {
                           listState.ClearSelection();
                           listState.SelectedValue = value.State;
                         }
                        if(!string.IsNullOrEmpty(value.Country)) {
                           listCountry.ClearSelection();
                           listCountry.SelectedValue = 
                           value.Country;
                           }
                  }
               } 
      }

SQL Server Type .NET Framework Type xType
bit Boolean xBoolean*
tinyint Byte xTinyInt*
smallint Int16 xInt16
int Int32 xInt32*
bigint Int64 xInt64*
real Single xSingle
decimal Decimal xDecimal
float Double xDouble
numeric Decimal xNumeric
smallmoney Decimal xSmallMoney
money Decimal xMoney
smalldatetime DateTime xSmallDateTime
datetime DateTime xDateTime*
timestamp Byte[] xTimeStamp
uniqueidentifier Guid xUniqueIdentifier
binary Byte[] xBinary
image Byte[] xImage*
varbinary Byte[] xVarBinary
char String xChar
nchar String xNChar
varchar String xString*
nvarchar String xNString
text String xText
ntext String xNText
sql_variant Object xSqlVariant

FormatString ReplacementIfNullDefault Value NullIfValuePostBackDefault Value Mapping Property Name
LiteralEx yes YesEmpty String No Single ID
LabelEx yes YesEmpty String No Single ID
TextBoxEx yes YesEmpty String Yesnull Single ID
RadioButtonEx No YesFalse No Single ID
CheckBoxEx No YesFalse No Single ID
HiddenFieldEx No YesEmpty String No Single ID
DropDownListEx No YesEmpty String Yesnull Single ID
SingleRadioButtonList No Yes No Single ID
MultiRadioButtonList No YesFalse No Multiple ListItem Value
CheckBoxListEx No YesFalse No Multiple ID
SingleListBox No Yes No Single ID
MultiListBox No YesFalse No Multiple ListItem Value

Open original image