ASP.NET Lab

The Best Web, The Best Future

博客园 首页 新随笔 订阅 管理

介绍

与 Web 开发者一样,我们需要考虑如何对数据进行操作。我们还需要创建数据库来存储数据,创建代码来获取并且更改数据,创建 Web 页面来收集并且描述数据。本文是在 ASP.NET 2.0 中实现公共开发模式的长篇技术探究系列中的第一个指南。我们将会从创建一个软件架构开始,这个架构中包含了一个使用类型化的 DataSet 的数据访问层(DAL)、一个执行自定义业务规则的业务逻辑层(BLL),以及一个由若干共享公共页面布局的 ASP.NET 页面所组成的表现层。一旦这个后端基础被铺设完毕,我们就将转移到数据的报告任务,并且说明如何从 Web 应用程序中对数据进行显示、描述、收集,以及验证。另外,这些指南都作出了简单化调整并且使用了大量的屏幕快照来提供逐步的可视化指导,以帮助你通过每一个步骤。每个指南都有可用的 C# 与 Visual Basic 版本并且包括了一个完整的源代码下载。(注意:第一个指南的内容是相当长的,但是其他的部分则被分别呈现成相对比较容易吸收的更多部分。)

在这些指南中我们将会使用 Microsoft SQL Server 2005 Express Edition 版本中的 Northwind 数据库,这个数据库被存放在 App_Data 目录中。除了数据库文件之外,App_Data 目录同样还包含了创建这个数据库的 SQL 脚本,以便在万一你需要使用其他数据库版本的时候才考虑使用。这些脚本同样也可以直接从 Microsoft 下载,如果你愿意的话。如果你使用了其他 SQL Server 版本中的 Northwind 数据库,那么你就需要更新应用程序 Web.config 文件中的 NORTHWNDConnectionString 设置。这个 Web 应用程序是使用 Visual Studio 2005 Professional Edition 并且作为一个基于文件系统的网站工程而被建立的。但是,所有的这些指南同样能够很好地在 Visual Studio 2005 的免费版本 Visual Web Developer 中运作。

在这个指南中我们将从非常基础的开始并且创建数据访问层(DAL),然后在第二个指南中创建业务逻辑层(BLL),然后在第三个指南中操作页面布局与导航。并且以后的所有指南都需要建立在前三个指南的基础之上。因此,我们会在第一个指南中涵盖大量的内容,OK,现在就启动你的 Visual Studio,然后我们就开始!

第 1 步:创建一个 Web 工程并且连接到数据库

在能够创建数据访问层(DAL)之前,我们首先需要创建一个网站并且设置数据库。首先从创建一个新的基于文件系统的 ASP.NET 网站作为开始。要完成这个任务,就需要转到 New 菜单并且选择 New Web Site,之后会显示 New Web Site 对话框。选择 ASP.NET Web Site 模板,在 Location 下拉列表框中设置成 File System,并且选择一个目录来存储这个网站,然后把编程语言设置成 C#。

图 1:创建一个新的基于文件系统的网站

这将创建一个拥有一个名为 Default.aspx 的 ASP.NET 页面和一个 App_Data 目录的新网站。

在网站被创建之后,接下来就是在 Visual Studio 的 Server Explorer 中添加数据库的引用。通过在 Server Explorer 中添加数据库,你就能够在 Visual Studio 中添加数据库、存储过程、视图,等等。你同样也能够通过 Query Builder 以手工或者图形化的方式来查看数据表的数据或者创建你自己的查询。另外,在我们为 DAL 建立类型化的 DataSet 时,我们需要把 Visual Studio 定位到将被构造成类型化 DataSet 的数据库中。虽然我们能够在那个时候提供连接的信息,但是 Visual Studio 会自动把已经在 Server Explorer 中被注册的数据库组装进一个下拉列表框中。

为 Server Explorer 添加 Northwind 数据库的步骤取决于你是否需要在 App_Date 目录中使用 SQL Server 2005 Express Edition 数据库或是你想要使用一个 Microsoft SQL Server 2000 或者 2005 数据库服务器的安装来作为替代。

使用 App_Data 目录中的数据库

如果你没有可供连接的 SQL Server 2000 或者 2005 数据库服务器,又或者你只是想要避免把这个数据库添加到数据库服务器中,那么你就可以使用位于被下载网站的 App_Data 目录中的 Northwind 数据库的 SQL Server 2005 Express Edition 版本(NORTHWIND.MDF)。

存放在 App_Data 目录中的数据库会自动添加到 Server Explorer 中。假设你已经在你的机器中安装了 SQL Server 2005 Express Edition,那么你应该能够在 Server Explorer 中看到一个名为 NORTHWIND.MDF 的节点,因此你可以展开并且浏览它的数据表、视图、存储过程,等等(如图 2)。

App_Data 目录同样能够保持 Microsoft Access .mdb 文件,与它们的 SQL Server 副本一样,也会自动被添加到 Server Explorer 中。如果你不想要使用任何 SQl Server 选项,那么你随时都可以下载 Northwind 数据库的 Microsoft Access 文件版本并且把它存放到 App_Data 目录中。切记,无论如何,Access 数据库都没有 SQL Server 那么丰富的特征,并且也不是被设计成用在网站开发情节中的。另外,本系列中将近 35 个以上的指南中都将对数据库级别的特定特征进行利用,并且这些特征都不会被 Access 所支持。

连接到 Microsoft SQL Server 2000 或者 2005 数据库服务器中的数据库

