共用方式為


實作樂觀並發控制 (C#)

斯科特·米切爾

下載 PDF

對於允許多個使用者編輯數據的 Web 應用程式,有兩個使用者可能同時編輯相同數據的風險。 在本教學課程中,我們將實作樂觀並行控制來處理此風險。

簡介

對於只允許使用者檢視數據的 Web 應用程式,或只包含可修改數據的單一使用者,則不會有兩個並行使用者不小心覆寫彼此變更的威脅。 不過,對於允許多個使用者更新或刪除數據的 Web 應用程式,可能會有一位使用者的修改與另一個並行用戶的衝突。 若沒有執行任何並行處理政策,當兩位用戶同時編輯單一記錄時,最後提交變更的使用者將會覆蓋第一位的變更。

例如,假設兩個使用者 Jisun 和 Sam 都流覽了應用程式中的頁面,可讓訪客透過 GridView 控制項更新和刪除產品。 兩者都在同一時間按兩下 GridView 中的 [編輯] 按鈕。 Jisun 會將產品名稱變更為 “Chai Tea”,然後按兩下 [更新] 按鈕。 淨結果是傳送至資料庫的語句,該語句會設定產品的所有可更新欄位,即便 Jisun 只更新了一個欄位。 此時,資料庫中此特定產品的值包括「印度奶茶」、類別為飲料、供應商為 Exotic Liquids,等等。 不過,Sam 畫面上的 GridView 仍然會將可編輯的 GridView 數據列中的產品名稱顯示為 “Chai”。 在 Jisun 的變更被認可的幾秒後,Sam 將類別更新為 [Condiments],然後按一下 [更新]。 這會導致 UPDATE 傳送至資料庫的語句,將產品名稱設定為 「Chai」 CategoryID 、對應飲料類別識別碼等等。 Jisun 對產品名稱所做的變更已被覆寫。 圖 1 以圖形方式描述這一系列事件。

當兩位使用者同時更新記錄時,可能會有一位的變更覆蓋另一位的修改

圖 1:當兩位使用者同時更新記錄時,可能會有一位使用者的變更覆寫另一位使用者的變更(點擊以檢視完整大小的影像

同樣地,當兩位使用者瀏覽頁面時,一位使用者可能會在另一位使用者刪除記錄時更新記錄。 或者,當使用者載入頁面和按下 [刪除] 按鈕時,其他使用者可能已修改該記錄的內容。

有三種可用的 並行控制 策略:

  • 不執行任何動作 -if 同時使用者正在修改相同的記錄,讓最後提交的修改成功(預設行為)
  • 樂觀並發控制 - 假設雖然偶爾會發生並行衝突,但大部分時間不會發生這類衝突;因此,如果發生衝突,只需通知使用者無法儲存其變更,因為其他使用者已修改相同的數據。
  • 悲觀並行控制 - 假設並行衝突是司空見慣的,而且使用者不會容忍因為其他使用者的並行活動而無法儲存變更;因此,當用戶開始更新記錄時,將其鎖定,以防止其他使用者在此使用者提交修改之前編輯或刪除該記錄。

到目前為止,我們所有的教學課程都使用了預設並行解析策略,也就是採用最後寫入優先。 在本教學中,我們將探討如何實作樂觀並發控制。

備註

我們不會在此教學課程系列中查看悲觀並行範例。 很少使用悲觀並行控制,因為這類鎖定若未正確放棄,可能會防止其他使用者更新數據。 例如,如果使用者鎖定記錄進行編輯,然後在解除鎖定前一天離開,其他使用者將無法更新該記錄,直到原始用戶傳回並完成更新。 因此,在使用悲觀並行的情況下,通常會設置超時機制,如果超過時間限制,就會取消鎖定機制。 票證銷售網站會在使用者完成訂單時,暫時鎖定特定座位位置,這是悲觀並行控制的範例。

步驟 1:查看如何實作樂觀並發控管

樂觀並發控制透過確保被更新或刪除的記錄在程序啟動時維持相同的值來運作。 例如,按兩下可編輯 GridView 中的 [編輯] 按鈕時,記錄的值會從資料庫讀取,並顯示在 TextBoxes 和其他 Web 控制件中。 GridView 會儲存這些原始值。 稍後,在用戶進行變更並按下 [更新] 按鈕之後,原始值加上新值會傳送至商業規則層,然後向下傳送至數據存取層。 數據存取層必須發出 SQL 語句,只有在用戶開始編輯的原始值與資料庫中的值相同時,才會更新記錄。 圖 2 描述此事件序列。

若要讓更新或刪除成功,原始值必須等於目前的資料庫值

圖 2:若要讓更新或刪除成功,原始值必須等於目前的資料庫值(按兩下即可檢視完整大小的影像

實作樂觀並發控制的方法有很多種(請參閱 Peter A. Bromberg樂觀並發更新邏輯,以簡要了解多種選項)。 ADO.NET 具型別數據集提供一個實作,只需勾選複選框即可輕鬆配置。 在 Typed DataSet 中啟用 TableAdapter 的樂觀並發控制會增強 TableAdapter 的 UPDATEDELETE 語句,在 WHERE 子句中包含對所有原始值的比較。 例如,下列 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 語句已簡化,以方便閱讀。 在實務上,UnitPrice 子句中的 WHERE 檢查會更加複雜,因為 UnitPrice 可能包含 NULL,需要檢查 NULL = NULL 是否總是傳回 False(而是您必須使用 IS NULL)。

除了使用不同的基礎 UPDATE 語句之外,將 TableAdapter 設定為使用樂觀並行也會修改其 DB 直接方法的簽名。 回想一下我們的第 一個教學課程:建立數據存取層,DB 直接方法是接受純量值清單做為輸入參數(而不是強型別的 DataRow 或 DataTable 實例)。 使用樂觀併發控制時,DB 直接 Update()Delete() 方法也會包含原始值的輸入參數。 此外,BLL 中用於使用批次更新模式的程式代碼( Update() 接受 DataRows 和 DataTable 而非純量值的方法多載)也必須變更。

而不是擴充現有的 DAL 的 TableAdapters 來使用樂觀並發控制(這需要變更 BLL 以容納),讓我們改為建立名為 NorthwindOptimisticConcurrency 的新具型別數據集,然後在其中新增一個使用樂觀並發控制的 Products TableAdapter。 接下來,我們將建立ProductsOptimisticConcurrencyBLL 業務邏輯層類別,並進行適當的修改以支援樂觀並發 DAL。 一旦奠定這個基礎,我們將準備好建立 ASP.NET 頁面。

步驟 2:建立支持開放式並行存取的數據存取層

若要建立新的具型別數據集,請以滑鼠右鍵按兩下 DAL 資料夾內的 App_Code 資料夾,然後新增名為 NorthwindOptimisticConcurrency的新 DataSet。 如我們在第一個教學課程中所見,這麼做會將新的 TableAdapter 新增至具類型的數據集,並自動啟動 TableAdapter 設定精靈。 在第一個畫面中,系統會提示您指定要連線的資料庫 - 使用 NORTHWNDConnectionString 設定從 Web.config連接到相同的 Northwind 資料庫。

連接到相同的 Northwind 資料庫

圖 3:連線到相同的 Northwind 資料庫 (按兩下以檢視完整大小的影像

接下來,我們會提示您如何查詢數據:透過臨機作 SQL 語句、新的預存程式或現有的預存程式。 由於我們在原始 DAL 中使用臨機作 SQL 查詢,因此也在這裡使用此選項。

指定要使用臨機作 SQL 語句擷取的數據

圖 4:指定要使用臨機作 SQL 語句擷取的數據(按兩下以檢視完整大小的影像

在下列畫面上,輸入用來擷取產品資訊的SQL查詢。 讓我們使用原始 DAL 中用於 Products TableAdapter 的完全相同的 SQL 查詢來傳回所有 Product 欄位,以及產品的供應商和類別名稱。

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

在原始 DAL 中使用來自 Products TableAdapter 的相同 SQL 查詢

圖 5:在原始 DAL 中使用 Products TableAdapter 的相同 SQL 查詢(點擊查看完整大小的影像

移至下一個畫面之前,請按兩下 [進階選項] 按鈕。 若要讓此 TableAdapter 採用樂觀並行存取控制,只需核取 [使用樂觀並行存取] 核取方塊即可。

勾選「使用樂觀並行」核取方塊以啟用樂觀並行控制

圖 6:勾選 [使用樂觀並行控制] 核取方塊來啟用樂觀並行控制(點擊以查看完整大小的影像

最後,指定 TableAdapter 應使用能填滿並返回 DataTable 的資料存取模式;同時,應創建具備資料庫直接存取功能的方法。 將傳回 DataTable 模式的方法名稱從 GetData 變更為 GetProducts,以反映我們在原始 DAL 中使用的命名慣例。

讓 TableAdapter 利用所有數據存取模式

圖 7:讓 TableAdapter 利用所有資料存取模式 (按兩下以檢視完整大小的影像

完成精靈之後,DataSet 設計工具會包含強類型的 Products DataTable 和 TableAdapter。 花點時間將 DataTable 從 Products 重新命名為 ProductsOptimisticConcurrency,您可以在 DataTable 的標題列上右鍵點擊,然後從快捷選單選擇重新命名。

已將 DataTable 和 TableAdapter 新增到類型化資料集

圖 8: DataTable 和 TableAdapter 已新增至型別化資料集(點擊以查看完整大小的影像

若要查看使用樂觀並行的UPDATE TableAdapter與不使用的Products TableAdapter之間,以及DELETEProductsOptimisticConcurrency查詢之間的差異,請在TableAdapter上按一下,然後移至屬性視窗。 在 DeleteCommandUpdateCommand 屬性的 CommandText 子屬性中,您可以看到叫用 DAL 更新或刪除相關方法時,傳送至資料庫的實際 SQL 語法。 關於 TableAdapter,所使用的語句為:ProductsOptimisticConcurrency

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

DELETE而原始 DAL 中 Product TableAdapter 的語句則更簡單:

DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

如您所見,使用樂觀並行的 TableAdapter 語句中的 WHERE 子句包含每個 DELETE 資料表現有欄位值與 GridView(或 DetailsView 或 FormView)上次填入時的原始值之間的比較。 由於除了ProductIDProductNameDiscontinued以外的所有欄位都可以有NULL值,因此納入了額外的參數和檢查,以便在NULL子句中正確比較WHERE值。

在這個教學中,我們不會將任何其他 DataTable 新增至啟用了樂觀並發控制的 DataSet,因為我們的 ASP.NET 頁面只會提供更新和刪除產品資訊。 不過,我們仍然需要將 GetProductByProductID(productID) 方法新增至 ProductsOptimisticConcurrency TableAdapter。

若要完成此操作,請以滑鼠右鍵按下 TableAdapter 的標題列(位於 FillGetProducts 方法名稱上方的區域),然後從快捷選單中選擇 [新增查詢]。 這會啟動 TableAdapter 查詢設定精靈。 如同 TableAdapter 的初始設定,選擇使用臨機作 SQL 語句來建立 GetProductByProductID(productID) 方法(請參閱圖 4)。 GetProductByProductID(productID)由於方法會傳回特定產品的相關信息,因此表示此查詢是SELECT傳回數據列的查詢類型。

將查詢類型標示為「傳回數據列的 SELECT」

圖 9:將查詢類型標示為「SELECT 傳回數據列」(按兩下以檢視完整大小的影像

在下一個畫面中,我們會被提示選擇要使用的 SQL 查詢,並且 TableAdapter 的預設查詢已預先載入。 擴大現有的查詢以包含 子句 WHERE ProductID = @ProductID,如圖 10 所示。

將 WHERE 子句新增至預先載入的查詢,以傳回特定的產品記錄

圖 10:新增子句 WHERE 至預先載入的查詢以傳回特定的產品紀錄(點擊以查看完整大小的影像

最後,將產生的方法名稱變更為 FillByProductIDGetProductByProductID

將方法重新命名為 FillByProductID 和 GetProductByProductID

圖 11:將方法重新命名為 FillByProductIDGetProductByProductID按兩下以檢視完整大小的影像

完成此精靈之後,TableAdapter 現在包含兩個擷取數據的方法: GetProducts()會傳回 所有 產品;而 GetProductByProductID(productID)會傳回指定的產品。

步驟 3:為樂觀式 Concurrency-Enabled DAL 建立商業邏輯層

我們現有的 ProductsBLL 類別有使用批次更新和 DB 直接模式的範例。 AddProduct方法和UpdateProduct多載都會使用批次更新模式,將 ProductRow 實例傳入 TableAdapter 的 Update 方法。 另一方面,方法 DeleteProduct 會使用 DB 直接模式,呼叫 TableAdapter 的 Delete(productID) 方法。

有了新的 ProductsOptimisticConcurrency TableAdapter,DB 直接方法現在會要求也傳入原始值。 例如,Delete方法現在需要十個輸入參數:原始ProductIDProductNameSupplierIDCategoryIDQuantityPerUnitUnitPriceUnitsInStockUnitsOnOrderReorderLevelDiscontinued。 它會在傳送至資料庫的 語句 子句WHERE中使用DELETE這些額外的輸入參數值,只有在資料庫的目前值對應至原始值時,才刪除指定的記錄。

雖然批次更新模式中使用的 TableAdapter Update 方法簽名尚未變更,但記錄原始和新值所需的程式代碼已改變。 因此,我們應該建立一個新的業務邏輯層類別來使用新的 DAL,而不是試圖與現有的 ProductsBLL 類別一起使用已啟用樂觀並行的 DAL。

將名為 ProductsOptimisticConcurrencyBLL 的類別新增至 BLL 資料夾內的 App_Code 資料夾。

將 ProductsOptimisticConcurrencyBLL 類別新增至 BLL 資料夾

圖 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 精靈的下拉式清單中包含此類別。

ProductsOptimisticConcurrencyBLLAdapter 屬性可讓您快速存取 類別的ProductsOptimisticConcurrencyTableAdapter實例,並遵循原始 BLL 類別 (ProductsBLLCategoriesBLL等等) 中使用的模式。 最後,GetProducts() 方法會深入呼叫 DAL 的 GetProducts() 方法,並傳回一個 ProductsOptimisticConcurrencyDataTable 物件,該物件中包含資料庫中每個產品記錄的 ProductsOptimisticConcurrencyRow 實例。

使用具有樂觀並行控制的資料庫直接模式移除產品

針對使用樂觀併發控制的 DAL 使用 DB 直接模式時,方法必須傳遞新的值和原始值。 若要刪除,沒有新的值,因此只需要傳入原始值。 在 BLL 中,我們必須接受所有原始參數作為輸入參數。 讓我們讓 DeleteProductProductsOptimisticConcurrencyBLL 類別中的方法使用 DB 直接方法。 這表示此方法必須採用這十個產品數據欄位作為輸入參數,並將這些專案傳遞至 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、DataRows 陣列、DataTable 或具類型的數據集。 沒有額外的輸入參數可指定原始值。 這是可能的,因為 DataTable 會追蹤其 DataRow 的原始和修改值。 當 DAL 發出其 UPDATE 語句時, @original_ColumnName 參數會填入 DataRow 的原始值,而 @ColumnName 參數則會填入 DataRow 的修改值。

ProductsBLL 類別中(使用我們原始的、非樂觀並行的 DAL),當使用批次更新模式來更新產品資訊時,我們的程式碼會執行以下事件順序:

  1. 使用 TableAdapter 的 ProductRow 方法,將目前的資料庫產品資訊讀入GetProductByProductID(productID)實例
  2. 將新值指派給步驟 1 中的 ProductRow 實例
  3. 呼叫 TableAdapter 的 Update 方法,傳入 ProductRow 實例

然而,這個步驟順序不會正確支持樂觀併發,因為 在步驟 1 中填入的 ProductRow 是直接從資料庫填入的,這意味著 DataRow 使用的原始值是資料庫中目前存在的值,而不是在編輯過程一開始系結至 GridView 的原始值。 相反地,使用啟用了樂觀併發控制的 DAL 時,我們需要改變 UpdateProduct 方法重載以使用下列步驟:

  1. 使用 TableAdapter 的 ProductsOptimisticConcurrencyRow 方法,將目前的資料庫產品資訊讀入GetProductByProductID(productID)實例
  2. 原始 值指派給步驟 1 中的 ProductsOptimisticConcurrencyRow 實例
  3. 呼叫 ProductsOptimisticConcurrencyRow 實例的 AcceptChanges() 方法,此方法指示 DataRow 其目前的值被視為「原始」值。
  4. 值指派給 ProductsOptimisticConcurrencyRow 實例
  5. 呼叫 TableAdapter 的 Update 方法,傳入 ProductsOptimisticConcurrencyRow 實例

步驟 1 會讀取指定產品記錄的所有目前資料庫值。 這個步驟在更新所有的UpdateProduct產品欄位的多載中是多餘的(因為步驟 2 會覆寫這些值),但對於只有一部分欄位值作為輸入參數傳入的多載而言,這是不可或缺的。 將原始值指派給 ProductsOptimisticConcurrencyRow 實例之後,AcceptChanges()會呼叫 方法,將目前的 DataRow 值標示為語句中@original_ColumnName參數中UPDATE要使用的原始值。 接下來,會將新的參數值指派給 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;
}

步驟 4:將原始和新的值從 ASP.NET 頁面傳遞至 BLL 方法

當 DAL 和 BLL 完成後,剩下的就是建立一個 ASP.NET 頁面,以利用系統中內建的樂觀並行控制邏輯。 具體而言,數據 Web 控件(GridView、DetailsView 或 FormView)必須記住其原始值,而且 ObjectDataSource 必須將這兩組值傳遞至商業規則層。 此外,必須將 ASP.NET 頁面設定為能妥善處理並行性衝突。

首先,開啟 OptimisticConcurrency.aspx 資料夾中的 EditInsertDelete 頁面,然後將 GridView 新增至設計工具,並將其 ID 屬性設定為 ProductsGrid。 從 GridView 的智慧標記中,選擇建立名為 ProductsOptimisticConcurrencyDataSource的新 ObjectDataSource。 由於我們想要此 ObjectDataSource 使用支援開放式並行存取的 DAL,因此請將它設定為使用 ProductsOptimisticConcurrencyBLL 物件。

讓 ObjectDataSource 使用 ProductsOptimisticConcurrencyBLL 物件

圖 13:讓 ObjectDataSource 使用 ProductsOptimisticConcurrencyBLL 物件 (按兩下即可檢視完整大小的影像

從精靈程式中的下拉式清單中選擇 GetProductsUpdateProductDeleteProduct 方法。 針對 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>

如您所見,類別DeleteParametersParameter方法中,每個輸入的十個ProductsOptimisticConcurrencyBLL參數都有一個DeleteProduct實例集合。 同樣地, UpdateParameters 集合也包含 ParameterUpdateProduct每個輸入參數的實例。

針對涉及數據修改的先前教學課程,我們此時會移除 ObjectDataSource 的 OldValuesParameterFormatString 屬性,因為此屬性表示 BLL 方法預期會傳入舊的 (或原始) 值,以及新的值。 此外,這個屬性值表示原始值的輸入參數名稱。 由於我們會將原始值傳入 BLL, 因此請勿 移除此屬性。

備註

屬性的值 OldValuesParameterFormatString 必須對應至 BLL 中預期原始值的輸入參數名稱。 由於我們將這些參數 original_productName命名為、 original_supplierID等等,因此您可以將 屬性值保留 OldValuesParameterFormatStringoriginal_{0}。 不過,如果 BLL 方法的輸入參數具有、 old_productNameold_supplierID等名稱,則必須將 OldValuesParameterFormatString 屬性更新為 old_{0}

必須進行最後一個屬性設定,才能讓 ObjectDataSource 正確地將原始值傳遞至 BLL 方法。 ObjectDataSource 具有 ConflictDetection 属性 ,可指派給 兩個值之一

  • OverwriteChanges - 預設值;不會將原始值傳送至 BLL 方法的原始輸入參數
  • CompareAllValues - 將原始值傳送至 BLL 方法;當使用樂觀並發控制時,請選擇此選項

花點時間將 ConflictDetection 屬性設定為 CompareAllValues

設定 GridView 的屬性和欄位

正確設定 ObjectDataSource 的屬性后,讓我們將注意力轉向設定 GridView。 首先,由於我們想要 GridView 支援編輯和刪除,因此請按下 GridView 智慧標記中的 [啟用編輯] 和 [啟用刪除] 複選框。 這會新增 CommandField,其 ShowEditButtonShowDeleteButton 都設定為 true

系結至 ProductsOptimisticConcurrencyDataSource ObjectDataSource 時,GridView 會包含每個產品數據欄位的欄位。 雖然這類 GridView 可以編輯,但用戶體驗卻相當不可接受。 CategoryIDSupplierID 識別欄位將會轉譯為文字方塊,要求使用者輸入適當的類別和供應商的識別號碼。 數值欄位不會有格式設定,也沒有驗證控件,以確保已提供產品名稱,而且單價、庫存單位、訂單單位和重新排序層級值都是適當的數值,而且大於或等於零。

如我們在將驗證控件新增至編輯和插入介面和自定義數據修改介面教學課程中所討論,使用者介面可以藉由將 BoundFields 取代為 TemplateFields 來自定義。 我已以下列方式修改此 GridView 及其編輯介面:

  • 已移除ProductIDSupplierNameCategoryName的BoundFields
  • ProductName BoundField 轉換成 TemplateField,並新增 RequiredFieldValidation 控件。
  • CategoryIDSupplierID BoundFields 轉換成 TemplateFields,並調整編輯介面以使用 DropDownLists 而非 TextBoxes。 在這些 TemplateFields 的 ItemTemplates中,會顯示 CategoryNameSupplierName 這兩個數據欄位。
  • UnitPriceUnitsInStockUnitsOnOrderReorderLevel BoundFields 轉換為 TemplateFields,並新增 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),GridView 的 EnableViewState 屬性必須設定為 true (預設值)。 如果您停用檢視狀態,原始值會在回傳時遺失。

將正確的原始值傳遞至 ObjectDataSource

GridView 設定的方式有幾個問題。 如果 ObjectDataSource 的 ConflictDetection 屬性設定 CompareAllValues 為 (如同我們的屬性),當 ObjectDataSource 的 Update()Delete() 方法由 GridView 叫用時(或 DetailsView 或 FormView),ObjectDataSource 會嘗試將 GridView 的原始值複製到其適當的 Parameter 實例中。 如需此程序的圖形表示,請參閱圖 2。

具體來說,每次數據系結至 GridView 時,GridView 的原始值會以雙向數據系結語句來指派值。 因此,必須透過雙向數據系結來擷取必要的原始值,並以可轉換格式提供它們。

若要查看為什麼這很重要,請花點時間在瀏覽器中瀏覽我們的頁面。 如預期般,GridView 會列出每個產品,其中包含最左邊數據行中的 [編輯] 和 [刪除] 按鈕。

產品會列在 GridView 中

圖 14:產品列在 GridView 中(按兩下以檢視完整大小的影像

如果您按下任何產品的 [刪除] 按鈕,會拋出一個FormatException

嘗試刪除 FormatException 中的任何產品結果

圖 15:嘗試刪除任何產品時的結果FormatException按一下以檢視完整大小的影像

當 ObjectDataSource 嘗試讀取原始 FormatException 值時,會引發 UnitPrice。 由於 ItemTemplateUnitPrice 格式化為貨幣(<%# Bind("UnitPrice", "{0:C}") %>),因此它包含貨幣符號,例如是 $19.95。 當 FormatException 發生時,當 ObjectDataSource 嘗試將此字串轉換為 decimal。 若要規避此問題,我們有一些選項:

  • ItemTemplate中移除貨幣格式。 也就是說,而不是使用 <%# Bind("UnitPrice", "{0:C}") %>,只需使用 <%# Bind("UnitPrice") %>即可。 缺點是價格不再格式化。
  • 使用UnitPrice關鍵字將ItemTemplate格式化為貨幣並顯示在Eval中。 回想一下, Eval 執行單向數據系結。 我們仍然需要提供UnitPrice原始值,因此我們仍然需要在ItemTemplate中使用雙向數據綁定語句,但這可以放在屬性設定為Visiblefalse Label Web 控件中。 我們可以在 ItemTemplate 中使用下列標記:
<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>
  • 移除ItemTemplate的貨幣格式,使用<%# Bind("UnitPrice") %>。 在 GridView 的 RowDataBound 事件處理程式中,以程式設計方式存取顯示 UnitPrice 值的 Label Web 控制項,並將其 Text 屬性設定為格式化的版本。
  • UnitPrice格式化留作貨幣。 在 GridView 的事件處理程式中RowDeleting,使用 UnitPrice將現有的原始Decimal.Parse值 ($19.95) 取代為實際的十進位值。 我們看到如何在RowUpdating事件處理程式中完成類似的操作,這是在處理 ASP.NET 頁面中的 BLL 和 DAL-Level 異常狀況教學中探討的內容。

針對我的範例,我選擇使用第二種方法,新增隱藏的 Label Web 控件,其 Text 屬性是系結至未格式化 UnitPrice 值的雙向數據。

解決此問題之後,請再次嘗試按兩下任何產品的 [刪除] 按鈕。 這次您會在 ObjectDataSource 嘗試叫用 BLL 的 InvalidOperationException 方法時取得 UpdateProduct

ObjectDataSource 找不到具有想要傳送之輸入參數的方法

圖 16:ObjectDataSource 找不到想要傳送的輸入參數的方法(按兩下以檢視完整大小的影像

查看例外狀況的訊息,很明顯 ObjectDataSource 想要叫用包含 DeleteProductoriginal_CategoryName 輸入參數的 BLL original_SupplierName 方法。 這是因為 ItemTemplateCategoryID TemplateFields 的 SupplierID s 目前包含具有 CategoryNameSupplierName 數據欄位的雙向 Bind 語句。 相反地,我們需要將 Bind 陳述與 CategoryIDSupplierID 資料欄位一起包含進去。 若要達成此目的,請將現有的 Bind 語句取代為 Eval 語句,然後新增隱藏的 Label 控件,其 Text 屬性是使用雙向數據系結系結至 CategoryIDSupplierID 數據欄位,如下所示:

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

有了這些變更,我們現在能夠成功刪除和編輯產品資訊! 在步驟 5 中,我們將探討如何確認偵測到並行違規。 但現在,請花幾分鐘的時間嘗試更新和刪除一些記錄,以確保單一使用者的更新和刪除如預期般運作。

步驟 5:測試樂觀併發支援

為了確認偵測到並行違規(而不是導致數據遭到盲目覆寫),我們需要開啟此頁面的兩個瀏覽器視窗。 在這兩個瀏覽器實例中,按兩下 Chai 的 [編輯] 按鈕。 然後,在其中一個瀏覽器中,將名稱變更為 “Chai Tea”,然後按兩下 [更新]。 更新應該會成功,並將 GridView 傳回其預先編輯狀態,並將 “Chai Tea” 作為新的產品名稱。

不過,在其他瀏覽器視窗實例中,產品名稱 TextBox 仍會顯示 “Chai”。 在第二個瀏覽器視窗中,將更新 UnitPrice25.00。 如果沒有樂觀併發控制支援,在第二個瀏覽器實例中按下 [更新] 會將產品名稱變更回 “Chai”,進而覆寫第一個瀏覽器實例所做的變更。 不過,在採用樂觀並行性的方法時,按下第二個瀏覽器實例中的 [更新] 按鈕會導致DBConcurrencyException

偵測到並行違規時,會擲回 DBConcurrencyException

圖 17:偵測到並行違規時,會拋出一個例外(DBConcurrencyException按兩下以檢視完整大小的影像

DBConcurrencyException只有在使用 DAL 的批次更新模式時,才會擲回 。 DB 直接模式不會引發例外狀況,只會指出沒有受影響的數據列。 為了說明這一點,請將這兩個瀏覽器實例的 GridView 傳回其預先編輯狀態。 接下來,在第一個瀏覽器實例中,按兩下 [編輯] 按鈕,並將產品名稱從 “Chai Tea” 變更回 “Chai”,然後按兩下 [更新]。 在第二個瀏覽器視窗中,按兩下 Chai 的 [刪除] 按鈕。

按一下 [刪除] 按鈕時,頁面會回傳,GridView 會呼叫 ObjectDataSource 的 Delete() 方法,而 ObjectDataSource 會呼叫 ProductsOptimisticConcurrencyBLL 類別的 DeleteProduct 方法,並傳遞原始值。 第二個瀏覽器實例的原始 ProductName 值為 “Chai Tea”,與資料庫中目前的 ProductName 值不符。 因此,對資料庫發出的 DELETE 語句影響的資料列為零,因為資料庫中沒有滿足 WHERE 子句條件的記錄。 方法 DeleteProduct 會傳 false 回 ,且 ObjectDataSource 的數據會反彈至 GridView。

從終端用戶的觀點來看,按兩下第二個瀏覽器視窗中的Chai Tea [刪除] 按鈕會導致畫面閃爍,回來後,產品仍然存在,雖然現在它已列為“Chai”(第一個瀏覽器實例所做的產品名稱變更)。 如果使用者再次按兩下 [刪除] 按鈕,則Delete將會成功,因為 GridView 的原始 ProductName 值 (“Chai”) 現在會與資料庫中的值相符。

在這兩種情況下,用戶體驗遠非理想。 我們顯然不想在使用批次更新模式時向用戶顯示例外狀況的詳細 DBConcurrencyException 數據。 使用 DB 直接模式的行為有點令人困惑,因為使用者命令失敗,但沒有確切的指示原因。

為了補救這兩個問題,我們可以在頁面上建立標籤 Web 控件,以提供更新或刪除失敗原因的說明。 針對批次更新模式,我們可以判斷 GridView 的後置事件處理程式中是否 DBConcurrencyException 發生例外狀況,並視需要顯示警告標籤。 針對 DB 直接方法,我們可以檢查 BLL 方法的傳回值(如果 true 表示有一筆資料列受到影響,否則為 false)並視需要顯示通知訊息。

步驟 6:新增資訊訊息,並在發生並發違規時顯示這些訊息

發生並行違規時,所顯示的行為取決於使用 DAL 的批次更新或 DB 直接模式。 我們的教學課程使用這兩種模式,其中批次更新模式用於更新,而DB直接模式用於刪除。 若要開始使用,讓我們將兩個標籤 Web 控件新增至我們的頁面,以說明嘗試刪除或更新數據時發生並行違規。 將 [標籤] 控制項的 VisibleEnableViewState 屬性設定為 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." />

除了設定其 VisibleEnabledViewStateText 屬性之外,我也會將 屬性設定 CssClassWarning,這會導致標籤以大型、紅色、斜體、粗體字型顯示。 此 CSS Warning 類別已在教學課程檢查與插入、更新和刪除相關的事件中定義並新增至 Styles.css。

新增這些標籤之後,Visual Studio 中的設計工具看起來應該類似圖 18。

已將兩個標籤控件新增至頁面

圖 18:已將兩個標籤新增至頁面(按兩下以檢視完整大小的影像

有了這些標籤 Web 控件,我們就可以檢查如何判斷何時發生並行違規,此時適當的 Label Visible 屬性可以設定為 true,以顯示資訊訊息。

在更新時處理並行違規

讓我們先看看在使用批次更新模式時如何處理並行違規。 由於使用批次更新模式時的違規會導致 DBConcurrencyException 擲回例外狀況,因此我們需要將程式碼新增至 ASP.NET 頁面,以判斷在更新過程中是否 DBConcurrencyException 發生例外狀況。 若是如此,我們應該向使用者顯示訊息,說明其變更未儲存,因為其他使用者在開始編輯記錄和按兩下 [更新] 按鈕時修改了相同的數據。

如我們在 處理 ASP.NET Page 教學課程中的 BLL 和 DAL-Level 例外 狀況中所見,可以在數據 Web 控件的後置事件處理程式中偵測和隱藏這類例外狀況。 因此,我們需要為 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 控件也會向使用者說明剛發生的情況。 此事件序列詳述於圖 19。

使用者更新在發生並行違規時遺失

圖 19:使用者更新在發生並行違規時遺失(按兩下以檢視完整大小的影像

備註

或者,與其將 GridView 傳回至編輯前狀態,我們可以透過將傳入的KeepInEditMode物件的GridViewUpdatedEventArgs屬性設定為 true,保留 GridView 的編輯狀態。 不過,如果您採用這種方法,請務必將數據重新系結至 GridView(藉由叫用其 DataBind() 方法),讓其他使用者的值載入編輯介面。 本教學課程中可供下載的程式碼在事件處理程式 RowUpdated 中,這兩行程式碼已被註解掉;只要取消這些程式碼的註解,就可以讓 GridView 在發生併發衝突後仍保持在編輯模式中。

在刪除操作中處理並發性違規

使用 DB 直接模式時,在發生並行存取違規時,不會引發任何例外狀況。 相反地,資料庫語句不會影響任何記錄,因為 WHERE 子句沒有與任何記錄匹配。 BLL 中建立的所有數據修改方法都經過設計,以便傳回布爾值,指出它們是否確實影響一筆記錄。 因此,若要判斷刪除記錄時是否發生並行違規,我們可以檢查 BLL 方法的 DeleteProduct 傳回值。

BLL 方法的傳回值可以在 ObjectDataSource 的後置事件處理程式中,透過傳遞至事件處理程式的 ReturnValue 物件的 ObjectDataSourceStatusEventArgs 屬性來檢查。 由於我們有興趣判斷DeleteProduct方法的傳回值,因此我們需要建立 ObjectDataSource 的Deleted事件處理常式。 ReturnValue 屬性的類型是 object ,如果發生例外並且方法在返回值之前被中斷,那麼該屬性可以是 null 。 因此,我們應該先確定 ReturnValue 屬性不是 null ,而且是布爾值。 假設此檢查通過,如果DeleteConflictMessageReturnValue,我們會顯示false標籤控制項。 這可以使用下列程式代碼來完成:

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 會顯示標籤,並說明剛發生的情況(請參閱圖 20)。

使用者刪除在發生並行違規時遭到取消

圖 20:在遇到並行違規時取消使用者刪除(按一下以檢視完整大小的影像

總結

每個應用程式都有並行違規的機會,可讓多個並行使用者更新或刪除數據。 如果未考慮這類問題,當兩位用戶同時更新相同數據時,誰最後寫入就「獲勝」,覆蓋其他用戶的變更。 或者,開發人員可以實作樂觀式或悲觀式並行控制。 開放式並行存取控制假設並行違規不常發生,而且只是不允許更新或刪除將構成並行違規的命令。 悲觀並行控制會假設並行違規經常發生,而且只拒絕一位使用者的更新或刪除命令是無法接受的。 使用悲觀式並行控制,更新記錄涉及鎖定該記錄,藉此防止其他使用者在鎖定期間修改或刪除該記錄。

.NET 中的具型別數據集提供支持樂觀並行控制的功能。 特別是 UPDATE ,發給資料庫的 和 DELETE 語句包含數據表的所有數據行,因此確保只有在記錄的目前數據符合使用者執行更新或刪除時所擁有的原始數據時,才會發生更新或刪除。 將 DAL 設定為支援樂觀並行控制之後,必須更新 BLL 方法。 此外,必須設定呼叫 BLL 的 ASP.NET 頁面,讓 ObjectDataSource 從其數據 Web 控件擷取原始值,並將其傳遞至 BLL。

如本教學課程中所見,在 ASP.NET Web 應用程式中實作開放式並行控制涉及更新 DAL 和 BLL,並在 ASP.NET 頁面中新增支援。 此新增的工作是否為您時間和精力的明智投資,取決於您的應用程式。 如果您不常同時有使用者更新數據,或他們更新的數據彼此不同,則並行控制不是一個關鍵問題。 不過,如果您的網站上經常有多個使用者處理相同的數據,併發控制可以協助防止一位使用者的更新或刪除意外地覆蓋另一個使用者的更新。

快樂的程序設計!

關於作者

斯科特·米切爾,七本 ASP/ASP.NET 書籍和 4GuysFromRolla.com 創始人的作者,自1998年以來一直與Microsoft Web 技術合作。 斯科特擔任獨立顧問、教練和作家。 他的最新書是 自己學習ASP.NET 2.0 的24小時教程。 可以透過 mitchell@4GuysFromRolla.com 聯絡他。