在 ASP.NET 2.0 和 ADO.NET 2.0 中编写通用数据访问代码

 

沙赫拉姆·霍斯拉维博士
信息解决方案

2005 年 4 月

适用于:

   Microsoft ADO.NET 2.0
   Microsoft ASP.NET 2.0
   Microsoft .NET Framework 2.0
   Microsoft Visual Web Developer 2005 Express Edition Beta
   C# 编程语言

摘要: 使用分步方法了解如何使用不同的 ASP.NET 2.0 和 ADO.NET 2.0 工具和技术来编写通用数据访问代码。 (打印页 18 页)

下载关联的示例代码:GenericDataAccessSample.exe

介绍

大多数 Web 应用程序包含用于访问基础数据存储的数据访问代码,以执行基本数据操作,例如 选择更新删除插入。 本文使用分步方法展示页面开发人员如何利用不同的 ASP.NET 2.0 和 ADO.NET 2.0 工具和技术编写可用于访问不同类型的数据存储的通用数据访问代码。 在数据驱动的 Web 应用程序中编写通用数据访问代码尤其重要,因为数据来自许多不同的源,包括Microsoft SQL Server、Oracle、XML 文档、平面文件和 Web 服务,仅举几例。

本文使用简单的 Web 应用程序作为此处介绍的所有代码的测试床。 应用程序由两部分组成:第一部分允许系统管理员向邮件列表的所有订阅者发送新闻稿。 第二部分允许用户订阅或取消订阅邮件列表。 本文的第一部分首先实现简单的数据访问代码(请参阅图 1),以访问 SQL Server Microsoft并提取订阅者列表。 代码经过修改,并在文章过程中更通用。

图 1. GetSubscribers 方法提取所有订阅者的列表。

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;
}

由于图 1 中的数据访问代码包含用于创建 ADO.NET 对象的代码,例如 sqlConnection、sqlCommandSqlDataAdapter 实例。 数据访问代码不能用于从其他数据存储(如 Oracle 数据库)检索订阅服务器列表。 页面开发人员每次需要访问新数据存储时,必须修改数据访问代码(使用 GetSubscribers 方法)。 接下来,了解 ADO.NET 2.0 如何使用提供程序模式帮助页面开发人员编写通用数据访问代码来访问不同类型的数据存储。

ADO.NET 2.0 中的提供程序模式

GetSubscribers 方法的主要问题是,它包含用于创建 ADO.NET 对象的代码。 根据提供程序模式,数据访问代码必须委托为另一个类创建 ADO.NET 对象提供代码的责任。 我将此类称为“代码提供程序类”,因为它提供用于创建 ADO.NET 对象的代码。 代码提供程序类公开 CreateConnectionCreateCommandCreateDataAdapter等方法,其中每个方法都提供了用于创建相应 ADO.NET 对象的代码。

由于代码提供程序类包含实际代码,因此不能使用同一类来访问不同的数据存储。 因此,必须修改和重新配置数据访问代码(GetSubscribers 方法),以在每次用于访问新数据存储时将代码提供给新的代码提供程序类的责任。 即使它不包含代码,GetSubscribers 方法仍与代码绑定。

提供程序模式提供解决此问题的解决方案,并包括以下步骤:

  1. 设计和实现抽象基提供程序类。

  2. 从抽象基提供程序类派生所有代码提供程序类。

  3. 让数据访问代码(GetSubscribers 方法)使用抽象基类而不是单个代码提供程序类。

    抽象基类委托向相应的子类提供创建 ADO.NET 对象的代码的责任。 抽象基类 DbProviderFactory命名。 下面提供了此类的一些方法:

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

    每个子类都提供用于为特定数据存储创建适当的 ADO.NET 对象的代码。 例如,SqlClientFactory 子类提供了用于创建 ADO.NET 对象以访问 SQL Server Microsoft的代码,如图 2 所示。

    图 2. SqlClientFactory 类及其一些方法

    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();
           }
    }
    

提供程序模式允许数据访问代码处理所有子类,因为它们都是同一基类的所有子类。 就数据访问代码而言,所有子类都属于 DbProviderFactory 类型。 数据访问代码无法知道正在使用的子类的特定类型。 这引入了新问题。 如果数据访问代码(GetSubscribers 方法)不知道子类的类型,那么它如何实例化子类的实例?

