微軟數據源樂觀併發Adapter

使用 SqlDataSource 實現併發優化

在本教程中,我們將討論併發優化基礎課程,然後探討如何使用 SqlDataSource 控件。

« 前一篇教程  |  下一篇教程 »

 

簡介

在前一篇教程中,我們探討了如何向 SqlDataSource 控件添加插入、更新和刪除功能。簡而言之,要提供這些功能,我們需要在控件的 InsertCommand 、UpdateCommand 或 DeleteCommand 屬性指定相應的 INSERT 、UPDATE 、或 DELETE SQL 語句,並在InsertParameters 、UpdateParameters 和DeleteParameters 集合中指定適當的參數。這些屬性和集合可以手動指定,Configure Data Source 嚮導的 Advanced 按鈕提供了一個 “ Generate INSERT, UPDATE, and DELETE statements” 複選框,該複選框將根據 SELECT 語句自動生成這些語句。

除了“Generate INSERT, UPDATE, and DELETE statements” 複選框之外,高級SQL 生成選項還包括一個“Use optimistic concurrency” 選項(見圖1 )。選中該選項後,如果在用戶上一次將數據加載到網格之後沒有修改基礎數據庫數據,則將修改UPDATE 和 DELETE 中自動生成的 WHERE 子句,只執行更新和刪除操作。

圖1 :可以從 Advanced SQL Generation Options 對話框添加併發優化支持

返回實施併發優化 教程,我們探討了併發優化的基礎知識,以及如何向ObjectDataSource 添加併發優化控件。在本教程中,我們將再次探討併發優化控件的基礎知識,然後探討如何使用 SqlDataSource 實現。

併發優化簡介

對於允許多個用戶同時編輯或刪除相同數據的Web 應用程序來說,某個用戶有可能會偶然覆蓋其它用戶的更改。在實施併發優化 教程中,我將給出下列示例:

假設兩個用戶Jisun 和 Sam 均在應用程序中訪問某一頁面,該頁面允許訪問者通過GridView 控件更新和刪除產品。兩者在差不多相同的時間均單擊了 “Chai” 的 Edit 按鈕。Jisun 將產品名稱更改爲 “Chai Tea” ,並單擊了Update 按鈕。最終結果是,UPDATE 語句被髮送到數據庫,該語句設置了產品所有的可更新字段(儘管 Jisun 僅更新了一個字段 ProductName )。 此時,數據庫中此產品包含了 “Chai Tea” ,類別 Beverages 和供應商 Exotic Liquids 等值。但是,Sam 屏幕上的 GridView 仍然在可編輯 GridView 行中顯示產品名稱爲 “Chai” 。Jisun 完成更改之後幾秒鐘 ,Sam 將類別更新爲 Condiments ,並單擊了Update 。這導致 PDATE 語句將被髮送到數據庫,該數據庫將產品名稱設置爲 “Chai” ,而CategoryID 則被設置爲相應的 Condiments 類別 ID ,其它的與此類似。Jisun 的對產品名稱的更改已經被覆蓋。

圖2 顯示了此交互過程。

圖2 :兩個用戶同時更新記錄時,一個用戶的更改可能覆蓋另外一個用戶的更改

爲了防止這種情況出現,必須執行某種形式的併發控件 。併發優化 作爲本教程的重點,將假定儘管偶爾存在併發衝突,但是絕大多數情況下這種衝突並不會出現。因此,如果發生衝突,併發優化控件將通知用戶無法保存他們所做的修改,因爲其它用戶已經修改了同一數據。

注意:對於那些假設會存在很多併發衝突,或者這些衝突是根本不容許的應用來說,可使用保守式控件。有關保守式控件的更詳細討論,請參照實施併發優化 教程。

併發優化控件的作用是確保正在更新或刪除的記錄保持其更新或刪除過程開始的同樣的值。例如,在可編輯 GridView 中單擊 Edit 按鈕時,將從數據庫讀取記錄值,並在文本框和其它 Web 控件中顯示記錄值。這些原始值由 GridView 保存。隨後,在用戶完成更改並單擊 Update 按鈕之後,使用的 UPDATE 語句必須考慮原始值和新值,並且如果開始編輯的原始值與數據庫中的值完全相同,則僅更新基礎數據庫記錄。圖 3 描述了事件的順序。

圖3 :爲使更新或刪除成功,原始值必須等於當前數據庫的值

有多種方式可執行併發優化(請參閱 Peter A. Bromberg 的併發優化更新邏輯 ,查看多種選擇)。SqlDataSource 使用的技術(和數據訪問層中使用的 ADO.NET 強類型 DataSets )將添加WHERE 子句,從而包含對所有原始值的比較。例如,只有當目前的數據庫值等於更新 GridView 的記錄時最初檢索到的值時,下面的 UPDATE 語句才更新產品的名稱和價格。參數 @ProductName 和 @UnitPrice 包含了由用戶輸入的新值,而 @original_ProductName 和 @original_UnitPrice 包含了單擊 Edit 按鈕時最初加載到 GridView 的值:

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

正如我們將在本教程中看到的一樣,使用SqlDataSource 啓用併發優化控件就像選中複選框一樣簡單。

步驟1:創建支持併發優化的SqlDataSource

首先,從SqlDataSource 文件夾打開 OptimisticConcurrency.aspx 頁面。從工具箱拖拽一個 SqlDataSource 控件,放置到設計器上,將其ID 屬性設置爲 ProductsDataSourceWithOptimisticConcurrency 。接下來,從控件的智能標記單擊 Configure Data Source 鏈接。在嚮導中的第一個屏幕上,選擇使用NORTHWINDConnectionString ,並單擊 Next 。

圖4 :選擇使用 NORTHWINDConnectionString

在本例中,我們將添加一個能夠幫助用戶編輯 Products 表的 GridView 。因此,在 “ Configure the Select Statement” 屏幕中,從下拉列表選擇 Products列表,並選擇 ProductID 、ProductName 、UnitPrice 和 Discontinued 列,如圖 5 所示。

