利用强类型数据集创建数据库访问层

创建数据访问层

本文档是 Visual C# 教程    (切换到 Visual Basic 教程)

该教程从头开始使用 Typed DataSet(强类型 DataSet)创建数据访问层 (DAL),以访问数据库中的信息。

« 前一篇教程  |  下一篇教程 »

 

简介

作为web 开发人员,我们的工作总是在和数据打交道。我们创建数据库来存储数据,编写代码来检索并修改数据,并创建Web 页面来收集和汇总数据。这是探讨在ASP.NET 2.0 中实现这些常用类型的技巧的系列教程中的首篇教程。我们从创建一个软件架构 开始,包括使用Typed DataSet 的数据访问层(DAL) 、实现自定义业务规则的业务逻辑层(BLL) 和共享同一页面布局的ASP.NET 页面组成的表示层。一旦奠定了这个基础,我们接下来会转向报表,说明如何显示、汇总、收集和验证来自Web 应用程序的数据。这些教程力求简明,使用大量屏幕截图逐步教您直观地了解整个流程。每个教程都提供C# 和Visual Basic 版本,并且可以下载所使用的全部代码。(这篇教程内容非常冗长,但接下来会分几大部分进行介绍,使人更容易理解和消化。)

针对这些教程,我们将使用放在 App_Data 目录下 Northwind 数据库的 Microsoft SQL Server 2005 Express Edition 版本。除数据库文件外,App_Data 文件夹也包含创建该数据库的 SQL 脚本,以满足您想使用不同数据库版本的需求。如果愿意,这些脚本也可以直接从 Microsoft 下载 。如果您使用的是 Northwind 数据库的不同 SQL Server 版本,需要更新该应用程序的Web.config 文件中的NORTHWNDConnectionString 设置。这个Web 应用程序是使用 Visual Studio 2005 Professional Edition 创建的基于文件系统的 Web 站点项目。不过,所有的这些教程同样适用于Visual Studio 2005 免费版,即 Visual Web Developer 。

该教程从头开始,先创建数据访问层 (DAL) ,然后在第二篇教程中创建业务逻辑层(BLL) ,并在第三篇教程中进行页面布局和导航。随后的教程以前三篇教程为基础。在这篇教程中我们有很多内容要学习,现在就让我们打开Visual Studio 开始吧!

步骤1 : 创建一个 Web 项目并连接到数据库

在创建我们的数据访问层 (DAL) 之前,我们首先需要创建一个网站并安装我们的数据库。开始创建一个新的基于文件系统的ASP.NET 网站:从 File 菜单选择 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 创建Typed DataSet 时,我们需要将 Visual Studio 指向需要建立 Typed DataSet 的数据库。当我们能够及时在那个点上提供这种连接信息时,Visual Studio 自动填充己在 Server Explorer 注册过的数据库的下拉列表。

将 Northwind 数据库添加到Server Explorer 的步骤取决于您是否使用App_Data 文件夹中的SQL Server 2005 Express Edition 数据库,或者是否有您想使用的Microsoft SQL Server 2000 或 2005 数据库服务器安装程序。

使用 App_Data 文件夹中的数据库

如果您没有 SQL Server 2000 或2005 数据库服务器可连接,或者您只想避免将该数据库添加到数据库服务器的麻烦,可以使用位于已下载网站源代码的App_Data 文件夹 中 的Northwind 数据库的 SQL Server 2005 Express Edition 版本(NORTHWND.MDF) 。

位于 App_Data 文件夹中的数据库将被自动添加到 Server Explorer 。假如您安装了SQL Server 2005 Express Edition ,在 Server Explorer 应该看到一个名为NORTHWND.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 ,但理想的是应使用强类型的对象来返回这些结果。强类型对象指的是编译时对其schema 进行严格定义的对象。相反,弱类型的对象指的是只有在运行时才知道其schema 的对象。

例如,DataReader 和DataSet (默认)是弱类型对象,因为它们的 schema 是由用来填充它们的数据库查询返回的列来定义的。要访问弱类型DataTable 中的某列,需要使用如下语法:DataTable.Rows[index]["columnName"] 。我们需要使用字符串或序号索引访问列名这一点就显示了该例中DataTable 的弱类型特性。另一方面,强类型的 DataTable 有各自作为属性实现的列,生成如下代码:DataTable.Rows[index].columnName 。

要返回强类型对象,开发人员可以创建他们自己的自定义业务对象或使用Typed DataSet 。一个业务对象由开发人员实现成一个类,该类的属性通常反映出该业务对象表示的基础数据库表的列。Typed DataSet 是由 Visual Studio 基于数据库 schema 为您生成的一个类,其成员都是强类型的。Typed DataSet 本身包括了拓展ADO.NET DataSet 、DataTable 和 DataRow 类的类。除强类型 DataTable 外,Typed DataSet 现在还包括 TableAdapter ,该类具有填充 DataSet 的DataTable 以及将 DataTable 中所作修改传回数据库的方法。

注意:有关使用Typed DataSet 与自定义业务对象的优劣比较的更多信息,请参见设计数据层组件并在层间传递数据 。

我们对这些教程的体系结构使用强类型的DataSet 。图 3 说明了使用 Typed DataSet 的应用程序不同层间的工作流程。

 

 

图3 :所有的数据访问代码都在 DAL 中定义

创建一个 Typed DataSet 和Table Adapter

要创建我们的 DAL ,要先将一个Typed DataSet 添加到我们的项目。为此,右键单击 Solution Explorer 中的项目节点,并选择 Add a New Item 。从模板列表中选择DataSet 选项,并将其命名为Northwind.xsd 。

 

 

图4 :选择添加一个新的DataSet 到您的项目

单击 Add 后,出现提示添加DataSet 到App_Code 文件夹,选择Yes 。接着出现 Typed DataSet 设计器并将启动 TableAdapter Configuration Wizard ,允许您将您的第一个 TableAdapter 添加到 Typed DataSet 。