此问题的提供程序模式解决方案由以下三个部分组成:

  1. 唯一字符串用于标识每个子类。 ADO.NET 2.0 使用子类的命名空间作为其唯一字符串 ID。例如,唯一字符串 ID 的 System.Data.SqlClientSystem.Data.OracleClient 分别标识 SqlClientFactoryOracleClientFactory 子类。
  2. 文本文件(通常为 XML 文件)用于存储有关所有子类的信息。 ADO.NET 2.0 使用 machine.config 和 web.config 文件来存储所需的信息。 有关子类的信息包括唯一的字符串 ID 和子类类型的名称。 例如,有关 SqlClientFactory 子类的信息包括 System.Data.SqlClient 的唯一字符串 ID 以及子类类型的名称,即 System.Data.SqlClient.SqlClientFactory
  3. 静态方法是设计和实现的。 该方法可以是抽象基类的一部分,也可以是单独类的一部分。 ADO.NET 2.0 使用名为 DbProviderFactories 的单独类 来公开 GetFactory 静态方法。 该方法采用所需子类的唯一字符串 ID 作为其唯一参数,并搜索具有给定唯一字符串 ID 的子类 machine.config 文件。该方法提取所需子类的类型的名称,并使用反射动态创建子类的实例。

数据访问代码(GetSubscribers 方法)调用 GetFactory 静态方法,并传递相应的唯一字符串 ID 以访问相应子类的实例。 GetSubscribers 方法访问实例后,它会调用相应的创建方法(如 CreateConnection()、CreateCommand()等来实例化相应的 ADO.NET 对象,如图 3 所示。

图 3. 使用新 ADO.NET 提供程序模式的 GetSubscribers 方法的版本

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;
}

数据访问代码(GetSubscribers 方法)委托向 GetFactory 方法实例实例实例创建 ADO.NET 对象的代码负责。 因此,相同的数据访问代码可用于访问不同的数据存储,例如Microsoft SQL Server 和 Oracle。

用于创建正确的 ADO.NET 对象的代码特定于数据存储。 ADO.NET 2.0 中的提供程序模式从数据访问代码(GetSubscribers 方法)中删除这些特定于数据存储的部分,使其更通用。 但是,提供程序模式不会删除所有特定于数据存储的部分。 仔细检查 GetSubscribers 方法会显示以下剩余的特定于数据存储的部分:

  1. 连接字符串
  2. 用于标识基础代码提供程序类的唯一字符串 ID
  3. 命令文本
  4. 命令类型

除非对上述部分执行了某些操作,否则数据访问代码仍绑定到特定类型的数据存储。 ADO.NET 2.0 中的提供程序模式对此问题没有帮助。 但是,ADO.NET 2.0 提供了其他工具和技术,用于从 GetSubscribers 方法中删除前两个特定于数据存储的部分,例如连接字符串和唯一字符串 ID。

连接字符串

连接字符串是 Web 应用程序中一些最有价值的资源。 它们非常重要,以至于 .NET Framework 2.0 将它们视为“一流公民”。 web.config 文件现在支持名为 <connectionStrings 的新节>,其中包含应用程序中使用的所有连接字符串。 因此,我们将连接字符串从 GetSubscribers 方法移到此部分:

<?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>

<添加 <connectionStrings> 元素的> 子元素公开以下三个重要属性:

  • 名称— 连接字符串的友好名称
  • connectionString- 实际连接字符串
  • providerName— 代码提供程序类的唯一字符串 ID 或固定

NET Framework 2.0 为数据访问代码(GetSubscribers 方法)提供正确的工具,以通用方式从 web.config 文件中提取连接字符串值,如下所述。 .NET Framework 2.0 中的 System.Configuration 命名空间包括名为 Configuration 的新类。 此类表示 web.config 或 machine.config 文件的整个内容。 数据访问代码不能使用新运算符直接创建此类的实例。

类本身公开一个名为 getWebConfiguration 的静态方法,该方法采用 web.config 文件的路径,并返回表示 web.config 文件的整个内容的 Configuration 类的实例:

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

继承自 ConfigurationSection 类的类表示 web.config 文件的每个部分。 类的名称由节的名称和关键字 Section 组成。 例如,ConnectionStringsSection 类表示 web.config 文件的 <connectionStrings> 节的内容。 数据访问代码(GetSubscribers 方法)不能使用新运算符直接创建 ConnectionStringsSection 类的实例。 Configuration 类公开一个名为 Sections 的集合属性,该属性包含表示 web.config 文件的不同节的所有对象:

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

