针对这些教程,我们将使用放在 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 支持的数据库级特性。
一旦安装了该数据库,转到 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) ,通常作为一个单独的类库项目来实现。这种分层体系结构的优势得到了很好的论述,我们在该系列教程中也采用了此法。
本示例中所使用的每个对象都是强类型的,允许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 我们不必编写代码也可以进行分页和排序!
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();
}
}
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);
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);
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
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
图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 覆盖了我们的自定义内容。
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);
}
}
}
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 控件:
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();
}
}
我们的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 的产品。
假设我们的业务规则规定:如果某产品是指定供应商的唯一产品,该产品就不能标记为discontinued 。即,如果产品 X 是我们从供应商Y 处购买的唯一产品,我们就不能将 X 标记为 discontinued ;但是如果供应商 Y 为我们提供了三个产品:A 、B 和 C ,那么我们可将其中任何一个或所有的标记为discontinued 。这是一个奇怪的业务规则,但业务规则并不总是符合一般常识!
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;
}
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);
}