程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> ASP.NET >> ASP.NET基礎 >> 在ASP.NET 2.0中操作數據之二十一:實現開放式並發

在ASP.NET 2.0中操作數據之二十一:實現開放式並發

編輯:ASP.NET基礎

導言

  對於那些僅僅允許用戶查看數據,或者僅有一個用戶可以修改數據的web應用軟件,不存在多用戶並發沖突的問題。然而對於那些允許多個用戶修改或刪除數據的web應用軟件,則有可能發生一個用戶所做的更改與另一個並發用戶的更改沖突。在沒有任何並發策略的地方,當兩個用戶同時編輯某一條記錄,最後提交的用戶的更改將覆蓋先提交的用戶所作的更改。

  例如,假設兩個用戶,Jisun和Sam,都訪問我們的應用軟件中的一個頁面,這個頁面允許訪問者通過一個GridView控件更新和刪除產品數據。他們都同時點擊GridView控件中的Edit按鈕。Jisun把產品名稱更改為“Chai Tea”並點擊Update按鈕,實質結果是向數據庫發送一個UPDATE語句,它將更新此產品的所有可修改的字段(盡管Jisun實際上只修改了一個字段:ProductName)。

  在這一刻,數據庫中包含有這條產品記錄“Chai Tea”—種類為Beverages、供應商為Exotic Liquids、等該產品的詳細信息。然而,在Sam的屏幕中的GridView裡,當前編輯行裡顯示的產片名稱依舊是“Chai”。在Jisun的更改被提交後片刻,Sam把種類更改為“Condiments”並點擊Update按鈕。這個發送到數據庫的UPDATE語句的結果是將產品名稱更改為“Chai”、CategoryID字段的值是種類Beverages對應的ID,等等。Jisun所作的對產品名稱的更改就被覆蓋了。圖1展示了這些連續的事件。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515077.png

圖 1: 當兩個用戶同時更新一條記錄,則存在一個用戶的更改覆蓋另一個的更改的可能性

  類似地,當兩個用戶同時訪問一個頁面,一個用戶可能更新的事另一個用戶已經刪除的記錄。或者,在一個用戶加載頁面跟他點擊刪除按鈕之間的時間裡,另一個用戶修改了這條記錄的內容。
有下面三中並發控制策略可供選擇:

1.什麼都不做 –如果並發用戶修改的是同一條記錄,讓最後提交的結果生效(默認的行為)
2.開放式並發(Optimistic Concurrency) - 假定並發沖突只是偶爾發生,絕大多數的時候並不會出現; 那麼,當發生一個沖突時,僅僅簡單的告知用戶,他所作的更改不能保存,因為別的用戶已經修改了同一條記錄
3.保守式並發(Pessimistic Concurrency) – 假定並發沖突經常發生,並且用戶不能容忍被告知自己的修改不能保存是由於別人的並發行為;那麼,當一個用戶開始編輯一條記錄,鎖定該記錄,從而防止其他用戶編輯或刪除該記錄,直到他完成並提交自己的更改

  注意:在本節裡,我們不討論保守式並附的例子。保守式並發控制很少使用,因為鎖定如果沒有完全釋放,會妨礙其他用戶進行數據更新。例如,如果一個用戶為了編輯而鎖定某一條記錄,但在解鎖之前就離開了,那麼其他任何用戶都不能更新這條記錄,直到最初的用戶返回並完成他的更新。因此,使用保守式並發控制的地方,相應地會作一個時間限制,如果到達這個時間限制,則取消鎖定。例如訂票網站,當用戶完成他的訂票過程時會鎖定某個特定的座位,這就是一個使用保守式並發控制的例子。

第一步:如何實現開放式並發控制

  開放式並發控制能夠確保一條記錄在更新或者刪除時跟它開始這次更新或修改過程時保持一致。例如,當在一個可編輯的GridView裡點擊編輯按鈕時,該記錄的原始值從數據庫中讀取出來並顯示在TextBox和其他Web控件中。這些原始的值保存在GridView裡。隨後,當用戶完成他的修改並點擊更新按鈕,這些原始值加上修改後的新值發送到業務邏輯層,然後到數據訪問層。數據訪問層必定發出一個SQL語句,它將僅僅更新那些開始編輯時的原始值根數據庫中的值一致的記錄。圖二描述了這些事件發生的順序。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515102.png