Typed DataSet 充当一个强类型的数据集;它由强类型的DataTable 实例组成,每个实例依次由强类型的 DataRow 实例组成。我们将为该教程系列所需的每个基础数据库表创建一个强类型的DataTable 。我们开始为Products 表创建 DataTable 。

要记住,强类型的 DataTable 不包含有关如何从其基础数据库表访问数据的任何信息。为了检索数据以填充DataTable ,我们使用一个 TableAdapter 类,作为我们的数据访问层。对于我们的Products 数据表,TableAdapter 将包含 GetProducts() 、GetProductByCategoryID(categoryID) 等方法,我们将从表示层调用它们。DataTable 的作用是充当用来在层间传送数据的强类型对象。

TableAdapter Configuration Wizard 从提示您选择要操作的数据库开始。下拉列表显示出Server Explorer 中的数据库。如果您没有把 Northwind 数据库添加到 Server Explorer ,此时单击 New Connection 按钮进行该操作。

 

 

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

选择了数据库单击 Next 后, 将会问您是否希望在Web.config 文件中保存连接字符串。保存了连接字符串,就避免了将连接字符串写在TableAdapter 类的代码中。这样一旦以后连接字符串信息改变,要做的工作也变得简单。如果选择将连接字符串保存到放在<connectionStrings> 区域的配置文件,以后可以通过 IIS GUI Admin Tool 内的新 ASP.NET 2.0 Property Page 选择加密 以改进安全性或修改。这样更适合管理员工作。

 

 

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

接下来,我们需要为第一个强类型的DataTable 定义 schema ,并为我们的 TableAdapter 提供填充强类型的 DataSet 时所用到的第一个方法。通过创建一个查询,可同时完成这两个步骤,这个查询能够返回我们希望映射我们的DataTable 中的数据表的列。在向导结束时,我们将为该查询提供一个方法名称。完成后,就可以从我们的表示层调用该方法。此法将执行所定义的查询并填充强类型的DataTable 。

要开始定义 SQL 查询,就必须先说明希望 TableAdapter 进行查询的方式。我们可以使用一个 ad-hoc SQL 语句,创建一个新的存储过程,或使用现有的存储过程。对于本系列教程,我们将使用ad-hoc SQL 语句。有关使用存储过程的例子,请参见Brian Noyes 的文章:使用 Visual Studio 2005 DataSet Designer 构建一个数据访问层 。

 

 

 

 

 

图7 :使用ad-hoc SQL 语句查询数据

此时,我们可以手动输入 SQL 查询。在TableAdapter 创建第一个方法时,您通常希望查询返回需要在相应DataTable 中存放的那些列。这可以通过创建返回来自Products 表的所有列和行的查询实现:

 

 

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

也可以使用 Query Builder 用图像构建查询,如图 9 所示。

 

 

图9 :通过Query Editor 用图像创建查询

创建查询之后,要在转到下一屏幕之前,单击Advanced Options 按钮。在 Web Site 项目中,“Generate Insert, Update, and Delete statements ”是默认选择的唯一高级选项;如果从Class Library 或Windows Project 运行该向导,“Use optimistic concurrency (使用并发优化)”选项将也被选中。现在暂不勾选该选项。我们将在以后的教程中详细介绍并发优化。

 

 

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

核实了高级选项后,单击 Next 进入最后一个屏幕。这时会询问我们选择那些方法添加到TableAdapter 。填充数据的模式有两种:

  • Fill a DataTable:通过这种途径创建的方法将 DataTable 作为一个参数,并根据查询结果对其进行填充。例如,ADO.NET DataAdapter 类使用其 Fill()方法实现了该模式。
  • Return a DataTable:通过这种途径创建的方法为您创建并填充DataTable ,并将 DataTable 作为方法的返回值。

您可以在 TableAdapter 实现这两种模式中的一种或两种。也可以重命名此处所提供的方法。即使我们在整个教程中只使用第二种模式,我们也可以同时选中两个复选框。并且将更通用的GetData 法重命名为GetProducts 。

如果选中最后一个复选框"GenerateDBDirectMethods" ,会为 TableAdapter 创建 Insert() 、Update() 和 Delete() 方法。如果没有选中该选项,所有的更新则需要通过TableAdapter 唯一的 Update() 方法进行,该方法接受 Typed DataSet 、DataTable 、单个DataRow 或 DataRow 数组。(如果没有选中图 9 高级选项中的"Generate Insert, Update, and Delete statements" 选项,该复选框的设置则不起作用。)在这里,我们勾选这个复选框。

 

 

图11 :将方法名称GetData 改为GetProducts

单击 Finish 结束向导。向导关闭后,返回到显示我们刚创建的DataTable 的 DataSet 设计器。可以看到Products 数据表中各列的列表(ProductID 、ProductName 等),以及ProductsTableAdapter 的方法(Fill() 和GetProducts() )。

 

 

图12 :Products DataTable 和ProductsTableAdapter 已被添加到Typed DataSet

这里,我们拥有一个带有单个 DataTable (Northwind.Products) 的Typed DataSet ,还有一个提供 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 和其它。下面举例说明如何在Page_Load 事件处理程序仅用三行代码就将 GetProducts() 法返回的 DataTable 绑定到 GridView 。

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>
        <h2>
            All Products</h2>
        <p>
            <asp:GridView ID="GridView1" runat="server"
             CssClass="DataWebControlStyle">
               <HeaderStyle CssClass="HeaderStyle" />
               <AlternatingRowStyle CssClass="AlternatingRowStyle" />
            </asp:GridView>
             </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 中的产品列表

尽管本例还需要我们在 ASP.NET 页面的 Page_Load 事件处理程序中编写三行代码,不过在以后的教程中我们将详细介绍如何使用ObjectDataSource ,用声明的方式获取 DAL 数据。使用 ObjectDataSource 我们不必编写代码也可以进行分页和排序!

 

步骤3 :向数据访问层添加带参数的方法

我们的 ProductsTableAdapter 类此时有且只有一个方法:GetProducts() ,它返回数据库内的所有产品信息。尽管能够处理所有产品是肯定有用的,不过有时候我们希望只检索特定产品或属于某个类别的所有产品的信息。要在我们的数据访问层中添加这个功能,可以通过带参数的方法添加到TableAdapter 来实现。

