實作開放式同步存取 (VB)

作者:Scott Mitchell

下載 PDF

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

簡介

對於只允許用戶檢視數據的 Web 應用程式,或只包含只能修改數據的單一使用者,兩個並行使用者不會意外覆寫彼此的變更威脅。 不過,對於允許多個使用者更新或刪除數據的 Web 應用程式,有可能會讓某個使用者的修改與另一個並行使用者發生衝突。 如果沒有任何並行原則,當兩位用戶同時編輯單一記錄時,最後認可其變更的使用者將會覆寫第一個使用者所做的變更。

例如,假設兩個使用者 Jisun 和 Sam 都流覽了應用程式中的頁面,允許訪客透過 GridView 控制項更新和刪除產品。 兩者都會同時按下 GridView 中的 [編輯] 按鈕。 Jisun 會將產品名稱變更為 “Chai Tea”,然後按兩下 [更新] 按鈕。 net result 是一個UPDATE語句,會傳送至資料庫,即使 Jisun 只更新一個字段, ProductName) 也會設定所有產品的可更新字段 (。 此時,資料庫的值為 「Chai Tea」、類別為[咖啡]、供應商「供應專案」、「供應專案」,依此類傳給此特定產品。 不過,Sam 畫面上的 GridView 仍然會在可編輯的 GridView 數據列中顯示為 “Chai”。 在 Jisun 的變更認可後幾秒鐘,Sam 會將類別更新為 Condiments,然後按兩下 [更新]。 這會導致 UPDATE 傳送至資料庫的語句,將產品名稱設定為 「Chai」,並將 CategoryID 設定為對應的[排序] 類別標識碼等等。 已覆寫 Jisun 對產品名稱所做的變更。 圖 1 以圖形方式描述這一系列事件。

當兩位用戶同時更新記錄時,可能會有一位使用者的變更覆寫另一個使用者的變更

圖 1:當兩位使用者同時更新記錄時,可能會有一位使用者的變更覆寫另一個使用者的 (按兩下即可檢視大小完整的映像)

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

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

  • 不執行任何 動作 -如果並行使用者修改相同的記錄,請讓最後一個認可成功 (默認行為)
  • 開放式並行存取 - 假設雖然現在可能會發生並行衝突,但大部分的這類衝突都不會發生;因此,如果發生衝突,只要通知使用者無法儲存其變更,因為其他使用者已修改相同的數據
  • 封閉式並行 存取 - 假設並行衝突很常見,而且使用者不會容忍因為其他使用者的並行活動而無法儲存變更;因此,當用戶開始更新記錄時,將其鎖定,進而防止其他使用者編輯或刪除該記錄,直到用戶認可修改

到目前為止,所有教學課程都已使用預設並行解析策略,也就是我們已讓最後一個寫入成功。 在本教學課程中,我們將探討如何實作開放式並行存取控制。

注意

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

步驟 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 數據表Adapters 使用開放式並行存取 (需要變更 BLL 以容納) ,而是改為建立名為 NorthwindOptimisticConcurrency的新具型別數據集,我們將新增 Products 使用開放式並行存取的 TableAdapter。 接下來,我們將建立 ProductsOptimisticConcurrencyBLL 商業規則層類別,此類別具有適當的修改,以支持開放式並行 DAL。 一旦配置此基礎,我們就可以建立 ASP.NET 頁面。

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

若要建立新的具型別 DataSet,請以滑鼠右鍵按兩下 DAL 資料夾內的 App_Code 資料夾,然後新增名為 NorthwindOptimisticConcurrency的新 DataSet。 如我們在第一個教學課程中所見,這麼做會將新的 TableAdapter 新增至 Typed DataSet,並自動啟動 TableAdapter 設定精靈。 在第一個畫面中,系統會提示您指定要連線的資料庫 - 使用 NORTHWNDConnectionStringWeb.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:在 Products 原始 DAL (按兩下以檢視大小完整的映像) 使用 TableAdapter 的相同 SQL 查詢

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

藉由檢查「使用開放式並行存取」CheckBox 來啟用開放式並行控制

圖 6:藉由檢查 [使用開放式並行存取] CheckBox 來啟用開放式並行控制 (按兩下即可檢視完整大小的映像)

最後,指出 TableAdapter 應該使用同時填滿 DataTable 並傳回 DataTable 的數據存取模式;也表示應該建立 DB 直接方法。 將傳回 DataTable 模式的方法名稱從 GetData 變更為 GetProducts,以反映我們在原始 DAL 中使用的命名慣例。

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

圖 7:讓 TableAdapter 利用所有數據存取模式 (按兩下即可檢視完整大小的映射)

完成精靈之後,DataSet Designer 將包含強型別Products的 DataTable 和 TableAdapter。 請花點時間將 DataTable 重新 Products 命名為 ProductsOptimisticConcurrency,您可以在 DataTable 的標題列上按下滑鼠右鍵,然後選擇操作功能表中的 [重新命名]。

DataTable 和 TableAdapter 已新增至具類型的數據集

圖 8:D ataTable 和 TableAdapter 已新增至具類型的數據集 (按兩下以檢視大小完整的影像)

若要查看 TableAdapter (之間使用開放式並行存取) 和未) 之 Products TableAdapter (之間的差異UPDATEDELETEProductsOptimisticConcurrency,請按下 TableAdapter 並移至 屬性視窗。 DeleteCommand在 和 UpdateCommand 屬性的CommandText子屬性中,您可以看到叫用 DAL 更新或刪除相關方法時傳送至資料庫的實際 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))

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

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