作为选择,你可以连接到一个被安装在数据库服务器中的 Northwind 数据库。如果数据库服务器没有安装 Northwind 数据库,那么你首先必须通过被包括在这个指南的下载中的安装脚本或者直接从 Microsoft 网站上下载 Northwind 的 SQL Server 2000 版本和安装脚本把它添加到数据库服务器中。

一旦你完成了这个数据库的安装,就可以转到 Visual Studio 中的 Server Explorer,右键单击 Data Connections 节点,然后选择 Add Connection。如果你没有看到 Server Explorer,就需要选择 View / Server Explorer 菜单,或者按下 Ctrl+Alt+S 快捷键来显示它。这将会显示 Add Connection 对话框,因此你可以指定要连接到的服务器、识别信息,以及数据库的名称。一旦你已经成功地配置了数据库连接信息并且点击了 OK 按钮,那么这个数据库就会被添加成 Data Connections 节点下面的一个子节点。然后你可以展开这个数据库节点来浏览它的数据表、视图、存储过程,等等。

图 2:添加数据库连接来连接到你的数据库服务器的 Northwind 数据库

第 2 步:创建数据访问层

在操作数据的时候有一个选项就是把特定的数据逻辑直接嵌入到表示层(在 Web 应用程序中,ASP.NET 页面就被用来构成表示层)中。这可能需要获取在 ASP.NET 页面的代码部分编写了 ADO.NET 代码的窗体或者在标记部分使用 SqlDataSource 控件。无论是哪一种情况,这种方法都把数据访问逻辑紧耦合到了表示层中。然而,被推荐的方法则是从表现层中分离出数据访问逻辑。这个被分离的层将被引用成数据访问层,简称 DAL,并且典型地被实现成一个分离的类库工程。已分层架构的好处是能够充分地文档化的(关于这些优势的信息,请参考本指南中的最后部分:[进一步阅读])并且也是我们在这个指南系列中将要使用的方法。

所有特定的底层数据源代码(比如创建数据库连接、发布 SELECT、INSERT、UPDATE、以及 DELETE 命令,等等)都应该位于 DAL 中。表示层不应该包含这些数据访问代码的任何引用,而是应该为所有数据请求而对 DAL 进行调用。数据访问层通常包含了用来访问底层数据库数据的方法。例如,Northwind 数据库中拥有记录产品的出售以及它们的所属目录的 Products 与 Categories 数据表,因此我们的 DAL 中将拥有下列方法:

  • GetCategories(),将会返回所有目录的相关信息
  • GetProducts(),将会返回所有产品的相关信息
  • GetProductsByCategoryID(categoryID),将会返回所有属于特定目录的产品
  • GetProductByProductID(productID),将会返回一个特定的产品信息

在调用的时候,这些方法会连接到数据库,发布适当的查询,并且返回结果。重要的是我们应该如何返回这些结果。这些方法能够简单地返回一个通过数据库查询而被组装的 DataSet 或者 DataReader,但是使用强类型化的对象来返回这些结果却是最理想的方式。强类型化的对象是一种在编译时被严格定义的结构,反之亦然。松散类型化的对象则是一种直到运行时都是无法被了解的结构。

例如,DataReader 与 DataSet(默认)都是松散的类型化对象,直到它们的结构通过使用对它们进行组装的数据库查询所返回的数据列而被定义为止。如果要从松散的类型化 DataTable 中访问特定的列,我们就需要使用这样的语法:DataTable.Rows[index]["columnName"]。这个范例中的 DataTable 的松散类型化通过我们需要使用一个字符串或者序列索引来访问列名称的实际情况而被展示。强类型化的 DataTable 在另一方面将拥有每个被实现成属性的列,因此代码将会像这样:

DataTable.Rows[index].columnName

如果要返回强类型化的对象,那么开发者既能够创建属于他们自己的自定义业务对象也可以使用类型化的 DataSet。开发者把业务对象实现成了一个类,而这个类的属性则典型地反射了业务对象所呈现的底层数据表的列。一个类型化的 DataSet 就是你通过使用 Visual Studio 并且基于数据库结构而被生成的类,并且这个类包含有符合结构而被强类型化的成员。类型化的 DataSet 本身则是由扩展了 ADO.NET DataSet、DataTable,以及 DataRow 等等的这些类所组成。除了强类型化的 DataTable 之外,类型花的 DataSet 现在同样也包括了 TableAdapter(是提供方法来组装 DataSet 的 DataTable,并且在 DataTable 中产生更改然后返回到数据的类)。

提示:关于使用类型化的 DataSet 与使用自定义业务逻辑的优势与劣势的更多信息,请参考:[设计数据层组件]与[通过层来传递数据]。

我们将会在这些指南的架构中使用强类型化的 DataSet。图 3 就说明了应用程序中使用类型化 DataSet 的不同层之间的工作流程。

图 3:所有数据访问代码都被转移到 DAL 中

创建类型化的 DataSet 与 TableAdapter

要开始创建我们的 DAL,我们首先需要把一个类型化的 DataSet 添加到工程中。要完成这个任务,就需要右键单击 Solution Explorer 中的工程节点并且选择 Add a New Item。然后从模板列表中选择 DataSet 选项并且把它命名成 Northwind.xsd。

图 4:选择把一个新的 DataSet 添加到你的工程中

在单击 Add 之后,并且在被提示把这个 DataSet 添加到 App_Code 目录中的时候,请选择 Yes。这时候就会显示类型化的 DataSet 的 Designer,并且 TableAdapter Configuration Wizard 也会启动,并且允许你把你的第一个 TableAdapter 添加到类型化的 DataSet 中。