圖5 :從 Products 表返回 ProductID 、ProductName 、UnitPrice 和 Discontinued 列

在選定列之後,單擊Advanced 按鈕,顯示 Advanced SQL Generation Options 對話框。選擇 “Generate INSERT, UPDATE, and DELETE statements” 和 “Use optimistic concurrency” 複選框,單擊OK (屏幕快照請參照圖 1) 。單擊 Next ,然後再單擊 Finish 完成嚮導。

完成 Configure Data Source 嚮導後,請花點時間檢查結果的 DeleteCommand 和 UpdateCommand 屬性,以及 DeleteParameters 和 UpdateParameters 集合。完成此操作的最簡單的方式是單擊左下角的 Source 選項卡,查看頁面的聲明式語法。在此處將會發現UpdateCommand 值爲:

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     [UnitPrice] = @original_UnitPrice AND
     [Discontinued] = @original_Discontinued

UpdateParameters 集合中有7個參數:

<asp:SqlDataSource ID="ProductsDataSourceWithOptimisticConcurrency"
    runat="server" ...>
    <DeleteParameters>
      ...
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="UnitPrice" Type="Decimal" />
        <asp:Parameter Name="Discontinued" Type="Boolean" />
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </UpdateParameters>
    ...
</asp:SqlDataSource>

與此類似,DeleteCommand 屬性和DeleteParameters 集合應如下面所示:

DELETE FROM [Products]
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     [UnitPrice] = @original_UnitPrice AND
     [Discontinued] = @original_Discontinued
<asp:SqlDataSource ID="ProductsDataSourceWithOptimisticConcurrency"
    runat="server" ...>
    <DeleteParameters>
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        ...
    </UpdateParameters>
    ...
</asp:SqlDataSource>

除了爲WHERE 子句增加 UpdateCommand 和DeleteCommand 屬性外(以及向各自的參數集添加其它參數外),選擇“Use optimistic concurrency” 選項,調整兩個其它屬性 :

Web 數據 控件調用 SqlDataSource 的 Update() 或者Delete() 方法時,它將傳遞原始值。如果 SqlDataSource 的 ConflictDetection 屬性設置爲 CompareAllValues ,命令中將添加這些原始值。OldValuesParameterFormatString 屬性提供了這些原始值參數使用的命名形式。Configure Data Source 嚮導使用 “original_{0}” ,並在UpdateCommand 和 DeleteCommand 屬性以及UpdateParameters 和 DeleteParameters 集中命名相應的原始參數。

注意:由於我們未使用 SqlDataSource 控件的插入功能,您可以按照您自己的意願刪除InsertCommand 屬性及其InsertParameters 集合。

正確處理 NULL 值

遺憾的是,在使用併發優化時,由 Configure Data Source 嚮導所自動生成的擴充的UPDATE 和 DELETE 語句無法處理包含 NULL 值的記錄。要想知道原因,請考慮我們的 SqlDataSource 的 UpdateCommand :

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     [UnitPrice] = @original_UnitPrice AND
     [Discontinued] = @original_Discontinued

Products 表中的 UnitPrice 列不能包含 NULL 值。如果某條特定記錄的UnitPrice 包含了一個 NULL 值,則 WHERE 子句部分“[UnitPrice] = @original_UnitPrice” 將始終等於 False ,這是因爲NULL = NULL 始終返回 False 。因此,包含 NULL 值的記錄不能進行修改或刪除,因爲 UPDATE 和 DELETE 語句的 WHERE 子句不會返回需要更新和刪除的任何行。

注意:這個問題已於 2004 年 6 月第一次在 SqlDataSource 生成錯誤的 SQL 語句 中報告給微軟,據傳將在下一版本的ASP.NET 中解決。

要解決該問題,我們必須在 UpdateCommand 和 DeleteCommand 屬性中爲可擁有 NULL 值的所有列手動更新WHERE 子句。通常情況下,請將[ColumnName] = @original_ColumnName 更改爲:

(
   ([ColumnName] IS NULL AND @original_ColumnName IS NULL)
     OR
   ([ColumnName] = @original_ColumnName)
)

此修改可直接通過聲明式標記完成,您可以從Properties 窗口使用 UpdateQuery 或DeleteQuery 選項,或者在 Configure Data Source 嚮導中使用 “Specify a custom SQL statement or stored procedure” 中的 UPDATE 和 DELETE 選項卡。同樣,必須對可能包含 NULL 值的 UpdateCommand 和 DeleteCommand 的 WHERE 子 句中的每一列 進行修改。

將這種情況應用到我們的例子中將使得UpdateCommand 和 DeleteCommand 值修改:

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
        OR ([UnitPrice] = @original_UnitPrice)) AND
     [Discontinued] = @original_Discontinued
DELETE FROM [Products]
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
        OR ([UnitPrice] = @original_UnitPrice)) AND
     [Discontinued] = @original_Discontinued

步驟2 :爲 GridView 添加 Edit 和 Delete 選項

將SqlDataSource 配置爲支持併發優化之後,剩下需要做的事情是向頁面中添加使用此併發控件的Web 數據 控件。在本教程中,我們將添加一個既可以支持編輯功能,又可以支持刪除功能的 GridView 。要完成此操作,從工具欄中拖拽一個 GridView 到 設計器 中,並將其ID 設置爲 Products 。在 GridView 的智能標記中,將其綁定到步驟 1 中添加的 ProductsDataSourceWithOptimisticConcurrency SqlDataSource 控件。最後,從智能標記選中 “Enable Editing” 和 “Enable Deleting” 選項。

圖6 :將 GridView 綁定到 SqlDataSource ,並啓用編輯和刪除功能