如您所見, WHERE 使用開放式並行存取之 TableAdapter 語句中的 DELETE 子句包含每個 Product 數據表現有數據行值與 GridView (或 DetailsView 或 FormView) 上次填入時的原始值之間的比較。 由於、 和 以外的ProductID所有欄位都可以有NULL值,因此會包含其他參數和檢查,以正確比較 NULL 子句中的WHEREDiscontinued值。 ProductName

我們不會將任何其他 DataTable 新增至本教學課程的開放式並行存取啟用 DataSet,因為我們的 ASP.NET 頁面只會提供更新和刪除產品資訊。 不過,我們仍然需要將 GetProductByProductID(productID) 方法新增至 ProductsOptimisticConcurrency TableAdapter。

若要達成此目的,請以滑鼠右鍵按下 TableAdapter 的標題列, (位於 和 GetProducts 方法名稱上方Fill的區域) ,然後從操作功能表選擇 [新增查詢]。 這會啟動 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:將方法重新命名為 FillByProductID (,然後按下GetProductByProductID即可檢視完整大小的影像)

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

步驟 3:建立開放式 Concurrency-Enabled DAL 的商業規則層

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

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

雖然在批次更新模式中使用的 TableAdapter Update 方法簽章尚未變更,但記錄原始值和新值所需的程式代碼有。 因此,讓我們建立新的商業規則層類別來處理新的 DAL,而不是嘗試使用已啟用開放式並行存取的 DAL 與現有的 ProductsBLL 類別。

將名為 ProductsOptimisticConcurrencyBLLBLLApp_Code類別新增至資料夾內的資料夾。

將 ProductsOptimisticConcurrencyBLL 類別新增至 BLL 資料夾

圖 12:將 ProductsOptimisticConcurrencyBLL 類別新增至 BLL 資料夾

接下來,將下列程式代碼新增至 ProductsOptimisticConcurrencyBLL 類別:

Imports NorthwindOptimisticConcurrencyTableAdapters
<System.ComponentModel.DataObject()> _
Public Class ProductsOptimisticConcurrencyBLL
    Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing
    Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter
        Get
            If _productsAdapter Is Nothing Then
                _productsAdapter = New ProductsOptimisticConcurrencyTableAdapter()
            End If
            Return _productsAdapter
        End Get
    End Property
    <System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Select, True)> _
    Public Function GetProducts() As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable
        Return Adapter.GetProducts()
    End Function
