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

作者 :Scott Mitchell

下載 PDF

在本教學課程中,我們會檢閱開放式並行訪問控制的基本概念,然後探索如何使用 SqlDataSource 控制項來實作它。

簡介

在上述教學課程中,我們已檢查如何將插入、更新和刪除功能新增至 SqlDataSource 控制件。 簡單地說,為了提供這些功能,我們需要在控件、 或 屬性中指定對應的INSERTUPDATE、 或 DELETEDeleteCommand SQL 語句,以及 、 UpdateCommandUpdateParametersDeleteParameters 集合中的InsertParameters適當參數。InsertCommand 雖然可以手動指定這些屬性和集合,但 [設定數據源精靈] 的 [進階] 按鈕會提供 [產生 INSERT] 、 UPDATEDELETE [語句] 複選框,以根據 SELECT 語句自動建立這些語句。

除了 [產生 INSERTUPDATEDELETE 語句] 複選框之外,[進階 SQL 產生選項] 對話框也包含 [使用開放式並行存取] 選項 (請參閱圖 1) 。 核取時, WHERE 自動產生的 UPDATEDELETE 語句中的 子句會修改為只在使用者上次將數據載入方格之後尚未修改基礎資料庫數據時執行更新或刪除。

您可以從 [進階 SQL 產生選項] 對話框新增開放式並行存取支援

圖 1:您可以從 [進階 SQL 產生選項] 對話框新增開放式並行支援

回到 實作開放式並行存取 教學課程,我們檢查了開放式並行訪問控制的基本概念,以及如何將其新增至 ObjectDataSource。 在本教學課程中,我們將重新了解開放式並行存取控制的基本概念,然後探索如何使用 SqlDataSource 來實作它。

開放式並行存取的回顧

對於允許多個同時編輯或刪除相同數據的 Web 應用程式,可能會有一位使用者不小心覆寫另一個變更的可能性。 在 實作開放式並行存取 教學課程中,我提供了下列範例:

假設兩個使用者 Jisun 和 Sam 都造訪應用程式中的頁面,可讓訪客透過 GridView 控件更新和刪除產品。 兩者都會同時按兩下 Chai 的 [編輯] 按鈕。 Jisun 會將產品名稱變更為 Chai Tea,然後按兩下 [更新] 按鈕。 net result 是 UPDATE 傳送至資料庫的語句, 它會將所有產品 可更新的欄位設定 (即使 Jisun 只更新一個字段, ProductName) 。 此時,資料庫具有 Chai Tea、類別為[咖啡]、供應商[飲料] 等值,依此類提此特定產品。 不過,Sam 上的 GridView 畫面仍會將可編輯的 GridView 數據列中的產品名稱顯示為 Chai。 在認可 Jisun 變更之後幾秒鐘,Sam 會將類別更新為 Condiments,然後按兩下 [更新]。 這會導致 UPDATE 將產品名稱設定為 Chai、 CategoryID 對應 Condiments 類別標識碼等的 語句傳送至資料庫。 已覆寫產品名稱的 Jisun 變更。

圖 2 說明此互動。

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

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

若要防止此案例展開,必須實作 並行控制 的形式。 開放式並行 存取本教學課程的重點在於假設雖然目前每次都可能會發生並行衝突,但大部分的時間都不會發生這類衝突。 因此,如果發生衝突,開放式並行訪問控制只會通知用戶無法儲存其變更,因為其他使用者已修改相同的數據。

注意

對於假設有許多並行衝突的應用程式,或如果這類衝突無法容忍,則可以改用封閉式並行控制。 請參閱 實作開放式並行存取 教學課程,以深入瞭解封閉式並行控制。

開放式並行存取控制的運作方式是確保更新或刪除的記錄具有與更新或刪除程序啟動時相同的值。 例如,按兩下可編輯 GridView 中的 [編輯] 按鈕時,記錄的值會從資料庫讀取,並顯示在 TextBoxes 和其他 Web 控制件中。 這些原始值會由 GridView 儲存。 之後,當使用者進行變更並按兩下 [更新] 按鈕之後, UPDATE 所使用的語句必須考慮原始值加上新的值,而且只有在用戶開始編輯的原始值與資料庫中的值相同時,才會更新基礎資料庫記錄。 圖 3 描述此事件序列。

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

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