由于 Sections 是 ConfigurationSection 对象的集合,因此数据访问代码必须键入强制转换返回的实例。 GetSubscribers 方法访问 ConnectionStringsSection 对象并使用它访问连接字符串值后:

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

图 4 显示了 GetSubscribers 方法的新版本,该方法包含以通用方式提取连接字符串所需的代码。

图 4. 从 web.config 文件中提取连接字符串的 GetSubscribers 方法的版本

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;
}

GetSubscribers 方法现在包括 MySqlConnectionString 字符串,因此我们仍必须修改 GetSubscribers 方法以添加对其他数据存储(如 Oracle 数据库)的支持。 我们似乎回到了正方形。 没有。 通过将连接字符串从数据访问代码移动到 web.config 文件,我们获得了一些重要优势:

  • 连接字符串现在是 <添加 web.config 文件的 connectionStrings 元素的 connectionStrings 元素的> 子元素的值,它是 XML 文档。 XML 文档的出色之处在于,我们可以加密文档中的单个元素。 如果只需要保护部分文档,则不必加密整个文档。 .NET Framework 2.0 附带了一个工具,可用于加密 <connectionStrings> 节以保护我们最重要的资源(连接字符串)。 想象一下,如果黑客在我们的连接字符串上得到他的手,黑客可以对我们的宝贵数据库造成多大的损害。 请记住,连接字符串都是黑客访问数据库所需的。

  • 我们所做的一切似乎是替换以下字符串

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

    包含新字符串 MySqlConnectionString。 然而,有一个很大的区别。 前字符串包含不适用于另一个数据库(如 Oracle)的 SQL Server 数据库特定信息,但后一个字符串只是一个友好名称。

但是,友好名称仍可能导致问题,因为它引用 web.config 文件的 <connectionStrings> 节中的特定连接字符串。 在本示例中,它引用用于访问 SQL Server Microsoft的连接字符串。 这意味着,必须修改 GetSubscribers 方法(数据访问代码),以使用不同的友好名称来访问不同的数据存储,如 Oracle。

为了避免修改数据访问代码,可以将友好名称从数据访问代码移动到 web.config 文件的 <appSettings> 节,并在运行时动态提取数据访问代码,如下所示:

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

我们还将提供程序名称移动到 <appSettings> 部分:

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

页面开发人员只需将 属性 <将 <appSettings> 元素的> 子元素添加到相同的数据访问代码,以访问不同的数据存储,而无需对数据访问代码本身进行任何更改。

图 5 显示了包含最近更改的数据访问代码(GetSubscribers 方法)的版本。

图 5. GetSubscribers 方法的版本,用于从 web.config 文件中提取提供程序名称和连接字符串的友好名称

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;
}

数据源控件

图 5 所示的 GetSubscribers 方法的最新版本仍然不通用,因为存在以下问题:

  • 该方法包含数据存储 - 特定信息,即命令文本和命令类型。 因此,页面开发人员仍必须修改方法,然后才能使用它来访问其他数据库。
  • 该方法将 DataView 类的实例返回到其调用方,以便该方法馈送其调用方表格数据。 我们可以将此视为 GetSubscribers 方法与其调用方之间的协定。 调用方希望 GetSubscribers 方法在所有情况下都遵循合同,即使基础数据存储本身不是表格。 由于 GetSubscribers 方法使用 ADO.NET 对象来访问基础数据存储,因此它无法访问分层数据存储,例如 XML 文件,其中必须使用 System.Xml 及其子命名空间中的类实例,而不是 ADO.NET 对象。

GetSubscribers 方法中使用的数据访问代码的主要问题是,它直接包含从基础数据存储中提取数据的实际代码。 这正是 .NET Framework 2.0 提供程序模式专门用于解决的问题。 根据提供程序模式,GetSubscribers 方法必须委托提供代码以将数据存储访问到其他类的责任。 它称为代码提供程序类。 代码提供程序类的类型取决于它访问的数据存储的类型。 这些代码提供程序类统称为数据源控件。 ASP.NET 2.0 附带多种不同类型的数据源控件,包括 SqlDataSourceAccessDataSourceObjectDataSourceXmlDataSourceSiteMapDataSource 控件。

