导言
对于可能存在多用户同时更新或删除数据的web程序来说,存在一个用户的修改覆盖另一个用户的情况。当设计这样的程序时,选择适当的并发控制技术非常重要。我们在实现开放式并发 里已经讨论过,有三种concurrency control (并发控制)的策略:
-
什么都不做—如果并发用户修改的是同一条记录,让最后提交的结果生效(默认的行为)
-
开放式并发(Optimistic Concurrency) — 假定并发冲突只是偶尔发生,绝大多数的时候并不会出现; 那么,当发生一个冲突时,仅仅简单的告知用户,他所作的更改不能保存,因为别的用户已经修改了同一条记录
-
保守式并发(Pessimistic Concurrency )— 假定并发冲突经常发生,并且用户不能容忍被告知自己的修改不能保存是由于别人的并发行为;那么,当一个用户开始编辑一条记录,锁定该记录,从而防止其他用户编辑或删除该记录,直到他完成并提交自己的更改
目前为止我们编辑DataList 的教程都是使用的默认的策略—也就是我们让最后写的结果生效。本章我们学习如何使用开放式并发。
第一步: 理解开放式并发是如何实现的
开放式并发控制能够确保一条记录在更新或者删除时跟它开始这次更新或修改过程时保持一致。例如,当在一个可编辑的DataList里点击编辑按钮时,该记录的原始值从数据库中读取出来并显示在TextBox和其他Web控件中。这些原始的值需要被保存下来。随后,当用户完成他的修改并点击更新按钮,这些原始值加上修改后的新值发送到业务逻辑层,然后到数据访问层。数据访问层发出一个SQL语句,它将仅仅更新那些开始编辑时的原始值根数据库中的值一致的记录。图一描述了这些事件发生的顺序。
图1: 为了更新或删除能够成功,原始值必须与数据库中相应的值一致
有多种方法可以实现开放式并发控制(查看Peter A. Bromberg的文章 Optmistic Concurrency Updating Logic,从摘要中看到许多选择)。ADO.NET类型化数据集提供了一种应用,这只需要在配置时勾选上一个CheckBox。开启TableAdapter 的开放并发需要在TableAdapter 的Update和Delete语句后面加一个比较所有原始值的WHERE从句。我们在实现开放式并发里创建了一个这样的类型化数据集(名为NorthwindOptimisticConcurrency)和一个名为ProductsOptimisticConcurrencyBLL的BLL类。
本章我们将使用上面的方法创建一个DataList。
注意:在继续前请阅读实现开放式并发 ,它提供了关于开放并发如何工作和如何让BLL和DAL执行开放并发的详细信息。
第二步: 创建一个可编辑和删除的 DataList
使用DataList执行开放并发时,需要我们保存原始值 —更新或删除时的值— 然后将这些值连同新的值一起传到BLL。首先我们来创建一个可编辑和删除的DataList,它列出product,并允许修改name和price。
打开EditDeleteDataList文件夹下的OptimisticConcurrency.aspx页。拖一个DataList进来。并将ID设为Products。通过它的智能标签,创建一个名为ProductsDataSource的ObjectDataSource,并用ProductsBLL类的GetProducts()方法配置它。在UPDATE,INSERT,DELETE标签里选择(None).
图 2: 在UPDATE, INSERT, DELETE标签里选择 (None)
配置完后,Visual Studio会自动创建一个ItemTemplate,它包含product的每个字段。修改它让它只显示ProductName和UnitPrice。
我们需要在ItemTemplate里添加两个button来支持编辑和删除功能 — 一个删除product,一个使product可编辑。记得这些button必须将CommandName的值设为"Edit"和"Delete",这样在被点击button时的postback中EditCommand和DelelteCommand事件才会激发。
我的ItemTemplate用<h4>显示product name,用货币格式显示price。你可以根据自己的喜好自定义。
注意:由于主要将精力集中在开放并发上,本章将创建ItemTemplate和EditItemTemplate的步骤带过。如果需要更详细的内容,请参考用DataList和Repeater来显示数据 和综叙:在DataList里编辑和删除数据
ASP.NET |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<ItemTemplate>
<h4>
<asp:Label ID="ProductNameLabel" runat="server"
Text='<%# Eval("ProductName") %>' />
</h4>
Price:
<asp:Label ID="UnitPriceLabel" runat="server"
Text='<%# Eval("UnitPrice", "{0:C}") %>' />
<br /><br />
<asp:Button runat="server" ID="EditButton" Text="Edit"
CommandName="Edit" />
<asp:Button runat="server" ID="DeleteButton" Text="Delete"
CommandName="Delete" />
<br /><br />
</ItemTemplate>
|
当Edit button被点时,会postback并激发EditCommand事件。我们马上为它创建event handler,将选择的product变为可编辑。因此我们需要创建EditItemTemplate。它包含TextBox显示product name和price,并且分别有一个RequiredFieldValidator 和CompareValidator (验证条件和前面章节一样),另外我们还需要两个button— 一个Update和一个Cancel— 将它们的CommandName分别设为"Update"和"Cancel"。
ASP.NET |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
<EditItemTemplate>
Product:
<asp:TextBox runat="server" ID="ProductName"
Text='<%# Eval("ProductName") %>' />
<asp:RequiredFieldValidator ID="RequiredFieldValidator1"
ControlToValidate="ProductName"
ErrorMessage="You must provide the product's name."
runat="server">*</asp:RequiredFieldValidator>
<br />
Price:
$<asp:TextBox runat="server" ID="UnitPrice"
Text='<%# Eval("UnitPrice", "{0:N2}") %>' Columns="8" />
<asp:CompareValidator ID="CompareValidator1" ControlToValidate="UnitPrice"
ErrorMessage="The product's price must be a valid currency value
greater than or equal to zero. Do not include the
currency symbol."
Operator="GreaterThanEqual" Type="Currency" ValueToCompare="0"
runat="server">*</asp:CompareValidator><br /><br />
<asp:Button runat="server" ID="UpdateButton" Text="Update" CommandName="Update" />
<asp:Button runat="server" ID="CancelButton" Text="Cancel" CommandName="Cancel"
CausesValidation="False" />
<br /><br />
</EditItemTemplate>
|
创建 EditCommand 和CancelCommand Event Handlers
现在我们已经创建完ItemTemplate和EditItemTemplate,我们还需要加工event handlers 来使ItemTemplate和EditItemTemplate之间可以切换。象在前面讨论的一样,EditCommand事件处理里需要将EditItemIndex设为被点击Edit button的product的index。相反的,CancelCommand事件处理需要将EditItemIndex设为一个不存在的product index(比如-1)。
C# |
1
2
3
4
5
6
7
8
9
10
11
|
protected void Products_EditCommand(object source, DataListCommandEventArgs e)
{
Products.EditItemIndex = e.Item.ItemIndex;
Products.DataBind();
}
protected void Products_CancelCommand(object source, DataListCommandEventArgs e)
{
Products.EditItemIndex = -1;
Products.DataBind();
}
|
现在浏览该页。开始会看到所有product的name和price显示,如图3。点Edit button会将选择的product展示为编辑模式。现在Update button还没有任何作用,Cancel button会将DataList返回到编辑前的状态,见图4。
图 3: 所有Product的 Name 和Price 被显示
图 4: 点 Edit Button 会将选择的Product的 EditItemTemplate显示出来
第三步: 编辑时记下原始值
当使用开放并发控制编辑记录时,必须记下所有的编辑字段的原始值,这样用户完成编辑并点Update时,原始值可以用来和当前数据库的值进行比较以便判断其他用户是否有修改过数据。GridView 在编辑时会自动记下这些原始值。而对DataList来说,需要我们编码来记下这些值。
GridView 在postback过程里使用control state来记下这些原始值。如综叙:在DataList里编辑和删除数据 里讨论的,control state是和view state相似的用来在postback过程里记录值的方法。control state不能被开发者禁用,而view state可以。对DataList来说我们可以在ItemDataBound event handler里手工的将原始值保存到view state里。这样在postback过程里它们的值将被记住。然后我们创建UpdateCommand event handler,将这些值传给BLL,它将只更新那些原始值和目前数据库值一样的那些product。
使用Page类的ViewState property来写view state。为ItemDataBound创建event handler,以便将原始值保存在view state里。
C# |
1
2
3
4
5
6
7
8
9
10
11
12
|
protected void Products_ItemDataBound(object sender, DataListItemEventArgs e)
{
if (e.Item.ItemType == ListItemType.EditItem)
{
// Read the edited item's original values into the Page's view state
TextBox ProductName = (TextBox)e.Item.FindControl("ProductName");
TextBox UnitPrice = (TextBox)e.Item.FindControl("UnitPrice");
ViewState["original_productName"] = ProductName.Text;
ViewState["original_unitPrice"] = UnitPrice.Text;
}
}
|
最开始的条件保证了仅仅只将那些被编辑的item的原始值保存下来。然后从被编辑的product的EditItemTemplate里读出ProductName和UnitPrice TextBox的值。FindControl("controlID")方法用来引用TextBox。这些值被保存在view state变量original_productName和original_unitPrice里。
我们的DataList只有两个可编辑的输入— product的name和price— 因此我们仅仅只需要两个view state变量。一般来说,对DataList里每个可编辑的项,使用FindControl("controlID")引用EditItemTemplate里的控件然后将值存在view state变量里。
第四步: 将原始值和新的值传给UpdateProduct
当用户点编辑按钮来更新product时,最新的数据从数据库获得并显示在选择的product的EditItemTemplate里。这些值被保存在两个view state变量里。在用户完成修改后点更新按钮,产生postback,激发UpdateCommand。我们需要创建一个event handler来将新的product的值从EditItemTemplate里读出来,并和原始值一起传到BLL。
回到实现开放式并发 里,我们学习了如何创建一个天生支持开放并发的类型化DataSet,并创建了一个这样的DAL(NorthwindOptimisticConcurrency.xsd)。在那章里我们也创建了ProductsOptimisticConcurrencyBLL类来和NorthwindOptimisticConcurrency.xsd DAL一同工作。在DataList的例子里我们将直接通过UpdateCommand和ProductsOptimisticConcurrencyBLL交互。
ProductsOptimisticConcurrencyBLL类的UpdateProduct方法有两个参数,分别表示新值和原始值。因此UpdateCOmmand需要读更新的值和原始值,见下面的代码:
C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
protected void Products_UpdateCommand(object source, DataListCommandEventArgs e)
{
// Make sure the validators on the page are valid
if (!Page.IsValid)
return;
// Read in the ProductID
int productID = Convert.ToInt32(Products.DataKeys[e.Item.ItemIndex]);
// Read in the original values
string original_productName = null;
decimal? original_unitPrice = null;
if (!string.IsNullOrEmpty((string)ViewState["original_productName"]))
original_productName = (string)ViewState["original_productName"];
if (!string.IsNullOrEmpty((string)ViewState["original_unitPrice"]))
original_unitPrice =
decimal.Parse((string)ViewState["original_unitPrice"],
System.Globalization.NumberStyles.Currency);
// Read in the new values
string new_productName = null;
decimal? new_unitPrice = null;
TextBox ProductName = (TextBox)e.Item.FindControl("ProductName");
TextBox UnitPrice = (TextBox)e.Item.FindControl("UnitPrice");
if (ProductName.Text.Trim().Length > 0)
new_productName = ProductName.Text.Trim();
if (UnitPrice.Text.Trim().Length > 0)
new_unitPrice =
decimal.Parse(UnitPrice.Text.Trim(),
System.Globalization.NumberStyles.Currency);
// Call the UpdateProduct method in ProductsOptimisticConcurrencyBLL
ProductsOptimisticConcurrencyBLL optimisticProductsAPI
= new ProductsOptimisticConcurrencyBLL();
optimisticProductsAPI.UpdateProduct(new_productName, new_unitPrice,
productID, original_productName, original_unitPrice, productID);
// Return the DataList to its pre-editing state
Products.EditItemIndex = -1;
Products.DataBind();
}
|
UpdateCommand首先判断page是否合法。如我们在在编辑和插入界面里添加验证控件 里讨论的那样,它在处理用户输入的数据前判断Page.IsValid是否返回True。(由于客户端验证可能因为用户的浏览器不支持或禁用了JavaScrip而被跳过)
接着,被从DataKeys集合里读出编辑的行的ProductID。然后从ViewState集合里将原始值读到局部变量里,然后从EditItemTemplate里的TextBox里读出新的值。然后创建ProductsOptimisticConcurrencyBLL的实例,调用UpdateProduct方法,将原始值和新值传进去。最后,更新完后,DataList返回到编辑前状态。
完成UpdateCommand事件处理后,浏览该页。点Chai的编辑按钮,然后将price修改为$20.00.由于你是唯一一个编辑这条记录的,因此更新会成功,你的感觉和没有使用开放并发控制的更新DataList是一样的。
打开另一个浏览器窗口并浏览OptimisticConcurrency.aspx页。在两个页面里都点Chai的编辑按钮。在第一个页里将product的name改为“Chai Tea”然后点更新。这样会更新数据库并返回到DataList的编辑前状态,这时product name会显示为"Chai Tea"。在第二个页里(它将继续保存编辑状态)将price改为 $25.50,并将product name保留为“Chai”。点击更新按钮,这时会抛出DBConcurrencyException异常,如图5所示。
图 5: 当检测到并发冲突时,会抛出 DBConcurrencyException 异常
见图6,这个异常抛出是因为第二个页里的原始值 — “Chai” 和$19.95 — 和当前数据库里的值不匹配(当前product name为“Chai Tea”)。碰到这样的并发冲突时,DAL会抛出DBConcurrencyException。
图 6: 原始值和数据库当前值不匹配
当发生这样的异常时,我们需要显示一个信息来提醒用户他们的更新由于并发冲突而被取消了。这个可以通过将调用UpdateProduct的代码放在Try...Catch块里完成。
首先在DataList上方加一个Label,将ID设为UPdateConcurrencyViolationMessage,将CssClass属性设为“Warning”,将Visible设为False,将EnableViewState设为False,将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.”Styles.css里定义的Warning类将Label的text显示为大红字体,见图7。
图 7: 在DataList上方添加Label
完成UpdateConcurrencyViolationMessage Label后,剩下的就是添加Try...Catch块了。我们需要将调用UpdateProduct的代码放在Try...Catch块里。如果发生DBConcurrencyException,我们就显示UpdateConcurrencyViolationMessage Label并重新绑定数据到DataList,这样别的用户作的修改就可以显示出来,并成为新的原始值。
C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
try
{
// Call the UpdateProduct method in ProductsOptimisticConcurrencyBLL
ProductsOptimisticConcurrencyBLL optimisticProductsAPI
= new ProductsOptimisticConcurrencyBLL();
optimisticProductsAPI.UpdateProduct(new_productName, new_unitPrice,
productID, original_productName, original_unitPrice, productID);
// Return the DataList to its pre-editing state
Products.EditItemIndex = -1;
Products.DataBind();
}
catch (DBConcurrencyException)
{
// Display the UpdateConcurrencyViolationMessage Label
UpdateConcurrencyViolationMessage.Visible = true;
// Re-read the values from the database
Products.DataBind();
}
catch
{
// Some other kind of exception occurred
throw;
}
|
如果有DBConcurrencyException发生,我的DataList会保持在编辑状态。如果你想让DataList返回到编辑前状态,可以将通过在catch里将DataList的EditItemIndex属性在调用Products.DataBind()方法前设为-1来完成。如果在更新过程中是除DBConcurrencyException以外的异常发生,这个异常将被抛出。
完成异常处理后,重新浏览页面并重复上面的步骤。这次并发冲突以一个友好的信息显示出来。见图8。
图 8: 当并发冲突发生时,显示友好的信息
第五步 为Delelte实现开放并发控制
删除的开放并发控制和更新一样。当用户删除product时,原始值—数据从数据库里读出并赋给DataList来显示的值—和当前数据库的值进行比较。如果有任何不一致的地方,删除会失败,见图9。
图9: 删除一条被修改了的记录会引起并发冲突
ProductsOptimisticConcurrencyBLL类包含一个DeleteProduct方法,它的参数除了ProductID外还有其它的字段的原始值(ProductName,CategoryID,SupplierID等)。如果原始值和数据库当前的值有任何不一致,删除会失败。由于我们仅仅只显示了product的name和price,因此我们是需要在任何product的字段值被改变时让删除失败,还是仅仅只在name和price被修改时才让删除失败?
答案取决于你希望开放并发控制怎么做。你是希望显示的值被修改的情况下不让用户删除(用户通过显示值来决定删除记录)还是希望任何的修改都不让用户删除?对我来说,第一种方法比较合理。
不管使用哪种方法,我们都需要添加一个Label用来在删除时有并发冲突的情况下显示信息。添加一个Label,ID设为DeleteConcurrencyViolatioinMessage。和UpdateConcurrencyViolationMessage Label一样,将CssClass设为"Warning",将EnableViewState和Visible设为False。最后将Text设为“The record you attempted to delete has been modified or deleted by another user since you visited the 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.” 完成这些后,设计界面看起来应该和图10差不多。
图 10: 新添加一个Label
为了完成“只在显示的值被其他用户修改的情况下停止删除”,我们需要创建一个DeleteProduct的重载方法,它接收ProductName和UnitPrice的原始值。因此我们需要从数据库读出当前数据并将那些显示在页面上的字段传给DAL的Delete方法。
将下面的DeleteProduct重载方法添加到ProductsOptimisticConcurrencyBLL类里:
C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, false)]
public bool DeleteProduct(int original_productID, string original_productName,
decimal? original_unitPrice)
{
// Read in the current values from the database
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products
= Adapter.GetProductByProductID(original_productID);
if (products.Count == 0)
// no matching record found, return false
return false;
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product
= products[0];
int? current_supplierID = null;
int? current_categoryID = null;
string current_quantityPerUnit = null;
short? current_unitsInStock = null;
short? current_unitsOnOrder = null;
short? current_reorderLevel = null;
bool current_discontinued;
if (!product.IsSupplierIDNull())
current_supplierID = product.SupplierID;
if (!product.IsCategoryIDNull())
current_categoryID = product.CategoryID;
if (!product.IsQuantityPerUnitNull())
current_quantityPerUnit = product.QuantityPerUnit;
if (!product.IsUnitsInStockNull())
current_unitsInStock = product.UnitsInStock;
if (!product.IsUnitsOnOrderNull())
current_unitsOnOrder = product.UnitsOnOrder;
if (!product.IsReorderLevelNull())
current_reorderLevel = product.ReorderLevel;
current_discontinued = product.Discontinued;
// Now, call the Delete method, passing in the original and current values,
// where appropriate
int rowsAffected = Adapter.Delete(original_productID,
original_productName,
current_supplierID,
current_categoryID,
current_quantityPerUnit,
original_unitPrice,
current_unitsInStock,
current_unitsOnOrder,
current_reorderLevel,
current_discontinued);
// Return true if precisely one row was deleted, otherwise false
return rowsAffected == 1;
}
|
这个方法开始先从数据库读出当前值,并将除了ProductName和UnitPrice以外的值都赋给局部变量。然后调用DAL的Delete方法,将product的name和price的原始值和其它的当前值传进去。 记得类型化的DataSet的TableAdapters 提供两种可以修改内容的模式:
-
批量更新模式— 一个 DataRow, DataRows的集合, 一个DataTable, 或者DataSet 传给TableAdapter的
Update 方法. Update 方法便利DataRows 的集合获取和根据DataRow的RowState 属性执行适当的UPDATE , INSERT , DELETE 语句。UpdateProduct 重载使用的这种模式.
-
数据库直接模式 — 一个scalar values 传给 TableAdapter的
Insert , Update , Delete 方法, 只影响数据库的一条记录. DeleteProduct 方法使用的这种模式。
我提出这两种模式的原因是因为每个开放并发控制的处理都不一样。如我们在第四步看到的,如果使用批量更新的时候发生了并发冲突,会抛出DBConcurrencyException。而使用数据库直接模式,没有异常抛出—更新或删除悄悄的失败。为了确定并发冲突是否发生,我们需要判断Update,Delete方法的返回值,它表示了有多少行被影响。如果没有行被影响,可能就是因为产生了并发冲突。当然也可能是因为试图更新或删除的记录已经被删除了或者主键值被修改了。
最后一步是创建DeleteCommand事件处理。在它里面我们需要读出ProductID,ProductName和UnitPrice的值,然后调用ProductsOptimisticConcurrencyBLL类的DeleteProduct方法。如果DeleteProduct方法返回false,表示没有记录被删除,我们需要显示DeleteConcurrencyViolationMessage Label。不管删除成功或者失败,我们需要重新绑定数据到Products DataList。下面的代码完成了这些:
C# |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
protected void Products_DeleteCommand(object source, DataListCommandEventArgs e)
{
// Read in the ProductID
int productID = Convert.ToInt32(Products.DataKeys[e.Item.ItemIndex]);
// Read in the current name and price values for the product being deleted
Label ProductNameLabel = (Label)e.Item.FindControl("ProductNameLabel");
Label UnitPriceLabel = (Label)e.Item.FindControl("UnitPriceLabel");
string original_productName = ProductNameLabel.Text;
decimal? original_unitPrice = null;
if (!string.IsNullOrEmpty(UnitPriceLabel.Text))
original_unitPrice =
decimal.Parse(UnitPriceLabel.Text,
System.Globalization.NumberStyles.Currency);
// Delete the product using the ProductsOptimisticConcurrencyBLL class
ProductsOptimisticConcurrencyBLL optimisticProductsAPI =
new ProductsOptimisticConcurrencyBLL();
bool deleteSucceeded =
optimisticProductsAPI.DeleteProduct
(productID, original_productName, original_unitPrice);
// If the delete failed, display the
// DeleteConcurrencyViolationMessage Label
if (!deleteSucceeded)
DeleteConcurrencyViolationMessage.Visible = true;
// Rebind the data to the DataList
Products.DataBind();
}
|
测试一下删除的开放并发控制。打开两个页都浏览OptimisticConcurrency.aspx页。在一个页里编辑Chai procut,将它的name改为“Chai Tea”。保存后,在第二个页里删除Chai。DeleteCommand event handler会将product原始的name("Chai")传到DAL的Delete方法里。由于我们的代码控制了只有传入的原始值和数据库的当前值匹配的情况下才可以删除,所以DAL的Delete方法发出的DELETE语句将不会影响任何行。当前的product的name为“Chai Tea”,因此删除会悄悄的失败。BLL的DeleteProduct方法会返回false。
回到DeleteCommand event handler里,一个为false的返回值会引起DeleteConcurrencyViolationMessage Label显示,见图11。
图 11: 如果删除没有影响到任何行,会显示一个信息
总结
当创建一个可能多个用户同时编辑或删除相同数据库记录的web程序时,实现开放并发是很有价值的。如我们在前面的章节里看到的,ADO.NET TableAdapter可以配置为支持开放并发,并且根据使用的更新模式,在并发冲突时,要么抛出DBConcurrencyException异常要么悄悄的失败。
为了让可编辑的DataList支持开放并发,我们需要记下原始值。这个可以在ItemDataBound里通过page的ViewState集合来完成。然后在UpdateCommand里,这些原始值会和新的值一起传给BLL。同样的,在删除的时候支持开放并发,绑定到DataList的原始值需要读出来并在DeleteCommand里传给BLL。
祝编程愉快!
|
请发表评论