一个类型化的 DataSet 就相当于一个强类型化的数据集合;它由若干强类型化的 DataTable 实例所组成,而每个 DataTable 则由若干强类型化的 DataRow 实例所组成。我们将要为每个我们需要在这个指南系列中进行操作的底层数据表来创建一个强类型化的 DataTable。让我们先从为 Products 数据库创建一个 DataTable 来作为开始。

切记,强类型化的 DataTable 并不包括任何以及如何从它们的底层数据表中访问数据相关的信息。为了获取数据来组装 DataTable,我们使用了一个 TableAdapter 类,这个类的作用与我们的数据访问层是一样的。至于我们的 Products DataTable,TableAdapter 将会包含我们将要从表示层中对这些方法进行调用的 GetProducts()、GetProductByCategoryID(categoryID) 等等方法。而 DataTable 的任务就是作为一个被用来在层之间传递数据的强类型化对象。

TableAdapter Configuration Wizard 开始会提示你选择需要进行操作的数据库。下拉列表框显示出了 Server Explorer 中的数据库。如果你没有在 Server Explorer 中添加 Northwind 数据库,那么你就可以在这个时候单击 New Connection 按钮来完成添加操作。

图 5:从下拉列表框中选择 Northwind 数据库

在选择数据库并且单击 Next 之后,你将被询问是否需要把连接字符串保存到 Web.config 文件中。通过保存连接字符串,你就可以避免在 TableAdapter 类中对它进行硬编码,从而简化在将来的某个时候对连接字符串所作的改变。如果你选择把连接字符串保存到配置文件中,那么它就会被存放在 <connectionStrings> 配置段中,从而能够有选择性地被加密来改进安全性或者在以后通过 IIS GUI Admin Tool 中全新的 ASP.NET 2.0 Property Page 而被更改,这对于管理员来说也是比较理想的方式。

图 6:把连接字符串保存到 Web.config 文件

接下来,我们需要为第一个强类型化的 DataTable 而定义结构并且为我们的 TableAdapter 提供在组装强类型化的 DataSet 时所使用的第一个方法。通过创建一个从需要在我们的 DataTable 中被反射的数据表中返回列的查询,这两个步骤就能够同时被完成。在向导的最后部分我们将为这个查询而提供一个方法名称。这些任务一旦被完成,这个方法就能够从我们的表示层中被调用。这个方法将执行被定义的查询并且组装成一个强类型化的 DataTable。

要开始定义 SQL 查询,我们就必须首先指出我们应该如何让 TableAdapter 来发布查询。我们可以使用一个特别的 SQL 声明来创建一个新的存储过程,或者也可以使用一个现有的存储过程。在这些指南中,我们将要使用的是特别的 SQL 声明。关于使用存储过程的范例,请参考 Brian Noyes 的文章:[使用 Visual Studio 2005 DataSet Designer 来建立一个数据访问层]。

图 7:使用特别的 SQL 声明来查询数据

现在我们能够以手工的方式来输入 SQL 查询。在 TableAdapter 中创建第一个方法的时候,你通常需要这个查询能够返回在适当的 DataTable 中被表示的列。我们还能够通过创建一个从 Products 数据表中返回所有列与所有行的查询来完成这个任务:

图 8:在文本框中输入 SQL 查询

作为选择,也可以使用 Query Builder 并通过图形化的方式来构造这个查询,如图 9 所示。

图 9:通过 Query Editor,以图形化的方式来创建查询

在创建查询之后,但是在转到下一个步骤之前,单击 Advanced Options 按钮。在 Web Site Projects 中,"Generate Insert, Update, and Delete statements" 是默认时唯一被选中的高级选项;如果你从一个 Class Library 中或者一个 Windows Project 中运行这个向导,那么 "Use optimistic concurrency" 选项将同样也是被选中的。现在我们让 "Use optimistic concurrency" 选项保持未选择的状态。并且在将来的指南中讲解并发优化。

图 10:只选择 "Generate Insert, Update, and Delete statements" 选项

在核实高级选项之后,单击 Next 继续转到最终步骤。在这时我们将被询问需要在 TableAdapter 中添加哪个方法。并且可以通过两个模式来组装数据:

  • 填充一个 DataTable

    通过这种方式而被创建的方法会获取一个 DataTable 来作为参数并且基于查询的结果来对它进行组装。例如,ADO.NET DataAdapter 类会以它的 Fill() 方法来实现这个模式。

  • 返回一个 DataTable

    通过这种方式而被创建的方法会为你填充 DataTable 并且把它作为方法的返回值。

你可以通过 TableAdapter 来实现这些模式中的任何一种或者全部。你同样可以对被提供的方法进行重命名。让我们保持所有的复选框都是被选中的,尽管我们在这些指南中只会使用后面的模式。同样,让我们把普通的 GetData 方法重命名为更合适的 GetProducts。

如果最终的复选框 "GenerateDBDirectMethods" 被选中,那么就会为 TableAdapter 创建 Insert()、Update(),以及 Delete() 方法。如果你想要保持这个选项是未被选中的,那么所有的更新就需要通过 TableAdapter 中唯一的 Update() 方法来完成,这个方法还会获取类型化的 DataSet、一个 DataTable、一个单独的 DataRow,或者一个 DataRow 数组。(如果你没有从图 9 所示的高级属性中选中 "Generate Insert, Update, and Delete statements" 选项,那么复选框的设定就不会起作用。)现在让我们保持这个复选框是未被选中的。

