Writing Generic Data Access Code in ASP.NET 2.0 and ADO.NET 2.0

 

Dr. Shahram Khosravi
Information Solutions

April 2005

Applies to:

   Microsoft ADO.NET 2.0
   Microsoft ASP.NET 2.0
   Microsoft .NET Framework 2.0
   Microsoft Visual Web Developer 2005 Express Edition Beta
   C# programming language

Summary: Use a step-by-step approach to learn how to use different ASP.NET 2.0 and ADO.NET 2.0 tools and techniques for writing generic data access code. (18 printed pages)

Download the associated sample code: GenericDataAccessSample.exe.

Introduction

Most Web applications contain data access code to access the underlying data store to perform basic data operations such as Select, Update, Delete, and Insert. This article uses a step-by-step approach to show how page developers can take advantage of different ASP.NET 2.0 and ADO.NET 2.0 tools and techniques to write generic data access code that can be used to access different types of data stores. Writing generic data access code is especially important in data-driven Web applications because data comes from many different sources, including Microsoft SQL Server, Oracle, XML documents, flat files, and Web services, just to name a few.

This article uses a simple Web application as a test bed for all the code presented here. The application consists of two parts: the first part allows the system administrator to send newsletters to all subscribers of a mailing list. The second part allows users to subscribe or unsubscribe from a mailing list. The first part of the article begins by implementing a simple data access code (see Figure 1) to access Microsoft SQL Server and extract the list of subscribers. The code is modified and made more generic over the course of the article.

Figure 1. The GetSubscribers method extracts the list of all subscribers.

public IEnumerable GetSubscribers()
{
    SqlConnection con = new SqlConnection();
    con.ConnectionString = @"Data Source=.\SQLExpress;Integrated Security=True;AttachDBFilename=D:\Application\Data\Database.mdf";

    SqlCommand com = new SqlCommand();
    com.Connection = con;
    com.CommandText = "Select * From Subscribers";
    com.CommandType = CommandType.Text;

    DataSet ds = new DataSet();
    SqlDataAdapter ad = new SqlDataAdapter();
    ad.SelectCommand = com;

    con.Open();
    ad.Fill(ds);
    con.Close();

    return ds.Tables[0].DefaultView;
}

Since the data access code in Figure 1 contains code for creating the ADO.NET objects, such as the SqlConnection, SqlCommand, and SqlDataAdapter instances. The data access code cannot be used to retrieve the list of subscribers from other data stores, such as an Oracle database. Page developers have to modify the data access code (using the GetSubscribers method) every time they need to access a new data store. Next, see how ADO.NET 2.0 uses the provider pattern to help page developers write generic data access code to access different types of data stores.

Provider Pattern in ADO.NET 2.0

The main problem with the GetSubscribers method is that it contains the code for creating the ADO.NET objects. According to the provider pattern, the data access code must delegate the responsibility of providing the code for creating the ADO.NET objects to another class. I refer to this class as the "code provider class" because it provides the code for creating the ADO.NET objects. The code provider class exposes methods such as CreateConnection, CreateCommand, and CreateDataAdapter, where each method provides the code for creating the corresponding ADO.NET object.

Since the code provider class contains the actual code, the same class cannot be used to access different data stores. Therefore, the data access code (the GetSubscribers method) has to be modified and reconfigured to delegate the responsibility of providing the code to a new code provider class each time it is used to access a new data store. The GetSubscribers method is still tied to the code even though it does not contain the code.

