導(dǎo)言
對(duì)于那些僅僅允許用戶查看數(shù)據(jù),或者僅有一個(gè)用戶可以修改數(shù)據(jù)的web應(yīng)用軟件,不存在多用戶并發(fā)沖突的問題。然而對(duì)于那些允許多個(gè)用戶修改或刪除數(shù)據(jù)的web應(yīng)用軟件,則有可能發(fā)生一個(gè)用戶所做的更改與另一個(gè)并發(fā)用戶的更改沖突。在沒有任何并發(fā)策略的地方,當(dāng)兩個(gè)用戶同時(shí)編輯某一條記錄,最后提交的用戶的更改將覆蓋先提交的用戶所作的更改。
例如,假設(shè)兩個(gè)用戶,Jisun和Sam,都訪問我們的應(yīng)用軟件中的一個(gè)頁面,這個(gè)頁面允許訪問者通過一個(gè)GridView控件更新和刪除產(chǎn)品數(shù)據(jù)。他們都同時(shí)點(diǎn)擊GridView控件中的Edit按鈕。Jisun把產(chǎn)品名稱更改為“Chai Tea”并點(diǎn)擊Update按鈕,實(shí)質(zhì)結(jié)果是向數(shù)據(jù)庫發(fā)送一個(gè)UPDATE語句,它將更新此產(chǎn)品的所有可修改的字段(盡管Jisun實(shí)際上只修改了一個(gè)字段:ProductName)。
在這一刻,數(shù)據(jù)庫中包含有這條產(chǎn)品記錄“Chai Tea”—種類為Beverages、供應(yīng)商為Exotic Liquids、等該產(chǎn)品的詳細(xì)信息。然而,在Sam的屏幕中的GridView里,當(dāng)前編輯行里顯示的產(chǎn)片名稱依舊是“Chai”。在Jisun的更改被提交后片刻,Sam把種類更改為“Condiments”并點(diǎn)擊Update按鈕。這個(gè)發(fā)送到數(shù)據(jù)庫的UPDATE語句的結(jié)果是將產(chǎn)品名稱更改為“Chai”、CategoryID字段的值是種類Beverages對(duì)應(yīng)的ID,等等。Jisun所作的對(duì)產(chǎn)品名稱的更改就被覆蓋了。圖1展示了這些連續(xù)的事件。
圖 1: 當(dāng)兩個(gè)用戶同時(shí)更新一條記錄,則存在一個(gè)用戶的更改覆蓋另一個(gè)的更改的可能性
類似地,當(dāng)兩個(gè)用戶同時(shí)訪問一個(gè)頁面,一個(gè)用戶可能更新的事另一個(gè)用戶已經(jīng)刪除的記錄?;蛘?,在一個(gè)用戶加載頁面跟他點(diǎn)擊刪除按鈕之間的時(shí)間里,另一個(gè)用戶修改了這條記錄的內(nèi)容。
有下面三中并發(fā)控制策略可供選擇:
1.什么都不做 –如果并發(fā)用戶修改的是同一條記錄,讓最后提交的結(jié)果生效(默認(rèn)的行為)
2.開放式并發(fā)(Optimistic Concurrency) - 假定并發(fā)沖突只是偶爾發(fā)生,絕大多數(shù)的時(shí)候并不會(huì)出現(xiàn); 那么,當(dāng)發(fā)生一個(gè)沖突時(shí),僅僅簡單的告知用戶,他所作的更改不能保存,因?yàn)閯e的用戶已經(jīng)修改了同一條記錄
3.保守式并發(fā)(Pessimistic Concurrency) – 假定并發(fā)沖突經(jīng)常發(fā)生,并且用戶不能容忍被告知自己的修改不能保存是由于別人的并發(fā)行為;那么,當(dāng)一個(gè)用戶開始編輯一條記錄,鎖定該記錄,從而防止其他用戶編輯或刪除該記錄,直到他完成并提交自己的更改
注意:在本節(jié)里,我們不討論保守式并附的例子。保守式并發(fā)控制很少使用,因?yàn)殒i定如果沒有完全釋放,會(huì)妨礙其他用戶進(jìn)行數(shù)據(jù)更新。例如,如果一個(gè)用戶為了編輯而鎖定某一條記錄,但在解鎖之前就離開了,那么其他任何用戶都不能更新這條記錄,直到最初的用戶返回并完成他的更新。因此,使用保守式并發(fā)控制的地方,相應(yīng)地會(huì)作一個(gè)時(shí)間限制,如果到達(dá)這個(gè)時(shí)間限制,則取消鎖定。例如訂票網(wǎng)站,當(dāng)用戶完成他的訂票過程時(shí)會(huì)鎖定某個(gè)特定的座位,這就是一個(gè)使用保守式并發(fā)控制的例子。
第一步:如何實(shí)現(xiàn)開放式并發(fā)控制
開放式并發(fā)控制能夠確保一條記錄在更新或者刪除時(shí)跟它開始這次更新或修改過程時(shí)保持一致。例如,當(dāng)在一個(gè)可編輯的GridView里點(diǎn)擊編輯按鈕時(shí),該記錄的原始值從數(shù)據(jù)庫中讀取出來并顯示在TextBox和其他Web控件中。這些原始的值保存在GridView里。隨后,當(dāng)用戶完成他的修改并點(diǎn)擊更新按鈕,這些原始值加上修改后的新值發(fā)送到業(yè)務(wù)邏輯層,然后到數(shù)據(jù)訪問層。數(shù)據(jù)訪問層必定發(fā)出一個(gè)SQL語句,它將僅僅更新那些開始編輯時(shí)的原始值根數(shù)據(jù)庫中的值一致的記錄。圖二描述了這些事件發(fā)生的順序。
圖2: 為了更新或刪除能夠成功,原始值必須與數(shù)據(jù)庫中相應(yīng)的值一致
有多種方法可以實(shí)現(xiàn)開放式并發(fā)控制(查看Peter A. Bromberg的文章 Optmistic Concurrency Updating Logic,從摘要中看到許多選擇)。ADO.NET類型化數(shù)據(jù)集提供了一種應(yīng)用,這只需要在配置時(shí)勾選上一個(gè)CheckBox。使用開發(fā)式并發(fā)的目的是使類型化數(shù)據(jù)集的TableAdapter的UPDATE和DELETE語句可以檢測自該記錄加載到DataSet中以來數(shù)據(jù)庫中的值是否被更改。例如下面的UPDATE語句,當(dāng)當(dāng)前數(shù)據(jù)庫中的值與GridView中開始編輯的原始值一致才更新某個(gè)產(chǎn)品的名稱和價(jià)格。@ProductName 和 @UnitPrice參數(shù)包含的是用戶輸入的新值,而參數(shù)@original_ProductName 和 @original_UnitPrice則包含最初點(diǎn)擊編輯按鈕時(shí)加載到GridView中的值:
UPDATE Products SET
ProductName = @ProductName,
UnitPrice = @UnitPrice
WHERE
ProductID = @original_ProductID AND
ProductName = @original_ProductName AND
UnitPrice = @original_UnitPrice
注意:這個(gè)UPDATE語句是為了易讀而簡單化了。實(shí)際上,在WHERE子句中檢測UnitPrice會(huì)比較棘手,這是因?yàn)閁nitPrice可以包含空值,而NULL = NULL則總是返回False(相應(yīng)地你必須用IS NULL)。
除了使用一個(gè)不同的UPDATE語句之外,配置TableAdapter使用開放式并發(fā)控制還需要修改它直接發(fā)送到數(shù)據(jù)庫的方法?;氐轿覀兊牡谝还?jié),創(chuàng)建一個(gè)數(shù)據(jù)訪問層,這些發(fā)送到數(shù)據(jù)庫的方法接收一列標(biāo)量的值作為輸入?yún)?shù)(不僅僅是強(qiáng)類型DataRow或DataTable的實(shí)例)。當(dāng)使用開放式并發(fā),直接發(fā)送到數(shù)據(jù)庫的Update() 和 Delete()方法就包含了對(duì)應(yīng)原始值的輸入?yún)?shù)。而且,業(yè)務(wù)邏輯層中批量方式更新的代碼(Update()的重載,它不僅接受標(biāo)量值,也接受DataRows 和 DataTables)也要做出相應(yīng)的更改。
與其擴(kuò)展我們現(xiàn)有得數(shù)據(jù)訪問層表適配器使用開放式并發(fā)(同時(shí)也必須修改業(yè)務(wù)邏輯層以協(xié)調(diào)),不如讓我們創(chuàng)建一個(gè)新的類型化數(shù)據(jù)集NorthwindOptimisticConcurrency,在它里面我們添加一個(gè)使用開放式并發(fā)的Products表適配器。然后,我們將在業(yè)務(wù)邏輯層中創(chuàng)建類ProductsOptimisticConcurrencyBLL,它為了支持開放式并發(fā)的DAL會(huì)有適當(dāng)?shù)母?。一旦這些基礎(chǔ)工作都已完成,我們就可以創(chuàng)建ASP.NET頁面。
第二步: 創(chuàng)建一個(gè)支持開放式并發(fā)的數(shù)據(jù)訪問層
為了創(chuàng)建一個(gè)新的類型化數(shù)據(jù)集,在App_Code文件夾里的DAL文件夾上右鍵點(diǎn)擊,選擇添加一個(gè)新的數(shù)據(jù)集并命名為NorthwindOptimisticConcurrency。正如我們?cè)诘谝还?jié)中看到過的那樣,系統(tǒng)會(huì)自動(dòng)添加一個(gè)表適配器(TableAdapter)到當(dāng)前的類型化數(shù)據(jù)集眾,并自動(dòng)地進(jìn)入TableAdapter配置向?qū)?。在第一屏中,向?qū)崾疚覀冞x擇數(shù)據(jù)庫連接 – 連接到同樣的數(shù)據(jù)庫Northwind并使用Web.config里設(shè)置好的連接字符串NORTHWNDConnectionString。
圖 3: 連接到同一個(gè)數(shù)據(jù)庫Northwind
下一步,向?qū)崾疚覀冞x擇如何訪問數(shù)據(jù)庫:通過一個(gè)指定的SQL語句,創(chuàng)建新的存儲(chǔ)過程,或者使用一個(gè)現(xiàn)有的存儲(chǔ)過程。既然我們最初的DAL是使用的是指定SQL查詢語句,這里我們還是使用它。
圖4: 使用指定SQL語句的方式訪問數(shù)據(jù)庫
下一步,進(jìn)入查詢分析器,返回產(chǎn)品信息。讓我們使用在最初的DAL中產(chǎn)品TableAdapter相同的SQL查詢,它返回產(chǎn)品的所有字段包括產(chǎn)品的供應(yīng)商和類別名稱。
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
圖5:使用在最初的DAL中產(chǎn)品TableAdapter相同的SQL查詢
在我們進(jìn)入下一步之前,點(diǎn)擊“高級(jí)選項(xiàng)”按鈕。要讓這個(gè)TableAdapter使用開放式并發(fā),僅僅需要勾選上“使用開放式并發(fā)”。
圖6:勾選“使用開放式并發(fā)”啟用開放式并發(fā)控制
最后,需要指出的是,該TableAdapter應(yīng)該同時(shí)使用“填充DataTable”和“返回DataTable”兩種要生成的方法;并且,勾選“創(chuàng)建方法以將更新直接發(fā)送到數(shù)據(jù)庫(GenerateDBDirectMethods)”。將返回DataTable的方法名稱從GetData改為GetProducts,使之與我們最初的DAL中的命名規(guī)則匹配。
圖7:讓這個(gè)TableAdapter利用所有的數(shù)據(jù)訪問方式
完成了配置向?qū)Ш螅摂?shù)據(jù)集設(shè)計(jì)器將包含一個(gè)強(qiáng)類型的Products DataTable和TableAdapter。讓我們花些時(shí)間把該DataTable的名稱Products改為ProductsOptimisticConcurrency,方法是右鍵點(diǎn)擊DataTable的標(biāo)題欄,從菜單中選擇“重命名”。
圖8:一個(gè)DataTable和TableAdapter已經(jīng)添加到類型化數(shù)據(jù)集
為了看看ProductsOptimisticConcurrency TableAdapter(使用開放式并發(fā))和Products TableAdapter(不使用并發(fā)控制)的UPDATE 和 DELETE查詢之間有什么不同,選中該TableAdapter并轉(zhuǎn)到屬性窗口。在DeleteCommand 和 UpdateCommand 這兩個(gè)屬性的 CommandText 子屬性里,我們可以看到調(diào)用DAL的update或者delete關(guān)聯(lián)的方法時(shí)發(fā)送到數(shù)據(jù)庫的實(shí)際的SQL語法。ProductsOptimisticConcurrency TableAdapter使用的DELETE語句是
DELETE FROM [Products]
WHERE (([ProductID] = @Original_ProductID)
AND ([ProductName] = @Original_ProductName)
AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
OR ([SupplierID] = @Original_SupplierID))
AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
OR ([CategoryID] = @Original_CategoryID))
AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
OR ([UnitPrice] = @Original_UnitPrice))
AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
OR ([UnitsInStock] = @Original_UnitsInStock))
AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
OR ([ReorderLevel] = @Original_ReorderLevel))
AND ([Discontinued] = @Original_Discontinued))
相反,最初的DAL的Products TableAdapter所使用的DELETE語句則簡單得多:
DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))
正如你所看到的,啟用了開發(fā)式并發(fā)的TableAdapter所使用的DELETE語句里的WHERE子句包含了對(duì)表Product每一個(gè)字段現(xiàn)有的值與GridView(或者DetailsView,F(xiàn)ormView)最后一次加載時(shí)的原始值的對(duì)比。因?yàn)槌薖roductID,ProductName, 和Discontinued之外,其他所有字段都可能為NULL值,所以WHERE子句里還包含了額外的參數(shù)以及與NULL值恰當(dāng)?shù)谋容^。
在這一節(jié)里,我們不會(huì)在啟用了開放式并發(fā)的數(shù)據(jù)集里增加其他的DataTable了,因?yàn)槲覀兊腁SP.NET頁面將僅提供更新和刪除產(chǎn)品信息的功能。然而,我們?nèi)匀恍枰赑roductsOptimisticConcurrency TableAdapter里添加GetProductByProductID(productID) 方法。
為了實(shí)現(xiàn)這一點(diǎn),在TableAdapter的標(biāo)題欄(在Fill和GetProducts方法名的上方)上右鍵并從菜單里選擇“添加查詢”。這將啟動(dòng)TableAdapter查詢配置向?qū)?。在TableAdapter的最初配置的基礎(chǔ)上,選擇指定SQL語句來創(chuàng)建GetProductByProductID(productID)方法(見圖四)。因?yàn)镚etProductByProductID(productID)方法返回指定產(chǎn)品的信息,因此需要指定SQL查詢類型為“SELECT(返回行)”。
圖9:標(biāo)記SQL查詢類型為“SELECT(返回行)”
進(jìn)入下一步,向?qū)崾疚覀冎付⊿QL語句,并且與載入TableAdapter默認(rèn)查詢語句。在現(xiàn)有的查詢語句的基礎(chǔ)上添加WHERE ProductID = @ProductID子句,如圖10:
圖10:在預(yù)載入的查詢語句上添加WHERE子句從而返回特定的產(chǎn)品記錄
最后,把生成的方法重命名為FillByProductID和GetProductByProductID。
圖11:把生成的方法重命名為FillByProductID和GetProductByProductID
完成這個(gè)向?qū)е?,現(xiàn)在這個(gè)TableAdapter包含兩個(gè)訪問數(shù)據(jù)的方法:GetProducts(),它返回所有 的產(chǎn)品;和GetProductByProductID(productID),它返回特定的產(chǎn)品。
第三步: 創(chuàng)建一個(gè)支持啟用了開放式并發(fā)的DAL的業(yè)務(wù)邏輯層
我們現(xiàn)有的ProductsBLL類包含批量更新和直接發(fā)送數(shù)據(jù)庫的模式的例子。AddProduct方法和 UpdateProduct重載都使用了批量更新模式,通過一個(gè)ProductRow實(shí)例發(fā)送到TableAdapter的Update方法。另一方面,DeleteProduct方法則使用直接發(fā)送到數(shù)據(jù)庫的模式,調(diào)用TableAdapter的Delete(productID)方法。在新的ProductsOptimisticConcurrency TableAdapter里,發(fā)送到數(shù)據(jù)庫的方法現(xiàn)還要求傳入原始的值。例如,Delete方法
現(xiàn)在要求十個(gè)輸入?yún)?shù):原始的ProductID、ProductName、SupplierID、CategoryID、QuantityPerUnit、UnitPrice、UnitsInStock、UnitsOnOrder、ReorderLevel和Discontinued。它在發(fā)送到數(shù)據(jù)庫的DELETE語句的WHERE子句里使用這些額外的輸入?yún)?shù),僅僅刪除當(dāng)前數(shù)據(jù)庫的值與原始值一致的指定記錄。
使用批量更新模式時(shí),如果標(biāo)記給TableAdapter的Update使用的方法沒有更改,那么代碼就需要同時(shí)記錄原始值和新的值。然而,與其在我們現(xiàn)有的ProductsBLL類的基礎(chǔ)上試圖使用啟用了開放式并發(fā)的DAL,不如讓我們重新創(chuàng)意一個(gè)業(yè)務(wù)邏輯類支持我們新的DAL。在App_Code文件夾下的BLL子文件夾里,添加一個(gè)名為ProductsOptimisticConcurrencyBLL的新類。
圖 12: 添加ProductsOptimisticConcurrencyBLL類到BLL文件夾
然后,在ProductsOptimisticConcurrencyBLL類里添加如下代碼:
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 NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
protected ProductsOptimisticConcurrencyTableAdapter Adapter
{
get
{
if (_productsAdapter == null)
_productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
return _productsAdapter;
}
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Select, true)]
public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
{
return Adapter.GetProducts();
}
}
注意在類的聲明開始之前的using NorthwindOptimisticConcurrencyTableAdapters語句。命名空間NorthwindOptimisticConcurrencyTableAdapters包含了類ProductsOptimisticConcurrencyTableAdapter,它提供DAL的方法。并且,在類聲明之前我們還能找到System.ComponentModel.DataObject屬性標(biāo)志,它指示Visual Studio把該類包含在ObjectDataSource向?qū)У臄?shù)據(jù)對(duì)象下拉列表中。
類ProductsOptimisticConcurrencyBLL的Adapter屬性提供快速訪問ProductsOptimisticConcurrencyTableAdapter類的一個(gè)實(shí)例,并和我們最初的BLL類(ProductsBLL、CategoriesBLL等等)相似。最后,方法GetProducts()僅僅是調(diào)用DAL的GetProdcuts()方法并返回一個(gè)ProductsOptimisticConcurrencyDataTable對(duì)象,該對(duì)象由對(duì)應(yīng)數(shù)據(jù)庫里每一個(gè)產(chǎn)品記錄的ProductsOptimisticConcurrencyRow實(shí)例組成。
使用支持開放式并發(fā)的發(fā)送到數(shù)據(jù)庫的模式刪除一個(gè)產(chǎn)品記錄
當(dāng)使用支持開放式并發(fā)的DAL發(fā)送到數(shù)據(jù)庫的模式,方法必須傳入新值和原始值。對(duì)刪除來說,這沒有新的值,所以僅僅需要傳入原始值。那么,在我們的BLL里,我們必須接收所有原始值所為輸入?yún)?shù)。讓ProductsOptimisticConcurrencyBLL類的DeleteProduct方法使用這個(gè)發(fā)送到數(shù)據(jù)的方法。這意味著此方法必須接受所有的十個(gè)產(chǎn)品數(shù)據(jù)字段作為輸入?yún)?shù),并傳送這些參數(shù)到DAL,如下面的代碼所示:
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
(int original_productID, string original_productName,
int? original_supplierID, int? original_categoryID,
string original_quantityPerUnit, decimal? original_unitPrice,
short? original_unitsInStock, short? original_unitsOnOrder,
short? original_reorderLevel, bool original_discontinued)
{
int rowsAffected = Adapter.Delete(original_productID,
original_productName,
original_supplierID,
original_categoryID,
original_quantityPerUnit,
original_unitPrice,
original_unitsInStock,
original_unitsOnOrder,
original_reorderLevel,
original_discontinued);
// Return true if precisely one row was deleted, otherwise false
return rowsAffected == 1;
}
如果這些在GridView(或者是DetailsView、FormView)最后一次加載時(shí)的原始值跟用戶點(diǎn)擊刪除按鈕時(shí)數(shù)據(jù)庫中的值不一致, WHERE子句將不能匹配任何數(shù)據(jù)庫記錄,這就沒有記錄會(huì)受到影響。因此,TableAdapter的Delete方法將返回0并且BLL的DeleteProduct方法返回false。
使用支持開放式并發(fā)的批量更新模式修改一個(gè)產(chǎn)品記錄
正如之前注意到的,批量更新模式時(shí)用的TableAdapter的Update方法也有同樣的方法聲明為不管是否支持開放式并發(fā)。也就是,Update方法可以接受一個(gè)DataRow,一批DataRow,一個(gè)DataTable,或者一個(gè)類型化的數(shù)據(jù)集。正是因?yàn)镈ataTable在它的DataRow(s)里保留了從原始值到修改后的值這個(gè)變化的軌跡使這成為可能。當(dāng)DAL生成它的UPDATE語句時(shí),參數(shù)@original_ColumnName裝入DataRow中的原始值,反之,參數(shù)@ColumnName裝入DataRow中修改后的值。
在類ProductsBLL(我們最初使用的不支持開放式并發(fā)DAL的)里,當(dāng)我們使用批量更新模式更新產(chǎn)品信息時(shí),我們的代碼執(zhí)行的則是按順序執(zhí)行下列事件:
1.使用TableAdapter的GetProductByProductID(productID)方法讀取當(dāng)前數(shù)據(jù)庫中的產(chǎn)品信息到ProductRow實(shí)例
2.在第1步里將新的值賦值到ProductRow實(shí)例
3.調(diào)用TableAdapter的Update方法,傳入該P(yáng)roductRow實(shí)例
這一連串的步驟,無論如何都不可能支持開放式并發(fā),因?yàn)樵诘谝徊街挟a(chǎn)生的ProductRow是直接從數(shù)據(jù)庫組裝的,這意味著,DataRow中使用的原始值是當(dāng)前存在于數(shù)據(jù)庫中值,而并非開始編輯過程時(shí)綁定到GridView的值。相反地,當(dāng)使用啟用開放式并發(fā)的DAL,我們需要修改UpdateProduct方法的重載以使用下面這些步驟:
1.使用TableAdapter的GetProductByProductID(productID)方法讀取當(dāng)前數(shù)據(jù)庫中的產(chǎn)品信息到ProductsOptimisticConcurrencyRow實(shí)例
2.在第1步里將原始 值賦值到ProductsOptimisticConcurrencyRow實(shí)例
3.調(diào)用ProductsOptimisticConcurrencyRow實(shí)例的AcceptChanges()方法,這指示DataRow目前這些值是“原始”的值
4.將新 的值賦值到ProductsOptimisticConcurrencyRow實(shí)例
5.調(diào)用TableAdapter的Update方法,傳入該P(yáng)roductsOptimisticConcurrencyRow實(shí)例
第1步讀取當(dāng)前數(shù)據(jù)庫里指定產(chǎn)品記錄的所有字段的值。對(duì)更新所有 產(chǎn)品字段的UpdateProduct的重載里,這一步是多余的(因?yàn)檫@些值在第2步中被改寫),而對(duì)那些僅僅傳入部分字段值的重載方法來說則是必要的。一旦原始值賦值到ProductsOptimisticConcurrencyRow實(shí)例,調(diào)用AcceptChanges()方法,這將當(dāng)前DataRow中的值標(biāo)記為原始值,這些值將用作UPDATE語句的@original_ColumnNam參數(shù)。然后,新的參數(shù)值被賦值到ProductsOptimisticConcurrencyRow,最后,調(diào)用Update方法,傳入這個(gè)DataRow。
下面這些代碼展示了重載方法UpdateProduct接受所有產(chǎn)品數(shù)據(jù)字段作為輸入?yún)?shù)。雖然這里沒有展示,實(shí)際上從本節(jié)教程下載的ProductsOptimisticConcurrencyBLL類里還包含了重載方法UpdateProduct,它僅僅接受產(chǎn)品名稱和單價(jià)作為輸入?yún)?shù)。
protected void AssignAllProductValues
(NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
string productName, int? supplierID, int? categoryID, string quantityPerUnit,
decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
short? reorderLevel, bool discontinued)
{
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;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(
// new parameter values
string productName, int? supplierID, int? categoryID, string quantityPerUnit,
decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
short? reorderLevel, bool discontinued, int productID,
// original parameter values
string original_productName, int? original_supplierID, int? original_categoryID,
string original_quantityPerUnit, decimal? original_unitPrice,
short? original_unitsInStock, short? original_unitsOnOrder,
short? original_reorderLevel, bool original_discontinued,
int original_productID)
{
// STEP 1: Read in the current database product information
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products =
Adapter.GetProductByProductID(original_productID);
if (products.Count == 0)
// no matching record found, return false
return false;
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = products[0];
// STEP 2: Assign the original values to the product instance
AssignAllProductValues(product, original_productName, original_supplierID,
original_categoryID, original_quantityPerUnit, original_unitPrice,
original_unitsInStock, original_unitsOnOrder, original_reorderLevel,
original_discontinued);
// STEP 3: Accept the changes
product.AcceptChanges();
// STEP 4: Assign the new values to the product instance
AssignAllProductValues(product, productName, supplierID, categoryID,
quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel,
discontinued);
// STEP 5: Update the product record
int rowsAffected = Adapter.Update(product);
// Return true if precisely one row was updated, otherwise false
return rowsAffected == 1;
}
第四步: 從ASP.NET頁面把原始值和新值傳入BLL 方法
完成了DAL和BLL后,剩下的工作就是創(chuàng)建一個(gè)能利用系統(tǒng)中內(nèi)建的開放式并發(fā)邏輯的ASP.NET頁面。特別地,數(shù)據(jù) Web 服務(wù)器控件(GridView,DetailsView或FormView)必須記住它的原始值,并且ObjectDataSource必須同時(shí)傳送這兩套值到業(yè)務(wù)邏輯層。此外,ASP.NET頁面必須加以配置從而適當(dāng)?shù)靥幚聿l(fā)沖突。
首先,打開EditInsertDelete文件夾中的OptimisticConcurrency.aspx頁面,添加一個(gè)GridView控件到設(shè)計(jì)器,設(shè)置它的ID屬性為ProductsGrid。從GridView的職能標(biāo)記里,選擇創(chuàng)建一個(gè)新的ObjectDataSource名為ProductsOptimisticConcurrencyDataSource。既然我們希望這個(gè)ObjectDataSource使用支持開放式并發(fā)的DAL,就把它配置為使用ProductsOptimisticConcurrencyBLL對(duì)象。
圖 13: 該ObjectDataSource使用ProductsOptimisticConcurrencyBLL對(duì)象
在向?qū)е袕南吕斜磉x擇GetProducts,UpdateProduct,和DeleteProduct方法。對(duì)UpdateProduct方法,則使用接受所有產(chǎn)品數(shù)據(jù)字段的重載。
配置ObjectDataSource控件的屬性
完成了向?qū)е?,該ObjectDataSource的聲明標(biāo)記應(yīng)該如下:
asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
UpdateMethod="UpdateProduct">
DeleteParameters>
asp:Parameter Name="original_productID" Type="Int32" />
asp:Parameter Name="original_productName" Type="String" />
asp:Parameter Name="original_supplierID" Type="Int32" />
asp:Parameter Name="original_categoryID" Type="Int32" />
asp:Parameter Name="original_quantityPerUnit" Type="String" />
asp:Parameter Name="original_unitPrice" Type="Decimal" />
asp:Parameter Name="original_unitsInStock" Type="Int16" />
asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
asp:Parameter Name="original_reorderLevel" Type="Int16" />
asp:Parameter Name="original_discontinued" Type="Boolean" />
/DeleteParameters>
UpdateParameters>
asp:Parameter Name="productName" Type="String" />
asp:Parameter Name="supplierID" Type="Int32" />
asp:Parameter Name="categoryID" Type="Int32" />
asp:Parameter Name="quantityPerUnit" Type="String" />
asp:Parameter Name="unitPrice" Type="Decimal" />
asp:Parameter Name="unitsInStock" Type="Int16" />
asp:Parameter Name="unitsOnOrder" Type="Int16" />
asp:Parameter Name="reorderLevel" Type="Int16" />
asp:Parameter Name="discontinued" Type="Boolean" />
asp:Parameter Name="productID" Type="Int32" />
asp:Parameter Name="original_productName" Type="String" />
asp:Parameter Name="original_supplierID" Type="Int32" />
asp:Parameter Name="original_categoryID" Type="Int32" />
asp:Parameter Name="original_quantityPerUnit" Type="String" />
asp:Parameter Name="original_unitPrice" Type="Decimal" />
asp:Parameter Name="original_unitsInStock" Type="Int16" />
asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
asp:Parameter Name="original_reorderLevel" Type="Int16" />
asp:Parameter Name="original_discontinued" Type="Boolean" />
asp:Parameter Name="original_productID" Type="Int32" />
/UpdateParameters>
/asp:ObjectDataSource>
正如你所看到的,DeleteParameters集合包含了對(duì)應(yīng)ProductsOptimisticConcurrencyBLL類的DeleteProduct方法的每一個(gè)輸入?yún)?shù)的Parameter實(shí)例。同樣地,UpdateParameters集合也包含了對(duì)應(yīng)UpdateProduct每一個(gè)輸入?yún)?shù)的Parameter實(shí)例。在先前的那些關(guān)于數(shù)據(jù)修改的教程中,我們?cè)谶@里都會(huì)移除ObjectDataSource的OldValuesParameterFormatString屬性,因?yàn)檫@個(gè)屬性需要BLL方法既要求傳入原始值也要求傳入修改后的值。此外,這個(gè)屬性還需要對(duì)應(yīng)原始值的輸入?yún)?shù)的名稱。既然我們現(xiàn)在要把原始值傳送到BLL,那就不要 刪除這個(gè)屬性。
注意:OldValuesParameterFormatString屬性的值必須映射到BLL里接收原始值的輸入?yún)?shù)的名稱。因?yàn)槲覀儼堰@些參數(shù)命名為original_productName,original_supplierID, 等等,我們可以讓OldValuesParameterFormatString屬性的值依舊是original_{0}。然而如果BLL方法的輸入?yún)?shù)名為的old_productName,old_supplierID等等,那么,你不得不把OldValuesParameterFormatString屬性的值改為old_{0}。為了ObjectDataSource能夠正確地將原始值傳送到BLL方法,還有最后一個(gè)屬性需要設(shè)置。ObjectDataSource有一個(gè) ConflictDetection屬性,它可以設(shè)定為下面的 下面兩個(gè)值之一:
OverwriteChanges – 默認(rèn)值; 不將原始值發(fā)送到BLL方法相應(yīng)的輸入?yún)?shù)
CompareAllValues – 將原始值發(fā)送到BLL方法;當(dāng)使用開放式并發(fā)時(shí)使用這一項(xiàng)
稍花些時(shí)間將ConflictDetection屬性設(shè)置為CompareAllValues。配置GridView的屬性和字段當(dāng)正確的配置完ObjectDataSource的屬性后,讓我們把注意力放在GridView的設(shè)置上。首先,因?yàn)槲覀兿M鸊ridView支持編輯和刪除,因此,從GridView的智能標(biāo)記中點(diǎn)擊添加新列,從下拉列表中選擇CommandField并勾選上“刪除”和“編輯/更新”。這將增加一個(gè)CommandField,它的ShowEditButton和ShowDeleteButton屬性都已設(shè)置為true。當(dāng)綁定ProductsOptimisticConcurrencyDataSource ObjectDataSource,該GridView對(duì)應(yīng)每一個(gè)產(chǎn)品數(shù)據(jù)字段都包含一列。
雖然這樣的一個(gè)GridView可以被編輯,但用戶的體驗(yàn)將是不可接受的。這沒有對(duì)數(shù)字欄作格式化處理,也沒有validation控件以確保提供product's name并且unit price、units in stock、units on order、和reorder level的值都是大于零的數(shù)字。
跟我們?cè)谥暗慕o編輯和新增界面增加驗(yàn)證控件 這一節(jié)里所論述的一樣,用戶界面可以通過將綁定列(BoundFields)替換為模板列(TemplateFields)實(shí)現(xiàn)自定義。我已經(jīng)通過以下方式修改了這個(gè)GridView和它的編輯界面:
1.刪除ProductID、SupplierName、和CategoryName這幾個(gè)綁定列;
2.將ProductName綁定列替換為模板列并添加一個(gè)RequiredFieldValidation控件;
3.將CategoryID和SupplierID綁定列替換為模板列,并調(diào)整編輯界面,使用DropDownList而不是TextBox。在這些模板列的ItemTemplates里,顯示CategoryName和SupplierName字段;
4.將UnitPrice、UnitsInStock、UnitsOnOrder、和ReorderLevel綁定列替換為模板列并添加CompareValidator控件。因?yàn)槲覀冊(cè)谥暗恼鹿?jié)里已經(jīng)詳細(xì)說明了如何完成這些任務(wù),我僅僅把最終的聲明語法列出并把具體執(zhí)行留給讀者作為練習(xí)。
asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
OnRowUpdated="ProductsGrid_RowUpdated">
Columns>
asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
asp:TemplateField HeaderText="Product" SortExpression="ProductName">
EditItemTemplate>
asp:TextBox ID="EditProductName" runat="server"
Text='%# Bind("ProductName") %>'>/asp:TextBox>
asp:RequiredFieldValidator ID="RequiredFieldValidator1"
ControlToValidate="EditProductName"
ErrorMessage="You must enter a product name."
runat="server">*/asp:RequiredFieldValidator>
/EditItemTemplate>
ItemTemplate>
asp:Label ID="Label1" runat="server"
Text='%# Bind("ProductName") %>'>/asp:Label>
/ItemTemplate>
/asp:TemplateField>
asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
EditItemTemplate>
asp:DropDownList ID="EditCategoryID" runat="server"
DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
DataTextField="CategoryName" DataValueField="CategoryID"
SelectedValue='%# Bind("CategoryID") %>'>
asp:ListItem Value=">(None)/asp:ListItem>
/asp:DropDownList>asp:ObjectDataSource ID="CategoriesDataSource"
runat="server" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetCategories" TypeName="CategoriesBLL">
/asp:ObjectDataSource>
/EditItemTemplate>
ItemTemplate>
asp:Label ID="Label2" runat="server"
Text='%# Bind("CategoryName") %>'>/asp:Label>
/ItemTemplate>
/asp:TemplateField>
asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
EditItemTemplate>
asp:DropDownList ID="EditSuppliersID" runat="server"
DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
DataTextField="CompanyName" DataValueField="SupplierID"
SelectedValue='%# Bind("SupplierID") %>'>
asp:ListItem Value=">(None)/asp:ListItem>
/asp:DropDownList>asp:ObjectDataSource ID="SuppliersDataSource"
runat="server" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
/asp:ObjectDataSource>
/EditItemTemplate>
ItemTemplate>
asp:Label ID="Label3" runat="server"
Text='%# Bind("SupplierName") %>'>/asp:Label>
/ItemTemplate>
/asp:TemplateField>
asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
SortExpression="QuantityPerUnit" />
asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
EditItemTemplate>
asp:TextBox ID="EditUnitPrice" runat="server"
Text='%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
asp:CompareValidator ID="CompareValidator1" runat="server"
ControlToValidate="EditUnitPrice"
ErrorMessage="Unit price must be a valid currency value without the
currency symbol and must have a value greater than or equal to zero."
Operator="GreaterThanEqual" Type="Currency"
ValueToCompare="0">*/asp:CompareValidator>
/EditItemTemplate>
ItemTemplate>
asp:Label ID="Label4" runat="server"
Text='%# Bind("UnitPrice", "{0:C}") %>'>/asp:Label>
/ItemTemplate>
/asp:TemplateField>
asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
EditItemTemplate>
asp:TextBox ID="EditUnitsInStock" runat="server"
Text='%# Bind("UnitsInStock") %>' Columns="6">/asp:TextBox>
asp:CompareValidator ID="CompareValidator2" runat="server"
ControlToValidate="EditUnitsInStock"
ErrorMessage="Units in stock must be a valid number
greater than or equal to zero."
Operator="GreaterThanEqual" Type="Integer"
ValueToCompare="0">*/asp:CompareValidator>
/EditItemTemplate>
ItemTemplate>
asp:Label ID="Label5" runat="server"
Text='%# Bind("UnitsInStock", "{0:N0}") %>'>/asp:Label>
/ItemTemplate>
/asp:TemplateField>
asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
EditItemTemplate>
asp:TextBox ID="EditUnitsOnOrder" runat="server"
Text='%# Bind("UnitsOnOrder") %>' Columns="6">/asp:TextBox>
asp:CompareValidator ID="CompareValidator3" runat="server"
ControlToValidate="EditUnitsOnOrder"
ErrorMessage="Units on order must be a valid numeric value
greater than or equal to zero."
Operator="GreaterThanEqual" Type="Integer"
ValueToCompare="0">*/asp:CompareValidator>
/EditItemTemplate>
ItemTemplate>
asp:Label ID="Label6" runat="server"
Text='%# Bind("UnitsOnOrder", "{0:N0}") %>'>/asp:Label>
/ItemTemplate>
/asp:TemplateField>
asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
EditItemTemplate>
asp:TextBox ID="EditReorderLevel" runat="server"
Text='%# Bind("ReorderLevel") %>' Columns="6">/asp:TextBox>
asp:CompareValidator ID="CompareValidator4" runat="server"
ControlToValidate="EditReorderLevel"
ErrorMessage="Reorder level must be a valid numeric value
greater than or equal to zero."
Operator="GreaterThanEqual" Type="Integer"
ValueToCompare="0">*/asp:CompareValidator>
/EditItemTemplate>
ItemTemplate>
asp:Label ID="Label7" runat="server"
Text='%# Bind("ReorderLevel", "{0:N0}") %>'>/asp:Label>
/ItemTemplate>
/asp:TemplateField>
asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
SortExpression="Discontinued" />
/Columns>
/asp:GridView>
我們已經(jīng)非常接近于完成一個(gè)完整的例子。然而,還有一些細(xì)節(jié)問題需要我們慢慢解決。另外,我們還需要一些界面,當(dāng)發(fā)生并發(fā)沖突時(shí)用來提示用戶。
注意: 為了讓數(shù)據(jù)Web服務(wù)器控件能夠正確地把原始的值傳送到ObjectDataSource(它隨之將其發(fā)送到BLL),將GirdView的EnableViewState屬性設(shè)置為true(默認(rèn)值)是至關(guān)重要的。如果禁用了視圖狀態(tài),這些原始值在postback的時(shí)候?qū)?huì)丟失。
傳送正確的原始值到ObjectDataSource完成了GridView的配置,還有幾個(gè)問題。如果這個(gè)ObjectDataSource的ConflictDetection 屬性設(shè)置為CompareAllValues (正如我們所做的),它會(huì)嘗試復(fù)制GridView的原始值到它的Parameter實(shí)例。回到圖2查看這個(gè)過程的圖解。
特別需要指出的是,這個(gè)GridView的原始值是被指定為雙向綁定的。因此,這些必需的原始值是通過雙向綁定獲取的,并且它們是規(guī)定為可改變的格式,這一點(diǎn)很重要。為了看看為什么這一點(diǎn)非常重要,花些時(shí)間通過瀏覽器訪問我們的頁面。正如所預(yù)料那樣,GridView列出每一個(gè)產(chǎn)品,并且每行最左邊的一列都顯示編輯和刪除按鈕。
圖14: GridView列出所有的產(chǎn)品信息
如果你點(diǎn)擊任意一行的刪除按鈕,則拋出一個(gè)FormatException異常。
圖15: 嘗試刪除任意一個(gè)產(chǎn)品導(dǎo)致FormatException異常
當(dāng)ObjectDataSource試圖讀取原始的UnitPrice值引發(fā)了一個(gè)FormatException異常。因?yàn)樵撃0辶袑nitPrice的值限制為貨幣格式(%# Bind("UnitPrice", "{0:C}") %>),它包含一個(gè)貨幣符號(hào),例如$19.95。該FormatException異常發(fā)生在ObjectDataSource試圖將字符產(chǎn)轉(zhuǎn)換成小數(shù)。為了繞過此問題,我們有許多種選擇:
1.從模板列里刪除貨幣格式限制。就是說,取代%# Bind("UnitPrice", "{0:C}") %>,簡單地使用%# Bind("UnitPrice") %>。下方的價(jià)格就是沒有格式化的。
2.在模板列中顯示UnitPrice時(shí)格式化為貨幣,但是使用Eval關(guān)鍵字實(shí)現(xiàn)綁定。記得Eval是實(shí)現(xiàn)單向綁定的。我們?nèi)匀恍枰峁︰nitPrice的值作為原始的值,因此在模板列里我們依舊需要一個(gè)雙向綁定的聲明,但這可以放在一個(gè)Visible屬性設(shè)置為false的Label服務(wù)器控件里。在模板列里我們可以使用下面的標(biāo)記:
ItemTemplate>
asp:Label ID="DummyUnitPrice" runat="server"
Text='%# Bind("UnitPrice") %>' Visible="false">/asp:Label>
asp:Label ID="Label4" runat="server"
Text='%# Eval("UnitPrice", "{0:C}") %>'>/asp:Label>
/ItemTemplate>
3.從模板列里刪除貨幣格式限制,使用 %# Bind("UnitPrice") %>。在GridView的RowDataBound事件處理里,編碼訪問顯示UnitPrice的值的Label服務(wù)器控件并設(shè)置其Text屬性為格式化的版本。
4.讓UnitPrice保留貨幣格式化。在GridView的RowDeleting事件處理里,將現(xiàn)存的UnitPrice的原始($19.95)替換為實(shí)際的小數(shù)值(使用Decimal.Parse)。在前面的 在ASP.NET頁面中處理BLL/DAL異常這一節(jié)教程里我們也已經(jīng)看過如何RowUpdating事件處理里實(shí)現(xiàn)類似的功能。 在我的例程里我選擇第二種方法,添加一個(gè)隱藏的Label服務(wù)器控件,并將它的Text屬性雙向綁定到無格式的UnitPrice值。解決了這個(gè)問題之后,再次點(diǎn)擊任意一個(gè)產(chǎn)品的刪除按鈕。這一次,當(dāng)ObjectDataSource嘗試調(diào)用BLL的UpdateProduct方法時(shí)我們得到一個(gè)InvalidOperationException異常。
圖 16: ObjectDataSource找不到具有它要發(fā)送的輸入?yún)?shù)的方法
仔細(xì)看看異常信息,明顯地ObjectDataSource希望調(diào)用一個(gè)BLL的DeleteProduct方法,此方法包含original_CategoryName和original_SupplierName輸入?yún)?shù)。這是因?yàn)镃ategoryID和SupplierID模板列的ItemTemplate當(dāng)前是雙向綁定到CategoryName和SupplierName數(shù)據(jù)字段。作為替換,我們需要包含對(duì)CategoryID和SupplierID數(shù)據(jù)字段的Bind聲明。為了實(shí)現(xiàn)這一點(diǎn),把現(xiàn)有的Bind聲明更改為Eval聲明,并且添加隱藏的Label服務(wù)器控件,這些Label的Text屬性使用雙向綁定的方式綁定到CategoryID和SupplierID數(shù)據(jù)字段,如下所示:
asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
EditItemTemplate>
...
/EditItemTemplate>
ItemTemplate>
asp:Label ID="DummyCategoryID" runat="server"
Text='%# Bind("CategoryID") %>' Visible="False">/asp:Label>
asp:Label ID="Label2" runat="server"
Text='%# Eval("CategoryName") %>'>/asp:Label>
/ItemTemplate>
/asp:TemplateField>
asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
EditItemTemplate>
...
/EditItemTemplate>
ItemTemplate>
asp:Label ID="DummySupplierID" runat="server"
Text='%# Bind("SupplierID") %>' Visible="False">/asp:Label>
asp:Label ID="Label3" runat="server"
Text='%# Eval("SupplierName") %>'>/asp:Label>
/ItemTemplate>
/asp:TemplateField>
通過這些更改,現(xiàn)在我們可以成功地刪除和編輯產(chǎn)品信息了!在第五步里,我們將看看如何驗(yàn)證刪除時(shí)發(fā)生并發(fā)沖突。但是現(xiàn)在,花幾分鐘嘗試更新和刪除一些記錄,確認(rèn)在單用戶的情況下更新和刪除能夠正常運(yùn)作。
第五步: 測試開放式并發(fā)支持
為了驗(yàn)證并發(fā)沖突是否能夠被發(fā)現(xiàn)(而不是導(dǎo)致數(shù)據(jù)被盲目改寫),我們需要打開兩個(gè)瀏覽器窗口來訪問這個(gè)頁面。在兩個(gè)瀏覽窗口里,都點(diǎn)擊產(chǎn)品“Chai”的編輯按鈕。然后,在其中一個(gè)窗口修改其名稱為“Chai Tea”并點(diǎn)擊更新。這個(gè)更新應(yīng)該會(huì)成功并且GridView回到預(yù)編輯狀態(tài),并且該產(chǎn)品的名稱已經(jīng)改為“Chai Tea”。
而在另一個(gè)瀏覽器窗口里,產(chǎn)品名稱域依舊顯示的是“Chai”。在這個(gè)瀏覽器窗口,將UnitPrice的值更新為25.00。如果沒有開放式并發(fā)支持的話,點(diǎn)擊第二個(gè)瀏覽器窗口的更新按鈕將把產(chǎn)品名稱改回“Chai”,從而覆蓋了第一個(gè)瀏覽器窗口里所作的修改。然而現(xiàn)在有了開發(fā)式并發(fā),當(dāng)點(diǎn)擊第二個(gè)窗口中的更新按鈕時(shí)導(dǎo)致了一個(gè)DBConcurrencyException異常。
圖 17: 發(fā)現(xiàn)并發(fā)沖突,拋出一個(gè)DBConcurrencyException異常
這個(gè)DBConcurrencyException異常僅當(dāng)利用DAL的批量更新模式時(shí)會(huì)被拋出。直接發(fā)送到數(shù)據(jù)庫的模式則不會(huì)引發(fā)異常,它僅僅會(huì)提示沒有行受到影響。為了舉例說明這個(gè),兩個(gè)瀏覽器窗口里的GridView都回到預(yù)編輯的狀態(tài)。然后,在第一個(gè)窗口里,點(diǎn)擊編輯按鈕,把產(chǎn)品名稱從“Chai”改為“Chai Tea”并點(diǎn)擊更新。在第二個(gè)窗口里,點(diǎn)擊產(chǎn)品“Chai”的刪除按鈕。點(diǎn)擊刪除按鈕,頁面會(huì)傳,GridView調(diào)用ObjectDataSource的Delete()方法,然后ObjectDataSource調(diào)用ProductsOptimisticConcurrencyBLL類的DeleteProduct方法,傳入原始的值。在第二個(gè)瀏覽器窗口里原始的ProductName值是“Chai Tea”,這個(gè)值與當(dāng)前數(shù)據(jù)庫中相應(yīng)的ProductName值是不一致的。因此,發(fā)送到數(shù)據(jù)庫的DELETE語句影響0行,因?yàn)閿?shù)據(jù)庫中沒有記錄能夠滿足WHERE子句。DeleteProduct方法返回false并且ObjectDataSource的數(shù)據(jù)重新綁定到GridView控件。
從最后一個(gè)用戶的觀點(diǎn)來看,在第二個(gè)瀏覽器窗口里點(diǎn)擊了產(chǎn)品“Chai Tea”的刪除按鈕導(dǎo)致屏幕閃爍,恢復(fù)后該產(chǎn)品依舊在,雖然現(xiàn)在它的名稱是“Chai”(在第一個(gè)瀏覽器窗口里修改了產(chǎn)品名稱)。如果用戶再次點(diǎn)擊刪除按鈕,這次就能成功刪除,因?yàn)镚ridView的原始的ProductName值(“Chai”)現(xiàn)在能夠與數(shù)據(jù)庫中相應(yīng)的值匹配。在這些例子里,用戶的體驗(yàn)跟理想的狀況還有頗遠(yuǎn)的距離。顯然我們?cè)谑褂门扛履J綍r(shí)不希望用戶看到DBConcurrencyException異常生硬的詳細(xì)信息。并且使用直接發(fā)送到數(shù)據(jù)庫模式的行為也會(huì)讓用戶有些疑惑,因?yàn)橛脩舨僮魇×说菦]有準(zhǔn)確的提示說明為什么。
為了補(bǔ)救這兩個(gè)小問題,我們可以在頁面上放置一個(gè)Label服務(wù)器控件,它用來提供為什么更新或刪除失敗的說明。在批量更新模式,我們可以在GridView的post級(jí)事件處理里判定是否引發(fā)了一個(gè)DBConcurrencyException異常,顯示必要的警告標(biāo)簽。對(duì)于直接發(fā)送到數(shù)據(jù)庫的方法,我們可以檢測BLL方法(它對(duì)一行或多行產(chǎn)生影響返回true,否則false)的返回值并顯示必要的提示信息。
第六步: 添加提示信息并且在發(fā)生并發(fā)沖突時(shí)顯示
當(dāng)一個(gè)并發(fā)沖突出現(xiàn)時(shí),展現(xiàn)出來的行為取決于是使用DAL的批量更新還是直接發(fā)送到數(shù)據(jù)庫的模式。我們這一節(jié)的教程兩種模式都用了,用批量更新模式實(shí)現(xiàn)修改,用直接發(fā)送到數(shù)據(jù)庫的方式實(shí)現(xiàn)刪除。首先,我們添加兩個(gè)Label服務(wù)器控件到頁面,它們用來解釋更新或刪除數(shù)據(jù)時(shí)出現(xiàn)的并發(fā)沖突。設(shè)置Label控件的Visible和EnableViewState屬性為false;這意味一般情況下它們都是隱藏的,除非是那些特別的頁面訪問,在那里它們的Visible屬性通過編碼設(shè)置為true。
asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
EnableViewState="False" CssClass="Warning"
Text="The record you attempted to delete has been modified by another user
since you last visited this page. Your delete was cancelled to allow
you to review the other user's changes and determine if you want to
continue deleting this record." />
asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
EnableViewState="False" CssClass="Warning"
Text="The record you attempted to update has been modified by another user
since you started the update process. Your changes have been replaced
with the current values. Please review the existing values and make
any needed changes." />
在設(shè)置了它們的Visible、EnabledViewState和Text屬性之外,我們還要把CssClass屬性設(shè)置為Warning,這讓標(biāo)簽顯示大的、紅色的、斜體、加粗的字體。這個(gè)CSS Warning 分類是在研究插入、更新和刪除的關(guān)聯(lián)事件這一節(jié)里添加到Styles.css并且定義好的。添加了這些標(biāo)簽之后,Visual Studio設(shè)計(jì)器里看起來應(yīng)該類似于圖18:
圖 18: 兩個(gè)Label控件添加到頁面
這些Label服務(wù)器控件放置到適當(dāng)?shù)奈恢煤?,我們?zhǔn)備好檢測當(dāng)并發(fā)沖突發(fā)生時(shí)如何判定,在哪個(gè)時(shí)間點(diǎn)把適當(dāng)?shù)腖abel的Visible屬性設(shè)置為true并顯示提示信息。
更新時(shí)處理并發(fā)沖突
讓我們首先看看當(dāng)使用批量更新模式是如何處理并發(fā)沖突。因?yàn)榕扛履J较碌倪@些沖突導(dǎo)致拋出一個(gè)DBConcurrencyException異常,我們需要在ASP.NET頁面中添加代碼來判定更新過程中出現(xiàn)的是否DBConcurrencyException異常。如果是,我們則顯示一個(gè)信息向用戶解釋他們的更改沒有被保存,由于別的用戶在他開始編輯和點(diǎn)擊更新按鈕之間的時(shí)間里修改了同樣的數(shù)據(jù)記錄。
正如我們?cè)谠贏SP.NET頁面中處理BLL/DAL異常 這一節(jié)里看過的那樣,這樣的異??梢栽跀?shù)據(jù)Web服務(wù)器控件的post級(jí)事件處理里被發(fā)現(xiàn)和排除。因此,我們需要?jiǎng)?chuàng)建一個(gè)GridView的RowUpdated事件的處理,它用來檢測是否拋出了一個(gè)DBConcurrencyException異常。這個(gè)事件處理通過一個(gè)不同的分支區(qū)別更新過程中引發(fā)的其它異常,如下面的時(shí)間處理代碼所示:
protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
if (e.Exception != null e.Exception.InnerException != null)
{
if (e.Exception.InnerException is System.Data.DBConcurrencyException)
{
// Display the warning message and note that the
// exception has been handled...
UpdateConflictMessage.Visible = true;
e.ExceptionHandled = true;
}
}
}
面對(duì)一個(gè)DBConcurrencyException異常,該事件處理顯示UpdateConflictMessage Label控件并且指出該異常已經(jīng)被處理。正確地編寫了這些代碼后,當(dāng)更新記錄時(shí)發(fā)生了并發(fā)沖突,用戶的更改會(huì)丟失,因?yàn)樗麄儾荒芨采w同時(shí)發(fā)生的另一個(gè)用戶的更改。特別地,GridView回到預(yù)編輯幢白并且綁定到當(dāng)前數(shù)據(jù)庫中數(shù)據(jù)。這將在GridView的行中顯示出別的用戶的更改,而之前這些更改是看不見的。另外,UpdateConflictMessage Label控件將向用戶說明發(fā)生了什么。圖19詳細(xì)展示了這一連串的事件。
圖 19: 面對(duì)并發(fā)沖突,一個(gè)用戶的更改丟失了
注意:作為另一種選擇,與其讓GridView回到預(yù)編輯狀態(tài),我們還不如讓GridView停留在編輯狀態(tài),通過設(shè)置傳入的GridViewUpdatedEventArgs對(duì)象的KeepInEditMode屬性為true。如果你接受這種方法,那么,必須重新綁定數(shù)據(jù)到GridView(通過調(diào)用它的DataBind()方法)從而將其他用戶更改后的值栽入到編輯界面。在這一節(jié)的可下載的代碼里,RowUpdated事件處理里有這兩行注悉掉的代碼;僅僅需要啟用這兩行代碼就可以讓GridView在發(fā)生了并發(fā)沖突之后保留編輯模式。
響應(yīng)刪除時(shí)的并發(fā)沖突
對(duì)于直接發(fā)送到數(shù)據(jù)庫的模式,面對(duì)并發(fā)沖突時(shí)并不會(huì)引發(fā)異常。然而,數(shù)據(jù)庫語句不影響任何記錄,因?yàn)閃HERE子句不能匹配任何記錄。所有在BLL里創(chuàng)建的修改數(shù)據(jù)的方法都被設(shè)計(jì)為返回一個(gè)布爾值指示它們是否正好影響了一條記錄。因此,為了確定刪除記錄時(shí)是否發(fā)生了并發(fā)沖突,我們可以檢查BLL的DeleteProduct方法的返回值。
BLL方法的返回值可以在ObjectDataSource的post級(jí)事件處理中通過傳入事件處理的ObjectDataSourceStatusEventArgs對(duì)象的ReturnValue屬性被檢測。因?yàn)槲覀兏信d趣的是判斷從DeleteProduct方法返回的結(jié)果,我們需要?jiǎng)?chuàng)建一個(gè)ObjectDataSource的Deleted事件的事件處理程序。該ReturnValue屬性是object類型的,并且如果在方法可以返回一個(gè)值之前引發(fā)了異常并且方法被中斷的情況下,它的值也可能為null。所以,我們應(yīng)該首先確保ReturnValue屬性非空并是個(gè)布爾值。若能通過這個(gè)檢查,如果ReturnValue是 false我們顯示DeleteConflictMessage Label控件。可以通過下面的代碼完成:
protected void ProductsOptimisticConcurrencyDataSource_Deleted(
object sender, ObjectDataSourceStatusEventArgs e)
{
if (e.ReturnValue != null e.ReturnValue is bool)
{
bool deleteReturnValue = (bool)e.ReturnValue;
if (deleteReturnValue == false)
{
// No row was deleted, display the warning message
DeleteConflictMessage.Visible = true;
}
}
}
面對(duì)一個(gè)并發(fā)沖突,用戶的刪除請(qǐng)求會(huì)被取消。GridView被刷新,顯示在用戶載入頁面跟點(diǎn)擊刪除按鈕之間的時(shí)間里發(fā)生在該記錄上面的更改。當(dāng)發(fā)生這樣的一個(gè)沖突,顯示DeleteConflictMessage Label控件,說明發(fā)生了什么(見圖20)。
圖 20: 面對(duì)并發(fā)沖突,一個(gè)用戶的刪除請(qǐng)求被取消了
總結(jié)
并發(fā)沖突可能存在于所有允許多用戶同時(shí)更新或刪除數(shù)據(jù)的應(yīng)用程序里。如果不解決這樣的沖突,當(dāng)兩個(gè)用戶同時(shí)更新同一條數(shù)據(jù),無論誰最后得到“勝利”,都將覆蓋掉另一個(gè)用戶所做的更改。作為另一種選擇,開發(fā)者可以實(shí)現(xiàn)開放式并發(fā)控制(optimistic concurrency control),或者保守式并發(fā)控制(pessimistic concurrency control)。開放式并發(fā)控制假定并發(fā)沖突很少發(fā)生,簡單地否決一個(gè)會(huì)提起并發(fā)沖突的更新或者刪除命名。保守式并發(fā)控制則假定并發(fā)沖突頻繁地發(fā)生,簡單地拒絕某個(gè)用戶的更新或者刪除命令是不可接受的。在保守式并發(fā)控制下,編輯一條記錄涉及到鎖定它,從而該記錄被鎖定時(shí)預(yù)防其他用戶的修改或刪除。
.NET中的類型化數(shù)據(jù)集提供了支持開放式并發(fā)控制的功能。特別地,發(fā)送到數(shù)據(jù)庫的UPDATE和DELETE語句包含了這個(gè)表的所有字段,從而確保了僅當(dāng)該記錄但前的值與用戶開始他們的修改或更新時(shí)的原始值相匹配時(shí),修改或刪除才會(huì)發(fā)生。一旦DAL配置為支持開放式并發(fā),BLL的方法就需要修改。另外,調(diào)用BLL的ASP.NET頁面也需要配置為ObjectDataSource能從它的數(shù)據(jù)Web服務(wù)器控件獲取到這些原始的值并將這些值傳送到BLL。
正如我們?cè)诒竟?jié)里所看到的,在ASP.NET web應(yīng)用程序中實(shí)現(xiàn)開放式并發(fā)控制包括修改DAL和BLL,還包括在ASP.NET頁面中添加相應(yīng)的支持。無論這些額外的工作對(duì)你的時(shí)間來說是否一項(xiàng)明智的投入,對(duì)你的應(yīng)用程序來說是否有所成效。如果你極少面對(duì)多個(gè)用戶同時(shí)更新數(shù)據(jù),或者不同的用戶對(duì)數(shù)據(jù)作出不同的更改,那么并發(fā)控制并非必選項(xiàng)。然而,如果你時(shí)常面對(duì)多個(gè)用戶在線并且對(duì)同一些數(shù)據(jù)進(jìn)行操作,并發(fā)控制可以幫助預(yù)防一個(gè)用戶的更新或刪除被另一個(gè)用戶在不知情的情況下覆蓋。
祝編程快樂!
作者簡介
Scott Mitchell,著有六本ASP/ASP.NET方面的書,是4GuysFromRolla.com的創(chuàng)始人,自1998年以來一直應(yīng)用微軟Web技術(shù)。Scott是個(gè)獨(dú)立的技 術(shù)咨詢顧問,培訓(xùn)師,作家,最近完成了將由Sams出版社出版的新作,24小時(shí)內(nèi)精通ASP.NET 2.0。他的聯(lián)系電郵為mitchell@4guysfromrolla.com,也可以通過他的博客http://ScottOnWriting.NET與他聯(lián)系。
您可能感興趣的文章:- 讓W(xué)in2008+IIS7+ASP.NET支持10萬并發(fā)請(qǐng)求
- c#實(shí)現(xiàn)服務(wù)器性能監(jiān)控并發(fā)送郵件保存日志
- C#線程執(zhí)行超時(shí)處理與并發(fā)線程數(shù)控制實(shí)例
- c#編寫的高并發(fā)數(shù)據(jù)庫控制訪問代碼
- C#使用隊(duì)列(Queue)解決簡單的并發(fā)問題
- 在ASP.NET 2.0中操作數(shù)據(jù)之四十四:DataList和Repeater數(shù)據(jù)排序(三)
- 在ASP.NET 2.0中操作數(shù)據(jù)之四十五:DataList和Repeater里的自定義Button
- 在ASP.NET 2.0中操作數(shù)據(jù)之四十六:使用SqlDataSource控件檢索數(shù)據(jù)
- 在ASP.NET 2.0中操作數(shù)據(jù)之四十七:用SqlDataSource控件插入、更新、刪除數(shù)據(jù)
- 在ASP.NET 2.0中操作數(shù)據(jù)之四十八:對(duì)SqlDataSource控件使用開放式并發(fā)