图 11:把方法的名称从 GetData 改成 GetProducts

单击 Finish 完成向导。在向导被关闭之后,我们就会返回到 DataSet Designer 中来,并且其中还会显示我们刚才所创建的 DataTable。你可以看见 Products DataTable 的数据列的列表(ProductID、ProductName,等等),以及 ProductsTableAdapter 的方法(Fill() 与 GetProducts())。

图 12:被添加到类型化的 DataSet 中的 Products DataTable 与 ProductsTableAdapter

现在我们已经拥有了一个类型化的 DataSet,它包含了一个单独的 DataTable(Northwind.Products)以及一个定义了 GetProducts() 方法的强类型化的 DataAdapter 类(NorthwindTableAdapters.ProductsTableAdapter)。这些对象都能够在代码中被用来访问所有产品的列表,如下所示:

NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new NorthwindTableAdapters.ProductsTableAdapter();
Northwind.ProductsDataTable products;

products = productsAdapter.GetProducts();

foreach (Northwind.ProductsRow productRow in products)
    Response.Write("Product: " + productRow.ProductName + "<br />");

这个代码交不需要我们编写任何与特定的数据访问相关的代码。我们并没有实例化任何的 ADO.NET 类,我们也没有引用到任何连接字符串、SQl 查询,或者存储过程。取而代之的是,TableAdapter 为我们提供了所有用于底层数据访问的代码。

在这个范例中被使用的每个对象都是强类型化的,并且允许 Visual Studio 来提供 IntelliSense 与编译时的类型检查。并且所有通过 TableAdapter 而被返回的 DataTable 最好是能够被绑定到 ASP.NET 的数据 Web 控件中(比如 GridView、DetailsView、DropDownList、CheckBoxList,等等)。下列范例说明了如何把通过 GetProducts() 方法而被返回的 DataTable 绑定到一个 GridView 控件,所有的这些只需要在 Page_Load 事件处理器中使用三行代码就能够很好地被完成。

AllProducts.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="AllProducts.aspx.cs" Inherits="AllProducts" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>View All Products in a GridView</title>
    <link href="Styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h1>
            All Products</h1>
        <p>
            <asp:GridView ID="GridView1" runat="server"
             CssClass="DataWebControlStyle">
               <HeaderStyle CssClass="HeaderStyle" />
               <AlternatingRowStyle CssClass="AlternatingRowStyle" />
            </asp:GridView>
            &nbsp;</p>

    </div>
    </form>
</body>
</html>
AllProducts.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;

public partial class AllProducts : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ProductsTableAdapter productsAdapter = new
         ProductsTableAdapter();
        GridView1.DataSource = productsAdapter.GetProducts();
        GridView1.DataBind();
    }
}

图 13:被显示在 GridView 中的产品列表

这个范例虽然在我们的 ADP.NET 页面的 Page_Load 事件处理器中编写了所必需的三行代码,但是在将来的指南中我们将会讲解如何使用 ObjectDataSource 声明来从 DAL 中获取数据。通过使用 ObjectDataSource,我们将不需要编写任何代码并且还能够获取很棒的分页与排序支持!

第 3 步:把参数化的方法添加到数据访问层

现在我们的 ProductTableAdapter 类只拥有一个方法 GetProducts(),它用来返回数据库中的所有产品。虽然操作所有产品是明确有用的,但是在我们需要获取与特定产品或者与属于特定目录的所有产品的相关信息的时候,我们就可以在数据访问层中添加这样的功能:为 TableAdapter 添加参数化的方法。

让我们添加 GetProductsByCategoryID(categoryID) 方法。要在 DAL 中添加新的方法,就需要返回到 DataSet Designer,然后右键单击其中的 ProductsTableAdapter 部分,并选择 Add Query。

图 14:右键单击 TableAdapter 然后选择 Add Query

我们首先被提示是否需要使用一个特别的 SQL 声明或者是一个新的还是现有的存储过程来访问数据库。我们再次选择使用一个特别的 SQL 声明。然后,我们会被询问将被使用的 SQL 查询类型。因为我们需要返回属于一个被指定目录中的所有产品,所以我们就需要编写一个返回数据行的 SELECT 声明。

图 15:选择创建一个能够返回数据行的 SELECT 声明

下一步就是定义被用来访问数据的 SQL 查询。因为我们只需要返回属于一个特定目录的产品,所以我使用了与 GetProducts() 方法中相同的 SELECT 声明,但是添加了下列 WHERE 子句:WHERE CategoryID = @CategoryID。参数 @CategoryID 表示我们使用 TableAdapter 向导所创建的方法将需要一个拥有适当类型的输入参数(即一个允许为空的整数)。

图 16:输入只用来返回被指定目录中的产品的查询

在最后的步骤中我们可以选择所使用的数据访问模式,并且自定义被产生的方法名称。关于 Fill 模式,我们将使用 FillByCategoryID 来作为名称,关于返回 DataTable 的返回模式(GetX 方法),我们则使用 GetProductsByCategoryID 来作为名称。

图 17:选择 TableAdapter 的方法名称

在完成这个向导之后,DataSet Designer 中就会包括新的 TableAdapter 方法。

图 18:现在就可以通过目录来查询产品了

花一点时间来使用相同的方式添加一个 GetProductByProductID(productID) 方法。

这些参数化的查询能够直接从 DataSet Designer 中进行测试。右键单击 TableAdapter 中的方法然后选择 Preview Data。接下来,输入要使用的参数值并且单击 Preview。

图 19:属于 Beverages 目录中的产品都将会被显示

通过在我们的 DAL 中使用 GetProductsByCategoryID(categoryID) 方法,现在我们就能够创建 ASP.NET 页面来显示被指定目录中的产品。下列范例说明了 Beverages 目录中的所有产品,这个目录的 CategoryID 是 1。

Beverages.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Beverages.aspx.cs" Inherits="Beverages" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
    <link href="Styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h1>Beverages</h1>
        <p>
            <asp:GridView ID="GridView1" runat="server"
             CssClass="DataWebControlStyle">
               <HeaderStyle CssClass="HeaderStyle" />
               <AlternatingRowStyle CssClass="AlternatingRowStyle" />
            </asp:GridView>
            &nbsp;</p>
    </div>
    </form>
</body>
</html>
Beverages.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;

public partial class Beverages : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        ProductsTableAdapter productsAdapter = new
         ProductsTableAdapter();
        GridView1.DataSource =
          productsAdapter.GetProductsByCategoryID(1);
        GridView1.DataBind();
    }
}

图 20:Beverages 目录中的所有产品被显示

第 4 步:插入、更新,并且删除数据

现在有两个通用的模式被用来插入、更新,并且删除数据。第一个模式,我们将其称之为数据库直接模式,包括了它所创建的方法,并且在被调用的时候会发布一个 INSERT、UPDATE,或者 DELETE 命令给操作单独数据库记录的数据库。这种方法通常被传递进一系列的标量值(整数、字符串、Boolean、DateTime,等等)来对应要插入、更新,或者删除的值。例如,符合这个模式的 Products 数据表的删除方法将获取一个整数参数,来表示要删除的记录的 ProductID,尽管插入方法将会为 ProductName 获取一个字符串、为 UnitPrice 获取一个十进制数字、为 UnitsOnStock 获取一个整数,等等。

图 21:每个插入、更新,以及删除请求都会直接发送到数据库中

另一个模式,我将把它归纳成批量更新模式,用来更新方法调用中的整个 DataSet、DataTable,或者 DataRow 集合。开发者可以使用这个模式来删除、插入,并且更改 DataTable 中的 DataRow,然后把这些 DataRow 或者 DataTable 传递到一个更新方法中。然后这个方法对被传递进来的 DataRow 进行枚举,并且检测它们是否或者没有被更改、被添加,或者被删除(通过 DataRow 的 RowState 属性值来判断),并且为每条记录发布适当的数据库请求。

图 22:在 Update 方法被调用的时候,所有的改变都能够与数据同步

TableAdapter 在默认时使用的是批量更新模式,但是同样也支持直接数据库模式。这是因为我们在创建 TableAdapter 的时候从 Advanced Properties 中选中了 "Generate Insert, Update, and Delete statements" 选项,所以 ProductsTableAdapter 中包含了一个 Update() 方法,并且实现了批量更新模式。特别要说的是,TableAdapter 包含了能够被传递到类型化的 DataRow 中的一个 Update() 方法。如果你在第一次创建 TableAdapter 的时候保持 "GenerateDBDirectmethods" 复选框是被选中的,那么直接数据库模式同样也会通过 Insert()、Update(),以及 Delete() 方法而被实现。

这些数据更改模式都使用了 TableAdapter 的 InsertCommand、UpdateCommand,以及 DeleteCommand 属性来把它们的 INSERT、UPDATE,以及 DELETE 命令发布到数据库。你还可以通过在 DataSet Designer 中单击 TableAdapter 然后转到 Properties 窗口中来检查并且更改 InsertCommand、UpdateCommand,以及 DeleteCommand 的属性。(这需要确保你已经选中了 TableAdapter,并且 ProductsTableAdapter 对象在 Properties 窗口的下拉列表框中是被选中的。)

图 23:TableAdapter 拥有 InsertCommand、UpdateCommand,以及 DeleteCommand 属性

要检查或者更改任何这些数据库命令属性,可以单击 CommandText 子属性,然后 Query Builder 就会出现。

图 24:配置 Query Builder 中的 INSERT、UPDATE,和 DELETE 声明

下列代码范例说明了如何使用批量更新模式来翻倍所有未被拆开并且备货等于或者少于 25 个单位的产品的价格:

NorthwindTableAdapters.ProductsTableAdapter productsAdapter =
  new NorthwindTableAdapters.ProductsTableAdapter();

// 翻倍每种产品的价格,如果它没有被拆开并且备货等于或者少于 25 个单位
Northwind.ProductsDataTable products = productsAdapter.GetProducts();
foreach (Northwind.ProductsRow product in products)
   if (!product.Discontinued && product.UnitsInStock &lt;= 25)
      product.UnitPrice *= 2;

// 更新产品
productsAdapter.Update(products);

下面的代码说明了如何使用数据库直接模式并且通过编程来删除一个特定产品,然后更新一个特定产品,然后添加一个新产品:

NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new NorthwindTableAdapters.ProductsTableAdapter();

// 删除 ProductID 是 3 的产品
productsAdapter.Delete(3);

// 更新 Chai(ProductID 是 1),把 UnitsOnOrder 设置成 15
productsAdapter.Update("Chai", 1, 1, "10 boxes x 20 bags",
  18.0m, 39, 15, 10, false, 1);

