標籤:
https://msdn.microsoft.com/zh-cn/dd255899
簡介
在教程一中建立的資料訪問層 (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) – 返回具有指定產品識別碼 的產品。
- 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 1try{ // 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 的代碼編寫後,我們就可以著手編寫我們的展示層代碼了。在下一教程 中,我們會短暫地偏離資料訪問主題,轉而去定義一個將為所有教程所使用的一致的頁面配置。
快樂編程!
微軟-建立商務邏輯層