實作開放式並行存取 (有各種方法,請參閱 Peter A。Bromberg開放式並行存取更新邏輯 ,如需) 幾個選項的簡短探討。 SqlDataSource (以及數據存取層中使用的 ADO.NET 具型別數據集所使用的技術,) 增強 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

如本教學課程中所見,使用 SqlDataSource 啟用開放式並行存取控制就像核取複選框一樣簡單。

步驟 1:建立支持開放式並行存取的 SqlDataSource

從開啟OptimisticConcurrency.aspxSqlDataSource資料夾的頁面開始。 將 SqlDataSource 控制件從 [工具箱] 拖曳到 Designer,並將其 ID 屬性設定為 ProductsDataSourceWithOptimisticConcurrency。 接下來,按兩下控件智慧標記中的 [設定資料源] 連結。 從精靈的第一個畫面中,選擇使用 , NORTHWINDConnectionString 然後按 [下一步]。

選擇 [使用 NORTHWINDConnectionString]

圖 4:選擇使用 NORTHWINDConnectionString (按下即可檢視完整大小的影像)

在此範例中,我們將新增 GridView,讓使用者編輯 Products 數據表。 因此,從 [設定選取語句] 畫面, Products 從下拉式清單中選擇數據表,然後選取 ProductIDProductNameUnitPriceDiscontinued 數據行,如圖 5 所示。

從 Products 數據表中,傳回 ProductID、ProductName、UnitPrice 和已中止的數據行

圖 5:從 Products 數據表中,傳回 ProductIDProductNameUnitPriceDiscontinued 數據行 (按兩下即可檢視完整大小的影像)

挑選數據行之後,按兩下 [進階] 按鈕,以顯示 [進階 SQL 產生選項] 對話框。 核取 [產生 INSERTUPDATE和 語句] 和 DELETE [使用開放式並行存取] 複選框,然後按兩下 [確定] (請參閱圖 1 以取得螢幕快照) 。 按兩下 [下一步],然後按兩下 [完成] 來完成精靈。

完成 [設定數據源精靈] 之後,請花一點時間檢查產生的 DeleteCommandUpdateCommand 屬性和 和 DeleteParametersUpdateParameters 集合。 若要這樣做,最簡單的方式是按兩下左下角的 [來源] 索引標籤,以查看頁面的宣告式語法。 您可以在該處找到下列 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 參數:

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

除了增強 WHEREDeleteCommand 屬性的 UpdateCommand 子句, (並將其他參數新增至個別參數集合) ,選取 [使用開放式並行存取] 選項可調整其他兩個屬性:

當數據 Web 控制項叫用 SqlDataSource s Update()Delete() 方法時,它會傳入原始值。 如果 SqlDataSource 的 ConflictDetection 屬性設定為 CompareAllValues,這些原始值會新增至 命令。 屬性 OldValuesParameterFormatString 提供用於這些原始值參數的命名模式。 [設定數據源] 精靈會使用 original_{0},並據以命名 和 DeleteCommand 屬性和UpdateParametersDeleteParameters集合中的每個UpdateCommand原始參數。

注意

因為我們不使用 SqlDataSource 控制件的插入功能,所以您可以隨意移除 InsertCommand 屬性及其 InsertParameters 集合。

正確處理NULL

不幸的是,使用開放式並行存取時,[設定數據源精靈] 自動產生的增強 UPDATEDELETE 語句 無法 與包含 NULL 值的記錄搭配使用。 若要查看原因,請考慮我們的 SqlDataSource s 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