End Class

請注意類別宣告開頭上方的using NorthwindOptimisticConcurrencyTableAdapters 語句。 NorthwindOptimisticConcurrencyTableAdapters命名空間包含 ProductsOptimisticConcurrencyTableAdapter 提供 DAL 方法的 類別。 此外,您也可以在類別宣告之前找到 System.ComponentModel.DataObject 屬性,這會指示Visual Studio將此類別包含在 ObjectDataSource 精靈的下拉式清單中。

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

使用具有開放式並行存取的資料庫直接模式刪除產品

針對使用開放式並行存取的 DAL 使用 DB 直接模式時,方法必須傳遞新的和原始值。 為了刪除,沒有新的值,因此只需要傳入原始值。 在 BLL 中,我們必須接受所有原始參數做為輸入參數。 DeleteProduct讓我們讓類別中的 ProductsOptimisticConcurrencyBLL 方法使用 DB 直接方法。 這表示此方法必須接受所有十個產品數據欄位做為輸入參數,並將其傳遞至 DAL,如下列程式代碼所示:

<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteProduct( _
    ByVal original_productID As Integer, ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean) _
    As Boolean
    Dim rowsAffected As Integer = 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
End Function

如果原始值 - 上次載入 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 的 GetProductByProductID(productID) 方法,將目前的資料庫產品資訊讀入ProductRow實例
  2. 將新值指派給步驟 1 中的 ProductRow 實例
  3. 呼叫 TableAdapter 的 Update 方法,傳入 ProductRow 實例

不過,這個步驟順序不會正確支持開放式並行存取,因為 ProductRow 步驟 1 中填入的 會直接從資料庫填入,這表示 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_ColumnName原始值。 接下來,會將新的參數值指派給 ProductsOptimisticConcurrencyRow ,最後會 Update 叫用 方法,並傳入 DataRow。

下列程式代碼顯示 UpdateProduct 可接受所有產品數據欄位做為輸入參數的多載。 雖然此處未顯示, ProductsOptimisticConcurrencyBLL 但本教學課程下載中包含的類別也包含 UpdateProduct 多載,只接受產品名稱和價格作為輸入參數。

Protected Sub AssignAllProductValues( _
    ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean)
    product.ProductName = productName
    If Not supplierID.HasValue Then
        product.SetSupplierIDNull()
    Else
        product.SupplierID = supplierID.Value
    End If
    If Not categoryID.HasValue Then
        product.SetCategoryIDNull()
    Else
        product.CategoryID = categoryID.Value
    End If
    If quantityPerUnit Is Nothing Then
        product.SetQuantityPerUnitNull()
    Else
        product.QuantityPerUnit = quantityPerUnit
    End If
    If Not unitPrice.HasValue Then
        product.SetUnitPriceNull()
    Else
        product.UnitPrice = unitPrice.Value
    End If
    If Not unitsInStock.HasValue Then
        product.SetUnitsInStockNull()
    Else
        product.UnitsInStock = unitsInStock.Value
    End If
    If Not unitsOnOrder.HasValue Then
        product.SetUnitsOnOrderNull()
    Else
        product.UnitsOnOrder = unitsOnOrder.Value
    End If
    If Not reorderLevel.HasValue Then
        product.SetReorderLevelNull()
    Else
        product.ReorderLevel = reorderLevel.Value
    End If
    product.Discontinued = discontinued
End Sub
<System.ComponentModel.DataObjectMethodAttribute( _
System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean, ByVal productID As Integer, _
    _
    ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean, _
    ByVal original_productID As Integer) _
    As Boolean
    'STEP 1: Read in the current database product information
    Dim products As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
        Adapter.GetProductByProductID(original_productID)
    If products.Count = 0 Then
        ' no matching record found, return false
        Return False
    End If
    Dim product As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = 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
    Dim rowsAffected As Integer = Adapter.Update(product)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function

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

在 DAL 和 BLL 完成之後,所有保留專案都是建立 ASP.NET 頁面,以利用內建至系統的開放式並行邏輯。 具體而言,數據 Web 控件 (GridView、DetailsView 或 FormView) 必須記住其原始值,而 ObjectDataSource 必須將這兩組值傳遞至商業規則層。 此外,ASP.NET 頁面必須設定為正常處理並行違規。

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

讓 ObjectDataSource 使用 ProductsOptimisticConcurrencyBLL 物件

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

GetProducts從精靈中的下拉式清單中選擇、 UpdateProductDeleteProduct 方法。 針對 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集合包含類別方法DeleteProductProductsOptimisticConcurrencyBLL十個Parameter輸入參數的實例。 同樣地, 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 會轉譯為 TextBox,要求使用者輸入適當的類別和供應商作為標識碼。 數值欄位和驗證控件不會有任何格式設定,以確保已提供產品名稱,以及單價、庫存單位、訂單單位和重新排序層級值都是適當的數值,且大於或等於零。

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

  • ProductID已移除、 SupplierNameCategoryName BoundFields
  • ProductName BoundField 轉換成 TemplateField,並新增 RequiredFieldValidation 控件。
  • CategoryIDSupplierID BoundFields 轉換為 TemplateFields,並調整編輯介面以使用 DropDownLists 而非 TextBoxes。 在這些 TemplateFields' ItemTemplatesCategoryName ,會顯示 和 SupplierName 數據欄位。
  • 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 (,如同我們的) ,當 GridView (或 DetailsView 或 FormView) 叫用 ObjectDataSource Update()Delete() 的 或 方法時,ObjectDataSource 會嘗試將 GridView 的原始值複製到其適當的 Parameter 實例。 請參閱圖 2,以取得此程序的圖形表示法。

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

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

產品列在 GridView 中

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

如果您按下任何產品的 [刪除] 按鈕, FormatException 則會擲回 。

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

圖 15:嘗試刪除 (按鍵FormatException即可檢視完整大小的影像)

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

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

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

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

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

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

查看例外狀況的訊息,ObjectDataSource 顯然想要叫用包含 original_CategoryNameoriginal_SupplierName 輸入參數的 BLL DeleteProduct 方法。 這是因為 ItemTemplateCategoryIDSupplierID TemplateFields 的 目前包含具有 和 SupplierName 數據欄位的CategoryName雙向 Bind 語句。 相反地,我們需要包含 Bind 具有 和 SupplierID 數據欄位的 CategoryID 語句。 若要達成此目的,請將現有的 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”。 在這個第二個瀏覽器視窗中,將 UnitPrice 更新為 25.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”, (第一個瀏覽器實例所做的產品名稱變更) 。 如果使用者再次按兩下 [刪除] 按鈕,[刪除] 將會成功,因為 GridView 的原始 ProductName 值 (“Chai”) 現在會與資料庫中的值相符。

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

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

步驟 6:新增資訊訊息,並在並行違規時顯示它們

發生並行違規時,所呈現的行為取決於使用 DAL 的批次更新或 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 屬性之外,我也已將 CssClass 屬性設定為 Warning,這會導致標籤以大型、紅色、斜體、粗體字型顯示。 這個 CSS Warning 類別已定義並新增至Styles.css檢查 與插入、更新和刪除相關聯的事件 教學課程。

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

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

圖 18:兩個標籤已新增至頁面 (按兩下即可檢視全大小影像)

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

在更新時處理並行違規

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