现在我们添加 GetProductsByCategoryID(categoryID) 方法。向 DAL 添加一个新方法,返回到 DataSet Designer ,右键单击ProductsTableAdapter 区域,并选择Add Query 。

 

 

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

我们首先被问到是否希望使用 ad-hoc SQL 语句或创建或使用现有的存储过程来访问数据库。再次选择使用ad-hoc SQL 语句。接下来会询问我们想使用的 SQL 查询类型。由于我们希望返回属于某一指定类别的所有产品信息,我们想编写一个返回行的SELECT 语句。

 

 

图15 :选择创建返回行的SELECT 语句

下一步是定义用来访问数据的 SQL 查询。由于我们希望只返回属于某一指定类别的那些产品信息,我使用的是GetProducts() 中的同一个 SELECT 语句,但是添加了下面的 WHERE 子句:WHERE CategoryID = @CategoryID 。@CategoryID 参数向TableAdapter 向导表明,我们正在创建的方法将要求一个对应类型的输入参数(也就是说,一个可为Null 的整数)。

 

 

图16 :查询只返回指定类别中的 产品信息

在最后一个步骤,我们可以选择要使用的数据访问模式,并自定义所生成方法的名称。将Fill 模式重命名为FillByCategoryID ,并使用 GetProductsByCategoryID 作为返回 DataTable 返回模式(GetX 方法)的名称。

 

 

图17 :为TableAdapter 方法选择名称

向导结束后,DataSet 设计器包含新的TableAdapter 方法。

 

 

图18 :产品现在可按类别进行查询

使用同样的技术再添加 GetProductByProductID(productID) 方法

可以直接从 DataSet 设计器对这些带参数的查询进行检验。右键单击TableAdapter 中的方法并选择 Preview Data 。然后输入用于参数的值并单击Preview 。

 

 

图19 :显示出的属于饮料类别的产品信息

通过 DAL 中的 GetProductsByCategoryID(categoryID) 方法,我们现在可以创建一个只显示指定类别的那些产品的ASP.NET 页面。下面举例显示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>
        <h2>Beverages</h2>
        <p>
            <asp:GridView ID="GridView1" runat="server"
             CssClass="DataWebControlStyle">
               <HeaderStyle CssClass="HeaderStyle" />
               <AlternatingRowStyle CssClass="AlternatingRowStyle" />
            </asp:GridView>
             </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 :显示出的饮料类别中的产品

步骤4 : 数据的添加、更新和删除

添加、更新和删除数据的常用模式有两种。第一种模式,我称之为数据库直接模式,当涉及的方法被调用时,会向数据库发送一个INSERT 、UPDATE 或DELETE 命令,该命令只对单个数据库记录进行操作。这些方法通常通过一系列的标量值(整数、字符串、布尔类型、DateTimes 等)来传递参数,这些值与要添加、更新或删除的值相对应。例如,采用这种模式对Products 表进行操作,删除方法将采用一个整数参数,指明要删除的记录的ProductID ,而添加法将对 ProductName 采用字符串,对 UnitPrice 采用十进制,对 UnitsOnStock 采用整数值等。

 

 

图21 :每个添加、更新和删除请求被立即送达数据库

我把另一种模式称为批量更新模式,就是在一次方法调用中更新整个DataSet 、DataTable 、或 DataRows 集合。通过这种模式,开发人员在 DataTable 中删除、添加并修改 DataRow ,然后将那些 DataRow 或 DataTable 传递给一个更新方法。该方法随后列举传入的DataRow ,确定它们是否要进行修改、添加或删除(通过DataRow 的RowState 属性 值),并为每条记录发出适当的数据库请求。

 

 

图22 :调用更新方法时,所有更改都和数据库保持同步

TableAdapter 默认采用的是批量更新模式,但也支持数据库直接模式。由于创建我们的TableAdapter 时选择了 Advanced Properties 中的“Generate Insert, Update, and Delete statements ”选项,所以 ProductsTableAdapter 包含一个实现批量更新模式的 Update() 方法。具体点说,TableAdapter 包含 Update() 方法,可以传入一个强类型的 DataTable ,即 Typed DataSet ,或一个或多个 DataRow 传递。如果您在首次创建 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();
// For each product, double its price if it is not discontinued and
// there are 25 items in stock or less
Northwind.ProductsDataTable products = productsAdapter.GetProducts();
foreach (Northwind.ProductsRow product in products)
   if (!product.Discontinued && product.UnitsInStock <= 25)
      product.UnitPrice *= 2;
// Update the products
productsAdapter.Update(products);

下面的代码表明如何使用数据库直接模式通过编码实现删除、更新某个产品,然后添加某个新产品:

NorthwindTableAdapters.ProductsTableAdapter productsAdapter =
    new NorthwindTableAdapters.ProductsTableAdapter();
// Delete the product with ProductID 3
productsAdapter.Delete(3);
// Update Chai (ProductID of 1), setting the UnitsOnOrder to 15
productsAdapter.Update("Chai", 1, 1, "10 boxes x 20 bags",
  18.0m, 39, 15, 10, false, 1);
// Add a new product
productsAdapter.Insert("New Product", 1, 1,
  "12 tins per carton", 14.95m, 15, 0, 10, false);

创建自定义Insert 、Update 和Delete 方法

由数据库直接方法创建的 Insert() 、Update() 和Delete() 方法有点麻烦,尤其是对于那些有许多列的表。看前面的代码示例,没有IntelliSense 的帮助,Products 表的列与Update() 和 Insert() 方法的每个输入参数的映射关系就很不明显。有时候我们可能只想更新一个或两个列,或者需要一个自定义Insert() 方法,该法可能返回新添加记录的IDENTITY (自动递增)字段的值。

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

 

 

图25 :创建一个向Products 表添加新行的方法