圖2: 為了更新或刪除能夠成功,原始值必須與數據庫中相應的值一致

  有多種方法可以實現開放式並發控制(查看Peter A. Bromberg的文章  Optmistic Concurrency Updating Logic,從摘要中看到許多選擇)。ADO.NET類型化數據集提供了一種應用,這只需要在配置時勾選上一個CheckBox。使用開發式並發的目的是使類型化數據集的TableAdapter的UPDATE和DELETE語句可以檢測自該記錄加載到DataSet中以來數據庫中的值是否被更改。例如下面的UPDATE語句,當當前數據庫中的值與GridView中開始編輯的原始值一致才更新某個產品的名稱和價格。@ProductName 和 @UnitPrice參數包含的是用戶輸入的新值,而參數@original_ProductName 和 @original_UnitPrice則包含最初點擊編輯按鈕時加載到GridView中的值: 

UPDATE Products SET
  ProductName = @ProductName,
  UnitPrice = @UnitPrice
WHERE
  ProductID = @original_ProductID AND
  ProductName = @original_ProductName AND
  UnitPrice = @original_UnitPrice

  注意:這個UPDATE語句是為了易讀而簡單化了。實際上,在WHERE子句中檢測UnitPrice會比較棘手,這是因為UnitPrice可以包含空值,而NULL = NULL則總是返回False(相應地你必須用IS NULL)。

  除了使用一個不同的UPDATE語句之外,配置TableAdapter使用開放式並發控制還需要修改它直接發送到數據庫的方法。回到我們的第一節,創建一個數據訪問層,這些發送到數據庫的方法接收一列標量的值作為輸入參數(不僅僅是強類型DataRow或DataTable的實例)。當使用開放式並發,直接發送到數據庫的Update() 和 Delete()方法就包含了對應原始值的輸入參數。而且,業務邏輯層中批量方式更新的代碼(Update()的重載,它不僅接受標量值,也接受DataRows 和 DataTables)也要做出相應的更改。

  與其擴展我們現有得數據訪問層表適配器使用開放式並發(同時也必須修改業務邏輯層以協調),不如讓我們創建一個新的類型化數據集NorthwindOptimisticConcurrency,在它裡面我們添加一個使用開放式並發的Products表適配器。然後,我們將在業務邏輯層中創建類ProductsOptimisticConcurrencyBLL,它為了支持開放式並發的DAL會有適當的更改。一旦這些基礎工作都已完成,我們就可以創建ASP.NET頁面。

第二步: 創建一個支持開放式並發的數據訪問層

  為了創建一個新的類型化數據集,在App_Code文件夾裡的DAL文件夾上右鍵點擊,選擇添加一個新的數據集並命名為NorthwindOptimisticConcurrency。正如我們在第一節中看到過的那樣,系統會自動添加一個表適配器(TableAdapter)到當前的類型化數據集眾,並自動地進入TableAdapter配置向導。在第一屏中,向導提示我們選擇數據庫連接 – 連接到同樣的數據庫Northwind並使用Web.config裡設置好的連接字符串NORTHWNDConnectionString。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515199.png

圖 3: 連接到同一個數據庫Northwind

下一步,向導提示我們選擇如何訪問數據庫:通過一個指定的SQL語句,創建新的存儲過程,或者使用一個現有的存儲過程。既然我們最初的DAL是使用的是指定SQL查詢語句,這裡我們還是使用它。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515128.png

圖4: 使用指定SQL語句的方式訪問數據庫

  下一步,進入查詢分析器,返回產品信息。讓我們使用在最初的DAL中產品TableAdapter相同的SQL查詢,它返回產品的所有字段包括產品的供應商和類別名稱。

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

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515187.png

圖5:使用在最初的DAL中產品TableAdapter相同的SQL查詢 

  在我們進入下一步之前,點擊“高級選項”按鈕。要讓這個TableAdapter使用開放式並發,僅僅需要勾選上“使用開放式並發”。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515180.png

圖6:勾選“使用開放式並發”啟用開放式並發控制

  最後,需要指出的是,該TableAdapter應該同時使用“填充DataTable”和“返回DataTable”兩種要生成的方法;並且,勾選“創建方法以將更新直接發送到數據庫(GenerateDBDirectMethods)”。將返回DataTable的方法名稱從GetData改為GetProducts,使之與我們最初的DAL中的命名規則匹配。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515241.png

圖7:讓這個TableAdapter利用所有的數據訪問方式

  完成了配置向導後,該數據集設計器將包含一個強類型的Products DataTable和TableAdapter。讓我們花些時間把該DataTable的名稱Products改為ProductsOptimisticConcurrency,方法是右鍵點擊DataTable的標題欄,從菜單中選擇“重命名”。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515244.png

