由 斯科特·米切爾
在本教學課程中,我們將瞭解如何將商務規則集中至商業規則層 (BLL),以作為表示層與 DAL 之間數據交換的媒介。
簡介
在第 一個教學課程 中建立的數據存取層 (DAL) 會清楚分隔數據存取邏輯與表示邏輯。 不過,雖然 DAL 清楚地將資料存取的細節與表示層分隔開來,但並不會套用任何可能的商業規則。 例如,針對我們的應用程式,我們可能想要禁止修改CategoryID資料表的SupplierID或Products欄位,當Discontinued欄位設定為1時,或者我們可能想要強制執行資歷規則,禁止員工被比他們晚受聘的人管理的情況。 另一個常見案例是授權,或許只有特定角色的使用者可以刪除產品或變更 UnitPrice 值。
在本教學課程中,我們將瞭解如何將這些商務規則集中到商業規則層(BLL),作為表示層與 DAL 之間數據交換的媒介。 在真實世界中,BLL 應該實作為個別的類別庫專案;不過,針對這些教學課程,我們將實作 BLL 作為資料夾中 App_Code 的一系列類別,以簡化項目結構。 圖 1 說明表示層、BLL 和 DAL 之間的架構關聯性。
圖 1:BLL 會將表示層與數據存取層分開,並強加商務規則
步驟 1:建立 BLL 類別
我們的 BLL 將由四個類別組成,其中一個用於 DAL 中的每個 TableAdapter;每一個 BLL 類別都會有方法可從 DAL 中的個別 TableAdapter 擷取、插入、更新和刪除,並套用適當的商務規則。
為了更清楚地分隔 DAL 和 BLL 相關類別,讓我們在 App_Code 資料夾中建立兩個子資料夾:DAL 和 BLL。 只要以滑鼠右鍵按兩下 App_Code [方案總管] 中的資料夾,然後選擇 [新增資料夾]。 建立這兩個資料夾之後,請將第一個教學課程中建立的 DAL 具類型數據集移至子資料夾。
接下來,在 BLL 子資料夾中建立四個 BLL 類別檔案。 若要達成此目的,請以滑鼠右鍵按兩下 BLL 子資料夾,選擇 [新增專案],然後選擇 [類別] 範本。 將四個ProductsBLL類別命名為、 CategoriesBLLSuppliersBLL與 EmployeesBLL。
圖 2:將四個新類別新增至 App_Code 資料夾
接下來,讓我們將方法新增至每個類別,以簡單地封裝第一個教學課程中為 TableAdapters 定義的方法。 目前,這些方法只會直接呼叫 DAL;我們稍後會返回以新增任何必要的商業規則。
備註
如果您使用 Visual Studio Standard Edition 或更新版本(也就是您 不是 使用 Visual Web Developer),則可以選擇性地使用 類別設計工具以可視化方式設計類別。 如需 Visual Studio 中這項新功能的詳細資訊,請參閱 類別設計工具部落格 。
針對 類別 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。 在某些情況下,可能需要在此層級實作商務規則(例如根據目前登入的使用者或使用者所屬角色的授權規則),我們只會將這些方法保留 as-is。 針對這些方法,BLL 僅僅作為演示層從資料存取層存取基礎數據的代理。
AddProduct和 UpdateProduct 方法都會以參數的形式接受各種產品欄位的值,並分別新增新產品或更新現有的產品。 由於許多Product數據表的列都可以接受NULL值(例如 CategoryID、 SupplierID和 UnitPrice),因此對映到這些列的 AddProduct 和 UpdateProduct 輸入參數會使用可為 Null 的型別。 在 .NET 2.0 中引入的可空類型是一項新特性,提供方法用來表示數值型別是否應為 null。 在 C# 中,您可以藉由在 型別之後新增 ? ,將實值類型標示為可為 Null 的類型(例如 int? x;)。 如需詳細資訊,請參閱 C# 程式設計指南中的可空值型別一節。
這三種方法都會傳回 Boolean 值,指出是否插入、更新或刪除數據列,因為作業可能不會造成受影響的數據列。 例如,如果頁面開發人員針對不存在的產品呼叫 DeleteProduct 傳入 ProductID,那麼對資料庫發出的語句將不會產生任何作用,因此 DELETE 方法會傳回 DeleteProduct。
請注意,新增新產品或更新現有產品時,我們會以新的或修改的產品域值作為純量清單,而不是接受 ProductsRow 實例。 之所以選擇這個方法,是因為 類別 ProductsRow 衍生自 ADO.NET DataRow 類別,該類別沒有預設無參數建構函式。 若要建立新的 ProductsRow 實例,我們必須先建立 ProductsDataTable 實例,然後叫用其 NewProductRow() 方法(我們在 中 AddProduct執行此動作)。 當我們轉向使用 ObjectDataSource 插入和更新產品時,這個缺點會顯現出來。 簡言之,ObjectDataSource 會嘗試建立輸入參數的實例。 如果 BLL 方法需要 ProductsRow 實例,ObjectDataSource 會嘗試建立實例,但因為缺少預設無參數建構函式而失敗。 如需此問題的詳細資訊,請參閱下列兩個 ASP.NET 論壇文章: 使用 Strongly-Typed DataSet 更新 ObjectDataSources,以及 使用 ObjectDataSource 和 Strongly-Typed DataSet 的問題。
接下來,在 AddProduct 和 UpdateProduct中,程式代碼會 ProductsRow 建立 實例,並填入剛傳入的值。 當將值指派給 DataRow 的 DataColumns 時,可能會進行各種欄位層級驗證檢查。 因此,手動將傳入的值放回 DataRow,有助於確保將數據傳遞至 BLL 方法的有效性。 不幸的是,Visual Studio 所生成的強型別 DataRow 類別並未使用可空類型。 相反地,為了使 DataRow 中的特定 DataColumn 對應至 NULL 的資料庫值,我們必須使用 SetColumnNameNull() 方法。
在UpdateProduct中,我們先載入需要更新的產品,然後使用GetProductByProductID(productID)進行更新。 雖然這似乎是多餘的資料庫操作,但在未來探討樂觀並發控制的教學課程中,這次額外操作將會證明其價值。 樂觀並發控制是一種技術,可確保同時對相同數據進行工作的兩位使用者不會不小心覆寫彼此的變更。 擷取整個記錄也可讓您更輕鬆地在 BLL 中建立只修改 DataRow 資料行子集的更新方法。 當我們探索 類別 SuppliersBLL 時,我們將會看到這類範例。
最後,請注意,ProductsBLL 類別套用了 DataObject 屬性([System.ComponentModel.DataObject] 語法位於檔案頂端附近的類別語句之前),而且方法具有 DataObjectMethodAttribute 屬性。 屬性 DataObject 會將 類別標示為適合系結至 ObjectDataSource 控件的物件,而 表示 DataObjectMethodAttribute 方法的用途。 如未來教學課程中所見,ASP.NET 2.0 的 ObjectDataSource 可讓您輕鬆地從類別宣告式存取數據。 為了協助篩選在 ObjectDataSource 精靈中系結至的可能類別清單,預設只會在精靈的下拉式清單中顯示標示為 DataObjects 的類別。 類別 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 方法。 這個方法會提供介面,只更新供應商的地址資料。 在內部,這個方法會讀取SupplierDataRow物件,該物件是使用supplierID在指定的GetSupplierBySupplierID中獲得的,並設定其位址相關屬性,然後呼叫SupplierDataTable的Update方法。 方法 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;
}
}
如需 BLL 類別的完整實作,請參閱這篇文章的下載。
步驟 2:透過 BLL 類別存取具類型的數據集
在第一個教學課程中,我們看到以程序設計方式直接使用具型別數據集的範例,但是隨著 BLL 類別的新增,表示層應該改為針對 BLL 運作。 在第一個教學課程的 AllProducts.aspx 範例中, ProductsTableAdapter 是用來將產品清單系結至 GridView,如下列程式代碼所示:
ProductsTableAdapter productsAdapter = new ProductsTableAdapter();
GridView1.DataSource = productsAdapter.GetProducts();
GridView1.DataBind();
若要使用新的 BLL 類別,所有需要變更的程式代碼都是第一行程式代碼,只需將 ProductsTableAdapter 物件取代為 ProductBLL 物件:
ProductsBLL productLogic = new ProductsBLL();
GridView1.DataSource = productLogic.GetProducts();
GridView1.DataBind();
BLL 類別以及具型別的數據集都可以透過使用 ObjectDataSource 以宣告方式存取。 我們將在下列教學課程中更詳細地討論 ObjectDataSource。
圖 3:產品清單會顯示在 GridView 中(按兩下以檢視完整大小的影像)
步驟 3:將 Field-Level 驗證新增至 DataRow 類別
欄位層級驗證會在插入或更新時,檢查與商務對象的屬性值有關。 產品的某些欄位層級驗證規則包括:
- 欄位
ProductName長度必須少於 40 個字元 - 欄位
QuantityPerUnit長度必須少於 20 個字元 -
ProductID、ProductName和Discontinued欄位是必填的,但所有其他欄位都是選填的。 -
UnitPrice、UnitsInStock、UnitsOnOrder和ReorderLevel欄位必須大於或等於零
這些規則可以且應該在資料庫層級表示。 在ProductName資料表中,QuantityPerUnit和Products欄位的字元限制由這些資料行的資料類型來擷取,分別為nvarchar(40)和nvarchar(20)。 在資料庫表中,字段是必填還是可選取決於數據列是否允許NULL。 存在四個 檢查約束,確保只有大於或等於零的值可以進入 UnitPrice、UnitsInStock、UnitsOnOrder 或 ReorderLevel 欄位。
除了在資料庫中強制執行這些規則之外,也應該在 DataSet 層級強制執行這些規則。 事實上,每個 DataTable 的 DataColumns 集合中都已經記錄了欄位長度,以及值是必需還是可選的。 若要查看自動提供的現有欄位層級驗證,請移至 [資料集設計工具],從其中一個 DataTable 選取字段,然後移至 [屬性] 視窗。 如圖 4 所示, QuantityPerUnit 中的 ProductsDataTable DataColumn 長度上限為 20 個字元,而且允許 NULL 值。 如果我們嘗試將 ProductsDataRow 的 QuantityPerUnit 屬性設定為長度超過 20 個字元的字串值,則會拋出 ArgumentException。
圖4:DataColumn 提供基本 Field-Level 驗證(點擊以查看完整大小的圖片)
不幸的是,我們無法透過 [屬性] 視窗指定界限檢查,例如 UnitPrice 值必須大於或等於零。 為了提供這種類型的欄位層級驗證,我們需要為 DataTable 的 ColumnChanging 事件建立事件處理程式。 如上一個 教學課程所述,Typed DataSet 所建立的 DataSet、DataTables 和 DataRow 物件可以透過使用部分類別來擴充。 使用這項技術,我們可以建立 ColumnChanging 類別的 ProductsDataTable 事件處理程式。 首先,在名為App_Code的ProductsDataTable.ColumnChanging.cs資料夾中建立類別。
圖 5:將新類別新增至 App_Code 資料夾 (按一下以檢視完整大小的影像)
接下來,為 ColumnChanging 事件建立事件處理程式,以確保 UnitPrice、 UnitsInStockUnitsOnOrder、 和 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 的類別
除了欄位層級驗證之外,可能還有一些涉及不同實體或概念的高階自定義商務規則,無法在單一數據行層級表示,例如:
- 如果產品已停產,則無法更新
UnitPrice - 員工的居住國家/地區必須與其經理的居住國相同
- 如果產品是唯一由供應商提供的產品,則無法停止產品
BLL 類別應該包含檢查,以確保遵守應用程式的商務規則。 這些檢查可以直接新增至它們所應用的方法上。
假設我們的商務規則規定,如果產品是唯一來自指定供應商的產品,則無法標示為已中止。 也就是說,如果 product X 是唯一從供應商 Y 購買的產品,我們無法將 X 標示為已中止:不過,如果供應商 Y 為我們提供三個產品 A、 B 和 C,我們可以將任何和所有產品標示為已停止。 奇怪的商務規則,但商務規則和常識不一定一致!
在 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 的 Error 事件)。 若要以程式設計方式處理 BLL 時處理例外狀況,我們可以使用 嘗試...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 控制項插入、更新或刪除資料時,可以直接在事件處理程式中處理從 BLL 擡升的例外狀況,而不需要將程式碼包裝在 try...catch 區塊中。
總結
架構良好的應用程式會設計成不同的層,每個層都會封裝特定角色。 在本文章系列的第一個教學課程中,我們使用具類型的數據集建立數據存取層;在本教學課程中,我們建置商業規則層作為應用程式 App_Code 資料夾中的一系列類別,可呼叫我們的 DAL。 BLL 會為應用程式實作欄位層級和業務層級邏輯。 除了建立個別的 BLL 之外,如本教學課程所示,另一個選項是使用部分類別來擴充 TableAdapters 的方法。 不過,使用這項技術不僅不允許我們修改現有的方法,也無法像本文中採用的方法那樣清晰地分隔 DAL 和 BLL。
完成 DAL 和 BLL 之後,我們就準備好開始進行演示層。 在下一個教學課程中,我們將從數據存取主題進行簡短的繞道,並定義一致的頁面配置,以在整個教學課程中使用。
快樂的程序設計!
關於作者
斯科特·米切爾,七本 ASP/ASP.NET 書籍和 4GuysFromRolla.com 創始人的作者,自1998年以來一直與Microsoft Web 技術合作。 斯科特擔任獨立顧問、教練和作家。 他的最新書是《Sams 24小時自學ASP.NET 2.0》。 可以聯絡他 mitchell@4GuysFromRolla.com。
特別感謝
本教學系列已由許多熱心的評論者審閱。 本教學課程的主要檢閱者是 Liz Shulok、Dennis Patterson、Carlos Santos 和希爾頓 Giesenow。 有興趣檢閱我即將推出的 MSDN 文章嗎? 如果是,請在 mitchell@4GuysFromRolla.com給我留言。