The provider pattern offers a solution to this problem and consists of the following steps:

  1. Design and implement an abstract base provider class.

  2. Derive all code provider classes from the abstract base provider class.

  3. Have the data access code (the GetSubscribers method) to use the abstract base class instead of the individual code provider classes.

    The abstract base class delegates the responsibility of providing the code for creating the ADO.NET objects to the appropriate subclass. The abstract base class is named DbProviderFactory. The following presents some of the methods of this class:

    public abstract class DbProviderFactory
    {
            public virtual DbConnection CreateConnection();
            public virtual DbCommand CreateCommand();
            public virtual DbDataAdapter CreateDataAdapter();
    }
    

    Each subclass provides the code for creating the appropriate ADO.NET objects for a particular data store. For instance, the SqlClientFactory subclass provides the code for creating the ADO.NET objects to access Microsoft SQL Server, as shown in Figure 2.

    Figure 2. The SqlClientFactory class and some of its methods

    public class SqlClientFactory : DbProviderFactory
    {
            public override DbConnection CreateConnection()
            {
                    return new SqlConnection();
            }
    
           public override DbCommand CreateCommand()
           {
                    return new SqlCommand();
           }
    
           public override DbDataAdapter CreateDataAdapter()
           {
                 return new SqlDataAdapter();
           }
    }
    

The provider pattern allows the data access code to treat all the subclasses the same because they are all subclasses of the same base class. As far as the data access code is concerned, all subclasses are of type DbProviderFactory. The data access code has no way of knowing the specific type of the subclass being used. This introduces a new problem. If the data access code (the GetSubscribers method) does not know the type of subclass, how can it then instantiate an instance of the subclass?

The provider pattern solution to this problem consists of the following three parts:

  1. A unique string is used to identify each subclass. ADO.NET 2.0 uses the namespace of the subclass as its unique string id. For instance, the unique string id's System.Data.SqlClient and System.Data.OracleClient identify SqlClientFactory and OracleClientFactory subclasses, respectively.
  2. A text file (normally an XML file) is used to store information about all the subclasses. ADO.NET 2.0 uses the machine.config and web.config files to store the required information. The information about a subclass contains, among other things, the unique string id and the name of the type of the subclass. For instance, the information about the SqlClientFactory subclass includes the unique string id System.Data.SqlClient and the name of the type of the subclass, i.e., System.Data.SqlClient.SqlClientFactory.
  3. A static method is designed and implemented. The method could be part of the abstract base class or part of a separate class. ADO.NET 2.0 uses a separate class named DbProviderFactories that exposes the GetFactory static method. The method takes the unique string id of the desired subclass as its only argument and searches through the machine.config file for a subclass with the given unique string id. The method extracts the name of the type of the desired subclass and uses reflection to dynamically create an instance of the subclass.

Data access code (the GetSubscribers method) calls the GetFactory static method and passes the appropriate unique string id to access the instance of the corresponding subclass. After the GetSubscribers method accesses the instance, it calls the appropriate creation methods, such as CreateConnection(), CreateCommand(), etc., to instantiate the appropriate ADO.NET objects, as shown in Figure 3.

Figure 3. The version of the GetSubscribers method that uses the new ADO.NET provider pattern

public IEnumerable GetSubscribers()
{
    DbProviderFactory provider = DbProviderFactories.GetFactory("System.Data.SqlClient");
    DbConnection con = provider.CreateConnection();
    con.ConnectionString = @"Data Source=.\SQLExpress;Integrated Security=True;AttachDBFilename=D:\Application\Data\Database.mdf";
    DbCommand com = provider.CreateCommand();
    com.Connection = con;
    com.CommandText = "Select * From Subscribers";
    com.CommandType = CommandType.Text;

    DataSet ds = new DataSet();
    DbDataAdapter ad = provider.CreateDataAdapter();
    ad.SelectCommand = com;

    con.Open();
    ad.Fill(ds);
    con.Close();

    return ds.Tables[0].DefaultView;
}

The data access code (the GetSubscribers method) delegates the responsibility of providing the code for creating the ADO.NET objects to the code provider class instance that the GetFactory method instantiates and returns. Therefore, the same data access code can be used to access different data stores, such as Microsoft SQL Server and Oracle.

The code for creating the right ADO.NET objects is data-store–specific. The provider pattern in ADO.NET 2.0 removes these data-store–specific parts from the data access code (the GetSubscribers method) to make it more generic. However, the provider pattern does not remove all the data-store–specific parts. Closer inspection of the GetSubscribers method reveals the following remaining data-store–specific parts:

  1. Connection string
  2. Unique string id that identifies the underlying code provider class
  3. Command text
  4. Command type

