Overcoming the Barriers Around Using Custom Classes in ASP.NET

ompared to classic ASP, ASP.NET makes a big leap forward in bringing you the advantages of object-oriented programming. However, some aspects like the SQL and .NET data type mismatch, lack of useful common interfaces for all data types, data access, and data binding in Web forms seem to not favor the object-oriented approach and result in lots of coding.

DataSet and DataTable are Microsoft’s solutions to these problems. Microsoft provides lots of built-in support for DataSet and DataTable classes 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. Developers often resort to building custom classes, but unfortunately, the .NET Framework and Visual Studio 2005 don’t provide good support for custom classes. As a result, developers often encounter the following three barriers around using custom classes.

  • 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 a 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.
  • 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.
  • 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 difficult or even impossible for custom classes. 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 the application’s code size increases.

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

  • The type mismatch between SQL and the .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 big problems with custom class data access.
  • Run-time identity loss of custom class properties. Business objects often don’t support getting or setting property values using the property name–property names often have little resemblance to the corresponding data field. In contrast, you can identify data in SQL and Web Forms via its identity (table column name and unique control 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 web applications 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 address these three fundamental inadequacies. The solution can completely overcome the barriers around custom class data access and data binding. The 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 data type and custom classes respectively. The xType interface always maintains its SQL type information. The xObject interface supports manipulating custom class properties by name. 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 a custom class property by name and resolve the real name of the property. The “real name” refers 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 the process of filling custom classes 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 a custom class:
  •    void FillBizObject(xObject obj, IDataReader dr);   IList 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 and discuss how you can use xType and xObject in practical Web applications. You will see the benefits that xType and xObject offer.

Editor’s Note: This article was first published in the May/June 2007 issue of CoDe Magazine, and is reprinted here by permission.

The Common Interface: xType and xObject
The common-interface xType code 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 custom class data access and data binding. It also allows you to easily abstract common functionalities into business object templates.

Some points to keep in mind:

  • 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.

Table 1. The table lists the SQL types along with their corresponding .NET subtypes.

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

I’ve implemented the xTypes marked with an 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. The GetValueByName function lets you retrieve the property value through the real name; 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 just means removing the prefix. You can also employ the property attributes and external XML configuration file to establish the name mapping, but that technique requires more coding.

Implementing xTypes
Implementating these derived types is fairly self-explanatory. Listing 1 shows two representative implementations: 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 a 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 a partial implementation of xType.

Custom Class Data Access
The most troublesome issues in custom data access come from populating a custom class or a custom class collection with data from a DataReader and building the SqlParameters. xType and xObject let you wrap these functionalities into static functions. The following code snippet depicts how you can reduce the process of populating a custom class or collection from a DataReader into 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 a 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 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. 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);

Implementing this technique requires three steps:

  1. Identify a list of controls where you can bind the data to the properties of a business object. Typically, controls that participate in data binding include Literal, Label, TextBox, RadioButton, CheckBox, DropDownList, RadioButtonList, CheckBoxList, ListBox, HiddenField, etc. You can expand this list according to your needs.
  2. 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);   }
  1. 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. You can map the items for list controls such as CheckBoxList, RadioButtonList and ListBox to a single property (single mapping) or to a group of properties of Boolean type (multiple mapping). For single mapping, the Control ID corresponds to the property name of the business object. For multiple mapping, each ListItem's Value will map to a custom class property. 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 a ListItem Value.
Table 2. The table shows the properties and default values for each common control type.

FormatString

ReplacementIfNull

Default Value

NullIfValuePostBack

Default Value

Mapping

Property Name

?

LiteralEx

yes

Yes

Empty String

No

Single

ID

LabelEx

yes

Yes

Empty String

No

Single

ID

TextBoxEx

yes

Yes

Empty String

Yes

null

Single

ID

RadioButtonEx

No

Yes

False

No

Single

ID

CheckBoxEx

No

Yes

False

No

Single

ID

HiddenFieldEx

No

Yes

Empty String

No

Single

ID

DropDownListEx

No

Yes

Empty String

Yes

null

Single

ID

SingleRadioButtonList

No

Yes

No

Single

ID

MultiRadioButtonList

No

Yes

False

No

Multiple

ListItem Value

CheckBoxListEx

No

Yes

False

No

Multiple

ID

SingleListBox

?

No

Yes

No

Single

ID

MultiListBox

No

Yes

False

No

Multiple

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 for 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.

For example, to use this technique in your application, consider two-way data binding with Repeater. RepeaterItem acts 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:

      

Creating Reusable Business Object Templates
I often encounter common functionalities in business objects. 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.

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

Active Record
Active Record is an extremely useful enterprise application pattern (References, [4]). 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 use dynamic SQL or a stored procedure to accomplish CRUD. However, dynamic updating only works with dynamic SQL. I assume that the database table uses an identity column as its primary key. 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 need only extend ActiveRecordBase and supply all class properties; the Active Record is then ready for use.

Dynamic Business Object
When developing Web applications using custom classes, 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. You pass in the property name and type information through the dynamic object constructor. A dynamic object implemented with xType has two advantages: the data is always strongly typed internally 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, in 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]));         }      }   }

Using the Techniques in Your Applications
You can easily adapt xType, xObject, and all the techniques in this article to your applications. All that 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 features of 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, so you can replace them with your own implementations to improve the performance. The only inconvenience is that you need to do a little more typing to access the value of a business object property.

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.

To better understand the advantages of xType and xObject, I'll show some code from a 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 Order.cs in the SQLServerDAL project of the Pet Shop. Look at the bulky code that deals 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, you can reduce the tens of lines of code into a couple lines of code. You can go further and inherit the custom classes from the active record. Note that Pet Shop emphasizes using stored procedures. It is not hard to create a stored-procedure based, active-record template. However, doing so 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 entire chunk of code with two data-binding function calls by using xControls instead of those plain server controls.

Conclusion
At this point you probably get the concept of xType and xObject. In this article I cannot discuss all the issues around using xType and xObject. But you should be able to see how you can 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 lost business object run-time identity. 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 reduce the dependency on code generation. xType can make your application more compact, more readable, and easier to change and maintain.

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Overview

Recent Articles: