使用 SqlDataSource 實作開放式同步存取 (C#)
在本教學課程中,我們會檢閱開放式並行存取控件的基本概念,然後探索如何使用 SqlDataSource 控件來實作它。
簡介
在上述教學課程中,我們檢查如何將插入、更新和刪除功能新增至 SqlDataSource 控制件。 簡單來說,為了提供這些功能,我們需要在控件的、 或屬性中指定對應的INSERT
、 UPDATE
或 DeleteCommand
DELETE
SQL 語句,以及、 UpdateParameters
和 DeleteParameters
集合中的InsertParameters
適當參數。 UpdateCommand
InsertCommand
雖然您可以手動指定這些屬性和集合,但 [設定數據源精靈] 的 [進階] 按鈕會提供 [產生 INSERT
、 UPDATE
和 DELETE
語句] 複選框,以根據 SELECT
語句自動建立這些語句。
除了 [產生 INSERT
、 UPDATE
和 DELETE
語句] 複選框之外,[進階 SQL 產生選項] 對話框也包含 [使用開放式並行存取] 選項 (請參閱圖 1) 。 核取時, WHERE
自動產生的 UPDATE
和 DELETE
語句中的 子句會修改為只在使用者上次將數據載入方格之後尚未修改基礎資料庫數據時執行更新或刪除。
圖 1:您可以從 [進階 SQL 產生選項] 對話框新增開放式並行支援
回到 實作開放式並行存取 教學課程,我們已檢查開放式並行控制的基本概念,以及如何將其新增至 ObjectDataSource。 在本教學課程中,我們將針對開放式並行存取控件的基本概念進行修改,然後探索如何使用 SqlDataSource 來實作它。
開放式並行存取的重新擷取
對於允許多個同時編輯或刪除相同數據的 Web 應用程式,有一個使用者可能會不小心覆寫另一個變更的可能性。 在 實作開放式並行存取 教學課程中,我提供了下列範例:
假設兩個使用者 Jisun 和 Sam 都瀏覽了應用程式中的頁面,允許訪客透過 GridView 控件更新和刪除產品。 兩者都會同時按兩下 Chai 的 [編輯] 按鈕。 Jisun 會將產品名稱變更為 Chai Tea,然後按兩下 [更新] 按鈕。 net result 是一個UPDATE
語句,會傳送至資料庫,即使 Jisun 只更新了一個字段, ProductName
) 也會設定所有產品可更新的欄位 (。 此時,資料庫的值為 Chai Tea、類別為「咖啡」、供應商「油油」,依此類傳給此特定產品。 不過,Sam s 畫面上的 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.aspx
頁面 SqlDataSource
開始。 將 SqlDataSource 控制件從 [工具箱] 拖曳至 Designer,並將其 ID
屬性設定為 ProductsDataSourceWithOptimisticConcurrency
。 接下來,按兩下控件智慧標記中的 [設定資料源] 連結。 從精靈的第一個畫面中,選擇使用 , NORTHWINDConnectionString
然後按 [下一步]。
圖 4:選擇 [使用 NORTHWINDConnectionString
(按兩下即可檢視完整大小的映像)
在此範例中,我們將新增 GridView,讓使用者編輯 Products
數據表。 因此,從 [設定選取語句] 畫面中,從下拉式清單中選擇數據表, 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>
除了增強 WHERE
和 DeleteCommand
屬性的 UpdateCommand
子句 (,並將其他參數新增至個別參數集合) ,選取 [使用開放式並行存取] 選項可調整其他兩個屬性:
- 將
ConflictDetection
屬性 從OverwriteChanges
(預設) 變更為CompareAllValues
- 將
OldValuesParameterFormatString
屬性 從 {0} (預設) 變更為 original_{0} 。
當數據 Web 控制項叫用 SqlDataSource s Update()
或 Delete()
方法時,它會傳入原始值。 如果 SqlDataSource s ConflictDetection
屬性設定 CompareAllValues
為 ,這些原始值就會新增至 命令。 屬性 OldValuesParameterFormatString
會提供用於這些原始值參數的命名模式。 [設定數據源精靈] 會使用 original_{0},並據以命名 和 DeleteCommand
屬性和UpdateParameters
DeleteParameters
集合中的每個UpdateCommand
原始參數。
注意
因為我們未使用 SqlDataSource 控制件的插入功能,因此您可以隨意移除 InsertCommand
屬性及其 InsertParameters
集合。
正確處理NULL
值
不幸的是,使用開放式並行存取時,[設定數據源精靈] 自動產生的增強 UPDATE
和 DELETE
語句 不適用於 包含 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
的值UnitPrice
,子WHERE
句部分[UnitPrice] = @original_UnitPrice
一律會評估為 False,因為NULL = NULL
一律會傳回 False。 因此,無法編輯或刪除包含 NULL
值的記錄,因為 UPDATE
和 DELETE
語句 WHERE
子句不會傳回任何要更新或刪除的數據列。
注意
此 Bug 第一次在 SqlDataSource 的 2004 年 6 月回報給 Microsoft 產生不正確的 SQL 語句 ,並已排定在下一個版本的 ASP.NET 中修正。
若要修正此問題,我們必須手動更新 WHERE
和 DeleteCommand
屬性中可以具有NULL
值之所有數據行的 UpdateCommand
子句。 一般而言,請變更 [ColumnName] = @original_ColumnName
為:
(
([ColumnName] IS NULL AND @original_ColumnName IS NULL)
OR
([ColumnName] = @original_ColumnName)
)
這項修改可以直接透過宣告式標記、透過來自 屬性視窗 的 UpdateQuery 或 DeleteQuery 選項,或透過 [設定數據源精靈] 中 [指定自定義 SQL 語句或預存程式] 選項中的 UPDATE 和 DELETE 索引標籤來進行。 同樣地,必須針對 可以包含NULL
值的 和 DeleteCommand
s WHERE
子句中的每個UpdateCommand
數據行進行這項修改。
將此套用至我們的範例會產生下列修改 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,並將其設定ID
為 Products
。 從 GridView 的智慧標記,將它系結至步驟 1 中新增的 ProductsDataSourceWithOptimisticConcurrency
SqlDataSource 控制件。 最後,檢查智慧標記中的 [啟用編輯] 和 [啟用刪除] 選項。
圖 6:將 GridView 系結至 SqlDataSource 並啟用編輯和刪除 (按兩下即可檢視大小完整的影像)
新增 GridView 之後,請移除 ProductID
BoundField、將 BoundField s HeaderText
屬性變更ProductName
為 Product,以及更新 UnitPrice
BoundField,使其屬性只是 Price 來設定其HeaderText
外觀。 在理想情況下,我們會增強編輯介面,以包含值的 RequiredFieldValidator,以及值 (CompareValidator ProductName
UnitPrice
,以確保它是正確格式化的數值) 。 如需自定義 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 類別。 最後,將 [標籤] Visible
和 EnableViewState
屬性設定為 false
。 除了我們明確將其屬性true
設定Visible
為的回傳以外,這隻會隱藏標籤。
圖 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
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。