在添加GridView 之後,通過刪除 ProductID BoundField ,將 ProductName BoundField 的HeaderText 屬性更改爲 “Product” ,以及更新UnitPrice BoundField 的方式配置其外觀,從而其 HeaderText 屬性變爲 “Price” 。理想情況下,我們應該加強編輯界面,使之包括一個ProductName 值的 RequiredFieldValidator,以及一個 UnitPrice 值的 CompareValidator (保證其數值可保持正確格式)有關定製 GridView 的編輯界面的更詳細信息,請參閱自定義數據修改界面 教程。

注意:由於從 GridView 獲取的原始值存儲在視圖狀態下,因此必須啓用GridView 的視圖狀態。

完成對 GridView 的 修改後,GridView 和 SqlDataSource 的聲明式標記應類似下面所示:

<asp:SqlDataSource ID="ProductsDataSourceWithOptimisticConcurrency"
    runat="server" ConflictDetection="CompareAllValues"
    ConnectionString="<%$ ConnectionStrings:NORTHWNDConnectionString %>"
    DeleteCommand=
        "DELETE FROM [Products]
         WHERE [ProductID] = @original_ProductID
         AND [ProductName] = @original_ProductName
         AND (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
              OR ([UnitPrice] = @original_UnitPrice))
         AND [Discontinued] = @original_Discontinued"
    OldValuesParameterFormatString=
        "original_{0}"
    SelectCommand=
        "SELECT [ProductID], [ProductName], [UnitPrice], [Discontinued]
         FROM [Products]"
    UpdateCommand=
        "UPDATE [Products]
         SET [ProductName] = @ProductName, [UnitPrice] = @UnitPrice,
            [Discontinued] = @Discontinued
         WHERE [ProductID] = @original_ProductID
         AND [ProductName] = @original_ProductName
         AND (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL)
            OR ([UnitPrice] = @original_UnitPrice))
        AND [Discontinued] = @original_Discontinued">
    <DeleteParameters>
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="ProductName" Type="String" />
        <asp:Parameter Name="UnitPrice" Type="Decimal" />
        <asp:Parameter Name="Discontinued" Type="Boolean" />
        <asp:Parameter Name="original_ProductID" Type="Int32" />
        <asp:Parameter Name="original_ProductName" Type="String" />
        <asp:Parameter Name="original_UnitPrice" Type="Decimal" />
        <asp:Parameter Name="original_Discontinued" Type="Boolean" />
    </UpdateParameters>
</asp:SqlDataSource>
<asp:GridView ID="Products" runat="server"
    AutoGenerateColumns="False" DataKeyNames="ProductID"
    DataSourceID="ProductsDataSourceWithOptimisticConcurrency">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="UnitPrice" HeaderText="Price"
            SortExpression="UnitPrice" />
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

要查看正在執行的併發優化控件,請打開兩個瀏覽器窗口,並在兩個窗口中分別加載 OptimisticConcurrency.aspx 頁。在兩個瀏覽器中分別單擊第一個產品的 Edit 按鈕。在第一個瀏覽器中,改變產品名稱,並單擊 Update 。此瀏覽器將回傳,而 GridView 將返回到其預編輯模式,顯示新編輯記錄的新的產品名稱。

在第二個瀏覽器窗口中,更改價格(但是保留產品名稱爲原始值),並單擊 Update 。回傳時,網格返回其預編輯模式,但並未記錄價格更改。第二個瀏覽器中顯示的值(新產品名稱和舊價格)與第一個瀏覽器中的完全一樣。第二個瀏覽器窗口中所作的更改已經丟失。而且,由於沒有顯示任何表明出現併發異常的異常或信息,更改丟失得非常平靜。

圖7 :第二個瀏覽器中所作的更改毫無聲息的丟失了

沒有實現對第二個瀏覽器進行修改的原因在於UPDATE 語句的 WHERE 子句濾掉了所有的記錄,因此不會影響任何行。下面,我們重新瞭解一下 UPDATE 語句:

UPDATE [Products] SET
     [ProductName] = @ProductName,
     [UnitPrice] = @UnitPrice,
     [Discontinued] = @Discontinued
WHERE
     [ProductID] = @original_ProductID AND
     [ProductName] = @original_ProductName AND
     (([UnitPrice] IS NULL AND @original_UnitPrice IS NULL) OR
        ([UnitPrice] = @original_UnitPrice)) AND
     [Discontinued] = @original_Discontinued

第二個瀏覽器更新記錄時,在WHERE 子句中指定的原始產品名稱未能與現有的產品名稱實現匹配(由於它已經被第一個瀏覽器修改)。因此 ,[ProductName] = @original_ProductName 語句將返回 False ,並且UPDATE 不會影響任何記錄。

注意 : 刪除的工作方式 相同。打開兩個瀏覽器窗口,首先,編輯某一特定產品,接下來,保存所作更改。在一個瀏覽器中保存所作更改後,在另外一個瀏覽器中單擊同一產品的 Delete 按鈕。由於DELETE 語句的 WHERE 子句中的原始值不匹配,因此刪除操作毫無聲息的以失敗告終。

從最終用戶的角度看,在第二個用戶的瀏覽器窗口中單擊 Update 按鈕後,網格將返回到預編輯模式,但是它們所作的修改已經丟失。但是,此處不存在不保持更改的可見回傳。理想情況下,如果由於併發衝突的原因導致用戶所作更改丟失,我們應該通知他們,或者將網格保持在編輯模式下。下面,我們探討一下如何完成此操作。

步驟3 :確定出現併發衝突的時間

由於併發衝突拒絕了我們所作的修改,因此,最好能夠在出現併發衝突時提醒用戶。要想提醒用戶,我們需要首先在頁面的頂部添加一個名爲ConcurrencyViolationMessage 的Web 標籤控件,其文本屬性顯示下列信息:" You have attempted to update or delete a record that was simultaneously updated by another user. Please review the other user's changes and then redo your update or delete."將標籤控件的CssClass 屬性設置爲 “Warning” ,該屬性爲 Styles.css 中定義的CSS 類,可以紅色、斜體、粗體和大字體顯示文本。最後,將標籤 的Visible 和 EnableViewState 屬性設置爲False 。除非我們在這些回傳中明確將其 Visible 屬性設置爲 True , 否則將隱藏標籤 。