圖8:一個DataTable和TableAdapter已經添加到類型化數據集

  為了看看ProductsOptimisticConcurrency TableAdapter(使用開放式並發)和Products TableAdapter(不使用並發控制)的UPDATE 和 DELETE查詢之間有什麼不同,選中該TableAdapter並轉到屬性窗口。在DeleteCommand 和 UpdateCommand 這兩個屬性的 CommandText 子屬性裡,我們可以看到調用DAL的update或者delete關聯的方法時發送到數據庫的實際的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))

  正如你所看到的,啟用了開發式並發的TableAdapter所使用的DELETE語句裡的WHERE子句包含了對表Product每一個字段現有的值與GridView(或者DetailsView,FormView)最後一次加載時的原始值的對比。因為除了ProductID,ProductName, 和Discontinued之外,其他所有字段都可能為NULL值,所以WHERE子句裡還包含了額外的參數以及與NULL值恰當的比較。

  在這一節裡,我們不會在啟用了開放式並發的數據集裡增加其他的DataTable了,因為我們的ASP.NET頁面將僅提供更新和刪除產品信息的功能。然而,我們仍然需要在ProductsOptimisticConcurrency TableAdapter裡添加GetProductByProductID(productID) 方法。

  為了實現這一點,在TableAdapter的標題欄(在Fill和GetProducts方法名的上方)上右鍵並從菜單裡選擇“添加查詢”。這將啟動TableAdapter查詢配置向導。在TableAdapter的最初配置的基礎上,選擇指定SQL語句來創建GetProductByProductID(productID)方法(見圖四)。因為GetProductByProductID(productID)方法返回指定產品的信息,因此需要指定SQL查詢類型為“SELECT(返回行)”。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515399.png

圖9:標記SQL查詢類型為“SELECT(返回行)”

  進入下一步,向導提示我們指定SQL語句,並且與載入TableAdapter默認查詢語句。在現有的查詢語句的基礎上添加WHERE ProductID = @ProductID子句,如圖10:

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515320.png

圖10:在預載入的查詢語句上添加WHERE子句從而返回特定的產品記錄

最後,把生成的方法重命名為FillByProductID和GetProductByProductID。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515434.png

圖11:把生成的方法重命名為FillByProductID和GetProductByProductID

完成這個向導之後,現在這個TableAdapter包含兩個訪問數據的方法:GetProducts(),它返回所有 的產品;和GetProductByProductID(productID),它返回特定的產品。

第三步: 創建一個支持啟用了開放式並發的DAL的業務邏輯層

  我們現有的ProductsBLL類包含批量更新和直接發送數據庫的模式的例子。AddProduct方法和 UpdateProduct重載都使用了批量更新模式,通過一個ProductRow實例發送到TableAdapter的Update方法。另一方面,DeleteProduct方法則使用直接發送到數據庫的模式,調用TableAdapter的Delete(productID)方法。在新的ProductsOptimisticConcurrency TableAdapter裡,發送到數據庫的方法現還要求傳入原始的值。例如,Delete方法

  現在要求十個輸入參數:原始的ProductID、ProductName、SupplierID、CategoryID、QuantityPerUnit、UnitPrice、UnitsInStock、UnitsOnOrder、ReorderLevel和Discontinued。它在發送到數據庫的DELETE語句的WHERE子句裡使用這些額外的輸入參數,僅僅刪除當前數據庫的值與原始值一致的指定記錄。

  使用批量更新模式時,如果標記給TableAdapter的Update使用的方法沒有更改,那麼代碼就需要同時記錄原始值和新的值。然而,與其在我們現有的ProductsBLL類的基礎上試圖使用啟用了開放式並發的DAL,不如讓我們重新創意一個業務邏輯類支持我們新的DAL。在App_Code文件夾下的BLL子文件夾裡,添加一個名為ProductsOptimisticConcurrencyBLL的新類。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515451.png

圖 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屬性標志,它指示Visual Studio把該類包含在ObjectDataSource向導的數據對象下拉列表中。

  類ProductsOptimisticConcurrencyBLL的Adapter屬性提供快速訪問ProductsOptimisticConcurrencyTableAdapter類的一個實例,並和我們最初的BLL類(ProductsBLL、CategoriesBLL等等)相似。最後,方法GetProducts()僅僅是調用DAL的GetProdcuts()方法並返回一個ProductsOptimisticConcurrencyDataTable對象,該對象由對應數據庫裡每一個產品記錄的ProductsOptimisticConcurrencyRow實例組成。

  使用支持開放式並發的發送到數據庫的模式刪除一個產品記錄

  當使用支持開放式並發的DAL發送到數據庫的模式,方法必須傳入新值和原始值。對刪除來說,這沒有新的值,所以僅僅需要傳入原始值。那麼,在我們的BLL裡,我們必須接收所有原始值所為輸入參數。讓ProductsOptimisticConcurrencyBLL類的DeleteProduct方法使用這個發送到數據的方法。這意味著此方法必須接受所有的十個產品數據字段作為輸入參數,並傳送這些參數到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)最後一次加載時的原始值跟用戶點擊刪除按鈕時數據庫中的值不一致, WHERE子句將不能匹配任何數據庫記錄,這就沒有記錄會受到影響。因此,TableAdapter的Delete方法將返回0並且BLL的DeleteProduct方法返回false。

  使用支持開放式並發的批量更新模式修改一個產品記錄

  正如之前注意到的,批量更新模式時用的TableAdapter的Update方法也有同樣的方法聲明為不管是否支持開放式並發。也就是,Update方法可以接受一個DataRow,一批DataRow,一個DataTable,或者一個類型化的數據集。正是因為DataTable在它的DataRow(s)裡保留了從原始值到修改後的值這個變化的軌跡使這成為可能。當DAL生成它的UPDATE語句時,參數@original_ColumnName裝入DataRow中的原始值,反之,參數@ColumnName裝入DataRow中修改後的值。

  在類ProductsBLL(我們最初使用的不支持開放式並發DAL的)裡,當我們使用批量更新模式更新產品信息時,我們的代碼執行的則是按順序執行下列事件:

1.使用TableAdapter的GetProductByProductID(productID)方法讀取當前數據庫中的產品信息到ProductRow實例
2.在第1步裡將新的值賦值到ProductRow實例
3.調用TableAdapter的Update方法,傳入該ProductRow實例

  這一連串的步驟,無論如何都不可能支持開放式並發,因為在第一步中產生的ProductRow是直接從數據庫組裝的,這意味著,DataRow中使用的原始值是當前存在於數據庫中值,而並非開始編輯過程時綁定到GridView的值。相反地,當使用啟用開放式並發的DAL,我們需要修改UpdateProduct方法的重載以使用下面這些步驟:

1.使用TableAdapter的GetProductByProductID(productID)方法讀取當前數據庫中的產品信息到ProductsOptimisticConcurrencyRow實例
2.在第1步裡將原始 值賦值到ProductsOptimisticConcurrencyRow實例
3.調用ProductsOptimisticConcurrencyRow實例的AcceptChanges()方法,這指示DataRow目前這些值是“原始”的值
4.將新 的值賦值到ProductsOptimisticConcurrencyRow實例
5.調用TableAdapter的Update方法,傳入該ProductsOptimisticConcurrencyRow實例

  第1步讀取當前數據庫裡指定產品記錄的所有字段的值。對更新所有 產品字段的UpdateProduct的重載裡,這一步是多余的(因為這些值在第2步中被改寫),而對那些僅僅傳入部分字段值的重載方法來說則是必要的。一旦原始值賦值到ProductsOptimisticConcurrencyRow實例,調用AcceptChanges()方法,這將當前DataRow中的值標記為原始值,這些值將用作UPDATE語句的@original_ColumnNam參數。然後,新的參數值被賦值到ProductsOptimisticConcurrencyRow,最後,調用Update方法,傳入這個DataRow。

  下面這些代碼展示了重載方法UpdateProduct接受所有產品數據字段作為輸入參數。雖然這裡沒有展示,實際上從本節教程下載的ProductsOptimisticConcurrencyBLL類裡還包含了重載方法UpdateProduct,它僅僅接受產品名稱和單價作為輸入參數。

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後,剩下的工作就是創建一個能利用系統中內建的開放式並發邏輯的ASP.NET頁面。特別地,數據 Web 服務器控件(GridView,DetailsView或FormView)必須記住它的原始值,並且ObjectDataSource必須同時傳送這兩套值到業務邏輯層。此外,ASP.NET頁面必須加以配置從而適當地處理並發沖突。

  首先,打開EditInsertDelete文件夾中的OptimisticConcurrency.aspx頁面,添加一個GridView控件到設計器,設置它的ID屬性為ProductsGrid。從GridView的職能標記裡,選擇創建一個新的ObjectDataSource名為ProductsOptimisticConcurrencyDataSource。既然我們希望這個ObjectDataSource使用支持開放式並發的DAL,就把它配置為使用ProductsOptimisticConcurrencyBLL對象。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515443.png

圖 13: 該ObjectDataSource使用ProductsOptimisticConcurrencyBLL對象

  在向導中從下拉列表選擇GetProducts,UpdateProduct,和DeleteProduct方法。對UpdateProduct方法,則使用接受所有產品數據字段的重載。

  配置ObjectDataSource控件的屬性

  完成了向導之後,該ObjectDataSource的聲明標記應該如下:

<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集合包含了對應ProductsOptimisticConcurrencyBLL類的DeleteProduct方法的每一個輸入參數的Parameter實例。同樣地,UpdateParameters集合也包含了對應UpdateProduct每一個輸入參數的Parameter實例。在先前的那些關於數據修改的教程中,我們在這裡都會移除ObjectDataSource的OldValuesParameterFormatString屬性,因為這個屬性需要BLL方法既要求傳入原始值也要求傳入修改後的值。此外,這個屬性還需要對應原始值的輸入參數的名稱。既然我們現在要把原始值傳送到BLL,那就不要 刪除這個屬性。

  注意:OldValuesParameterFormatString屬性的值必須映射到BLL裡接收原始值的輸入參數的名稱。因為我們把這些參數命名為original_productName,original_supplierID, 等等,我們可以讓OldValuesParameterFormatString屬性的值依舊是original_{0}。然而如果BLL方法的輸入參數名為的old_productName,old_supplierID等等,那麼,你不得不把OldValuesParameterFormatString屬性的值改為old_{0}。為了ObjectDataSource能夠正確地將原始值傳送到BLL方法,還有最後一個屬性需要設置。ObjectDataSource有一個 ConflictDetection屬性,它可以設定為下面的 下面兩個值之一:  

 OverwriteChanges – 默認值; 不將原始值發送到BLL方法相應的輸入參數       

 CompareAllValues – 將原始值發送到BLL方法;當使用開放式並發時使用這一項

  稍花些時間將ConflictDetection屬性設置為CompareAllValues。配置GridView的屬性和字段當正確的配置完ObjectDataSource的屬性後,讓我們把注意力放在GridView的設置上。首先,因為我們希望GridView支持編輯和刪除,因此,從GridView的智能標記中點擊添加新列,從下拉列表中選擇CommandField並勾選上“刪除”和“編輯/更新”。這將增加一個CommandField,它的ShowEditButton和ShowDeleteButton屬性都已設置為true。當綁定ProductsOptimisticConcurrencyDataSource ObjectDataSource,該GridView對應每一個產品數據字段都包含一列。

  雖然這樣的一個GridView可以被編輯,但用戶的體驗將是不可接受的。這沒有對數字欄作格式化處理,也沒有validation控件以確保提供product's name並且unit price、units in stock、units on order、和reorder level的值都是大於零的數字。

  跟我們在之前的給編輯和新增界面增加驗證控件 這一節裡所論述的一樣,用戶界面可以通過將綁定列(BoundFields)替換為模板列(TemplateFields)實現自定義。我已經通過以下方式修改了這個GridView和它的編輯界面:

1.刪除ProductID、SupplierName、和CategoryName這幾個綁定列;
2.將ProductName綁定列替換為模板列並添加一個RequiredFieldValidation控件;
3.將CategoryID和SupplierID綁定列替換為模板列,並調整編輯界面,使用DropDownList而不是TextBox。在這些模板列的ItemTemplates裡,顯示CategoryName和SupplierName字段;
4.將UnitPrice、UnitsInStock、UnitsOnOrder、和ReorderLevel綁定列替換為模板列並添加CompareValidator控件。因為我們在之前的章節裡已經詳細說明了如何完成這些任務,我僅僅把最終的聲明語法列出並把具體執行留給讀者作為練習。

<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>

  我們已經非常接近於完成一個完整的例子。然而,還有一些細節問題需要我們慢慢解決。另外,我們還需要一些界面,當發生並發沖突時用來提示用戶。

  注意: 為了讓數據Web服務器控件能夠正確地把原始的值傳送到ObjectDataSource(它隨之將其發送到BLL),將GirdView的EnableViewState屬性設置為true(默認值)是至關重要的。如果禁用了視圖狀態,這些原始值在postback的時候將會丟失。

  傳送正確的原始值到ObjectDataSource完成了GridView的配置,還有幾個問題。如果這個ObjectDataSource的ConflictDetection 屬性設置為CompareAllValues (正如我們所做的),它會嘗試復制GridView的原始值到它的Parameter實例。回到圖2查看這個過程的圖解。

  特別需要指出的是,這個GridView的原始值是被指定為雙向綁定的。因此,這些必需的原始值是通過雙向綁定獲取的,並且它們是規定為可改變的格式,這一點很重要。為了看看為什麼這一點非常重要,花些時間通過浏覽器訪問我們的頁面。正如所預料那樣,GridView列出每一個產品,並且每行最左邊的一列都顯示編輯和刪除按鈕。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515413.png

圖14: GridView列出所有的產品信息

  如果你點擊任意一行的刪除按鈕,則拋出一個FormatException異常。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515472.png

圖15: 嘗試刪除任意一個產品導致FormatException異常

  當ObjectDataSource試圖讀取原始的UnitPrice值引發了一個FormatException異常。因為該模板列將UnitPrice的值限制為貨幣格式(<%# Bind("UnitPrice", "{0:C}") %>),它包含一個貨幣符號,例如$19.95。該FormatException異常發生在ObjectDataSource試圖將字符產轉換成小數。為了繞過此問題,我們有許多種選擇:

1.從模板列裡刪除貨幣格式限制。就是說,取代<%# Bind("UnitPrice", "{0:C}") %>,簡單地使用<%# Bind("UnitPrice") %>。下方的價格就是沒有格式化的。
2.在模板列中顯示UnitPrice時格式化為貨幣,但是使用Eval關鍵字實現綁定。記得Eval是實現單向綁定的。我們仍然需要提供UnitPrice的值作為原始的值,因此在模板列裡我們依舊需要一個雙向綁定的聲明,但這可以放在一個Visible屬性設置為false的Label服務器控件裡。在模板列裡我們可以使用下面的標記:

<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服務器控件並設置其Text屬性為格式化的版本。
4.讓UnitPrice保留貨幣格式化。在GridView的RowDeleting事件處理裡,將現存的UnitPrice的原始($19.95)替換為實際的小數值(使用Decimal.Parse)。在前面的 在ASP.NET頁面中處理BLL/DAL異常這一節教程裡我們也已經看過如何RowUpdating事件處理裡實現類似的功能。 在我的例程裡我選擇第二種方法,添加一個隱藏的Label服務器控件,並將它的Text屬性雙向綁定到無格式的UnitPrice值。解決了這個問題之後,再次點擊任意一個產品的刪除按鈕。這一次,當ObjectDataSource嘗試調用BLL的UpdateProduct方法時我們得到一個InvalidOperationException異常。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515501.png

圖 16: ObjectDataSource找不到具有它要發送的輸入參數的方法

  仔細看看異常信息,明顯地ObjectDataSource希望調用一個BLL的DeleteProduct方法,此方法包含original_CategoryName和original_SupplierName輸入參數。這是因為CategoryID和SupplierID模板列的ItemTemplate當前是雙向綁定到CategoryName和SupplierName數據字段。作為替換,我們需要包含對CategoryID和SupplierID數據字段的Bind聲明。為了實現這一點,把現有的Bind聲明更改為Eval聲明,並且添加隱藏的Label服務器控件,這些Label的Text屬性使用雙向綁定的方式綁定到CategoryID和SupplierID數據字段,如下所示:

<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>

  通過這些更改,現在我們可以成功地刪除和編輯產品信息了!在第五步裡,我們將看看如何驗證刪除時發生並發沖突。但是現在,花幾分鐘嘗試更新和刪除一些記錄,確認在單用戶的情況下更新和刪除能夠正常運作。

第五步: 測試開放式並發支持

  為了驗證並發沖突是否能夠被發現(而不是導致數據被盲目改寫),我們需要打開兩個浏覽器窗口來訪問這個頁面。在兩個浏覽窗口裡,都點擊產品“Chai”的編輯按鈕。然後,在其中一個窗口修改其名稱為“Chai Tea”並點擊更新。這個更新應該會成功並且GridView回到預編輯狀態,並且該產品的名稱已經改為“Chai Tea”。
而在另一個浏覽器窗口裡,產品名稱域依舊顯示的是“Chai”。在這個浏覽器窗口,將UnitPrice的值更新為25.00。如果沒有開放式並發支持的話,點擊第二個浏覽器窗口的更新按鈕將把產品名稱改回“Chai”,從而覆蓋了第一個浏覽器窗口裡所作的修改。然而現在有了開發式並發,當點擊第二個窗口中的更新按鈕時導致了一個DBConcurrencyException異常。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515588.png

圖 17: 發現並發沖突,拋出一個DBConcurrencyException異常

  這個DBConcurrencyException異常僅當利用DAL的批量更新模式時會被拋出。直接發送到數據庫的模式則不會引發異常,它僅僅會提示沒有行受到影響。為了舉例說明這個,兩個浏覽器窗口裡的GridView都回到預編輯的狀態。然後,在第一個窗口裡,點擊編輯按鈕,把產品名稱從“Chai”改為“Chai Tea”並點擊更新。在第二個窗口裡,點擊產品“Chai”的刪除按鈕。點擊刪除按鈕,頁面會傳,GridView調用ObjectDataSource的Delete()方法,然後ObjectDataSource調用ProductsOptimisticConcurrencyBLL類的DeleteProduct方法,傳入原始的值。在第二個浏覽器窗口裡原始的ProductName值是“Chai Tea”,這個值與當前數據庫中相應的ProductName值是不一致的。因此,發送到數據庫的DELETE語句影響0行,因為數據庫中沒有記錄能夠滿足WHERE子句。DeleteProduct方法返回false並且ObjectDataSource的數據重新綁定到GridView控件。

  從最後一個用戶的觀點來看,在第二個浏覽器窗口裡點擊了產品“Chai Tea”的刪除按鈕導致屏幕閃爍,恢復後該產品依舊在,雖然現在它的名稱是“Chai”(在第一個浏覽器窗口裡修改了產品名稱)。如果用戶再次點擊刪除按鈕,這次就能成功刪除,因為GridView的原始的ProductName值(“Chai”)現在能夠與數據庫中相應的值匹配。在這些例子裡,用戶的體驗跟理想的狀況還有頗遠的距離。顯然我們在使用批量更新模式時不希望用戶看到DBConcurrencyException異常生硬的詳細信息。並且使用直接發送到數據庫模式的行為也會讓用戶有些疑惑,因為用戶操作失敗了但是沒有准確的提示說明為什麼。

  為了補救這兩個小問題,我們可以在頁面上放置一個Label服務器控件,它用來提供為什麼更新或刪除失敗的說明。在批量更新模式,我們可以在GridView的post級事件處理裡判定是否引發了一個DBConcurrencyException異常,顯示必要的警告標簽。對於直接發送到數據庫的方法,我們可以檢測BLL方法(它對一行或多行產生影響返回true,否則false)的返回值並顯示必要的提示信息。

第六步: 添加提示信息並且在發生並發沖突時顯示

  當一個並發沖突出現時,展現出來的行為取決於是使用DAL的批量更新還是直接發送到數據庫的模式。我們這一節的教程兩種模式都用了,用批量更新模式實現修改,用直接發送到數據庫的方式實現刪除。首先,我們添加兩個Label服務器控件到頁面,它們用來解釋更新或刪除數據時出現的並發沖突。設置Label控件的Visible和EnableViewState屬性為false;這意味一般情況下它們都是隱藏的,除非是那些特別的頁面訪問,在那裡它們的Visible屬性通過編碼設置為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." />

  在設置了它們的Visible、EnabledViewState和Text屬性之外,我們還要把CssClass屬性設置為Warning,這讓標簽顯示大的、紅色的、斜體、加粗的字體。這個CSS Warning 分類是在研究插入、更新和刪除的關聯事件這一節裡添加到Styles.css並且定義好的。添加了這些標簽之後,Visual Studio設計器裡看起來應該類似於圖18:

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515535.png

圖 18: 兩個Label控件添加到頁面

  這些Label服務器控件放置到適當的位置後,我們准備好檢測當並發沖突發生時如何判定,在哪個時間點把適當的Label的Visible屬性設置為true並顯示提示信息。

  更新時處理並發沖突

  讓我們首先看看當使用批量更新模式是如何處理並發沖突。因為批量更新模式下的這些沖突導致拋出一個DBConcurrencyException異常,我們需要在ASP.NET頁面中添加代碼來判定更新過程中出現的是否DBConcurrencyException異常。如果是,我們則顯示一個信息向用戶解釋他們的更改沒有被保存,由於別的用戶在他開始編輯和點擊更新按鈕之間的時間裡修改了同樣的數據記錄。

  正如我們在在ASP.NET頁面中處理BLL/DAL異常 這一節裡看過的那樣,這樣的異常可以在數據Web服務器控件的post級事件處理裡被發現和排除。因此,我們需要創建一個GridView的RowUpdated事件的處理,它用來檢測是否拋出了一個DBConcurrencyException異常。這個事件處理通過一個不同的分支區別更新過程中引發的其它異常,如下面的時間處理代碼所示:

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;
    }
  }
}

  面對一個DBConcurrencyException異常,該事件處理顯示UpdateConflictMessage Label控件並且指出該異常已經被處理。正確地編寫了這些代碼後,當更新記錄時發生了並發沖突,用戶的更改會丟失,因為他們不能覆蓋同時發生的另一個用戶的更改。特別地,GridView回到預編輯幢白並且綁定到當前數據庫中數據。這將在GridView的行中顯示出別的用戶的更改,而之前這些更改是看不見的。另外,UpdateConflictMessage Label控件將向用戶說明發生了什麼。圖19詳細展示了這一連串的事件。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515558.png

圖 19: 面對並發沖突,一個用戶的更改丟失了

  注意:作為另一種選擇,與其讓GridView回到預編輯狀態,我們還不如讓GridView停留在編輯狀態,通過設置傳入的GridViewUpdatedEventArgs對象的KeepInEditMode屬性為true。如果你接受這種方法,那麼,必須重新綁定數據到GridView(通過調用它的DataBind()方法)從而將其他用戶更改後的值栽入到編輯界面。在這一節的可下載的代碼裡,RowUpdated事件處理裡有這兩行注悉掉的代碼;僅僅需要啟用這兩行代碼就可以讓GridView在發生了並發沖突之後保留編輯模式。

  響應刪除時的並發沖突

  對於直接發送到數據庫的模式,面對並發沖突時並不會引發異常。然而,數據庫語句不影響任何記錄,因為WHERE子句不能匹配任何記錄。所有在BLL裡創建的修改數據的方法都被設計為返回一個布爾值指示它們是否正好影響了一條記錄。因此,為了確定刪除記錄時是否發生了並發沖突,我們可以檢查BLL的DeleteProduct方法的返回值。

  BLL方法的返回值可以在ObjectDataSource的post級事件處理中通過傳入事件處理的ObjectDataSourceStatusEventArgs對象的ReturnValue屬性被檢測。因為我們感興趣的是判斷從DeleteProduct方法返回的結果,我們需要創建一個ObjectDataSource的Deleted事件的事件處理程序。該ReturnValue屬性是object類型的,並且如果在方法可以返回一個值之前引發了異常並且方法被中斷的情況下,它的值也可能為null。所以,我們應該首先確保ReturnValue屬性非空並是個布爾值。若能通過這個檢查,如果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;
    }
  }
}

  面對一個並發沖突,用戶的刪除請求會被取消。GridView被刷新,顯示在用戶載入頁面跟點擊刪除按鈕之間的時間裡發生在該記錄上面的更改。當發生這樣的一個沖突,顯示DeleteConflictMessage Label控件,說明發生了什麼(見圖20)。