SqlDataSource 控件专门用于更新、删除、插入和提取关系数据存储中的数据,例如Microsoft SQL Server 和 Oracle。 AccessDataSource 控件是 SqlDataSource 控件的子类,知道如何使用 Microsoft Access 数据库。 另一方面,ObjectDataSource 控件使用内存中业务对象作为其数据存储。 XmlDataSource 控件专门用于从 XML 文档中提取数据。 但是,XmlDataSource 控件不提供对基础 XML 文档的写入访问权限。

每个数据源控件都公开其基础数据存储的一个或多个视图。 每个视图都是相应类的实例。 例如,SqlDataSource、AccessDataSource 和 ObjectDataSource 控件分别公开 SqlDataSourceViewAccessDataSourceViewObjectDataSourceView 类的实例的视图。 视图隐藏基础数据存储的实际类型,使其类似于数据访问代码所需的类型。 例如,GetSubscribers 方法需要表格类型的数据存储,因为它会馈送其客户端表格数据。 表格视图允许 GetSubscribers 方法从基础数据存储中提取表格数据,即使数据存储本身是分层数据源(如 XML 文档)。 这允许 GetSubscribers 方法将表格数据存储和分层数据存储视为表格数据存储。

数据源控件可以为其客户端提供两种类型的视图:表格视图和分层视图。 ASP.NET 2.0 附带两个数据源控件,这些控件提供两种类型的视图、XmlDataSource 和 SiteMapDataSource。 数据源控件的其余部分(SqlDataSource、AccessDataSource 和 ObjectDataSource)仅显示表格视图。 但是,可以扩展它们以提供表格视图和分层视图。

表格数据源控件

表格数据源控件使其基础数据存储类似于表格数据存储,无论数据存储是否为表格。 表格数据存储由行和列的表组成,其中每行表示一个数据项。 表的名称唯一标识表,并在表格数据存储中的其他表中找到该表。 表格视图的作用类似于表,这意味着视图被命名。

如前所述,每个数据源控件类及其关联的视图类(例如 SqlDataSource 类及其关联的 SqlDataSourceView 类)都提供用于更新、删除、插入和提取基础数据存储中的数据的实际代码。 显然,每个数据源控件及其关联的视图类的代码专门用于处理特定类型的数据存储。 因此,每个数据源控件类及其关联的视图类都是特定于数据存储的。 这给 GetSubscribers 方法带来了严重的问题,该方法使用数据源控件及其表格视图访问其基础数据存储,因为它将该方法与特定类型的数据存储关联,这意味着同一方法不能用于访问不同类型的数据存储。

ASP.NET 2.0 提供了一个解决方案,该解决方案使用提供程序模式来:

  1. 介绍 IDataSource 接口和 DataSourceView 抽象类
  2. 从 IDataSource 接口派生所有表格数据源控件
  3. 从 DataSourceView 抽象类派生所有表格视图

IDataSource 接口和 DataSourceView 抽象类委托提供用于更新、删除、插入和提取数据存储中数据的实际代码的责任,并将其提取到相应的子类。 数据访问代码(如 GetSubscribers 方法)必须使用 IDataSource 接口和 DataSourceView 抽象类的方法和属性。 它们不得使用特定于特定数据源控件类的任何方法或属性,例如 SqlDataSource 或特定的数据源视图类(如 SqlDataSourceView)。 提供程序模式允许数据访问代码以通用方式处理所有数据源控件及其各自的数据源视图。 就数据访问代码而言,所有数据源控件都是 IDataSource 类型,所有数据源视图都属于 DataSourceView 类型。 数据访问代码无法知道要使用的数据源控件和数据源视图对象的实际类型。 这会导致新问题。 如果数据访问代码(GetSubscribers 方法)不知道数据源控件的类型,那么它如何实例化它的实例?

如前所述,提供程序模式提供由以下步骤组成的解决方案:

  1. 唯一的字符串 ID 用于标识每个数据源控件类。
  2. 文本文件(通常为 XML 文件)用于存储有关所有数据源控件类的信息。
  3. 设计并实现一种机制,用于搜索具有给定字符串 ID 的子类的 XML 文件。

现在让我们看看 ASP.NET 2.0 如何实现提供程序模式的上述三个任务。 ASP.NET 2.0 从 Control 类派生所有数据源控件。 如果数据源控件不呈现 HTML 标记文本,为什么数据源控件派生自控件类? 它们派生自 Control 类,以便它们可以继承以下三个重要功能:

  1. 它们可以以声明方式实例化。
  2. 它们跨回发保存和还原其属性值。
  3. 它们将添加到包含页的控件树中。