Unless something is done about the above parts, the data access code is still tied to a particular type of data store. The provider pattern in ADO.NET 2.0 does not help with this problem. However, ADO.NET 2.0 provides us with other tools and techniques to remove the first two data-store–specific parts, such as the connection string and unique string id from the GetSubscribers method.

Connection Strings

Connection strings are some of the most valuable resources in a Web application. They are so important that the .NET Framework 2.0 treats them as "first-class citizens". The web.config file now supports a new section named <connectionStrings> that contains all the connection strings used in an application. Therefore, we will move the connection string from the GetSubscribers method to this section:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <connectionStrings>
          <add 
            name="MySqlConnectionString" 
            connectionString="Data Source=.\SQLExpress;Integrated Security=True;AttachDBFilename=D:\Application\Data\Database.mdf"
      providerName="System.Data.SqlClient"/>
    </connectionStrings>
</configuration>

The <add> subelement of the <connectionStrings> element exposes the following three important attributes:

  • Name—The friendly name of the connection string
  • connectionString—The actual connection string
  • providerName—The unique string id or invariant of the code provider class

NET Framework 2.0 provides the data access code (the GetSubscribers method) with the right tools to generically extract the connection string value from the web.config file as described in the following. The System.Configuration namespace in .NET Framework 2.0 includes a new class named Configuration. This class represents the entire content of a web.config or machine.config file. The data access code cannot use the new operator to directly create an instance of this class.

The class itself exposes a static method named GetWebConfiguration that takes the path to the web.config file and returns an instance of the Configuration class that represents the entire contents of the web.config file:

Configuration configuration = Configuration.GetWebConfiguration("~/");

A class that inherits from the ConfigurationSection class represents every section of the web.config file. The name of the class consists of the name of the section plus the keyword Section. For instance, the ConnectionStringsSection class represents the content of the <connectionStrings> section of the web.config file. The data access code (the GetSubscribers method) cannot use the new operator to directly create an instance of the ConnectionStringsSection class. The Configuration class exposes a collection property named Sections that contains all the objects that represent different sections of the web.config file:

ConnectionStringsSection section = (ConnectionStringsSection)configuration.Sections["connectionStrings"];

Since Sections is a collection of ConfigurationSection objects, the data access code must type cast the returned instance. After the GetSubscribers method accesses the ConnectionStringsSection object and uses it to access the connection string value:

string connectionString = section.ConnectionStrings["MySqlConnectionString"].ConnectionString;

Figure 4 shows the new version of the GetSubscribers method that contains the required code to extract the connection string in generic fashion.

Figure 4. The version of the GetSubscribers method that extracts the connection string from the web.config file

public IEnumerable GetSubscribers()
{
    DbProviderFactory provider = DbProviderFactories.GetFactory("System.Data.SqlClient");
    DbConnection con = provider.CreateConnection();

    Configuration configuration = Configuration.GetWebConfiguration("~/");
    ConnectionStringsSection section = (ConnectionStringsSection)configuration.Sections["connectionStrings"];
    con.ConnectionString = section.ConnectionStrings["MySqlConnectionString"].ConnectionString;

    DbCommand com = provider.CreateCommand();
    com.Connection = con;
    com.CommandText = "Select * From Subscribers";
    com.CommandType = CommandType.Text;

    DataSet ds = new DataSet();
    DbDataAdapter ad = provider.CreateDataAdapter();
    ad.SelectCommand = com;

    con.Open();
    ad.Fill(ds);
    con.Close();

    return ds.Tables[0].DefaultView;
}