圖8 :在頁面上添加一個顯示警告的標籤控件

執行更新或刪除操作時,GridView 的 RowUpdated 和 RowDeleted Event Handler 將在其數據源控件完成所需的更新或刪除操作後釋放。我們可以確定這些 Event Handler 操作所影響的行數。如果影響爲零行,我們希望顯示ConcurrencyViolationMessage 標籤 。

請爲 RowUpdated 和 RowDeleted 事件創建 Event Handler ,並添加下列代碼:

protected void Products_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.AffectedRows == 0)
    {
        ConcurrencyViolationMessage.Visible = true;
        e.KeepInEditMode = true;
        // Rebind the data to the GridView to show the latest changes
        Products.DataBind();
    }
}
protected void Products_RowDeleted(object sender, GridViewDeletedEventArgs e)
{
    if (e.AffectedRows == 0)
        ConcurrencyViolationMessage.Visible = true;
}

在兩個Event Handler 中,選中 e.AffectedRows 屬性,如果它等於 0 ,則將 ConcurrencyViolationMessage 標籤 的Visible 屬性設置爲 True 。在 RowUpdated Event Handler 中,我們還可通過將 KeepInEditMode 設置爲 true 來指定 GridView 保持編輯模式。通過這種方式,我們需要將數據重新綁定到網格,從而保證其它用戶的數據將被加載到編輯界面。此操作可通過調用 GridView 的 DataBind() 方法完成。

如圖9 所示,通過使用這兩個 Event Handler ,出現併發衝突時,屏幕上將顯示一條非常醒目的信息。

圖9 :出現併發衝突時顯示信息

小結

在多個併發用戶可能編輯相同數據的情況下,創建web 應用程序時需要考慮併發控件選項就變得十分重要。在默認情況下, ASP.NET Web 數據控件和數據源控件不使用任何併發控件。正如我們在本教程中所看到的,使用 SqlDataSource 實現併發優化控件非常快捷和便利。SqlDataSource 可以爲您處理大多數日常工作—— 向自動生成的 UPDATE 和 DELETE 語句添加擴充 WHERE 子句,但是,正如在 “正確處理NULL 值” 部分所討論的,幾乎不存在處理 NULL 值列的示例。

本教程是最後一篇對 SqlDataSource 進行 探討的教程。此後的教程中探討使用 ObjectDataSource 和分層架構處理數據。


 