// 添加一个新产品
productsAdapter.Insert("New Product", 1, 1,
  "12 tins per carton", 14.95m, 15, 0, 10, false);

创建自定义的插入、更新,和删除方法

通过数据库直接方法而创建的 Insert()、Update(),和 Delete() 方法可能会有一点琐碎,尤其是在拥有多个列的数据表的情况下。回顾前面的代码范例,Products 数据表的列映射到了 Update() 方法的每个输入参数并且 Insert() 方法在没有 IntelliSense 的帮助下也不是特别清晰的。这也许是在我们只需要更新一两个单独列的时候,或者需要一个将要、或许会返回最新被插入到记录的 IDENTITY(自动增量)字段的值的自定义 Insert() 方法。

如果要创建这样的自定义方法,就可以返回到 DataSet Designer。右键单击 TableAdapter 并且选择 Add Query,返回到 TableAdapter 向导。在第二个屏幕中我们就可以指定需要被创建的查询类型。让我们创建一个方法来添加新产品并且返回最新被添加的记录中的 ProductID 值。因此,我们选择创建一个 INSERT 查询。

图 25:创建一个方法把新行添加到 Products 数据表中

在 InsertCommand 的 CommandText 出现之后的下一个屏幕中。通过在查询的未尾部分添加 SELECT SCOPE_IDENTITY() 来增加这个查询,它将返回被插入到相同范围中的 IDENTITY 列的最后标识值。(更多关于 SCOPE_IDENTITY() 以及你为什么需要在 @@IDENTITY 中使用 SCOPE_IDENTITY() 的信息,请参考相关的技术文档。)并且确保你在添加 SELECT 声明的时候已经在 INSERT 声明的未尾部分使用了分号(;)。

图 26:增加返回 SCOPE_IDENTITY() 值的查询

最后,把新方法命名为 InsertProduct。

图 27:把新方法的名称设置成 InsertProduct

在你返回 DataSet Designer 的时候,你就会看到 ProductsTableAdapter 中包含了一个新的方法 InsertProduct。如果这个新方法没有 Products 数据表中每个列的参数,就有可能是你忘记在 INSERT 声明的结尾部分使用分号。所以,需要配置 InsertProduct 方法并且确保你使用分号来划分了 INSERT 和 SELECT 声明。

默认时,在插入方法的时候会发布非查询方法,表示它们会返回受影响行的数量。但是,我们需要通过 InsertProduct 方法来返回由查询所返回的值,而不是被影响行的数量。要完成这个任务,就可以把 InsertProduct 方法的 ExecuteMode 属性调整成 Scalar。

图 28:把 ExecuteMode 属性改成 Scalar

下列代码说明了这个新的 InsertProduct 方法正在活动:

NorthwindTableAdapters.ProductsTableAdapter productsAdapter = new NorthwindTableAdapters.ProductsTableAdapter();

// 添加一个新产品
int new_productID = Convert.ToInt32(productsAdapter.InsertProduct("New Product", 1, 1, "12 tins per carton", 14.95m, 10, 0, 10, false));

// 经过重新考虑,删除这个产品
productsAdapter.Delete(new_productID);

第 5 步:完成数据访问层

注意:ProductsTableAdapters 类从 Products 数据表中返回了 CategoryID 和 SupplierID 值,但是并没有包括 Categories 数据表中的 CategoryName 列或者 Suppliers 数据表中的 CompanyName 列,尽管这些都是我们在显示产品信息的时候很可能需要显示的列。所以我们能够添加 TableAdapter 的初始化方法 GetProducts(),来同时包括 CategoryName 和 CompanyName 列的值,它们将更新强类型的 DataTable 来很好地包括这些新列。

虽然这样做能够呈现一个问题,但是,与 TableAdapter 中不基于这个初始化方法并且用来插入、更新,并且删除数据的方法一样,自动生成的用来插入、更新,并且删除的方法并不会受到子句中的子查询的影响。谨慎地把我们的查询作为子句添加到 Categories 和 Suppliers 中要胜于使用 JOIN,并且我们将避免重写这些用来更改数据的方法。右键单击 ProductsTableAdapter 中的 GetProducts() 方法并且选择 Configure。然后,把 SELECT 子句调整成如下所示:

SELECT     ProductID, ProductName, SupplierID, CategoryID,
QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
(SELECT CategoryName FROM Categories
WHERE Categories.CategoryID = Products.CategoryID) as CategoryName,
(SELECT CompanyName FROM Suppliers
WHERE Suppliers.SupplierID = Products.SupplierID) as SupplierName
FROM         Products

图 29:更新 GetProducts() 方法的 SELECT 声明

在把 GetProducts() 方法更新成使用这个新查询之后,这个 DataTable 中就会包括两个新列:CategoryName 和 SupplierName。

图 30:Products DataTable 中有两个新列

最好还是花一点时间来更新 GetProductsByCategoryID(categoryID) 方法中的 SELECT 子句。

如果你使用 JOIN 语法来更新 GetProducts() 中的 SELECT 声明,那么 DataSet Designer 就不能自动生成使用数据库直接模式来插入、更新,并且删除数据库数据的方法。作为替代,你可以使用本指南中较早部分的 InsertProduct 方法中相同的方式来手动创建它们。此外,你还将手动提供 InsertCommand、UpdateCommand,和 DeleteCommand 属性值,如果你需要使用批量更新模式的话。

添加其余的 TableAdapters