下一个屏幕上出现 InsertCommand 的 CommandText 。在查询末尾添加SELECT SCOPE_IDENTITY() 语句,这样将返回同一范围内添加到IDENTITY 列的最后一个identity 值。(参见技术文档 了解有关SCOPE_IDENTITY() 的更多信息以及您可能希望使用 SCOPE_IDENTITY() 代替 @@IDENTITY 的原因。)确保您在添加SELECT 语句之前用一个分号结束INSERT 语句。

 

 

图26 :增大返回SCOPE_IDENTITY() 值的查询范围

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

 

 

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

当您返回到 DataSet 设计器时,您会发现ProductsTableAdapter 包含了新方法:InsertProduct 。如果对应 Products 表中的每个列,这个新方法没有对应的参数,可能就是您忘记用分号来终止INSERT 语句。配置InsertProduct 方法并确保您使用了分号来终止INSERT 和 SELECT 语句。

默认状态下,添加方法调用的是非查询方法,意味着它们返回的是受影响的行数。不过,我们希望InsertProduct 方法返回查询返回的值,而不是受影响的行数。为此,将InsertProduct 方法的 ExecuteMode 的属性修改为Scalar 。

图28 :将 ExecuteMode 属性更改为Scalar

下面的代码表明了运行中的这个新的InsertProduct 方法:

NorthwindTableAdapters.ProductsTableAdapter productsAdapter =
    new NorthwindTableAdapters.ProductsTableAdapter();
// Add a new product
int new_productID = Convert.ToInt32(productsAdapter.InsertProduct
    ("New Product", 1, 1, "12 tins per carton", 14.95m, 10, 0, 10, false));
// On second thought, delete the product
productsAdapter.Delete(new_productID);

 

步骤 5:完成数据访问层

注意,ProductsTableAdapters 类返回Products 表的 CategoryID 和 SupplierID 值,但不包含Categories 表的 CategoryName 列,或 Suppliers 表的 CompanyName 列,尽管在显示产品信息时我们可能也希望显示这些列。我们可以扩充TableAdapter 初始方法GetProducts() ,使其包括 CategoryName 和 CompanyName 列值,这将更新强类型的 DataTable ,使其同样包含这些新列。

但是,这样就会出现一个问题,因为TableAdapter 的添加、更新和删除方法并不是基于这个初始方法。幸运的是,自动生成的添加、更新和删除方法不受SELECT 子句中的子查询影响。通过把对 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 数据表有两个新列

花点时间也来更新 GetProductsByCategoryID(categoryID) 方法中的 SELECT 子句。

如果使用 JOIN 语法更新 GetProducts() 中的 SELECT ,DataSet 设计器将不能使用数据库直接模式自动生成数据插入、更新和删除的方法。您不得不手动的生成这些方法,就好象在本教程早先时候我们对InsertProduct 方法的做法一样。另外,如果您希望使用批量更新模式,就必须手动提供InsertCommand 、UpdateCommand 和DeleteCommand 属性值。

添加剩余的TableAdapters

至今为止,我们只是介绍了单个数据库表的单个TableAdapter 。但是,Northwind 数据库包含需要我们在我们的 Web 应用程序中操作的几个相关表。一个 Typed DataSet 可以包含多个相关的 DataTable 。因此,要完成我们的 DAL ,还需要为我们在这些教程中用到的其它表添加DataTable 。要添加新的 TableAdapter 到 Typed DataSet ,打开 DataSet Designer ,右键单击Designer 并选择 Add / TableAdapter 。这将创建一个新的 DataTable 和 TableAdapter ,并引导你完成我们在前面教程所讨论的配置向导。

花几分钟的时间,用下面的查询语句创建对应的TableAdapters 及其方法。注意,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 设计器

向 DAL 添加自定义代码

添加到 Typed DataSet 的TableAdapter 和 DataTable 由 XML Schema Definition 文件 (Northwind.xsd) 来描述。通过右键单击 Solution Explorer 中的Northwind.xsd 文件并选择View Code ,可以查看该 schema 的信息。

 

 

图32 :针对Northwinds Typed DataSet 的 XML Schema Definition (XSD) 文件

编译或运行时(如果需要),该schema 信息在设计时被译成 C# 或 Visual Basic 代码,此时您可以使用调试器进行调试。要查看这个自动生成的代码,转入Class View 并找到TableAdapter 或 Typed DataSet 类。如果在屏幕上看不到 Class View ,转入View 菜单并选中它,或按下 Ctrl+Shift+C 。从 Class View 上可以看到 Typed DataSet 和TableAdapter 类的属性、方法和事件。要查看某个方法的代码,双击Class View 中该方法的名称或右键单击它并选择 Go To Definition 。

 

 

图33 : 通过选择Class View 的 Selecting 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() 方法。如果您 build 您的项目,然后返回到 Class View ,将看见GetProducts() 现在被列为Northwind.SuppliersRow 的方法。

 

 

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

GetProducts() 方法现在可用来列举某供应商的全套产品,如下面的代码所示:

NorthwindTableAdapters.SuppliersTableAdapter suppliersAdapter =
    new NorthwindTableAdapters.SuppliersTableAdapter();
// Get all of the suppliers
Northwind.SuppliersDataTable suppliers =
  suppliersAdapter.GetSuppliers();
// Enumerate the suppliers
foreach (Northwind.SuppliersRow supplier in suppliers)
{
    Response.Write("Supplier: " + supplier.CompanyName);
    Response.Write("<ul>");
    // List the products for this supplier
    Northwind.ProductsDataTable products = supplier.GetProducts();
    foreach (Northwind.ProductsRow product in products)
        Response.Write("<li>" + product.ProductName + "</li>");
    Response.Write("</ul><p> </p>");
}

该数据也可以在任何 ASP.NET 的Web 数据控件中显示。以下页面使用了具有两个字段的GridView 控件:

  • 显示每个供应商名称的 BoundField 和
  • 一个 TemplateField ,它包含了一个 BulletedList 控件,该控件与每个供应商的 GetProducts() 方法的返回结果绑定。

我们会在后面的教程中探讨如何显示这种主/ 明细报表。就目前而言,该示例旨在说明添加到Northwind.SuppliersRow 类的自定义方法的使用。