如我們在 ASP.NET Page 教學課程中 處理 BLL 和 DAL-Level 例外 狀況中所見,可以在數據 Web 控件的後置事件處理程式中偵測並隱藏這類例外狀況。 因此,我們需要建立 GridView RowUpdated 事件的事件處理程式,以檢查是否 DBConcurrencyException 擲回例外狀況。 此事件處理程式會傳遞參考至更新程式期間引發的任何例外狀況,如下列事件處理程式程式代碼所示:

Protected Sub ProductsGrid_RowUpdated _
        (ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
        Handles ProductsGrid.RowUpdated
    If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
        If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
            ' Display the warning message and note that the exception has
            ' been handled...
            UpdateConflictMessage.Visible = True
            e.ExceptionHandled = True
        End If
    End If
End Sub

在例外狀況的臉部中 DBConcurrencyException ,此事件處理程式會顯示 UpdateConflictMessage Label 控件,並指出已處理例外狀況。 有了此程式代碼,在更新記錄時發生並行違規時,用戶的變更會遺失,因為它們會同時覆寫其他使用者的修改。 特別是,GridView 會傳回至其預先編輯狀態,並系結至目前的資料庫數據。 這會使用其他用戶的變更來更新 GridView 數據列,這之前看不到。 此外,標籤 UpdateConflictMessage 也會向使用者說明剛發生的狀況。 此事件順序詳述於圖 19。

使用者 匯報 在發生並行違規時遺失

圖 19:使用者 匯報 在發生並行違規時遺失 (按兩下即可檢視大小完整的映像)

注意

或者,除了將 GridView 傳回至預先編輯狀態之外,我們可以將傳入GridViewUpdatedEventArgs物件的 屬性設定KeepInEditMode為 true,讓 GridView 保持其編輯狀態。 不過,如果您採用這種方法,請務必藉由叫用其 DataBind() 方法) 將數據重新系結至 GridView (,讓其他使用者的值載入編輯介面。 本教學課程中可供下載的程式代碼在事件處理程式中 RowUpdated 已批注化這兩行程序代碼;只要取消批注這些程式代碼行,方格檢視就會在並行違規之後保持編輯模式。

在刪除時回應並行違規

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

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

Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
        (ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
        Handles ProductsOptimisticConcurrencyDataSource.Deleted
    If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
        Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
        If deleteReturnValue = False Then
            ' No row was deleted, display the warning message
            DeleteConflictMessage.Visible = True
        End If
    End If
End Sub

在發生並行違規時,會取消使用者的刪除要求。 GridView 會重新整理,顯示使用者載入頁面時和按兩下 [刪除] 按鈕之間,該記錄所發生的變更。 當這類違規轉譯時,會顯示卷標, DeleteConflictMessage 說明剛才發生的情況, (請參閱圖 20) 。

發生並行違規時,使用者刪除已取消

圖 20:使用者刪除在發生並行違規時取消 (按兩下即可檢視大小完整的影像)

摘要

並行違規的機會存在於允許多個並行使用者更新或刪除數據的每個應用程式中。 如果未考慮這類違規,當兩位用戶同時更新上次寫入“wins” 中取得的相同數據時,會覆寫其他用戶的變更。 或者,開發人員可以實作開放式或封閉式並行控制。 開放式並行存取控制假設並行違規不常發生,而且只不允許構成並行違規的更新或刪除命令。 封閉式並行控制假設並行存取違規經常發生,而且只拒絕一位使用者的更新或刪除命令便無法接受。 透過封閉式並行控制,更新記錄牽涉到鎖定記錄,進而防止其他使用者在鎖定時修改或刪除記錄。

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

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

快樂的程序設計!

關於作者

Scott Mitchell 是七份 ASP/ASP.NET 書籍的作者,以及 1998 年以來與 Microsoft Web 技術合作的 4GuysFromRolla.com 作者。 Scott 是獨立顧問、訓練員和作者。 他的最新書籍是 Sams 在 24 小時內自行 ASP.NET 2.0。 您可以透過mitchell@4GuysFromRolla.com部落格來連線到 ,您可以在 找到http://ScottOnWriting.NET