The GetSubscribers method now includes the MySqlConnectionString string, so we still have to modify the GetSubscribers method to add support for a different data store such as Oracle database. It would seem that we are back to square one. Not really. We have gained a few important benefits by moving the connection string from the data access code to the web.config file:

  • The connection string is now the value of the connectionString attribute of the <add> sub element of the connectionStrings element of the web.config file, which is an XML document. The great thing about an XML document is that we can encrypt a single element in the document. We do not have to encrypt the entire document if we only need to protect small part of it. The .NET Framework 2.0 comes with a tool that allows us to encrypt the <connectionStrings> section to protect our most important resource, the connection strings. Imagine how much damage a hacker can do to our valuable database if he gets his hand on our connection strings. Remember connection strings are all a hacker needs to access our database.

  • It may seem that all we have done is to replace the following string

    "Data Source=.\SQLExpress;Integrated Security=True;AttachDBFilename=D:\Application\Data\Database.mdf"
    

    with the new string, MySqlConnectionString. However, there is one big difference. The former string contains the SQL Server database-specific information that does not apply to another database such as Oracle, but the latter string is just a friendly name.

However, the friendly name could still cause problems because it refers to a specific connection string within the <connectionStrings> section of the web.config file. In our example, it refers to the connection string used to access Microsoft SQL Server. This means that the GetSubscribers method (the data access code) has to be modified to use a different friendly name to access a different data store such as Oracle.

To avoid modifying the data access code, we can move the friendly name from the data access code to the <appSettings> section of the web.config file and have the data access code dynamically extract it in runtime as follows:

string connectionStringName = ConfigurationSettings.AppSettings["ConnectionStringName"];

We also move the provider name to the <appSettings> section:

string providerName = ConfigurationSettings.AppSettings["ProviderName"];

Page developers simply change the value attribute of the <add> subelement of the <appSettings> element to the same data access code to access a different data store without making any changes in the data access code itself.

Figure 5 presents the version of the data access code (the GetSubscribers method) that contains the recent changes.

Figure 5. The version of the GetSubscribers method to extract the provider name and the friendly name of the connection string from the web.config file

public IEnumerable GetSubscribers()
{
    string connectionStringName = ConfigurationSettings.AppSettings["ConnectionStringName"];
    string providerName = ConfigurationSettings.AppSettings["ProviderName"];
    Configuration configuration = Configuration.GetWebConfiguration("~/");
    ConnectionStringsSection section = (ConnectionStringsSection)configuration.Sections["connectionStrings"];

    

    DbProviderFactory provider = DbProviderFactories.GetFactory(providerName);
    DbConnection con = provider.CreateConnection();
    con.ConnectionString = section.ConnectionStrings[connectionStringName].ConnectionString;

    DbCommand com = provider.CreateCommand();
    com.Connection = con;
    com.CommandText = "Select * From Subscribers";
    com.CommandType = CommandType.Text;

    DataSet ds = new DataSet();
    DbDataAdapter ad = provider.CreateDataAdapter();
    ad.SelectCommand = com;

    con.Open();
    ad.Fill(ds);
    con.Close();

    return ds.Tables[0].DefaultView;
}

Data Source Controls

The latest version of the GetSubscribers method as shown in Figure 5 is still not generic because of the following issues:

  • The method contains data-store—specific information, i.e., the command text and command type. Therefore, page developers still have to modify the method before they can use it to access a different database.
  • The method returns an instance of the DataView class to its callers so that the method feeds its callers tabular data. We can think of this as a contract between the GetSubscribers method and its callers. The callers expect the GetSubscribers method to honor the contract under all circumstances even when the underlying data store itself is not tabular. Since the GetSubscribers method uses ADO.NET objects to access the underlying data store, it cannot access a hierarchical data store, such as an XML file, where instances of the classes in the System.Xml and its sub namespaces must be used instead of ADO.NET objects.

The main problem with the data access code used in the GetSubscribers method is that it directly contains the actual code that extracts the data from the underlying data store. This is exactly the kind of problem that the .NET Framework 2.0 provider pattern is specifically designed to solve. According to the provider pattern, the GetSubscribers method must delegate the responsibility of providing the code for accessing the data store to a different class. It is called the code provider class. The type of code provider class depends on the type of the data store it accesses. These code provider classes are collectively known as data source controls. ASP.NET 2.0 comes with several different types of data source controls including SqlDataSource, AccessDataSource, ObjectDataSource, XmlDataSource, and SiteMapDataSource controls.

The SqlDataSource control is specifically designed to update, delete, insert, and extract data from relational data stores such as Microsoft SQL Server and Oracle. The AccessDataSource control is a subclass of the SqlDataSource control that knows how to work with Microsoft Access databases. The ObjectDataSource control, on the other hand, uses in-memory business objects as its data store. The XmlDataSource control is specifically designed to extract data from XML documents. However, the XmlDataSource control does not provide write access to the underlying XML document.

Each data source control exposes one or more views of its underlying data store. Each view is an instance of an appropriate class. For instance, the SqlDataSource, AccessDataSource, and ObjectDataSource controls expose views that are instances of SqlDataSourceView, AccessDataSourceView, and ObjectDataSourceView classes, respectively. Views hide the real type of the underlying data store and make it act like the type that the data access code expects. For instance, the GetSubscribers method expects a tabular type of data store because it feeds its clients tabular data. The tabular views allow the GetSubscribers method to extract tabular data from the underlying data store even when the data store itself is a hierarchical data source, such as an XML document. This allows the GetSubscribers method to treat both tabular and hierarchical data stores as tabular data stores.

Data source controls can provide their clients with two types of views: tabular and hierarchical. ASP.NET 2.0 comes with two data source controls that provide both types of views, XmlDataSource and SiteMapDataSource. The rest of the data source controls—SqlDataSource, AccessDataSource, and ObjectDataSource—only present tabular views. However, they can be extended to provide both tabular and hierarchical views.

Tabular Data Source Controls

A tabular data source control makes its underlying data store act like a tabular data store whether or not the data store is tabular. A tabular data store consists of tables of rows and columns where each row represents a data item. The name of a table uniquely identifies and locates the table among other tables in the tabular data store. A tabular view acts like a table, which means views are named.

As discussed before, each data source control class and its associated view class (e.g., the SqlDataSource class and its associated SqlDataSourceView class) provide the actual code for updating, deleting, inserting, and extracting data from the underlying data store. Obviously the code for each data source control and its associated view class is specifically designed to work with a specific type of data store. Therefore, each data source control class and its associated view class are data-store–specific. This poses a serious problem to the GetSubscribers method that uses data source controls and their tabular views to access its underlying data store because it ties the method down to a specific type of data store, which means that the same method cannot be used to access different types of data stores.

ASP.NET 2.0 offers a solution that uses the provider pattern to:

  1. Introduce the IDataSource interface and DataSourceView abstract class
  2. Derive all tabular data source controls from the IDataSource interface
  3. Derive all tabular views from the DataSourceView abstract class

The IDataSource interface and the DataSourceView abstract class delegate the responsibility of providing the actual code for updating, deleting, inserting, and extracting the data from the data store to their appropriate subclasses. The data access code such as the GetSubscribers method must use the methods and properties of the IDataSource interface and DataSourceView abstract class. They must not use any method or property that is specific to a particular data source control class such as SqlDataSource or a particular data source view class such as SqlDataSourceView. The provider pattern allows the data access code to treat all data source controls and their respective data source views in a generic fashion. As far as the data access code is concerned, all data source controls are of type IDataSource and all data source views are of type DataSourceView. The data access code has no way of knowing the actual type of the data source control and data source view object being used. This causes a new problem. If the data access code (the GetSubscribers method) does not know the type of the data source control, how can it then instantiate an instance of it?

As discussed before, the provider pattern provides a solution that consists of the following steps:

  1. A unique string id is used to identify each data source control class.
  2. A text file (normally an XML file) is used to store information about all the data source control classes.
  3. A mechanism is designed and implemented that searches the XML file for a subclass with a given string id.

Now let us see how ASP.NET 2.0 implements the above three tasks of the provider pattern. ASP.NET 2.0 derives all data source controls from the Control class. Why do data source controls derive from the Control class if they do not render HTML markup text? They derive from the Control class so they can inherit the following three important features:

  1. They can be instantiated declaratively.
  2. They save and restore their property values across post backs.
  3. They are added to the control tree of the containing page.

The first feature allows page developers to declaratively instantiate data source controls in their respective .aspx files. Therefore the .aspx file acts as the text or XML file that the second step of the provider pattern requires. The ASP.NET 2.0 control architecture dynamically creates an instance of the declared data source control and assigns the instance to a variable whose name is the value of the ID property of the declared data source control. This takes care of the above first and third steps that the provider pattern requires.

Figure 6 shows the version of the GetSubscribers method that uses the methods and properties of the IDataSource interface and DataSourceView abstract class to access the underlying data store:

Figure 6. The version of the GetSubscribers method that uses the methods and properties of the IDataSource interface and the DataSourceView abstract class

void GetSubscribers()
{
    IDataSource ds = (IDataSource)MySource;
    DataSourceView dv = ds.GetView(String.Empty);
    DataSourceSelectArguments args = new DataSourceSelectArguments();
    if (dv.CanSort)
        args.SortExpression = "Email";
    DataSourceViewSelectCallback callback = new DataSourceViewSelectCallback(SendMail);
    dv.Select(args, callback);
}

The first line of the GetSubscribers method clearly shows that the method treats the data source control as an object of type IDataSource. The method does not and should not care about the real type of the data source control, such as whether it is an SqlDataSource, AccessDataSource, ObjectDataSource, or XmlDataSource control. This will allow page developers to switch from one data source control to another without having to modify the data access code (the GetSubscribers method). The next section will discuss this important issue in more details.

The GetSubscribers method calls the GetView method of the IDataSource object to access its default tabular view object. Notice the GetView method returns an object of type DataSourceView. The GetSubscribers method does not and should not care about the real type of the view object, such as whether it is an SqlDataSourceView, AccessDataSourceView, ObjectDataSourceView, or XmlDataSourceView object.

Next, the GetSubscribers method creates an instance of the DataSourceSelectArguments class to request extra operations such as inserting, paging, or retrieving total row count on the data that the Select operation returns. The method first has to check the value of the CanInsert, CanPage, or CanRetrieveTotalRowCount property of the DataSourceView class to make sure that the view object supports the respective operation before it makes the request.

Since the Select operation is asynchronous, the GetSubscribers method registers the SendMail method as the callback. The Select method automatically calls the SendMail method after it queries the data and passes the data as its argument, as shown in Figure 7.

Figure 7. The SendMail method enumerates the data and extracts the necessary information.

void SendMail(IEnumerable data)
{
    string firstName = String.Empty;
    string lastName = String.Empty;
   
    IEnumerator iter = data.GetEnumerator();
    while (iter.MoveNext())
    {
        MailMessage message = new MailMessage();
        message.From = "admin@greatnews.com";
        message.To = DataBinder.Eval(iter.Current, "Email").ToString();
        message.Subject = "NewsLetter";
        firstName = DataBinder.Eval(iter.Current, "FirstName").ToString();
        lastName = DataBinder.Eval(iter.Current, "LastName").ToString();
        string mes = "Dear " + firstName + " " + lastName + ",<br/>";
        mes += MessageBody.Text;
        message.Body = mes;
        message.BodyFormat = MailFormat.Html;
      SmtpMail.SmtpServer = "<myserver>";
      SmtpMail.Send(message);
    }
}

The SendMail method calls the GetEnumerator method of the object passed in as its first argument to access its enumerator object and uses the enumerator to enumerate the data. The SendMail method uses the Eval method of the DataBinder class to extract the e-mail address, first name, and last name of each subscriber, and sends the news letter to each one.

Switching from One Data Source Control to Another