SuppliersAndProducts.aspx

<%@ Page Language="C#" CodeFile="SuppliersAndProducts.aspx.cs"
    AutoEventWireup="true" 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>
        <h2>
            Suppliers and Their Products</h2>
        <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>
             </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 :供应商的公司名列在左列,他们的产品在右列

小结

创建DAL 应该是开发一个Web 应用程序的第一步,这要在开始创建您的表示层之前进行。通过Visual Studio ,创建一个基于Typed DataSet 的DAL 就成为一项不需要编写一行代码,在10 到15 分钟内就可以完成的任务。教程后面的内容仍旧围绕DAL 进行。下一教程 我们将定义几个业务规则并看看如何在单个业务逻辑层中实现它们。

快乐编程!

 

创建业务逻辑层

本文档是 Visual C# 教程 (切换到 Visual Basic 教程)

本教程中,我们将了解怎样将业务规则集中到在表示层与 DAL 之间,充当数据交互中介的业务逻辑层 (BLL) 中。

« 前一篇教程  |  下一篇教程 »

 

简介

在教程一中创建的数据访问层 (DAL) 将数据访问逻辑与表示逻辑清晰地分离开来。然而,尽管 DAL 从表示层中清晰地分离出数据访问层细节,它却并没有实施任何可能采用的业务规则。例如,我们想让我们的应用程序在 Discontinued 字段设为 1 时禁止对 Products 表的 CategoryID 或 SupplierID 字段的修改,还有,我们可能想实施一些资历规则以便禁止发生这样的情况:雇员被其后入职的另一雇员所管理。另一种常见的情形是授权 – 可能只有处于特定职位的用户可以删除产品或更改 UnitPrice 值。

通过本教程,我们可以了解怎样将业务规则集中到在表示层与DAL 之间充当数据交互中介的业务逻辑层 (BLL) 中。在真实的应用程序中,BLL 应作为一个单独的类库项目而实现。然而,为了简化项目结构,在这些教程中,我们以 App_Code 文件夹下的一系列的类来实现 BLL 。图 1 展示了表示层、BLL 和 DAL 之间的结构关系。

 

 

图1 :BLL 将表示层与数据访问层分隔开来并且实施业务规则。

步骤1 :创建 BLL 类

我们的BLL 将由四个类组成,分别对应 DAL 中不同的 TableAdapter 。每个 BLL 类都具有一些方法,这些方法可以从 DAL 中该类对应的 TableAdapter 中检索、插入、更新或删除数据并应用相应的业务规则。

为了更清楚地区分 DAL 的相关类与 BLL 的相关类,我们在 App_Code 文件夹下创建两个子文件夹:DAL 和 BLL 。创建时,只需右健单击 Solution Explorer 中的 App_Code 文件夹并选择 New Folder 。创建了这两个文件夹后,将教程一中创建的 Typed DataSet 移动到 DAL 子文件夹中。

然后,在BLL 子文件夹中创建四个 BLL 类文件。为此,右键单击 BLL 子文件夹,选择 Add a New Item ,然后选择 Class 模板。将这四个类分别命名为 ProductsBLL 、 CategoriesBLL 、 SuppliersBLL 和 EmployeesBLL 。

 

 

图2 :在App_Code 文件夹中添加四个新类

接下来让我们在每个类中添加一些方法,这些方法只是简单地封装教程一中为TableAdapters 定义的方法。目前,这些方法只是对 DAL 中内容的直接调用,稍后我们会返回到这些方法中来添加任何所需的业务逻辑。

注意: 如果您当前使用的是Visual Studio Standard Edition 或以上版本 ( 即,当前使用的不是Visual Web Developer ),您可以使用Class Designer 以可视的方式随意设计自己的类。有关 Visual Studio 中该新特性的详细信息,请参见Class Designer Blog 。

对于ProductsBLL 类,总共需要添加七个方法 :

  • GetProducts() – 返回所有产品。
  • GetProductByProductID(productID) – 返回具有指定产品 ID 的产品。
  • GetProductsByCategoryID(categoryID) – 返回指定 种类 中的所有产品。
  • GetProductsBySupplier(supplierID) – 返回来自指定供应商的所有产品。
  • AddProduct(productName, supplierID, categoryID, quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued) – 通过传入值将一个新产品插入到数据库中 ; 返回新插入记录的 ProductID 值。
  • UpdateProduct(productName, supplierID, categoryID, quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel, discontinued, productID) – 通过传入值更新数据库中的一个现有产品 ; 如果正好更新了一行则返回 true , 否则返回 false 。
  • DeleteProduct(productID) – 从数据库中删除指定产品。

ProductsBLL.cs