https://www.aspphp.online/bianchen/UploadFiles_4619/201701/2017010916515574.png

圖 20: 面對並發沖突,一個用戶的刪除請求被取消了

總結

  並發沖突可能存在於所有允許多用戶同時更新或刪除數據的應用程序裡。如果不解決這樣的沖突,當兩個用戶同時更新同一條數據,無論誰最後得到“勝利”,都將覆蓋掉另一個用戶所做的更改。作為另一種選擇,開發者可以實現開放式並發控制(optimistic concurrency control),或者保守式並發控制(pessimistic concurrency control)。開放式並發控制假定並發沖突很少發生,簡單地否決一個會提起並發沖突的更新或者刪除命名。保守式並發控制則假定並發沖突頻繁地發生,簡單地拒絕某個用戶的更新或者刪除命令是不可接受的。在保守式並發控制下,編輯一條記錄涉及到鎖定它,從而該記錄被鎖定時預防其他用戶的修改或刪除。

  .NET中的類型化數據集提供了支持開放式並發控制的功能。特別地,發送到數據庫的UPDATE和DELETE語句包含了這個表的所有字段,從而確保了僅當該記錄但前的值與用戶開始他們的修改或更新時的原始值相匹配時,修改或刪除才會發生。一旦DAL配置為支持開放式並發,BLL的方法就需要修改。另外,調用BLL的ASP.NET頁面也需要配置為ObjectDataSource能從它的數據Web服務器控件獲取到這些原始的值並將這些值傳送到BLL。

  正如我們在本節裡所看到的,在ASP.NET web應用程序中實現開放式並發控制包括修改DAL和BLL,還包括在ASP.NET頁面中添加相應的支持。無論這些額外的工作對你的時間來說是否一項明智的投入,對你的應用程序來說是否有所成效。如果你極少面對多個用戶同時更新數據,或者不同的用戶對數據作出不同的更改,那麼並發控制並非必選項。然而,如果你時常面對多個用戶在線並且對同一些數據進行操作,並發控制可以幫助預防一個用戶的更新或刪除被另一個用戶在不知情的情況下覆蓋。

祝編程快樂!

作者簡介

Scott Mitchell,著有六本ASP/ASP.NET方面的書,是4GuysFromRolla.com的創始人,自1998年以來一直應用微軟Web技術。Scott是個獨立的技 術咨詢顧問,培訓師,作家,最近完成了將由Sams出版社出版的新作,24小時內精通ASP.NET 2.0。他的聯系電郵為[email protected],也可以通過他的博客http://ScottOnWriting.NET與他聯系。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved