实现开放式并发41

简介

有多个用户同时更新或删除数据的 Web 应用程序可能引入一个用户的修改覆盖另一个用户的修改的可能性。当设计这样的应用程序时,选择适当的并发控制技术很重要。如同我们在 实现并发优化 教程中所介绍的,有三种 并发控制 策略可供使用:

  • 不采取任何措施—— 如果并发用户正在修改相同的记录,让最后的提交成功(默认行为)
  • 并发优化—— 假设虽然可能不时地出现并发冲突,但是绝大部分时间不会出现这种冲突;因此,如果出现冲突,只需通知用户他们的更改不能保存,因为另一个用户已经修改了相同的数据。
  • 保守式并发——假设并发冲突是常见的,用户无法忍受被告知由于另一个用户的并发行为导致他们的更改不能保存;因此,当一个用户开始更新记录时,将记录锁定,从而防止任何其他用户编辑或删除该记录,直到用户提交他们的修改。

迄今为止,我们的所有DataList 编辑教程都使用默认并发解决策略—— 即,我们让最后一次写入成功。本教程中,我们将介绍如何实现并发优化控制。

步骤1 :了解并发优化是如何实现的

并发优化控制的作用是确保正在更新或删除的记录的值与更新或删除过程开始时的值相同。例如,当单击可编辑 DataList 中的 Edit 按钮时,从数据库读取记录并显示在 TextBox 或 其他 Web 控件中。需要保留这些原始值。一旦用户完成修改并单击 Update 按钮,原始值被发送到 BLL ,然后再发送到 DAL 将它们与当前数据库的值进行比较。数据访问层发出一个 SQL 语句,只有当用户开始进行编辑时的原始值与数据库中的值相同时,该语句才更新记录。图 1 描述了事件事件发生的先后次序。

图1 :为使更新或删除成功,原始值必须等于当前数据库的值

有各种方法可以用来实现并发优化(请参见 Peter A. Bromberg 的 并发优化更新逻辑 ,简要了解许多可以选择的方法)。ADO.NET Typed DataSet (强类型 DataSet )给出一种只需勾选复选框便可配置的实现方法。为 强类型DataSet 中的 TableAdapter 启用并发优化,可以对TableAdapter 的 UPDATE 和 DELETE 进行增强以便在 WHERE 子句中包括对所有原始数据的比较。我们在 实现并发优化教程中创建了这样一个强类型DataSet (名称为 NorthwindOptimisticConcurrency )以及一个名称为 ProductsOptimisticConcurrencyBLL 的 BLL 类,该类用于与 NorthwindOptimisticConcurrency 强类型 DataSet 一起使用。

本教程中,我们将使用一个DataList ,其编辑和删除界面使用启用了并发优化的BLL 和 DAL 。

注意:如果您还未完成 实现并发优化教程,在继续本教程之前,请先完成该教程。 实现并发优化教程对并发优化的原理提供更全面的概述,并介绍创建 BLL 和 DAL 以实现并发优化的过程。

步骤2 :创建其项可以编辑和删除的 DataList

当通过DataList 实现并发优化时,我们的职责是保留原始值—— 更新或删除时呈现的值—— 以及将原始值和与编辑和删除有关的值传递到业务逻辑层。在我们考虑保留这些信息之前,让我们构建一个其项可以编辑和删除的 DataList 。具体来说,让我们创建一个列出产品的 DataList ,允许对产品的名称和价格进行更新。

首先打开EditDeleteDataList 文件夹中的OptimisticConcurrency.aspx 页。将 DataList 从工具箱拖放到设计器并将其 ID 属性设置为 Products 。从 DataList 的智能标记,创建一个名称为 ProductsDataSource 的新的 ObjectDataSource ,将它配置为从ProductsBLL 类的 GetProducts() 方法检索数据。如同前面的教程一样,确保将ObjectDataSource 的 UPDATE 、INSERT 和 DELETE 选项卡中的下列表设置为(None) 。

图2 :将 UPDATE 、INSERT 和 DELETE 选项卡中的下拉列表设置为 (None)

配置完ObjectDataSource 之后,Visual Studio 将为DataList 创建一个包含每种产品数据字段的名称和值的默认ItemTemplate 。用只显示产品的 ProductName 和 UnitPriceItem 字段的 ItemTemplate 替换此 ItemTemplate 。

若要提供对编辑和删除的支持,需要向ItemTemplate 添加两个按钮—— 一个用于删除产品,另一个用于使产品可编辑。记住这两个按钮的CommandName 值必须为 “Edit” 和 “Delete” ,以便单击它们时,刚一回发就触发 DataList 的 EditCommand 和DeleteCommand 事件。

ItemTemplate 在 <h4> 元素中显示产品的名称和货币格式的价格;可以随意地将 ItemTemplate 的外观自定义为自己喜欢的样式。

注意:本教程没有逐步地创建 DataList 的 ItemTemplate 或 EditItemTemplate ,而是集中于实现并发优化。有关为DataList 创建这两个模板的详细介绍,请参阅 使用 DataList 和 Repeater 控件显示数据 和 在 DataList 中进行数据编辑与删除操作概述 教程。

<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 按钮时,出现回发并触发 DataList 的 EditCommand 事件。我们将马上为此事件创建一个 Event Handler ,使所选产品成为可编辑的,这将导致该特定产品的 EditItemTemplate 取代 ItemTemplate 而呈现。因此,需要为 Products DataList 创建一个 EditItemTemplate 。此 EditItemTemplate 应该包含用于产品名称和价格的TextBox ,它带有用于名称的RequiredFieldValidator 和CompareValidator 以确保价格以合法的货币格式提供并且它的值(如果提供)大于或等于0 。此外,还需要两个按钮控件—— 一个 Update 按钮和一个 Cancel 按钮—— 它们的 CommandName 属性分别设置为 “Update” 和 “Cancel” 。

<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 ID="UpdateButton" Text="Update" CommandName="Update" 
        runat="server" /> 
        
    <asp:Button ID="CancelButton" Text="Cancel" CommandName="Cancel" 
        CausesValidation="False" runat="server" /> 
    <br /><br /> 
</EditItemTemplate>

创建 EditCommand 和 CancelCommand Event Handler

目前我们已经创建了 DataList 的 ItemTemplate 和 EditItemTemplate ,但仍需要创建 Event Handler 以便将产品从制度模式切换到可编辑模式,反之亦然。正如在在DataList中编辑和删除数据概述 教程中所介绍的,EditCommand Event Handler 需要将 DataList 的 EditItemIndex 属性设置为其 Edit 按钮被单击的产品的索引。反之,CancelCommand Event Handler 需要将 EditItemIndex 重置为一个与任何产品索引都不相符的值( 如-1 ) 。

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(); 
}

准备好这些 Event Handler 之后,花些时间通过浏览器来访问页面。最初,所有产品的名称和价格都应该显示出来,如图 3 所示。单击 Edit 按钮将以编辑模式呈现所选产品。此时,Update 按钮不起任何作用;但是,Cancel 按钮可以使 DataList 返回到它的编辑前的状态(参见图 4 )。

图3 :显示每种产品的名称和价格

图4 :单击 Edit 按钮可以呈现所选产品的 EditItemTemplate

步骤3 :编辑时记住原始值

当使用并发优化控制编辑记录时,必须记住所有可编辑字段的原始值以便当用户完成编辑并单击Update 时,可以将原始值和当前数据库的值进行比较以确定其他用户是否修改过数据。进行编辑时 GridView 可以自动记住原始值。但是使用 DataList ,则需要我们编写代码来记住这些值。

GridView 使用其控件状态可以可以在回发中记住原始值。正如在教程 在 DataList 中编辑和删除数据概述 中所介绍的,控件状态和视图状态是相似的,因为它用于在回发中记住原始值。但是,控件状态不能被页面开发人员禁用,而视图状态则可以。对于DataList , 当在 ItemDataBound Event Handler 中将数据绑定到可编辑的行时,我们可以向 ASP.NET 页的视图状态集合手动添加原始值。通过在视图状态中放置这些原始值,在回传中将记住这些值。稍后,当创建 UpdateCommand Event Handler 时,我们将取得这些值并将它们传递到 BLL ,只有当原始值与当前数据库的值相符时才更新产品。

若要写入页的视图状态,可以使用 Page 类的 ViewState 属性 。为 DataList 的 ItemDataBound 事件创建一个Event Handler 以便当处理可编辑的项时,原始值可以保存在视图状态中:

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

此 Event Handler 开始处的条件只保存正被编辑的项的原始值。然后它从被编辑产品的EditItemTemplate 读取 ProductName 和 UnitPrice Web 文本框 控件中的值。FindControl("controlID") 方法用于引用保存刚从数据库检索的名称和价格值的TextBox 。然后将这些值存储在视图状态变量original_productName 和original_unitPrice 中。

我们的 DataList 只有两个可编辑的输入—— 产品的名称和价格—— 因此我们只需使用两个视图状态变量。通常,对于DataList 中的每种可编辑项,使用FindControl("controlID") 方法引用适合的 Web 控件,然后将其值存储在视图状态变量中。

步骤4 :将原始值和新值传递到 UpdateProduct 方法

当用户单击Edit 按钮来更新产品时,将从数据库中检索最新的数据并显示在所选产品的EditItemTemplate 中。这些数据库值存储在两个视图状态变量中。用户完成更改并单击 Update 按钮之后,接着出现回发并且触发 DataList 的 UpdateCommand 事件。我们需要为此事件创建一个从 EditItemTemplate 读取新的产品值的 Event Handler 并将新值和原始值传递到业务逻辑层。

DAL 返回 实现并发优化 教程,我们了解如何创建支持并发优化控制的强类型DataSet 并构建这样的 DAL (NorthwindOptimisticConcurrency.xsd) 。在该教程中,还创建了与NorthwindOptimisticConcurrency.xsd DAL 一起工作的  ProductsOptimisticConcurrencyBLL 类。为了在我们的 DataList 示例中使用并发优化控制,我们在 UpdateCommand Event Handler 中直接与ProductsOptimisticConcurrencyBLL 类交互。

ProductsOptimisticConcurrencyBLL 类的 UpdateProduct 方法重载需要新值和原始值。因此,UpdateCommand Event Handler 需要读取已更新的值并从视图状态引用原始值,如以下代码所示:

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 Event Handler 首先确保页面是有效的。正如我们在 为DataList 控件的编辑界面添加验证控件教程中所介绍的,在使用用户输入的数据之前确保 Page.IsValid 返回 True 是有远见的,因为浏览器不包含或者禁用 JavaScript 支持的用户可以绕过校验逻辑。

下一步,从DataKeys 集合读取所编辑的行的 ProductID 。接下来,将原始值从ViewState 读入到本地变量中,然后从 EditItemTemplate 中的 TextBox 访问新值。下一步,创建ProductsOptimisticConcurrencyBLL 类的实例并调用 UpdateProduct 方法,传递原始值和新值。最后,完成更新之后,DataList 返回到它的编辑前的状态。

完成UpdateCommand Event Handler 之后,通过浏览器访问页面。单击 Chai 的 Edit 按钮并将价格更改为 $20.00 。由于您是惟一编辑此记录的人,更新应该成功,用户体验应该与没有并发优化控制的 DataList 相同。

打开另一个浏览器窗口并将其指向 OptimisticConcurrency.aspx 网页。在两个浏览器中都单击 Chai 的 Edit 按钮。在第一个浏览器窗口中,将产品名称从 “Chai” 更改为 “Chai Tea” ,然后单击 Update 。这样做应该更新数据库并使 DataList 返回到它的编辑前的状态,现在将产品名称显示为 “Chai Tea” 。在第二个浏览器窗口(还应该处于 Edit 模式)中,将价格更改为 $25.50 ,但将产品名称保留为 “Chai” 。单击 Update 后,将抛出一个 DBConcurrencyException 异常,如图 5 所示。

图5 :当检测到并发冲突时,将抛出一个DBConcurrencyException i 异常

如图6 所示,抛出此异常的原因是第二个浏览器窗口中的原始值——“Chai” 和 $19.95 —— 与当前数据库中的值不相符(产品名称为 “Chai Tea”) 。遇到这样的并发冲突时,DAL 抛出 DBConcurrencyException 异常。

图6 :原始值与当前数据的值不相符

当引发这样的异常时,我们应该向用户显示一条消息,通知他们更新由于并发冲突而被取消。这可以通过在Try ...Catch 块中。

首先将一个Label 添加到紧挨 DataList 的上面。将其ID 属性设置为 UpdateConcurrencyViolationMessage ,CssClass 属性设置为 “Warning” ,Visible 属性设置为 False ,EnableViewState 属性设置为False 以及将 Text 属性设置为“您试图更新的记录自从开始更新过程以来已被另一用户修改。您的更改已被当前的值替换。请检查现有的值并进行任何所需的更改。” 在 Styles.css 中定义的 Warning CSS 类使 Label 的文本显示为大号、红色和斜体字体,如图 7 所示。

图7 :在 DataList 上方添加一个 Label

准备好UpdateConcurrencyViolationMessage 标签后,所剩的只是向 UpdateCommand Event Handler 添加Try ...Catch 块。特别是,我们需要将对 UpdateProduct 方法的调用放到 Try ...Catch 块中。如果引发 DBConcurrencyException 异常,我们希望显示UpdateConcurrencyViolationMessage Label 并将数据重新绑定到DataList ,以便显示其他用户的更改并成为新的原始值。

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

通过在Try 块内放置使 DataList 返回到编辑前状态的代码代码,我使得在引发DBConcurrencyException 异常后 DataList 可以仍然处于编辑模式。但是 , 如果想让DataList 返回到它的编辑前的状态,需要确保在调用 Catch 块中的 Products.DataBind() 之前将DataList 的 EditItemIndex 属性设置回-1 。如果试图更新产品时引发了除 DBConcurrencyException 以外的异常,异常被重新抛出。

完成异常处理代码之后,在浏览器中重新访问 OptimisticConcurrency.aspx 页并在浏览器中重复前面导致 DBConcurrencyException 消息的步骤。这次,并发冲突生成一个用户友好的消息(参见图 8 )。

图8 :当出现并发冲突时,显示用户友好的错误消息

步骤5 :实现删除的并发优化控制

并发优化控制像应用到更新一样应用到删除。对于并发优化控制,当用户删除产品时,原来产品的值 —— 当从数据库读取数据并分配给 DataList 时呈现的值—— 与当前数据库的值相比较。如果有任何更改,删除就会失败(参见图 9 )。

图9 :删除一个数据已经更改的记录会导致并发冲突

ProductsOptimisticConcurrencyBLL 类包含一个 DeleteProduct 方法,该方法除了需要 ProductID 值外,还需要将传入的所有其他产品数据字段值(ProductName 、CategoryID 、SupplierID )的原始值。如果任何原始值与当前数据库中的数据有任何不同,删除将会失败。由于我们在网页中只显示产品的名称和价格,但我们是想在任何产品值有改变时,还是只有产品名称或价格在数据绑定到 DataList 以来有改变时让删除失败?

这个问题的答案取决于您对并发优化有什么期望。当只有一个显示的值已更改时您是否想停止用户删除记录(因为用户删除记录的决定可能是由于它的显示的值),或者如果对记录有任何修改则想让删除失败?对于我来说,前一种方法似乎更合逻辑。

不管采用哪种方法,当删除时存在并发冲突时,我们都需要首先向页面添加一个 Web 标签控件以显示解释。添加一个新的 Web 标签控件并将其 ID 设置为 DeleteConcurrencyViolationMessage 。如同 UpdateConcurrencyViolationMessage Label 一样,将DeleteConcurrencyViolationMessage Label 的CssClass 属性设置为 “Warning” 以及将EnableViewState 和 Visible 属性设置为 False 。最后,将其 Text 属性设置为 “从访问页面以来您试图删除的记录已被另一用户修改或删除。您的删除被取消以是您可以查看其他用户的更改并确定是否要继续删除此记录。” 添加此 Label 之后,设计器应该与图 10 类似。

图10 :另一个 Label 已经添加到页面

要继续“只有当一个显示的值已被其他用户更改时才停止删除” 选择,我们需要创建一个只接受原始的ProductName 和 UnitPrice 值的 DeleteProduct 重载。但是 DAL 的 Delete 方法则需要所有产品数据字段的原始值。因此,我们需要从数据库读取当前的数据并向 DAL 的 Delete 方法传递那些在网页中显示的字段的原始值和其他数据字段的当前数据库的值。

将以下的 DeleteProduct 方法重载添加到ProductsOptimisticConcurrencyBLL 类:

[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 方法,传递产品名称和价格的原始值和所有其他行的当前值。

记住强类型DataSet 中的TableAdapters 提供两种模式来修改内容:

  • Batch Update模式——一个 DataRow 、一个 DataRow 的集合、一个 DataTable 或 DataSet 传入到 TableAdapter 的 Update 方法中。Update 方法枚举接收的 DataRow 集合并根据 DataRow 的 RowState 属性执行相应的 UPDATE 、INSERT 和 DELETE 语句。UpdateProduct 重载使用 Batch Update 模式。
  • DB-Direct模式——标量值传递到 TableAdapter 的 Insert 、Update 或 Delete 方法,并影响单个数据库记录。DeleteProduct 方法使用 DB-Direct 模式。

我之所以提到这些不同的 TableAdapter 模式是因为每种模式处理并发优化控制的方式都不同。正如我们在第4 步中看到的,当使用 Batch Update 模式有并发冲突时,将抛出 DBConcurrencyException 异常。而对于 DB-Direct 模式,则不抛出异常—— 更新或删除悄悄地失败了。要确定是否出现并发冲突 , 我们可以查看Update 或 Delete 方法的返回值,它可以指示有多少行受到影响。如果没有行受到影响,可能就是由于并发冲突的原因。此外,还可能因为试图更新或删除的记录已被删除或它的主键值更改了。

最后一步是为 DataList 的 DeleteCommand 事件创建Event Handler 。在此Event Handler 中,我们需要读取 ProductID 、ProductName 和 UnitPrice 的值,并且调用ProductsOptimisticConcurrencyBLL 类的DeleteProduct 方法。如果 DeleteProduct 方法返回 false ,说明没有记录被删除,我们需要显示DeleteConcurrencyViolationMessage Label 。无论删除成功与否,我们都需要将数据重新绑定到Products DataList 。以下代码可以实现这些目标:

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 产品,将它的名称更改为 “Chai Tea” 。保存更改之后,尝试在第二个浏览器窗口中删除 Chai 。DeleteCommand Event Handler 将原始的产品名称 (“Chai”) 传递到 DAL 的 Delete 方法。由 DAL 的 Delete 方法执行的 SQL DELETE 语句将不影响任何行,因为它编写为只有当传入的值与当前数据库的相符时才删除一行。但是,当前的产品名称是 “Chai Tea” ,因此删除将失败。由于没有行受到影响,BLL 的 DeleteProduct 方法将返回 false 。

返回到DeleteCommand Event Handler ,返回值为 false 将导致显示DeleteConcurrencyViolationMessage Label ,显示如图 11 所示的用户友好的错误消息。

图11 :如果删除没有影响任何行,则显示解释消息

小结

当构建可能有多个并发用户编辑和删除相同的数据库记录的Web 应用程序时,或许值得我们去实现并发优化。正如我们在前面的教程中看到的,可以将ADO.NET TableAdapters 配置为支持并发优化,并且取决于使用的更新模式,或者抛出 DBConcurrencyException 异常,或者在遇到并发冲突时失败。

为了在可编辑的 DataList 中包含并发优化支持,我们需要采取几个步骤记住回发中的原始值。在ItemDataBound Event Handler 中可以将这些信息保存在页面的ViewState 集合中。然后在 UpdateCommand Event Handler 中,可以将这些原始数据和新的值发送到业务逻辑层。同样,为了提供删除时的并发优化支持,必须从DeleteCommand Event Handler 中读取绑定到 DataList 的原始值并发送到BLL 。

快乐编程!

posted @ 2016-05-01 23:25  迅捷之风  阅读(180)  评论(0编辑  收藏  举报