數據表 UnitPrice 中的數據 Products 行可以有 NULL 值。 如果特定記錄具有 NULL 的值 UnitPriceWHERE 子句部分 [UnitPrice] = @original_UnitPrice 一律會評估為 False,因為一 會傳 NULL = NULL 回 False。 因此,無法編輯或刪除包含 NULL 值的記錄,因為 UPDATEDELETE 語句 WHERE 子句不會傳回任何要更新或刪除的數據列。

注意

此錯誤最初在 SqlDataSource 的 2004 年 6 月回報給 Microsoft 產生不正確的 SQL 語句 ,並已排程在下一版的 ASP.NET 中修正。

若要修正此問題,我們必須針對可以有NULL的所有數據行,手動更新 WHEREDeleteCommand 屬性中的 UpdateCommand 子句。 一般而言,請變更 [ColumnName] = @original_ColumnName 為:

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

此修改可以直接透過宣告式標記、透過來自 屬性視窗 的 UpdateQuery 或 DeleteQuery 選項,或透過 [設定數據源精靈] 中 [指定自定義 SQL 語句或預存程式] 選項中的 UPDATE 和 DELETE 索引標籤來進行。 同樣地,必須針對 和 DeleteCommandWHERE句中UpdateCommand可包含NULL值的一個數據行進行這項修改。

將此套用至我們的範例會產生下列已 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

設定為支持開放式並行存取的 SqlDataSource 後,所有保留專案都是將數據 Web 控制項新增至利用此並行控制的頁面。 在本教學課程中,讓我們新增 GridView,以提供編輯和刪除功能。 若要達成此目的,請將 GridView 從 [工具箱] 拖曳到 Designer,並將其設定IDProducts。 從 GridView 智慧標記,將它系結至步驟 1 中新增的 ProductsDataSourceWithOptimisticConcurrency SqlDataSource 控制件。 最後,檢查智慧標記中的 [啟用編輯] 和 [啟用刪除] 選項。

將 GridView 系結至 SqlDataSource 並啟用編輯和刪除

圖 6:將 GridView 系結至 SqlDataSource 並啟用編輯和刪除 (按兩下即可檢視大小完整的影像)

新增 GridView 之後,請移除 ProductID BoundField、將 BoundField s HeaderText 屬性變更ProductName為 Product,以及更新 UnitPrice BoundField,使其屬性只是 Price 來設定其HeaderText外觀。 在理想情況下,我們會增強編輯介面,以包含值的 RequiredFieldValidator,以及值 (CompareValidator ProductNameUnitPrice ,以確保它是正確格式化的數值) 。 如需自定義 GridView 編輯介面的深入探討,請參閱 自定義數據修改介面 教學課程。

注意

必須啟用 GridView 的檢視狀態,因為從 GridView 傳遞至 SqlDataSource 的原始值會儲存在檢視狀態中。

對 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 頁面。 按兩個瀏覽器中第一個產品的 [編輯] 按鈕。 在一個瀏覽器中,變更產品名稱,然後按兩下 [更新]。 瀏覽器會回傳,GridView 會返回其預先編輯模式,顯示剛編輯記錄的新產品名稱。

在第二個瀏覽器視窗中,變更價格 (,但將產品名稱保留為其原始值) ,然後按兩下 [更新]。 在回傳時,網格線會返回其預先編輯模式,但不會記錄價格的變更。 第二個瀏覽器會顯示與第一個具有舊價格的新產品名稱相同的值。 第二個瀏覽器視窗中所做的變更已遺失。 此外,變更會以無訊息方式遺失,因為沒有任何例外狀況或訊息指出剛發生並行違規。

第二個瀏覽器視窗中的變更以無訊息方式遺失

圖 7:第二個瀏覽器視窗中的變更在單擊以 檢視完整大小的影像 (無訊息遺失)

第二個瀏覽器變更未認可的原因,是因為 UPDATE 語句 s 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 不會影響任何記錄。

注意

刪除的運作方式相同。 開啟兩個瀏覽器視窗後,請先編輯具有一個特定產品,然後儲存其變更。 在一個瀏覽器中儲存變更之後,請按下另一個瀏覽器中相同產品的 [刪除] 按鈕。 由於原始值與語句 s WHERE 子句中DELETE不相符,因此刪除會以無訊息方式失敗。

