利用強類型數據集創建數據庫訪問層

創建數據訪問層

本文檔是 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 的代碼編寫後,我們就可以着手編寫我們的表示層代碼了。在下一教程 中,我們會短暫地偏離數據訪問主題,轉而去定義一個將爲所有教程所使用的一致的頁面佈局。

快樂編程!

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