直到现在,我们只看到了单个数据表的单个 TableAdapter 操作。但是,Northwind 数据库中包含有几个我们需要在 Web 应用程序中进行操作的相关数据表。类型化的 DataSet 也能够包含多个相关的 DataTable。因此,要完成我们的 DAL,我们就需要为指南中使用的其他数据表而添加 DataTable。要添加新的 TableAdapter 到类型化的 DataSet 中,就需要打开 DataSet Designer,在 Designer 中右击,并且选择 Add / TableAdapter。这将创建一个新的 DataTable 和 TableAdapter 并且帮助你通过我们在本指南较早部分所介绍的向导。

花几分钟的时间来创建下列 TableAdapter 和使用下列查询的方法。注意:ProductsTableAdapter 中的查询包括了提取每个产品目录和提供者名称的子查询。另外,如果你已经完成了下列任务,那么你就已经添加了 ProductsTableAdapter 类的 GetProducts() 方法和 GetProductsByCategoryID(categoryID) 方法。

  • ProductsTableAdapter
    • GetProducts:

      SELECT ProductID, ProductName, SupplierID, CategoryID, 
      QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
      ReorderLevel, Discontinued , (SELECT CategoryName FROM 
      Categories WHERE Categories.CategoryID = 
      Products.CategoryID) as CategoryName, (SELECT CompanyName 
      FROM Suppliers WHERE Suppliers.SupplierID = 
      Products.SupplierID) as SupplierName
      FROM Products
    • GetProductsByCategoryID:

      SELECT ProductID, ProductName, SupplierID, CategoryID, 
      QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
      ReorderLevel, Discontinued , (SELECT CategoryName FROM 
      Categories WHERE Categories.CategoryID = 
      Products.CategoryID) as CategoryName, 
      (SELECT CompanyName FROM Suppliers WHERE 
      Suppliers.SupplierID = Products.SupplierID) as SupplierName
      FROM Products
      WHERE CategoryID = @CategoryID
    • GetProductsBySupplierID

      SELECT ProductID, ProductName, SupplierID, CategoryID, 
      QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
      ReorderLevel, Discontinued , 
      (SELECT CategoryName FROM Categories WHERE 
      Categories.CategoryID = Products.CategoryID) 
      as CategoryName, (SELECT CompanyName FROM Suppliers 
      WHERE Suppliers.SupplierID = Products.SupplierID) 
      as SupplierName
      FROM Products
      WHERE SupplierID = @SupplierID
    • GetProductByProductID

      SELECT ProductID, ProductName, SupplierID, CategoryID, 
      QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, 
      ReorderLevel, Discontinued , (SELECT CategoryName 
      FROM Categories WHERE Categories.CategoryID = 
      Products.CategoryID) as CategoryName, 
      (SELECT CompanyName FROM Suppliers 
      WHERE Suppliers.SupplierID = Products.SupplierID) 
      as SupplierName
      FROM Products
      WHERE ProductID = @ProductID
  • CategoriesTableAdapter
    • GetCategories

      SELECT CategoryID, CategoryName, Description
      FROM Categories
    • GetCategoryByCategoryID

      SELECT CategoryID, CategoryName, Description
      FROM Categories
      WHERE CategoryID = @CategoryID
  • SuppliersTableAdapter
    • GetSuppliers

      SELECT SupplierID, CompanyName, Address, City, 
      Country, Phone
      FROM Suppliers
    • GetSuppliersByCountry

      SELECT SupplierID, CompanyName, Address, 
      City, Country, Phone
      FROM Suppliers
      WHERE Country = @Country
    • GetSupplierBySupplierID

      SELECT SupplierID, CompanyName, Address, 
      City, Country, Phone
      FROM Suppliers
      WHERE SupplierID = @SupplierID
  • EmployeesTableAdapter
    • GetEmployees

      SELECT EmployeeID, LastName, FirstName, 
      Title, HireDate, ReportsTo, Country
      FROM Employees
    • GetEmployeesByManager

      SELECT EmployeeID, LastName, FirstName, 
      Title, HireDate, ReportsTo, Country
      FROM Employees
      WHERE ReportsTo = @ManagerID
    • GetEmployeeByEmployeeID

      SELECT EmployeeID, LastName, FirstName, 
      Title, HireDate, ReportsTo, Country
      FROM Employees
      WHERE EmployeeID = @EmployeeID

图 31:添加了四个 TableAdapter 之后的 DataSet Designer

在 DAL 中添加自定义代码

被添加到类型化的 DataSet 中的 TableAdapter 和 DataTable 都被表达成了一个 XML 结构定义文件(Northwind.xsd)。另外,你还可以在 Solution Explorer 中右击 Northwind.xsd 文件并且选择 View Code 来查看这些结构的信息。

图 32:Northwinds 数据库中类型化的 DataSet 的 XML 结构定义(XSD)文件

在已编译或者运行时(如果是必需的话),这个结构信息会被转译成设计时的 C# 或者 Visual Basic 代码,到时候你就能够逐步地通过调试器来完成这些任务。要查看自动生成的代码,你可以转到 Class View 并且追溯 TableAdapter 类或者类型化的 DataSet 类。如果你没有在屏幕上看到 Class View,那么可以转到 View 菜单并且从那里进行选择,或者单击 Ctrl + Shift + C。然后,从 Class View 中你就能够看到类型化的 DataSet 类和 TableAdapter 类的属性、方法,以及事件。要查看特定方法的代码,可以双击 Class View 中的方法名称或者右击方法然后选择 Go To Definition。

图 33:检查通过从 Class View 中选择 Go To Definition 而自动生成的代码

虽然自动生成的代码可能是伟大的时间救星,但是被生成的代码通常都是非常通用的并且需要经过自定义才能够适应应用程序的需要。并且扩展自动生成的代码也是有风险的,因为生成代码的工具可能会自行决定 “生成” 的时机并且覆盖掉你的自定义。在 .NET 2.0 的类的部分新概念中将更加容易地把类分割进多个文件中。这允许我们把自定义的方法、属性,和事件添加到自动生成的类中,从而不需要担心 Visual Studio 会覆盖掉我们的自定义。

要示范如何自定义 DAL,就先让我们把 GetProducts() 方法添加到 SuppliersRow 类中。SuppliersRow 类呈现了 Suppliers 数据表中的一个单独记录;每个提供者都能够提供零到多个产品,所以 GetProducts() 将会返回指定提供者的产品。要完成这个任务,就需要在 App_Code 目录中创建一个名为 SuppliersRow.cs 的新类文件然后添加下列代码:

using System;
using System.Data;
using NorthwindTableAdapters;

public partial class Northwind
{
    public partial class SuppliersRow
    {
        public Northwind.ProductsDataTable GetProducts()
        {
            ProductsTableAdapter productsAdapter =
             new ProductsTableAdapter();
            return
              productsAdapter.GetProductsBySupplierID(this.SupplierID);
        }
    }
}

这个特定的类会通知编译器:需要建立 Northwind.SuppliersRow 类来包括我们刚才所定义的 GetProducts() 方法。如果你建立了你的工程然后返回到 Class View 中,那么你将看到 GetProducts() 现在已经列表成了 Northwind.SuppliersRow 的一个方法。

图 34:GetProducts() 方法现在已经是 Northwind.SuppliersRow 类的一部分了

GetProducts() 方法现在能够被用来枚举特定提供者的产品集合,如下代码所示:

NorthwindTableAdapters.SuppliersTableAdapter suppliersAdapter = new NorthwindTableAdapters.SuppliersTableAdapter();

// 获取所有提供者
Northwind.SuppliersDataTable suppliers =
  suppliersAdapter.GetSuppliers();

// 枚举提供者
foreach (Northwind.SuppliersRow supplier in suppliers)
{
    Response.Write("Supplier: " + supplier.CompanyName);
    Response.Write("<ul>");

    // 列出这个提供者的产品
    Northwind.ProductsDataTable products = supplier.GetProducts();
    foreach (Northwind.ProductsRow product in products)
        Response.Write("<li>" + product.ProductName + "</li>");

    Response.Write("</ul><p>&nbsp;</p>");
}

这个数据同样能够显示在任何 ASP.NET 数据 Web 控件中。下列页面使用了一个拥有两个字段的 GridView 控件:

  • 一个显示每个提供者名称的 BoundField。
  • 一个包含绑定到由每个提供者的 GetProducts() 方法所返回结果的 BulletedList 控件的 TemplateField。

在将来的指南中我们将检查如何显示相关的详细报告。但是现在,这个范例被设计成用来说明使用被添加到 Northwind.SuppliersRow 类中的自定义方法。

SuppliersAndProducts.aspx
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="SuppliersAndProducts.aspx.cs" Inherits="SuppliersAndProducts" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title>Untitled Page</title>
    <link href="Styles.css" rel="stylesheet" type="text/css" />
</head>
<body>
    <form id="form1" runat="server">
    <div>
        <h1>
            Suppliers and Their Products</h1>
        <p>
            <asp:GridView ID="GridView1" runat="server"
             AutoGenerateColumns="False"
             CssClass="DataWebControlStyle">
                <HeaderStyle CssClass="HeaderStyle" />
                <AlternatingRowStyle CssClass="AlternatingRowStyle" />
                <Columns>
                    <asp:BoundField DataField="CompanyName"
                      HeaderText="Supplier" />
                    <asp:TemplateField HeaderText="Products">
                        <ItemTemplate>
                            <asp:BulletedList ID="BulletedList1"
                             runat="server" DataSource="<%#
              ((Northwind.SuppliersRow)((System.Data.DataRowView)
              Container.DataItem).Row).GetProducts() %>"
                                    DataTextField="ProductName">
                            </asp:BulletedList>
                        </ItemTemplate>
                    </asp:TemplateField>
                </Columns>
            </asp:GridView>
            &nbsp;</p>

    </div>
    </form>
</body>
</html>
SuppliersAndProducts.aspx.cs
using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindTableAdapters;

public partial class SuppliersAndProducts : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        SuppliersTableAdapter suppliersAdapter = new
          SuppliersTableAdapter();
        GridView1.DataSource = suppliersAdapter.GetSuppliers();
        GridView1.DataBind();
    }
}

图 35:提供者的公司名称被列在左边的列中,而他们的产品则列在右边

概述

在建立 Web 应用程序的时候创建 DAL 应该是第一个步骤,并且发生在你开始创建表示层之前。在 Visual Studio 中,创建基于类型化的 DataSet 的 DAL 的任务能够在 10 到 15 分钟之内就被完成。本指南的所有进度都依赖于这个 DAL。另外,在下一个指南中我们将定义更多的业务规则并且了解如何在一个单独的业务逻辑层中实现它们。

编程快乐!

进一步的阅读

更多关于本指南中已讨论主题的信息,请参考下列资源:

posted on 2007-02-25 21:38  Laeb  阅读(5553)  评论(0编辑  收藏  举报