As discussed earlier, the ASP.NET control architecture dynamically creates an instance of the data source control that is declared in the respective .aspx page and assigns it to the variable whose name is the value of the ID property of the declared data source control. Such a dynamic instantiation of the declared data source control isolates the GetSubscribers method from the actual type of the data source control and allows the method to treat all data source controls as objects of type IDataSource. This allows page developers to switch from one type of data source control to another without modifying the data access code (the GetSubscribers method). This section presents an example for such a case.

Let us say our Web application uses the GetSubscribers method in conjunction with the ObjectDataSource control to retrieve the list of subscribers from a relational data store such as Microsoft SQL Server:

<asp:SqlDataSource ID="MySource" Runat="Server"
ConnectionString="<%$ ConnectionStrings:MySqlConnectionString %>"
SelectCommand="Select * From Subscribers" />

Suppose our Web application works in an environment where the list of subscribers might also come from an XML document. The XML document might be a local XML file or a remote resource accessed via URL. Therefore our application must be able to retrieve the list of subscribers from XML documents. Obviously the ObjectDataSource control is not designed to retrieve tabular data from XML documents, which means that we have to use a data source control that can retrieve tabular data from XML documents, such as the XmlDataSource control.

The GetSubscribers method does not use any property or method specific to the ObjectDataSource and ObjectDataSourceView classes, and only uses the methods and properties of the IDataSource interface and DataSourceView abstract class to deal with data source controls. We can easily switch from the ObjectDataSource to the XmlDataSource control and use the same data access code as the GetSubscribers method to retrieve the list of subscribers:

<asp:XmlDataSource ID="MySource" Runat="Server"
      DataFile="data.xml" XPath="/Subscribers/Subscriber" />

The value of the XPath attribute of the XmlDataSource control is set to /Subscribers/Subscriber to select all the subscribers.

Insert and Delete Operation

Recall our Web application consists of two parts. The second part of the application allows users to subscribe/unsubscribe from a mailing list. The Subscribe method is called when the Subscribe button is clicked, as shown in Figure 8.

Figure 8. The Subscribe method is called when the Subscribe button is clicked.

void Subscribe(Object sender, EventArgs e)
{
    IDataSource ds = (IDataSource)MySource;
    DataSourceView dv = ds.GetView(String.Empty);
    KeyedList values = new KeyedList();
    values.Add("Email", Email.Text);
    values.Add("FirstName", FirstName.Text);
    values.Add("LastName", LastName.Text);
    DataSourceViewOperationCallback callback = new DataSourceViewOperationCallback(OperationCallback);
    if (dv.CanInsert)
        dv.Insert(values, callback);
}

The first line of the Subscribe method shows that the method does not care about the real type of the data source control. Therefore, we can switch to a different data source control to support a new data store without having to change the code in the Subscribe method.

The method uses an instance of the KeyedList class to collect the e-mail, first name, and last name of the subscriber. We do not have to use the KeyedList class. We can use any class that implements the IDictionary interface including ArrayList, KeyedList, etc.

The Subscribe method checks the value of the CanInsert property of the data source view object to make sure that the view object supports the Insert operation before it calls the Insert method. The Subscribe method passes the KeyedList instance as the first argument to the Insert method.

The Unsubscribe method works similar to the Subscribe method. The main difference is that the Unsubscribe method calls the Delete method of the respective view object to remove the subscription from the underlying data store, as shown in Figure 9.

Figure 9. The Unsubscribe method is called when the Unsubscribe button is clicked.

void Unsubscribe(Object sender, EventArgs e)
{
    IDataSource ds = (IDataSource)MySource;
    DataSourceView dv = ds.GetView(String.Empty);
    KeyedList keys = new KeyedList();
    keys.Add("Email", Email.Text);
    KeyedList oldValues = new KeyedList();
    oldValues.Add("Email", Email.Text);
    DataSourceViewOperationCallback callback = new DataSourceViewOperationCallback(OperationCallback);
    if (dv.CanDelete)
        dv.Delete(keys, oldValues, callback);
}

Hierarchical Data Source Controls