第一项功能允许页面开发人员在其各自的.aspx文件中以声明方式实例化数据源控件。 因此,.aspx文件充当提供程序模式的第二步所需的文本或 XML 文件。 ASP.NET 2.0 控件体系结构动态创建声明数据源控件的实例,并将实例分配给一个变量,该变量的名称是声明数据源控件的 ID 属性的值。 这会处理提供程序模式所需的上述第一和第三个步骤。

图 6 显示了 GetSubscribers 方法的版本,该方法使用 IDataSource 接口和 DataSourceView 抽象类的方法和属性来访问基础数据存储:

图 6. 使用 IDataSource 接口和 DataSourceView 抽象类的方法和属性的 GetSubscribers 方法的版本

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);
}

GetSubscribers 方法的第一行清楚地表明,该方法将数据源控件视为 IDataSource 类型的对象。 该方法不关心数据源控件的实际类型,例如是 SqlDataSource、AccessDataSource、ObjectDataSource 还是 XmlDataSource 控件。 这将允许页面开发人员从一个数据源控件切换到另一个数据源控件,而无需修改数据访问代码(GetSubscribers 方法)。 下一部分将在更多详细信息中讨论这一重要问题。

GetSubscribers 方法调用 IDataSource 对象的 GetView 方法来访问其默认表格视图对象。 请注意,GetView 方法返回 DataSourceView 类型的对象。 GetSubscribers 方法不关心视图对象的实际类型,例如是 SqlDataSourceView、AccessDataSourceView、ObjectDataSourceView 还是 XmlDataSourceView 对象。

接下来,GetSubscribers 方法创建 DataSourceSelectArguments 类的实例,以请求额外的操作,例如插入、分页或检索 Select 操作返回的数据的总行计数。 该方法首先必须检查 CanInsertCanPage的值,或 DataSourceView 类的 CanRetrieveTotalRowCount 属性,以确保视图对象在发出请求之前支持相应的操作。

由于 Select 操作是异步的,GetSubscribers 方法会将 SendMail 方法注册为回调。 Select 方法在查询数据并将数据作为参数传递后自动调用 SendMail 方法,如图 7 所示。

图 7. SendMail 方法枚举数据并提取必要的信息。

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);
    }
}

SendMail 方法调用作为第一个参数传入的对象 GetEnumerator 方法,以访问其枚举器对象,并使用枚举器枚举数据。 SendMail 方法使用 DataBinder 类的 Eval 方法提取每个订阅者的电子邮件地址、名字和姓氏,并将新闻信发送到每个订阅者。

从一个数据源控件切换到另一个数据源控件

如前所述,ASP.NET 控件体系结构动态创建在相应.aspx页中声明的数据源控件的实例,并将其分配给名称为声明数据源控件 属性 ID 的值的变量。 声明数据源控件的动态实例化可将 GetSubscribers 方法与数据源控件的实际类型隔离开来,并允许该方法将所有数据源控件视为 IDataSource 类型的对象。 这样,页面开发人员就可以从一种类型的数据源控件切换到另一种,而无需修改数据访问代码(GetSubscribers 方法)。 本部分提供了此类情况的示例。

假设 Web 应用程序将 GetSubscribers 方法与 ObjectDataSource 控件结合使用,从关系数据存储(如 Microsoft SQL Server)检索订阅者列表:

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

假设 Web 应用程序在订阅者列表可能也来自 XML 文档的环境中工作。 XML 文档可能是本地 XML 文件或通过 URL 访问的远程资源。 因此,应用程序必须能够从 XML 文档检索订阅服务器列表。 显然,ObjectDataSource 控件不是为了从 XML 文档检索表格数据而设计的,这意味着我们必须使用可从 XML 文档检索表格数据的数据源控件,例如 XmlDataSource 控件。

GetSubscribers 方法不使用特定于 ObjectDataSource 和 ObjectDataSourceView 类的任何属性或方法,并且仅使用 IDataSource 接口和 DataSourceView 抽象类的方法和属性来处理数据源控件。 我们可以轻松地从 ObjectDataSource 切换到 XmlDataSource 控件,并使用与 GetSubscribers 方法相同的数据访问代码来检索订阅者列表:

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

XmlDataSource 控件的 XPath 属性的值设置为 /Subscribers/Subscriber 以选择所有订阅服务器。

插入和删除操作