using System;
using System.Data;
using System.Configuration;
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;
[System.ComponentModel.DataObject]
public class ProductsBLL
{
    private ProductsTableAdapter _productsAdapter = null;
    protected ProductsTableAdapter Adapter
    {
        get {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, true)]
    public Northwind.ProductsDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, false)]
    public Northwind.ProductsDataTable GetProductByProductID(int productID)
    {
        return Adapter.GetProductByProductID(productID);
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, false)]
    public Northwind.ProductsDataTable GetProductsByCategoryID(int categoryID)
    {
        return Adapter.GetProductsByCategoryID(categoryID);
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Select, false)]
    public Northwind.ProductsDataTable GetProductsBySupplierID(int supplierID)
    {
        return Adapter.GetProductsBySupplierID(supplierID);
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Insert, true)]
    public bool AddProduct(string productName, int? supplierID, int? categoryID,
        string quantityPerUnit, decimal? unitPrice,  short? unitsInStock,
        short? unitsOnOrder, short? reorderLevel, bool discontinued)
    {
        // Create a new ProductRow instance
        Northwind.ProductsDataTable products = new Northwind.ProductsDataTable();
        Northwind.ProductsRow product = products.NewProductsRow();
        product.ProductName = productName;
        if (supplierID == null) product.SetSupplierIDNull();
          else product.SupplierID = supplierID.Value;
        if (categoryID == null) product.SetCategoryIDNull();
          else product.CategoryID = categoryID.Value;
        if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
          else product.QuantityPerUnit = quantityPerUnit;
        if (unitPrice == null) product.SetUnitPriceNull();
          else product.UnitPrice = unitPrice.Value;
        if (unitsInStock == null) product.SetUnitsInStockNull();
          else product.UnitsInStock = unitsInStock.Value;
        if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
          else product.UnitsOnOrder = unitsOnOrder.Value;
        if (reorderLevel == null) product.SetReorderLevelNull();
          else product.ReorderLevel = reorderLevel.Value;
        product.Discontinued = discontinued;
        // Add the new product
        products.AddProductsRow(product);
        int rowsAffected = Adapter.Update(products);
        // Return true if precisely one row was inserted,
        // otherwise false
        return rowsAffected == 1;
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Update, true)]
    public bool UpdateProduct(string productName, int? supplierID, int? categoryID,
        string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
        short? unitsOnOrder, short? reorderLevel, bool discontinued, int productID)
    {
        Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
        if (products.Count == 0)
            // no matching record found, return false
            return false;
        Northwind.ProductsRow product = products[0];
        product.ProductName = productName;
        if (supplierID == null) product.SetSupplierIDNull();
          else product.SupplierID = supplierID.Value;
        if (categoryID == null) product.SetCategoryIDNull();
          else product.CategoryID = categoryID.Value;
        if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
          else product.QuantityPerUnit = quantityPerUnit;
        if (unitPrice == null) product.SetUnitPriceNull();
          else product.UnitPrice = unitPrice.Value;
        if (unitsInStock == null) product.SetUnitsInStockNull();
          else product.UnitsInStock = unitsInStock.Value;
        if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
          else product.UnitsOnOrder = unitsOnOrder.Value;
        if (reorderLevel == null) product.SetReorderLevelNull();
          else product.ReorderLevel = reorderLevel.Value;
        product.Discontinued = discontinued;
        // Update the product record
        int rowsAffected = Adapter.Update(product);
        // Return true if precisely one row was updated,
        // otherwise false
        return rowsAffected == 1;
    }
    [System.ComponentModel.DataObjectMethodAttribute
        (System.ComponentModel.DataObjectMethodType.Delete, true)]
    public bool DeleteProduct(int productID)
    {
        int rowsAffected = Adapter.Delete(productID);
        // Return true if precisely one row was deleted,
        // otherwise false
        return rowsAffected == 1;
    }
}

这些方法 —GetProducts 、GetProductByProductID 、GetProductsByCategoryID 和 GetProductBySuppliersID ,只是返回数据,它们相当直接、简单,因为它们只是向下调用 DAL 中的内容。在一些场合下,可能会有一些业务规则需要在此层实现(例如基于当前已登录用户或用户所处职位的授权规则,可以访问不同的数据),但在这里我们只是保留这些方法不变。因此,对于这些方法, BLL 只是充当了一个代理的作用,表示层通过这个代理来访问数据访问层中的底层数据。

AddProduct 和 UpdateProduct 方法将产品各字段的值以参数形式传入,它们的作用分别是:添加一个新产品,更新一个现有产品。由于 Product 表的许多列,如 CategoryID 、 SupplierID 和 UnitPrice ,都可接受 NULL 值, AddProduct 和 UpdateProduct 中与这样的列相对应的输入参数使用nullable 类型 。 Nullable 类型对于 .NET 2.0 来说是新类型,利用该类型所提供的技术,我们可以指示一个值类型是否可以是空类型。在 C# 中,可以通过在类型后加问号 ? 将一个值类型标记为 nullable 类型(例如 int? x; )。有关详情,请参见C# 编程指南 中的 Nullable 类型 一节。

这三个方法均返回布尔值,该值指示是否成功的插入、更新或删除了一行。返回该值的原因是,方法的操作并不一定会影响到一行。例如,如果页面开发人员调用 DeleteProduct 时传入的 ProductID 并非一个现有产品的 ID ,则发给数据库的 DELETE 语句不会产生任何影响,因而 DeleteProduct 方法会返回 false。

请注意,当添加一个新产品或更新一个现有产品时,我们将新的或更改的产品的字段值用一组数值传入,而不是为此接受一个ProductsRow 实例。选择该方式的原因是, ProductsRow 类派生于 ADO.NET DataRow 类,而后者并没有一个默认的无参数构造函数。为了创建一个新的 ProductsRow 实例,首先要创建一个 ProductsDataTable 实例,然后调用它的 NewProductRow() 方法(就像我们在AddProduct方法中作的那样)。当我们使用 ObjectDataSource 插入或更新产品时,其缺陷就会暴露出来。简言之, ObjectDataSource 会尝试为输入的参数创建一个实例。如果 BLL 方法期待的是一个 ProductsRow 实例,则 ObjectDataSource 会尝试创建一个这样的实例,但是,由于缺少默认的无参数构造函数,该尝试失败。有关该问题的详细信息,请参见以下两个 ASP.NET 论坛:使用强类型DataSet 更新ObjectDataSources 、ObjectDataSource 与强类型DataSet 的问题 。

另外,AddProduct 和 UpdateProduct 中的代码都会创建一个ProductsRow 实例并以刚传入的值对该实例进行赋值。当向 DataRow 的一些 DataColumn 赋值时,可发生各种字段级的验证检查。因此,将传入的值进行一下人工的验证有助于确保传递给 BLL 方法的数据的有效性。不幸的是, Visual Studio 生成的强类型的 DataRow 类并不使用 nullable 类型。而为了给 DataRow 中的特定 DataColumn 赋数据库空值,我们必须使用 SetColumnNameNull() 方法。

在UpdateProduct 中,我们 首先用 GetProductByProductID(productID) 载入要更新的产品。尽管这看似是一次不必要的对数据库的操作,在将来的介绍并发优化的教程中,该往返将会被证明是值得的。并发优化技术可确保两个同时对同一数据进行操作的用户不会在不经意间覆盖彼此所作的更改。获取整个记录还使以下事情变得容易:在 BLL 中创建更新方法,使该方法只修改 DataRow 的所有列的一个子集。当我们研究 SuppliersBLL 类时,我们会看到这样一个例子。