從第二個瀏覽器視窗中的使用者觀點,按兩下 [更新] 按鈕之後,方格會返回預先編輯模式,但其變更已遺失。 不過,沒有視覺意見反應表示其變更沒有黏附。 在理想情況下,如果使用者對並行違規的變更遺失,我們會通知使用者,而且或許會讓方格保持編輯模式。 讓我們看看如何完成這項作業。

步驟 3:判斷何時發生並行違規

由於並行違規會拒絕已進行的變更,因此當發生並行違規時,對使用者發出警示會相當好。 若要警示使用者,讓我們將標籤 Web 控制項新增至名為 ConcurrencyViolationMessage 的網頁頂端,其 Text 屬性會顯示下列訊息:您嘗試更新或刪除另一位使用者同時更新的記錄。 請檢閱其他使用者的變更,然後重做您的更新或刪除。 將標籤的 CssClass 屬性設定為 Warning,這是定義在 中 Styles.css 以紅色、斜體、粗體和大型字型顯示文字的 CSS 類別。 最後,將 [標籤] VisibleEnableViewState 屬性設定為 False。 除了我們明確將其屬性True設定Visible為的回傳以外,這隻會隱藏標籤。

將標籤新增至頁面以顯示警告

圖 8:將標籤控件新增至頁面以顯示警告 (按兩下即可檢視完整大小的影像)

執行更新或刪除時,GridView 的 RowUpdatedRowDeleted 事件處理程式會在其數據源控件執行要求的更新或刪除之後引發。 我們可以從這些事件處理程序判斷作業所影響的數據列數目。 如果零個數據列受到影響,我們想要顯示 ConcurrencyViolationMessage 標籤。

建立 RowUpdatedRowDeleted 事件的事件處理程式,並新增下列程序代碼:

Protected Sub Products_RowUpdated(sender As Object, e As GridViewUpdatedEventArgs) _
    Handles Products.RowUpdated
    If e.AffectedRows = 0 Then
        ConcurrencyViolationMessage.Visible = True
        e.KeepInEditMode = True
        ' Rebind the data to the GridView to show the latest changes
        Products.DataBind()
    End If
End Sub
Protected Sub Products_RowDeleted(sender As Object, e As GridViewDeletedEventArgs) _
    Handles Products.RowDeleted
    If e.AffectedRows = 0 Then
        ConcurrencyViolationMessage.Visible = True
    End If
End Sub

在這兩個事件處理程式中 e.AffectedRows ,我們會檢查 屬性,如果屬性等於 0,請將 ConcurrencyViolationMessage Label s Visible 屬性設定為 True。 在事件處理程式中 RowUpdated ,我們也指示 GridView 將 屬性設定 KeepInEditMode 為 true,以保持編輯模式。 如此一來,我們需要將數據重新系結至方格,讓其他用戶的數據載入編輯介面。 這可藉由呼叫 GridView s DataBind() 方法來完成。

如圖 9 所示,使用這兩個事件處理程式時,每當發生並行違規時,就會顯示非常明顯的訊息。

出現並行違規時會顯示訊息

圖 9:[並行違規] (按兩下即可檢視大小完整影像)

摘要

建立多個並行使用者可能會編輯相同數據的 Web 應用程式時,請務必考慮並行控制選項。 根據預設,ASP.NET 數據 Web 控制項和數據源控件不會採用任何並行控制。 如本教學課程中所見,使用 SqlDataSource 實作開放式並行存取控件相當快速且容易。 SqlDataSource 會處理您在自動產生的 UPDATE 和 語句中新增增強WHERE子句的大部分工作,但處理值數據行有一些細微之處NULL,如正確處理NULL值一節DELETE所述。

本教學課程將結束對 SqlDataSource 的檢查。 我們的其餘教學課程會傳回使用 ObjectDataSource 和階層式架構來處理數據。

快樂的程序設計!

關於作者

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