回想一下我们的 Web 应用程序由两个部分组成。 应用程序的第二部分允许用户从邮件列表中订阅/取消订阅。 单击“订阅”按钮 时,将调用 Subscribe 方法,如图 8 所示。

图 8. 单击“订阅”按钮时调用 Subscribe 方法。

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);
}

Subscribe 方法的第一行显示该方法不关心数据源控件的实际类型。 因此,我们可以切换到不同的数据源控件来支持新的数据存储,而无需在 Subscribe 方法中更改代码。

该方法使用 KeyedList 类的实例来收集订阅者的电子邮件、名字和姓氏。 我们不必使用 KeyedList 类。 可以使用任何实现 IDictionary 接口的类,包括 ArrayList、KeyedList 等。

Subscribe 方法检查数据源视图对象的 CanInsert 属性的值,以确保视图对象在调用 Insert 方法之前支持 Insert 操作。 Subscribe 方法将 KeyedList 实例作为第一个参数传递给 Insert 方法。

Unsubscribe 方法的工作方式类似于 Subscribe 方法。 主要区别在于,Unsubscribe 方法调用相应视图对象的 Delete 方法,以从基础数据存储中删除订阅,如图 9 所示。

图 9. 单击“取消订阅”按钮时,将调用“取消订阅”方法。

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);
}

分层数据源控件

GetSubscribers 方法(数据访问代码)将表格数据馈送给其调用方。 但是,有时数据访问代码必须将分层数据返回到其调用方。 本部分介绍返回分层数据的 GetSubscribers 方法版本的实现。 我们可以将此视为方法与其调用方之间的协定。 调用方希望该方法从分层数据存储和表格数据存储返回分层数据。

ASP.NET 2.0 使用提供程序模式将 GetSubscribers 方法与基础数据存储的实际类型隔离开来,并将该方法与数据存储的分层视图一起呈现。 这样,该方法就可以将分层数据存储和表格数据存储视为分层数据存储。

每个分层数据源控件专门用于处理特定的数据存储。 但是,由于所有分层数据源控件都实现 IHierarchicalDataSource 接口,并且所有分层数据源视图都派生自 HierarchicalDataSourceView 类,GetSubscribers 方法不必处理每个数据源控件的具体细节,并且可以以泛型方式处理所有这些视图。

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

GetSubscribers 方法的第一行显示,该方法将数据源控件视为 IHierarchicalDataSource 类型的对象,并且不关心数据源控件的实际类型。 这将允许页面开发人员切换到新的分层数据源控件,以添加对新数据存储的支持,而无需修改 GetSubscribers 方法中的代码。

然后,GetSubscribers 方法调用分层DataSourceView 类的 GetHierarchicalView 方法,以访问具有给定路径的分层视图,例如“/Subscribers”。 请注意,Select 方法不是异步的。 应用程序将从 GetSubscribers 方法返回的数据传递给 SendMail 方法(请参阅图 15)。 请注意,数据的类型 IHierarchicalEnumerable

IHierarchicalEnumerable 实现 IEnumerable,这意味着它将公开 getEnumerator 方法 。 SendMail 方法调用 GetEnumerator 方法以访问相应的 IEnumerator 对象,后者随后用于枚举数据。 IHierarchicalEnumerable 还公开了一个名为 getHierarchyData 的方法,该方法采用枚举对象并返回与之关联的 IHierarchyData 对象。

IHierarchyData 接口公开了名为 Item的重要属性,该属性只不过是数据项。 SendMail 方法使用 XPathBinder 类的 Eval 方法对 Item 对象计算 XPath 表达式。

图 10. SendMail 方法枚举数据,提取必要的信息,并将新闻稿发送到每个订阅者。

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);
    }
}

结论

本文使用显示不同 ASP.NET 2.0 和 ADO.NET 2.0 工具和技术的分步方法,本文演示了页面开发人员如何编写可用于访问不同类型的数据存储的通用数据访问代码。

Shahram Khosravi 博士是施伦伯格信息解决方案(SIS)的高级软件工程师。Shahram 专门从事 ASP.NET、XML Web 服务、.NET 技术、XML 技术、3D 计算机图形、HI/可用性、设计模式以及开发 ASP.NET 服务器控件和组件。他在面向对象的编程方面拥有 10 多年的经验。他使用各种Microsoft工具和技术,如 SQL Server 和 ADO.NET。Shahram 已经为 asp.netPRO 杂志撰写了有关 .NET 的文章和 ASP.NET 技术。