最后,请注意对ProductsBLL 类使用了 DataObject 属性 ( 接近文件开头 , 类声明语句前面的 [System.ComponentModel.DataObject] 标签 ), 而其方法有DataObjectMethodAttribute属性 。 DataObject 属性将该类标记为一个适合绑定到 ObjectDataSource 控件 的对象,而 DataObjectMethodAttribute 属性则指示该方法的用途。在将来的教程中可以看到, ASP.NET 2.0 的 ObjectDataSource 使得以声明的方式从类中访问数据变得容易。默认情况下,在 ObjectDataSource 向导的下拉列表中只显示出标记为 DataObject 的那些类,这样有助于在该向导中筛选出可绑定的那些类。 ProductsBLL 类没有这些属性一样会工作良好,但是,加入这些属性可以使得在 ObjectDataSource 向导下的工作更为轻松。

添加其它类

在完成 ProductsBLL 类的编写后,我们还需要添加一些处理种类、供应商及雇员数据的类。我们花一些时间用上面例子中的概念来创建下面的类和方法:

CategoriesBLL.cs
  • GetCategories()
  • GetCategoryByCategoryID(categoryID)
SuppliersBLL.cs
  • GetSuppliers()
  • GetSupplierBySupplierID(supplierID)
  • GetSuppliersByCountry(country)
  • UpdateSupplierAddress(supplierID, address, city, country)
EmployeesBLL.cs
  • GetEmployees()
  • GetEmployeeByEmployeeID(employeeID)
  • GetEmployeesByManager(managerID)

值得注意的一个方法是 SuppliersBLL 类的UpdateSupplierAddress 方法。该方法提供了一个接口以便只更新供应商的地址信息。在内部实现上,该方法读取指定supplierID 的 SupplierDataRow 对象(使用GetSupplierBySupplierID 来读取),设置其相关地址属性,然后向下调用SupplierDataTable 的更新方法。UpdateSupplierAddress 方法如下:

[System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateSupplierAddress
    (int supplierID, string address, string city, string country)
{
    Northwind.SuppliersDataTable suppliers =
        Adapter.GetSupplierBySupplierID(supplierID);
    if (suppliers.Count == 0)
        // no matching record found, return false
        return false;
    else
    {
        Northwind.SuppliersRow supplier = suppliers[0];
        if (address == null) supplier.SetAddressNull();
          else supplier.Address = address;
        if (city == null) supplier.SetCityNull();
          else supplier.City = city;
        if (country == null) supplier.SetCountryNull();
          else supplier.Country = country;
        // Update the supplier Address-related information
        int rowsAffected = Adapter.Update(supplier);
        // Return true if precisely one row was updated,
        // otherwise false
        return rowsAffected == 1;
    }
}

步骤 2:通过 BLL 类访问 Typed DataSets

在教程一中我们看到了直接使用 Typed DataSet 的编程例子。而现在我们已添加了一些 BLL 类,因此表示层应转而基于 BLL 而工作。教程一的 AllProducts.aspx 例子使用了 ProductsTableAdapter 来将产品列表绑定到一个 GridView,见下面的代码:

ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();

要使用 BLL 类,只需改变代码的第一行 – 只需用 ProductBLL 对象代替 ProductsTableAdapter 对象:

ProductsBLL productLogic = new ProductsBLL();
GridView1.DataSource = productLogic.GetProducts();
GridView1.DataBind();

也可以使用 ObjectDataSource 以声明的方式来访问BLL 类(如同 Typed DataSet )。我们将在后续教程中更为详细地讨论ObjectDataSource 。

 

 

图3 : 产品列表显示于GridView 中

步骤3 :向 DataRow 类添加字段级验证

字段级验证是进行插入或更新操作时针对业务对象的属性值而进行的检查。下面是对产品的一些字段级验证规则:

  • ProductName 字段的长度不能超过 40 个字符。
  • QuantityPerUnit 字段的长度不能超过 20 个字符。
  • ProductID 、ProductName 和 Discontinued 字段是必需的,但所有其它字段是可选的。
  • UnitPrice 、UnitsInStock 、UnitsOnOrder 和 ReorderLevel 字段必须大于等于零。

这些规则可以并且应该在数据库级表达出来。Products 表的相应列的数据类型可反映对 ProductName 和 QuantityPerUnit 字段的字符数限制(分别为 nvarchar(40) 和nvarchar(20) )。对字段是可选还是必需的表达是这样的:数据库表列允许还是不允许NULL 。四个检查约束 的存在确保只有大于等于零的值才可赋值给UnitPrice 、UnitsInStock 、UnitsOnOrder 和 ReorderLevel 列。

这些规则除了在数据库级实施外还应在DataSet 级实施。事实上,字段长度以及某值是必需的还是可选的,已被DataTable 的 DataColumn 集定义。要查看现有的自动提供的字段级验证,可转到DataSet 设计器,从其中一个DataTable 中选择一个域,然后转至 Properties 窗口。如图 4 所示,ProductsDataTable 中的 QuantityPerUnit DataColumn 允许的最大长度是 20 个字符,并且允许 NULL 值。如果我们试图将 ProductsDataRow 的 QuantityPerUnit 属性设置为一个超过 20 个字符的字符串值,系统会抛出 ArgumentException 异常 。

 

 

图4 :DataColumn 提供基本域级验证

不幸的是,我们不能通过 Properties 窗口指定边界检查,如,UnitPrice 必须大于等于零这样的检查。为了提供此类字段级验证,需要创建一个针对DataTable 的ColumnChanging 事件的Event Handler。如前一教程 所述,Typed DataSet 创建的 DataSet 、DataTables 和 DataRow 对象可以通过使用部分类来扩展。利用该技术我们可以为ProductsDataTable 类创建一个 ColumnChanging 事件的Event Handler。首先,在 App_Code 文件夹下创建一个名为 ProductsDataTable.ColumnChanging.cs 的类。

 

 

图5 :在App_Code 文件夹中添加一个新类

其次,创建一个针对 ColumnChanging 事件的Event Handler以确保UnitPrice 、UnitsInStock 、UnitsOnOrder 和 ReorderLevel 列的值(如果不是 NULL )大于等于零。其中任何一列超出范围,系统都会给出ArgumentException 。

ProductsDataTable.ColumnChanging.cs

public partial class Northwind
{
    public partial class ProductsDataTable
    {
        public override void BeginInit()
         {
            this.ColumnChanging += ValidateColumn;
         }
         void ValidateColumn(object sender,
           DataColumnChangeEventArgs e)
         {
            if(e.Column.Equals(this.UnitPriceColumn))
            {
               if(!Convert.IsDBNull(e.ProposedValue) &&
                  (decimal)e.ProposedValue < 0)
               {
                  throw new ArgumentException(
                      "UnitPrice cannot be less than zero", "UnitPrice");
               }
            }
            else if (e.Column.Equals(this.UnitsInStockColumn) ||
                     e.Column.Equals(this.UnitsOnOrderColumn) ||
                     e.Column.Equals(this.ReorderLevelColumn))
            {
                if (!Convert.IsDBNull(e.ProposedValue) &&
                    (short)e.ProposedValue < 0)
                {
                    throw new ArgumentException(string.Format(
                        "{0} cannot be less than zero", e.Column.ColumnName),
                        e.Column.ColumnName);
                }
            }
         }
    }
}

步骤 4:向 BLL 类添加定制的业务规则

除了字段级验证外,可能还有高级定制的业务规则,这些规则涉及不同的实体,或者涉及到不能在单个列中表达的概念,例如:

  • 如果一产品为断货 (discontinued) 产品,其 UnitPrice 就不能被更新。
  • 雇员的居住国必须与其经理的居住国相同。
  • 如果某产品是其供应商提供的唯一产品,该产品就不能为断货产品。

BLL 类应含有检查,以确保遵守应用程序的业务规则。可将这些检查直接添加到它们所应用到的方法中。

假设我们的业务规则规定:如果某产品是指定供应商的唯一产品,该产品就不能标记为discontinued 。即,如果产品 X 是我们从供应商Y 处购买的唯一产品,我们就不能将 X 标记为 discontinued ;但是如果供应商 Y 为我们提供了三个产品:A 、B 和 C ,那么我们可将其中任何一个或所有的标记为discontinued 。这是一个奇怪的业务规则,但业务规则并不总是符合一般常识!

为了对 UpdateProducts 方法实施此业务规则,我们首先检查Discontinued 是否设置为 true ,如是,我们会调用GetProductsBySupplierID 来确定我们从该产品的供应商处购买了多少个产品。如果从该供应商处只购买了一个产品,我们就抛出ApplicationException 异常 。

public bool UpdateProduct(string productName, int? supplierID, int? categoryID,
    string quantityPerUnit, decimal? unitPrice, short? unitsInStock,
    short? unitsOnOrder, short? reorderLevel, bool discontinued, int productID)
{
    Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    Northwind.ProductsRow product = products[0];
    // Business rule check - cannot discontinue
    // a product that is supplied by only
    // one supplier
    if (discontinued)
    {
        // Get the products we buy from this supplier
        Northwind.ProductsDataTable productsBySupplier =
            Adapter.GetProductsBySupplierID(product.SupplierID);
        if (productsBySupplier.Count == 1)
            // this is the only product we buy from this supplier
            throw new ApplicationException(
                "You cannot mark a product as discontinued if it is the only
                  product purchased from a supplier");
    }
    product.ProductName = productName;
    if (supplierID == null) product.SetSupplierIDNull();
      else product.SupplierID = supplierID.Value;
    if (categoryID == null) product.SetCategoryIDNull();
      else product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null) product.SetQuantityPerUnitNull();
      else product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null) product.SetUnitPriceNull();
      else product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null) product.SetUnitsInStockNull();
      else product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null) product.SetUnitsOnOrderNull();
      else product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null) product.SetReorderLevelNull();
      else product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;
    // Update the product record
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated,
    // otherwise false
    return rowsAffected == 1;
}

在表示层对验证错误进行响应

从表示层调用BLL 时,我们可以决定是尝试对任何可能出现的异常情况进行处理,还是让这些异常直接抛给ASP.NET (它们会引发HttpApplication 的错误事件)。要在编程使用 BLL 时处理一个异常,我们可以使用 try...catch 块,如下所示:

ProductsBLL productLogic = new ProductsBLL();
// Update information for ProductID 1
try
{
    // This will fail since we are attempting to use a
    // UnitPrice value less than 0.
    productLogic.UpdateProduct(
        "Scott s Tea", 1, 1, null, -14m, 10, null, null, false, 1);
}
catch (ArgumentException ae)
{
    Response.Write("There was a problem: " + ae.Message);
}

在以后的教程中我们会看到,当使用一个Web 数据控件来插入、更新或删除数据时,可以通过一个Event Handler对从 BLL 抛出的异常进行处理而不用将该处理代码封装于try...catch 块中。

小结

一个结构良好的应用程序都有清晰的层次结构,每层都封装有特定的任务。在本系列文章的第一篇教程中,我们用Typed DataSet 创建了一个数据访问层;在本篇教程中,我们建立了一个业务逻辑层,该层包括我们的应用程序的App_Code 文件夹下的一系列类,这些类向下调用DAL 中的内容。我们的应用程序通过 BLL 实现了字段级和业务级逻辑。在本教程中,我们创建了一个独立的BLL ,除此之外的另一个选择是,利用部分类来扩展TableAdapters 的方法。但是,使用这一技术,我们并不能重写现有的方法,也不能象本文中采用的方式一样清晰地分隔开我们的DAL 和 BLL 。

完成 DAL 和BLL 的代码编写后,我们就可以着手编写我们的表示层代码了。在下一教程 中,我们会短暂地偏离数据访问主题,转而去定义一个将为所有教程所使用的一致的页面布局。

快乐编程!

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章