使用 SqlDataSource 實作開放式同步存取 (C#)
在本教學課程中,我們將檢閱開放式同步存取控制的基本概念,然後探索如何使用 SqlDataSource 控制項來加以實作。
簡介
在上一個教學課程中,我們已了解如何為 SqlDataSource 控制項新增插入、更新和刪除功能。 簡單來說,若要提供這些功能,我們需要在控制項的 InsertCommand
、UpdateCommand
或 DeleteCommand
中指定對應的 INSERT
、UPDATE
或 DELETE
SQL 陳述式,並在 InsertParameters
、UpdateParameters
和 DeleteParameters
集合中指定適當的參數。 雖然這些屬性和集合可手動指定,[設定資料來源] 精靈的 [進階] 按鈕也提供 [產生 INSERT
、UPDATE
和 DELETE
陳述式] 核取方塊,可根據 SELECT
陳述式自動建立這些陳述式。
除了 [產生 INSERT
、UPDATE
和 DELETE
陳述式] 核取方塊之外,[進階 SQL 產生選項] 對話方塊也包含 [使用開放式同步存取] 選項 (請參閱圖 1)。 只要勾選,自動產生之 UPDATE
和 DELETE
陳述式中的 WHERE
子句就會做出修改,若使用者最後一次將資料載入方格後,基礎資料庫資料未遭到修改,就只會執行更新或刪除。
圖 1:您可以從 [進階 SQL 產生選項] 對話方塊新增 [開放式同步存取] 支援
在先前的實作開放式同步存取教學課程中,我們檢視了開放式同步存取控制的基本概念,以及如何將其新增至 ObjectDataSource。 在本教學課程中,我們將再次回顧開放式同步存取控制的基本概念,然後探索如何使用 SqlDataSource 來加以實作。
回顧開放式同步存取
在同時允許多名使用者編輯和刪除相同資料的 Web 應用程式中,其中一名使用者可能會不小心覆寫另一名使用者的變更。 在實作開放式同步存取教學課程中,我曾提供下列範例:
假設 Jisun 和 Sam 這兩名使用者同時造訪應用程式的某個頁面,而該應用程式可讓訪客透過 GridView 控制項來更新和刪除產品。 兩人幾乎在同一時間按下印度香料茶的 [編輯] 按鈕。 Jisun 將產品名稱更改為「印度香料奶茶」,並按一下 [更新] 按鈕。 這項操作會將 UPDATE
陳述式傳送至資料庫,進而設定產品的所有可更新欄位 (即使 Jisun 只更新 ProductName
欄位)。 此時,資料庫的此一特定產品會具有名為「印度香料奶茶」的值、名為「飲料」的類別,以及名為「異國飲品」的供應商等資訊。 然而,在 Sam 畫面上的 GridView,仍會在可編輯的 GridView 資料列中將產品名稱顯示為「印度香料茶」。 在 Jisun 認可變更的數秒後,Sam 將類別更新為「調味料」,並按一下 [更新]。 這項操作會將 UPDATE
陳述式傳送至資料庫,進而將產品名稱設為「印度香料茶」,並將 CategoryID
設為對應的「調味料」類別 ID。 Jisun 對產品名稱做出的變更將遭到覆寫。
圖 2 會說明此一互動。
圖 2:兩名使用者同時更新記錄時,其中一名使用者的變更可能會覆寫另一名使用者的變更 (按一下以檢視完整大小的影像)
若要避免發生這類型的情況,請務必實作任一形式的同步存取控制, 例如開放式同步存取。本教學課程的重點,在於假設雖然目前和後續有機會發生同步存取衝突,但這類衝突在多數情況下都不會發生。 因此,如果確實發生衝突,開放式同步存取控制只會告知使用者系統無法儲存其變更,因為另一名使用者已修改相同的資料。
注意
如果應用程式後續將發生許多同步存取衝突,或是無法容忍這類型的衝突,則可改用封閉式同步存取控制。 如需詳細探討封閉式同步存取控制,請參閱實作開放式同步存取教學課程。
開放式同步存取控制會確保遭到更新或刪除的記錄會保留在更新或刪除流程開始前的相同值。 舉例來說,在可編輯的 GridView 中按一下 [編輯] 按鈕,系統就會從資料庫讀取記錄的值,並將其顯示在 TextBox 和其他 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
首先,請打開 SqlDataSource
資料夾中的 OptimisticConcurrency.aspx
頁面。 將 SqlDataSource 控制項從 [工具箱] 拖曳至 [設計工具],並將其 ID
屬性設為 ProductsDataSourceWithOptimisticConcurrency
。 接著,請按一下控制項智慧標籤的 [設定資料來源] 連結。 在精靈的第一個畫面中,選擇使用 NORTHWINDConnectionString
,並按一下 [下一步]。
圖 4:選擇使用 NORTHWINDConnectionString
(按一下以檢視完整大小的影像)
在這個範例中,我們要新增可讓使用者編輯 Products
資料表的 GridView。 因此,請在 [設定 Select 陳述式] 畫面的下拉式清單中選擇 Products
資料表,然後選擇 ProductID
、ProductName
、UnitPrice
和 Discontinued
資料行,如圖 5 所示。
圖 5:從 Products
資料表傳回 ProductID
、ProductName
、UnitPrice
和 Discontinued
資料行 (按一下以檢視完整大小的影像)
挑選資料行後,請按一下 [進階] 按鈕以顯示 [進階 SQL 產生選項] 對話方塊。 勾選 [產生 INSERT
、UPDATE
和 DELETE
陳述式] 和 [使用開放式同步存取] 等核取方塊,然後按一下 [確定] (如需螢幕擷取畫面,請參閱圖 1)。 請依序按一下 [下一步] 和 [結束],以完成精靈。
完成 [設定資料來源] 精靈後,請花點時間檢查所產生的 DeleteCommand
和 UpdateCommand
屬性,以及 DeleteParameters
和 UpdateParameters
集合。 進行這項操作的最簡單方式,就是按一下右下角的 [來源] 索引標籤,以查看頁面的宣告式語法。 您可以在此看到下列 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>
除了擴增 UpdateCommand
和 DeleteCommand
屬性的 WHERE
子句 (並將額外的參數新增至個別參數集合) 之外,選擇 [使用開放式同步存取] 選項也會調整其他兩個屬性:
- 將
ConflictDetection
屬性 從OverwriteChanges
(預設值) 變更為CompareAllValues
- 將
OldValuesParameterFormatString
屬性 從 {0} (預設值) 變更為 original_{0}
資料 Web 控制項叫用 SqlDataSource 的 Update()
或 Delete()
方法時,將會傳入原始值。 如果 SqlDataSource 的 ConflictDetection
屬性設為 CompareAllValues
,這些原始值就會新增至命令。 OldValuesParameterFormatString
屬性會提供用於這些原始值參數的命名模式。 [設定資料來源] 精靈會使用 original_{0},並據此為 UpdateCommand
和 DeleteCommand
屬性 UpdateParameters
以及 DeleteParameters
集合中的每個原始參數命名。
注意
由於我們並未使用 SqlDataSource 控制項的插入功能,因此可自行移除 InsertCommand
屬性及其 InsertParameters
集合。
正確處理 NULL
值
遺憾的是,使用開放式同步存取時,[設定資料來源] 精靈自動產生的已擴增 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
子句不會傳回任何要更新或刪除的資料列。
注意
Microsoft 已在 2004 年 6 月透過 SqlDataSource 產生不正確的 SQL 陳述式回報這個錯誤,且計畫在下一版 ASP.NET 中做出修正。
若要修正這個問題,我們需要針對可能包含 NULL
值的所有資料行,手動更新 UpdateCommand
和 DeleteCommand
屬性中的 WHERE
子句。 整體而言,請將 [ColumnName] = @original_ColumnName
變更為:
(
([ColumnName] IS NULL AND @original_ColumnName IS NULL)
OR
([ColumnName] = @original_ColumnName)
)
這項修改可直接透過宣告式標記、透過 [屬性] 視窗的 UpdateQuery 或 DeleteQuery 選項,或透過 [設定資料來源] 精靈內 [指定自訂 SQL 陳述式或預存程序] 選項中的 [UPDATE] 和 [DELETE] 索引標籤進行。 當然,您必須針對 UpdateCommand
和 DeleteCommand
內 WHERE
子句中包含 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 從 [工具箱] 拖曳至 [設計工具],並將其 ID
設為 Products
。 請從 GridView 的智慧標籤將其繫結至步驟 1 新增的 ProductsDataSourceWithOptimisticConcurrency
SqlDataSource 控制項。 最後,請檢查智慧標籤的 [啟用編輯] 和 [啟用刪除] 選項。
圖 6:將 GridView 繫結至 SqlDataSource 並啟用編輯和刪除 (按一下以檢視完整大小的影像)
新增 GridView 後,請移除 ProductID
BoundField、將 ProductName
BoundField 的 HeaderText
屬性變更為 [產品],並更新 UnitPrice
BoundField 以讓其 HeaderText
屬性只包含 [價格],藉此設定其外觀。 在理想的情況下,我們會強化編輯介面,以包含適用於 ProductName
值的 RequiredFieldValidator,以及適用於 UnitPrice
值的 CompareValidator (藉此確保它是經過適當格式化的數值)。 如需深入了解如何自訂 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
陳述式的 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
陳述式的 WHERE
子句不相符,因此,刪除作業會在無訊息的情況下以失敗坐收。
從第二個瀏覽器視窗之終端使用者的觀點來看,按一下 [更新] 按鈕後,方格會回到編輯前的模式,但變更則會遺失。 然而,系統不會提供任何視覺化回饋來告知變更並未生效。 在理想的情況下,如果使用者變更因同步存取違規而遺失,我們會告知使用者此一情況,或許會讓方格保持在編輯模式下。 讓我們看看如何達到此一目的。
步驟 3:判斷同步存取違規的發生時間
由於同步存取違規會拒絕使用者進行的多項變更,因此,發生同步存取違規時,最好向使用者發出警示。 為了向使用者發出警示,我們要在名為 ConcurrencyViolationMessage
的頁面頂端新增標籤 Web 控制項,而其 Text
屬性會顯示下列訊息:您已嘗試更新或刪除遭另一名使用者同步更新的記錄。 請檢閱其他使用者的變更,然後重做您的更新或刪除。 將標籤控制項的 CssClass
屬性設為 [警告],這是在 Styles.css
中定義的 CSS 類別,會以紅色、斜體、粗體和大型字型顯示文字。 最後,請將標籤的 Visible
和 EnableViewState
屬性設為 false
。 這將隱藏標籤,只有我們明確將其 Visible
屬性設為 true
的回傳不在此限。
圖 8:將標籤控制項新增至頁面以顯示警告 (按一下以檢視完整大小的影像)
執行更新或刪除時,GridView 的 RowUpdated
和 RowDeleted
事件處理常式會在其資料來源控制項已執行所要求的更新或刪除後引發。 我們可以透過這些事件處理常式判斷作業所影響的資料列數量。 如果受到影響的資料列數量為零,我們會顯示 ConcurrencyViolationMessage
標籤。
請為 RowUpdated
和 RowDeleted
事件建立事件處理常式,並新增下列程式碼:
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;
}
在兩個事件處理常式中,我們要檢查 e.AffectedRows
屬性,若其等於 0,請將 ConcurrencyViolationMessage
標籤的 Visible
屬性設為 true
。 在 RowUpdated
事件處理常式中,我們也會將 KeepInEditMode
屬性設為 true,以指示 GridView 保持在編輯模式下。 這樣一來,我們就需將資料重新繫結至方格,好讓其他使用者的資料能載入編輯介面中。 這項作業可透過呼叫 GridView 的 DataBind()
方法完成。
如圖 9 所示,只要使用這兩個事件處理常式,每當發生同步存取違規時,系統就會顯示相當明顯的訊息。
圖 9:在發生同步存取違規時顯示的訊息 (按一下以檢視完整大小的影像)
摘要
建立可讓多名使用者同時編輯相同資料的 Web 應用程式時,請務必考慮使用同步存取控制選項。 根據預設,ASP.NET 資料 Web 控制項和資料來源控制項都不會採用任何同步存取控制。 如同本教學課程所示,使用 SqlDataSource 來實作開放式同步存取控制,為相對快速且簡單的因應之道。 SqlDataSource 可處理將擴增 WHERE
子句新增至 UPDATE
和 DELETE
陳述式的多數雜務,但如同<正確處理 NULL
值>一節所述,在處理 NULL
值資料行方面,需要留意一些微妙之處。
SqlDataSource 的部分已在本教學課程畫下句點。 其餘的教學課程將繼續討論如何使用 ObjectDataSource 和多層式架構使用資料。
祝您程式設計愉快!
關於作者
Scott Mitchell,七本 ASP/ASP.NET 書籍的作者和 4GuysFromRolla.com 創始人,自 1998 年以來便開始使用 Microsoft Web 技術。 Scott 擔任獨立顧問、講師和作家。 他的新書是 Sams Teach Yourself ASP.NET 2.0 in 24 Hours。 您可以透過 mitchell@4GuysFromRolla.com 或他的部落格 (可以在 http://ScottOnWriting.NET 找到) 與他聯繫。