實現樂觀併發 (C#)

作者 :Scott Mitchell

下載 PDF

對於允許多個用戶編輯數據的 Web 應用程序,有兩個用戶可能同時編輯相同數據的風險。 在本教程中,我們將實現樂觀併發控制來處理此風險。

介紹

對於僅允許用戶查看數據的 Web 應用程序,或者僅包含一個可以修改數據的用戶,兩個併發用戶不會意外覆蓋彼此的更改的威脅。 但是,對於允許多個用戶更新或刪除數據的 Web 應用程序,一個用戶的修改可能會與另一個併發用戶的修改發生衝突。 如果沒有任何併發策略,當兩個用戶同時編輯單個記錄時,提交更改的用戶最後一次將覆蓋第一個用戶所做的更改。

例如,假設兩個用戶 Jisun 和 Sam 都訪問了應用程序中的一個頁面,允許訪問者通過 GridView 控件更新和刪除產品。 兩者同時單擊 GridView 中的“編輯”按鈕。 Jisun 將產品名稱更改爲“柴茶”,然後單擊“更新”按鈕。 淨結果是 UPDATE 發送到數據庫的語句,該語句將產品 的所有 可更新字段設置爲 (即使 Jisun 僅更新了一個字段, ProductName) 也是如此。 此時,數據庫具有“柴茶”、飲料、供應商異國液體等值。此特定產品。 但是,Sam 屏幕上的 GridView 仍會將可編輯 GridView 行中的產品名稱顯示爲“Chai”。 提交 Jisun 更改幾秒後,Sam 會將類別更新爲 Condiments 並單擊“更新”。 這將導致 UPDATE 發送到數據庫的語句,該語句將產品名稱設置爲“Chai”,以及 CategoryID 相應的飲料類別 ID 等。 已覆蓋 Jisun 對產品名稱所做的更改。 圖 1 以圖形方式描繪了這一系列事件。

當兩個用戶同時更新記錄時,可能會有一個用戶的更改覆蓋另一個用戶

圖 1:當兩個用戶同時更新記錄時,一個用戶的更改可能會覆蓋另一個用戶 (單擊以查看全尺寸圖像)

同樣,當兩個用戶訪問頁面時,一個用戶可能會在另一個用戶刪除記錄時更新記錄。 或者,當用戶加載頁面和單擊“刪除”按鈕時,其他用戶可能修改了該記錄的內容。

有三種可用的 併發控制 策略:

  • 不執行任何操作 - 如果併發用戶正在修改同一記錄,則讓上次提交獲勝 (默認行爲)
  • 樂觀併發 - 假設雖然現在和以後可能會發生併發衝突,但大多數此類衝突都不會出現:因此,如果發生衝突,只需通知用戶無法保存其更改,因爲其他用戶修改了相同的數據
  • 悲觀 併發 - 假設併發衝突是常見的,並且用戶不會容忍被告知由於其他用戶的併發活動而無法保存更改:因此,當用戶開始更新記錄時,將其鎖定,從而阻止任何其他用戶編輯或刪除該記錄,直到用戶提交其修改

到目前爲止,我們的所有教程都使用了默認併發解析策略,即我們讓最後一次寫入獲勝。 本教程介紹如何實現樂觀併發控制。

 備註

本系列教程不會介紹悲觀併發示例。 很少使用悲觀併發,因爲此類鎖(如果未正確放棄)可以防止其他用戶更新數據。 例如,如果用戶鎖定記錄進行編輯,然後在解鎖前一天離開,則其他用戶將無法更新該記錄,直到原始用戶返回並完成其更新。 因此,在使用悲觀併發的情況下,通常會有一個超時,如果達到,則會取消鎖。 票務銷售網站在用戶完成訂單流程時鎖定特定座位位置,是悲觀併發控制的示例。

步驟 1:查看如何實現樂觀併發

樂觀併發控制的工作原理是確保更新或刪除的記錄與更新或刪除進程啓動時的值相同。 例如,單擊可編輯 GridView 中的“編輯”按鈕時,記錄的值將從數據庫中讀取,並在 TextBoxes 和其他 Web 控件中顯示。 GridView 保存這些原始值。 稍後,在用戶進行更改並單擊“更新”按鈕後,原始值加上新值將發送到業務邏輯層,然後向下發送到數據訪問層。 如果用戶開始編輯的原始值與數據庫中的值相同,則數據訪問層必鬚髮出 SQL 語句,該語句只會更新記錄。 圖 2 描述了此事件序列。

若要使更新或刪除成功,原始值必須等於當前數據庫值

圖 2:若要使更新或刪除成功,原始值必須與當前數據庫值相等, (單擊以查看全尺寸圖像)

可通過各種方法來實現樂觀併發 (請參閱 Peter A. Bromberg 的 樂觀併發更新邏輯 ,簡要了解) 的多個選項。 ADO.NET 類型化數據集提供一個實現,只需勾選複選框的刻度即可配置。 爲 Typed DataSet 中的 TableAdapter 啓用樂觀併發會增強 TableAdapter 的 UPDATE 和 DELETE 語句,以包含子句中 WHERE 所有原始值的比較。 例如,以下 UPDATE 語句僅噹噹前數據庫值等於更新 GridView 中的記錄時最初檢索的值時,纔會更新產品的名稱和價格。 @ProductName參數@UnitPrice包含用戶輸入的新值,而@original_ProductName包含@original_UnitPrice單擊“編輯”按鈕時最初加載到 GridView 中的值:

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

 備註

此 UPDATE 語句已簡化爲可讀性。 實際上,將更涉及子WHERE句中的簽入,UnitPrice因爲UnitPrice可以包含 NULL s 並檢查是否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 ,並添加新名爲 NorthwindOptimisticConcurrencyDataSet 的數據集。 正如我們在第一個教程中看到的那樣,這樣做會將新的 TableAdapter 添加到 Typed DataSet,從而自動啓動 TableAdapter 配置嚮導。 在第一個屏幕中,系統會提示指定要連接到的數據庫 - 使用 NORTHWNDConnectionString 設置從 Web.config中連接到同一 Northwind 數據庫。

連接到同一 Northwind 數據庫

圖 3:連接到同一 Northwind 數據庫 (單擊以查看全尺寸圖像)

接下來,我們將提示如何查詢數據:通過即席 SQL 語句、新的存儲過程或現有存儲過程。 由於我們在原始 DAL 中使用了即席 SQL 查詢,因此也在此處使用此選項。

指定要使用即席 SQL 語句檢索的數據

圖 4:指定要使用即席 SQL 語句檢索的數據 (單擊以查看全尺寸圖像)

在以下屏幕上,輸入用於檢索產品信息的 SQL 查詢。 讓我們使用原始 DAL 中用於 Products TableAdapter 的完全相同的 SQL 查詢,該查詢返回所有 Product 列以及產品的供應商和類別名稱:

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

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

圖 5:使用原始 DAL 中的 TableAdapter 的相同 SQL 查詢 Products (單擊以查看全尺寸圖像)

轉到下一個屏幕之前,請單擊“高級選項”按鈕。 若要讓此 TableAdapter 採用樂觀併發控制,只需選中“使用樂觀併發”複選框即可。

通過檢查“使用樂觀併發”CheckBox 啓用樂觀併發控制

圖 6:通過選中“使用樂觀併發”CheckBox (單擊以查看全尺寸圖像) 啓用樂觀併發控制

最後,指示 TableAdapter 應使用填充 DataTable 和返回 DataTable 的數據訪問模式;還指示應創建 DB 直接方法。 將返回 DataTable 模式的方法名稱從 GetData 更改爲 GetProducts,以鏡像我們在原始 DAL 中使用的命名約定。

讓 TableAdapter 利用所有數據訪問模式

圖 7:讓 TableAdapter 利用所有數據訪問模式 (單擊以查看全尺寸圖像)

完成嚮導後,數據集設計器將包含強類型 Products DataTable 和 TableAdapter。 花點時間將 DataTable 重命名爲ProductsOptimisticConcurrency,可以通過右鍵單擊 DataTable Products 的標題欄並選擇上下文菜單中的“重命名”來執行此操作。

已將 DataTable 和 TableAdapter 添加到類型化數據集

圖 8:已將 DataTable 和 TableAdapter 添加到類型化數據集 (單擊以查看全尺寸圖像)

若要查看 TableAdapter ((使用樂觀併發) 和DELETE不) 的產品 TableAdapter ()之間的差異UPDATEProductsOptimisticConcurrency,請單擊 TableAdapter 並轉到okno Vlastnosti。 在DeleteCommandUpdateCommand屬性的CommandText子屬性中,可以看到在調用 DAL 更新或刪除相關方法時發送到數據庫的實際 SQL 語法。 對於 TableAdapter,ProductsOptimisticConcurrencyDELETE使用的語句爲:

SQL
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 的語句更簡單:

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

可以看到, WHERE 使用樂觀併發的 TableAdapter 語句中的 DELETE 子句包括每個 Product 表的現有列值與 GridView (或 DetailsView 或 FormView) 上次填充時的原始值之間的比較。 由於除 ProductNameDiscontinued 之外ProductID的所有字段都可以包含NULL值,因此包括其他參數和檢查,以便正確比較NULL子句中的WHERE值。

我們不會將任何其他 DataTable 添加到本教程中啓用樂觀併發的 DataSet,因爲我們的 ASP.NET 頁僅提供更新和刪除產品信息。 但是,我們仍然需要將 GetProductByProductID(productID) 方法添加到 ProductsOptimisticConcurrency TableAdapter。

爲此,請右鍵單擊 TableAdapter 的標題欄, (Fill 右側的區域和 GetProducts 方法名稱) 並選擇上下文菜單中的“添加查詢”。 這將啓動 TableAdapter 查詢配置嚮導。 與 TableAdapter 的初始配置一樣,選擇使用即席 SQL 語句創建 GetProductByProductID(productID) 方法 (請參閱圖 4) 。 由於該方法 GetProductByProductID(productID) 返回有關特定產品的信息,因此指示此查詢是 SELECT 返回行的查詢類型。

將查詢類型標記爲“返回行的 SELECT”

圖 9:將查詢類型標記爲“返回行”SELECT (單擊以查看全尺寸圖像)

在下一個屏幕上,系統會提示 SQL 查詢使用,並預加載 TableAdapter 的默認查詢。 擴充現有查詢以包含子句 WHERE ProductID = @ProductID,如圖 10 所示。

將 WHERE 子句添加到預加載查詢以返回特定產品記錄

圖 10:向預加載的查詢添加子 WHERE 句以返回特定產品記錄 (單擊以查看全尺寸圖像)

最後,將生成的方法名稱更改爲 FillByProductID 和 GetProductByProductID

將方法重命名爲 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現在需要十個輸入參數:原始ProductID、、ProductNameSupplierIDCategoryIDUnitPriceQuantityPerUnitUnitsOnOrderUnitsInStock、、和。 ReorderLevelDiscontinued 它在發送到數據庫的語句的子句DELETE中使用WHERE這些附加輸入參數的值,僅當數據庫的當前值映射到原始記錄時,才刪除指定的記錄。

雖然在批處理更新模式中使用的 TableAdapter Update 方法的方法簽名尚未更改,但記錄原始值和新值所需的代碼已更改。 因此,讓我們創建一個新的業務邏輯層類,而不是嘗試將已啓用樂觀併發的 DAL 與現有 ProductsBLL 類配合使用。

將名爲ProductsOptimisticConcurrencyBLL文件夾的App_Code類添加到BLL文件夾中。

將 ProductsOptimisticConcurrencyBLL 類添加到 BLL 文件夾

圖 12:將 ProductsOptimisticConcurrencyBLL 類添加到 BLL 文件夾

接下來,將以下代碼添加到 ProductsOptimisticConcurrencyBLL 類:

C#
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() 的方法,並返回一個ProductsOptimisticConcurrencyDataTableProductsOptimisticConcurrencyRow對象,該對象使用數據庫中每個產品記錄的實例填充。

使用具有樂觀併發的 DB 直接模式刪除產品

對使用樂觀併發的 DAL 使用 DB 直接模式時,必須傳遞新值和原始值。 若要刪除,沒有新值,因此只需傳入原始值。 在 BLL 中,我們必須接受所有原始參數作爲輸入參數。 讓我們 DeleteProduct 讓類中的 ProductsOptimisticConcurrencyBLL 方法使用 DB 直接方法。 這意味着此方法需要將所有十個產品數據字段作爲輸入參數,並將其傳遞給 DAL,如以下代碼所示:

C#
[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

使用具有樂觀併發的 Batch 更新模式更新產品

如前所述,無論是否採用樂觀併發,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 實例

但是,由於步驟 1 中填充的內容直接從數據庫填充,因此 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_ColumnName原始值。 接下來,將新參數值分配給 ProductsOptimisticConcurrencyRow 其中,最後調用 Update 該方法,傳入 DataRow。

以下代碼顯示了 UpdateProduct 接受所有產品數據字段作爲輸入參數的重載。 雖然此處未顯示, ProductsOptimisticConcurrencyBLL 但本教程下載中包含的類還包含一個 UpdateProduct 重載,僅接受產品的名稱和價格作爲輸入參數。

C#
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 對象 (單擊以查看全尺寸圖像)

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

配置 ObjectDataSource 控件的屬性

完成嚮導後,ObjectDataSource 的聲明性標記應如下所示:

ASP.NET
<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集合包含類DeleteProduct方法中ProductsOptimisticConcurrencyBLL十個Parameter輸入參數中的每個實例。 同樣,集合 UpdateParameters 包含 Parameter 每個輸入參數的 UpdateProduct實例。

對於涉及數據修改的前面的教程,我們此時會刪除 ObjectDataSource OldValuesParameterFormatString 的屬性,因爲此屬性指示 BLL 方法需要傳入舊 (或原始) 值以及新值。 此外,此屬性值指示原始值的輸入參數名稱。 由於我們將原始值傳入 BLL, 因此不要 刪除此屬性。

 備註

屬性的值 OldValuesParameterFormatString 必須映射到需要原始值的 BLL 中的輸入參數名稱。 由於我們命名這些參數original_productNameoriginal_supplierID,等等,因此可以將屬性值保留OldValuesParameterFormatString爲 original_{0} a0/>。 但是,如果 BLL 方法的輸入參數具有名稱,old_productNameold_supplierID等等,則需要將OldValuesParameterFormatString屬性更新爲 old_{0}

需要進行最後一個屬性設置,以便 ObjectDataSource 正確將原始值傳遞給 BLL 方法。 ObjectDataSource 具有一個 ConflictDetection 屬性 ,該屬性可分配給 以下兩個值之一

  • OverwriteChanges - 默認值;不將原始值發送到 BLL 方法的原始輸入參數
  • CompareAllValues - 將原始值發送到 BLL 方法;使用樂觀併發時選擇此選項

花點時間將屬性設置爲 ConflictDetectionCompareAllValues

配置 GridView 的屬性和字段

正確配置 ObjectDataSource 的屬性後,讓我們注意如何設置 GridView。 首先,由於我們希望 GridView 支持編輯和刪除,因此單擊 GridView 智能標記中的“啓用編輯”和“啓用刪除”複選框。 這將添加一個 CommandField,其 ShowEditButton 兩 ShowDeleteButton 者均設置爲 true

綁定到 ProductsOptimisticConcurrencyDataSource ObjectDataSource 時,GridView 包含每個產品數據字段的字段。 雖然可以編輯此類 GridView,但用戶體驗是可以接受的。 和 CategoryIDSupplierID BoundFields 將呈現爲 TextBox,要求用戶輸入相應的類別和供應商作爲 ID 號。 數字字段沒有格式設置,也沒有驗證控件,以確保已提供產品名稱,並且單價、庫存單位、訂單單位和重新排序級別值都是正確的數值,並且大於或等於零。

如我們在“將驗證控件添加到編輯和插入接口”和“自定義數據修改接口”教程中所述,可以通過將 BoundFields 替換爲 TemplateFields 來自定義用戶界面。 我通過以下方式修改了此 GridView 及其編輯界面:

  • 刪除了ProductID和 SupplierNameCategoryName BoundFields
  • 將 ProductName BoundField 轉換爲 TemplateField 並添加了 RequiredFieldValidation 控件。
  • 將 CategoryID And SupplierID BoundFields 轉換爲 TemplateFields,並調整了編輯界面以使用 DropDownLists 而不是 TextBoxes。 在這些 TemplateFields 中 ItemTemplates, CategoryName 將顯示數據字段和數據 SupplierName 字段。
  • UnitPrice和 UnitsInStockUnitsOnOrderReorderLevel BoundFields 轉換爲 TemplateFields,並添加了 CompareValidator 控件。

由於我們已經瞭解瞭如何在前面的教程中完成這些任務,因此我將在此處列出最終聲明性語法,並將實現保留爲實踐。

ASP.NET
<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 或Delete()方法時,ObjectDataSource Update() 會嘗試將 GridView 的原始值複製到相應的Parameter實例中。 有關此過程的圖形表示形式,請參閱圖 2。

具體而言,每次將數據綁定到 GridView 時,都會在雙向數據綁定語句中分配 GridView 的原始值。 因此,必須通過雙向數據綁定捕獲所需的原始值,並且它們以可轉換格式提供。

若要查看爲什麼這一點很重要,請花點時間在瀏覽器中訪問我們的頁面。 如預期的那樣,GridView 列出每個產品,其中最左側列中帶有“編輯和刪除”按鈕。

產品在 GridView 中列出

圖 14:產品列在 GridView (單擊以查看全尺寸圖像)

如果單擊任何產品的“刪除”按鈕,則會引發 a 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雙向數據綁定語句,但這可以放置在屬性設置爲falseVisible標籤 Web 控件中。 可以在 ItemTemplate 中使用以下標記:
ASP.NET
<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>
  • Remove the currency formatting from the ItemTemplate, using <%# Bind("UnitPrice") %>. 在 GridView 的事件處理程序中 RowDataBound ,以編程方式訪問顯示值並將其 UnitPrice 屬性設置爲 Text 格式化版本的標籤 Web 控件。
  • 將 UnitPrice 格式保留爲貨幣。 在 GridView 的事件處理程序中RowDeleting,使用實際十進制Decimal.Parse值替換現有原始UnitPrice值 ($19.95) 。 我們瞭解瞭如何在 ASP.NET Page 教程中處理 BLL 和DAL-Level異常的事件處理程序中RowUpdating完成類似操作。

對於我的示例,我選擇採用第二種方法,添加隱藏的標籤 Web 控件,其 Text 屬性是綁定到未格式化 UnitPrice 值的雙向數據。

解決此問題後,再次嘗試單擊任何產品的“刪除”按鈕。 這一次,你將獲得 InvalidOperationException ObjectDataSource 嘗試調用 BLL UpdateProduct 方法的時間。

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

圖 16:ObjectDataSource 找不到具有要發送的輸入參數的方法 (單擊以查看全尺寸圖像)

查看異常的消息,很明顯,ObjectDataSource 想要調用包含original_CategoryNameoriginal_SupplierName輸入參數的 BLL DeleteProduct 方法。 這是因爲ItemTemplate,s for the CategoryID and SupplierID TemplateFields 當前包含具有和SupplierName數據字段的CategoryName雙向 Bind 語句。 相反,我們需要包含Bind包含和數據字段的CategoryIDSupplierID語句。 爲此,請將現有 Bind 語句替換爲Eval語句,然後添加隱藏的 Label 控件,其Text屬性使用雙向數據綁定綁定到CategoryIDSupplierID數據字段,如下所示:

ASP.NET
<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 返回到其預編輯狀態,並將“柴茶”作爲新產品名稱。

但是,在其他瀏覽器窗口實例中,產品名稱 TextBox 仍顯示“Chai”。 在此第二個瀏覽器窗口中,將更新 UnitPrice 爲 25.00。 如果沒有樂觀併發支持,在第二個瀏覽器實例中單擊更新會將產品名稱改回“Chai”,從而覆蓋第一個瀏覽器實例所做的更改。 但是,在採用樂觀併發的情況下,單擊第二個瀏覽器實例中的“更新”按鈕會導致 DBConcurrencyException

檢測到併發衝突時,將引發 DBConcurrencyException

圖 17:檢測到併發衝突時,將引發 (DBConcurrencyException單擊以查看全尺寸圖像)

DBConcurrencyException僅當使用 DAL 的批處理更新模式時纔會引發該模式。 DB 直接模式不會引發異常,它只是表示沒有受影響的行。 爲了說明這一點,請同時將兩個瀏覽器實例的 GridView 返回到其預編輯狀態。 接下來,在第一個瀏覽器實例中,單擊“編輯”按鈕,將產品名稱從“柴茶”更改爲“Chai”,然後單擊“更新”。 在第二個瀏覽器窗口中,單擊 Chai 的“刪除”按鈕。

單擊“刪除”後,GridView 將調用 ObjectDataSource 的方法,而 ObjectDataSource Delete() 會向下 ProductsOptimisticConcurrencyBLL 調用類 DeleteProduct 的方法,並傳遞原始值。 第二個瀏覽器實例的原始 ProductName 值爲“Chai Tea”,它與數據庫中的當前 ProductName 值不匹配。 DELETE因此,向數據庫發出的語句會影響零行,因爲該子句滿足的數據庫WHERE中沒有記錄。 該方法 DeleteProduct 返回 false ,ObjectDataSource 的數據將反彈到 GridView。

從最終用戶的角度來看,在第二個瀏覽器窗口中單擊“刪除”按鈕會導致屏幕閃爍,回來後,產品仍然存在,儘管現在它被列爲“Chai” (第一個瀏覽器實例) 的產品名稱更改。 如果用戶再次單擊“刪除”按鈕,“刪除”將成功,因爲 GridView 的原始 ProductName 值 (“Chai”) 現在與數據庫中的值匹配。

在這兩種情況下,用戶體驗遠非理想。 我們顯然不想在使用批處理更新模式時向用戶顯示異常的 DBConcurrencyException nitty-gritty 詳細信息。 使用 DB 直接模式時的行爲有點令人困惑,因爲用戶命令失敗,但沒有確切說明原因。

爲了修復這兩個問題,我們可以在頁面上創建標籤 Web 控件,該控件提供有關更新或刪除失敗的原因的說明。 對於批處理更新模式,我們可以確定 GridView 的後級別事件處理程序中是否 DBConcurrencyException 發生異常,並根據需要顯示警告標籤。 對於 DB 直接方法,我們可以檢查 BLL 方法 (的返回值,即 true 如果一行受到影響, false 否則) 並根據需要顯示信息性消息。

步驟 6:添加信息性消息,並在出現併發衝突時顯示它們

發生併發衝突時,顯示的行爲取決於 DAL 的批處理更新還是使用 DB 直接模式。 本教程使用這兩種模式,以及用於更新的批處理更新模式和用於刪除的 DB 直接模式。 若要開始,讓我們將兩個標籤 Web 控件添加到頁面,說明嘗試刪除或更新數據時發生併發衝突。 將“標籤”控件Visible和屬性設置爲 false;這將導致它們在每一頁訪問中隱藏,但那些以編程方式設置爲trueVisible屬性的特定EnableViewState頁面訪問除外。

ASP.NET
<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 中的設計器應類似於圖 18。

已將兩個標籤控件添加到頁面

圖 18:已將兩個標籤控件添加到頁面 (單擊以查看全尺寸圖像)

在這些標籤 Web 控件準備就緒後,我們已準備好檢查如何確定何時發生併發衝突,此時可以將相應的標籤 Visible 的屬性設置爲 true顯示信息性消息。

處理更新時的併發衝突

讓我們首先了解如何在使用批處理更新模式時處理併發衝突。 由於此類與批處理更新模式衝突會導致 DBConcurrencyException 引發異常,因此我們需要將代碼添加到 ASP.NET 頁,以確定更新過程中是否 DBConcurrencyException 發生異常。 如果是這樣,我們應該向用戶顯示一條消息,說明更改未保存,因爲其他用戶在開始編輯記錄時和單擊“更新”按鈕時修改了相同的數據。

正如我們在 ASP.NET Page 教程的處理 BLL 和DAL-Level異常 中看到的那樣,可以在數據 Web 控件的後期事件處理程序中檢測和禁止此類異常。 因此,我們需要爲 GridView RowUpdated 的事件創建事件處理程序,用於檢查是否已 DBConcurrencyException 引發異常。 此事件處理程序將傳遞對更新過程中引發的任何異常的引用,如以下事件處理程序代碼所示:

C#
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:用戶更新因併發衝突而丟失 (單擊以查看全尺寸圖像)

 備註

或者,我們可以通過將傳入GridViewUpdatedEventArgs對象的屬性設置爲 KeepInEditMode true,而不是將 GridView 返回到預編輯狀態,而不是將 GridView 保留爲其編輯狀態。 但是,如果採用此方法,請務必通過調用 DataBind() 其方法) 將數據重新綁定到 GridView (,以便將其他用戶的值加載到編輯界面中。 本教程提供的可供下載的代碼在事件處理程序中 RowUpdated 註釋掉了這兩行代碼;只需取消註釋這些代碼行,使 GridView 在併發衝突後仍處於編輯模式。

在刪除時響應併發衝突

使用 DB 直接模式時,如果出現併發衝突,則不會引發異常。 相反,數據庫語句隻影響任何記錄,因爲 WHERE 子句與任何記錄不匹配。 BLL 中創建的所有數據修改方法都經過設計,以便返回一個布爾值,該值指示它們是否僅影響一條記錄。 因此,若要確定刪除記錄時是否發生了併發衝突,我們可以檢查 BLL 方法的 DeleteProduct 返回值。

可以通過傳遞給事件處理程序的對象的屬性,在 ObjectDataSource 的後級事件處理程序 ReturnValue 中檢查 BLL 方法的 ObjectDataSourceStatusEventArgs 返回值。 由於我們有興趣確定方法中的Deleted返回值DeleteProduct,因此我們需要爲 ObjectDataSource 的事件創建事件處理程序。 該 ReturnValue 屬性的類型 object ,可以是 null 引發異常並且方法在返回值之前中斷。 因此,我們首先應確保 ReturnValue 屬性不是 null 布爾值,並且是布爾值。 假設此檢查通過,我們將顯示 DeleteConflictMessage Label 控件(如果爲 ReturnValuefalse)。 這可以通過使用以下代碼來實現:

C#
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 技術合作。 斯科特是一名獨立顧問、教練員和作家。 他的最新書是 山姆斯教自己在24小時內 ASP.NET 2.0。 他可以通過他的博客訪問[email protected],也可以通過他的博客找到http://ScottOnWriting.NET

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章