The GetSubscribers method (the data access code) feeds tabular data to its callers. However, there are times when data access code must return hierarchical data to its callers. This section presents the implementation of the version of the GetSubscribers method that returns hierarchical data. We can think of this as a contract between the method and its callers. The callers expect the method to return hierarchical data from both hierarchical and tabular data stores.

ASP.NET 2.0 uses the provider pattern to isolate the GetSubscribers method from the actual type of the underlying data store and presents the method with the hierarchical views of the data store. This allows the method to treat both hierarchical and tabular data stores as hierarchical data stores.

Each hierarchical data source control is specifically designed to work with a specific data store. However, since all hierarchical data source controls implement the IHierarchicalDataSource interface and all hierarchical data source views derive from the HierarchicalDataSourceView class, the GetSubscribers method does not have to deal with the specifics of each data source control and can treat all of them in a generic fashion.

IHierarchicalEnumerable GetSubscribers()
{
    IHierarchicalDataSource ds = (IHierarchicalDataSource)MySource;
    HierarchicalDataSourceView dv = ds.GetHierarchicalView("/Subscribers");
    return dv.Select();
}

The first line of the GetSubscribers method shows that the method treats the data source control as an object of type IHierarchicalDataSource and does not care about the real type of the data source control. This will allow page developers to switch to a new hierarchical data source control to add support for a new data store without having to modify the code in the GetSubscribers method.

The GetSubscribers method then calls the GetHierarchicalView method of the HierarchicalDataSourceView class to access the hierarchical view with the given path, such as "/Subscribers". Notice the Select method is not asynchronous. The application passes the data returned from the GetSubscribers method to the SendMail method (see Figure 15). Notice the data is of type IHierarchicalEnumerable.

The IHierarchicalEnumerable implements IEnumerable, which means that it exposes the GetEnumerator method. The SendMail method calls the GetEnumerator method to access the respective IEnumerator object, which is subsequently used to enumerate the data. The IHierarchicalEnumerable also exposes a method named GetHierarchyData that takes the enumerated object and returns the IHierarchyData object associated with it.

The IHierarchyData interface exposes an important property named Item, which is nothing but the data item. The SendMail method uses the Eval method of the XPathBinder class to evaluate XPath expressions against the Item object.

Figure 10. The SendMail method enumerates the data, extracts the necessary information, and sends the newsletter to each subscriber.

void SendMail(IHierarchicalEnumerable data)
{
    string firstName = String.Empty;
    string lastName = String.Empty;

    IEnumerator iter = data.GetEnumerator();
    while (iter.MoveNext())
    {
        IHierarchyData ihdata = data.GetHierarchyData(iter.Current);
        MailMessage message = new MailMessage();
        message.From = "admin@subscribers.com";
        message.To = XPathBinder.Eval(ihdata.Item, "@Email").ToString();
        message.Subject = "NewsLetter";
        firstName = XPathBinder.Eval(ihdata.Item, "@FirstName").ToString();
        lastName = XPathBinder.Eval(ihdata.Item, "@LastName").ToString();
        string mes = "Hi " + firstName + " " + lastName + ",<br/>";
        mes += MessageBody.Text;
        message.Body = mes;
        message.BodyFormat = MailFormat.Html;
        SmtpMail.SmtpServer = "MyServer";
        SmtpMail.Send(message);
    }
}

Conclusion

Using a step-by-step approach showing different ASP.NET 2.0 and ADO.NET 2.0 tools and techniques, this article demonstrates how page developers can write generic data access code that can be used to access different types of data stores.

Dr. Shahram Khosravi is a Senior Software Engineer with Schlumberger Information Solutions (SIS). Shahram specializes in ASP.NET, XML Web services, .NET technologies, XML technologies, 3D Computer Graphics, HI/Usability, Design Patterns, and developing ASP.NET server controls and components. He has more than 10 years of experience in object-oriented programming. He uses variety of Microsoft tools and technologies such as SQL Server and ADO.NET. Shahram has written articles on .NET and ASP.NET technologies for asp.netPRO magazine.