XSLT存档  

不及格的程序员-八神

 查看分类:  ASP.NET XML/XSLT JavaScripT   我的MSN空间Blog

使用 SqlDataSource 实现并发优化

在本教程中,我们将讨论并发优化基础课程,然后探讨如何使用 SqlDataSource 控件。

« 前一篇教程  |  下一篇教程 »

 

简介

在前一篇教程中,我们探讨了如何向 SqlDataSource 控件添加插入、更新和删除功能。简而言之,要提供这些功能,我们需要在控件的 InsertCommand 、UpdateCommand 或 DeleteCommand 属性指定相应的 INSERT 、UPDATE 、或 DELETE SQL 语句,并在InsertParameters 、UpdateParameters 和DeleteParameters 集合中指定适当的参数。这些属性和集合可以手动指定,Configure Data Source 向导的 Advanced 按钮提供了一个 “ Generate INSERT, UPDATE, and DELETE statements” 复选框,该复选框将根据 SELECT 语句自动生成这些语句。

除了“Generate INSERT, UPDATE, and DELETE statements” 复选框之外,高级SQL 生成选项还包括一个“Use optimistic concurrency” 选项(见图1 )。选中该选项后,如果在用户上一次将数据加载到网格之后没有修改基础数据库数据,则将修改UPDATE 和 DELETE 中自动生成的 WHERE 子句,只执行更新和删除操作。

图1 :可以从 Advanced SQL Generation Options 对话框添加并发优化支持

返回实施并发优化 教程,我们探讨了并发优化的基础知识,以及如何向ObjectDataSource 添加并发优化控件。在本教程中,我们将再次探讨并发优化控件的基础知识,然后探讨如何使用 SqlDataSource 实现。

并发优化简介

对于允许多个用户同时编辑或删除相同数据的Web 应用程序来说,某个用户有可能会偶然覆盖其它用户的更改。在实施并发优化 教程中,我将给出下列示例:

假设两个用户Jisun 和 Sam 均在应用程序中访问某一页面,该页面允许访问者通过GridView 控件更新和删除产品。两者在差不多相同的时间均单击了 “Chai” 的 Edit 按钮。Jisun 将产品名称更改为 “Chai Tea” ,并单击了Update 按钮。最终结果是,UPDATE 语句被发送到数据库,该语句设置了产品所有的可更新字段(尽管 Jisun 仅更新了一个字段 ProductName )。 此时,数据库中此产品包含了 “Chai Tea” ,类别 Beverages 和供应商 Exotic Liquids 等值。但是,Sam 屏幕上的 GridView 仍然在可编辑 GridView 行中显示产品名称为 “Chai” 。Jisun 完成更改之后几秒钟 ,Sam 将类别更新为 Condiments ,并单击了Update 。这导致 PDATE 语句将被发送到数据库,该数据库将产品名称设置为 “Chai” ,而CategoryID 则被设置为相应的 Condiments 类别 ID ,其它的与此类似。Jisun 的对产品名称的更改已经被覆盖。

图2 显示了此交互过程。

图2 :两个用户同时更新记录时,一个用户的更改可能覆盖另外一个用户的更改

为了防止这种情况出现,必须执行某种形式的并发控件 。并发优化 作为本教程的重点,将假定尽管偶尔存在并发冲突,但是绝大多数情况下这种冲突并不会出现。因此,如果发生冲突,并发优化控件将通知用户无法保存他们所做的修改,因为其它用户已经修改了同一数据。

注意:对于那些假设会存在很多并发冲突,或者这些冲突是根本不容许的应用来说,可使用保守式控件。有关保守式控件的更详细讨论,请参照实施并发优化 教程。

并发优化控件的作用是确保正在更新或删除的记录保持其更新或删除过程开始的同样的值。例如,在可编辑 GridView 中单击 Edit 按钮时,将从数据库读取记录值,并在文本框和其它 Web 控件中显示记录值。这些原始值由 GridView 保存。随后,在用户完成更改并单击 Update 按钮之后,使用的 UPDATE 语句必须考虑原始值和新值,并且如果开始编辑的原始值与数据库中的值完全相同,则仅更新基础数据库记录。图 3 描述了事件的顺序。

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

有多种方式可执行并发优化(请参阅 Peter A. Bromberg 的并发优化更新逻辑 ,查看多种选择)。SqlDataSource 使用的技术(和数据访问层中使用的 ADO.NET 强类型 DataSets )将添加WHERE 子句,从而包含对所有原始值的比较。例如,只有当目前的数据库值等于更新 GridView 的记录时最初检索到的值时,下面的 UPDATE 语句才更新产品的名称和价格。参数 @ProductName 和 @UnitPrice 包含了由用户输入的新值,而 @original_ProductName 和 @original_UnitPrice 包含了单击 Edit 按钮时最初加载到 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 。接下来,从控件的智能标记单击 Configure Data Source 链接。在向导中的第一个屏幕上,选择使用NORTHWINDConnectionString ,并单击 Next 。

图4 :选择使用 NORTHWINDConnectionString

在本例中,我们将添加一个能够帮助用户编辑 Products 表的 GridView 。因此,在 “ Configure the Select Statement” 屏幕中,从下拉列表选择 Products列表,并选择 ProductID 、ProductName 、UnitPrice 和 Discontinued 列,如图 5 所示。

图5 :从 Products 表返回 ProductID 、ProductName 、UnitPrice 和 Discontinued 列

在选定列之后,单击Advanced 按钮,显示 Advanced SQL Generation Options 对话框。选择 “Generate INSERT, UPDATE, and DELETE statements” 和 “Use optimistic concurrency” 复选框,单击OK (屏幕快照请参照图 1) 。单击 Next ,然后再单击 Finish 完成向导。

完成 Configure Data Source 向导后,请花点时间检查结果的 DeleteCommand 和 UpdateCommand 属性,以及 DeleteParameters 和 UpdateParameters 集合。完成此操作的最简单的方式是单击左下角的 Source 选项卡,查看页面的声明式语法。在此处将会发现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 集合中有7个参数:

<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 子句增加 UpdateCommand 和DeleteCommand 属性外(以及向各自的参数集添加其它参数外),选择“Use optimistic concurrency” 选项,调整两个其它属性 :

Web 数据 控件调用 SqlDataSource 的 Update() 或者Delete() 方法时,它将传递原始值。如果 SqlDataSource 的 ConflictDetection 属性设置为 CompareAllValues ,命令中将添加这些原始值。OldValuesParameterFormatString 属性提供了这些原始值参数使用的命名形式。Configure Data Source 向导使用 “original_{0}” ,并在UpdateCommand 和 DeleteCommand 属性以及UpdateParameters 和 DeleteParameters 集中命名相应的原始参数。

注意:由于我们未使用 SqlDataSource 控件的插入功能,您可以按照您自己的意愿删除InsertCommand 属性及其InsertParameters 集合。

正确处理 NULL 值

遗憾的是,在使用并发优化时,由 Configure Data Source 向导所自动生成的扩充的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 子句不会返回需要更新和删除的任何行。

注意:这个问题已于 2004 年 6 月第一次在 SqlDataSource 生成错误的 SQL 语句 中报告给微软,据传将在下一版本的ASP.NET 中解决。

要解决该问题,我们必须在 UpdateCommand 和 DeleteCommand 属性中为可拥有 NULL 值的所有列手动更新WHERE 子句。通常情况下,请将[ColumnName] = @original_ColumnName 更改为:

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

此修改可直接通过声明式标记完成,您可以从Properties 窗口使用 UpdateQuery 或DeleteQuery 选项,或者在 Configure Data Source 向导中使用 “Specify a custom SQL statement or stored procedure” 中的 UPDATE 和 DELETE 选项卡。同样,必须对可能包含 NULL 值的 UpdateCommand 和 DeleteCommand 的 WHERE 子 句中的每一列 进行修改。

将这种情况应用到我们的例子中将使得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 添加 Edit 和 Delete 选项

将SqlDataSource 配置为支持并发优化之后,剩下需要做的事情是向页面中添加使用此并发控件的Web 数据 控件。在本教程中,我们将添加一个既可以支持编辑功能,又可以支持删除功能的 GridView 。要完成此操作,从工具栏中拖拽一个 GridView 到 设计器 中,并将其ID 设置为 Products 。在 GridView 的智能标记中,将其绑定到步骤 1 中添加的 ProductsDataSourceWithOptimisticConcurrency SqlDataSource 控件。最后,从智能标记选中 “Enable Editing” 和 “Enable Deleting” 选项。

图6 :将 GridView 绑定到 SqlDataSource ,并启用编辑和删除功能

在添加GridView 之后,通过删除 ProductID BoundField ,将 ProductName BoundField 的HeaderText 属性更改为 “Product” ,以及更新UnitPrice BoundField 的方式配置其外观,从而其 HeaderText 属性变为 “Price” 。理想情况下,我们应该加强编辑界面,使之包括一个ProductName 值的 RequiredFieldValidator,以及一个 UnitPrice 值的 CompareValidator (保证其数值可保持正确格式)有关定制 GridView 的编辑界面的更详细信息,请参阅自定义数据修改界面 教程。

注意:由于从 GridView 获取的原始值存储在视图状态下,因此必须启用GridView 的视图状态。

完成对 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 页。在两个浏览器中分别单击第一个产品的 Edit 按钮。在第一个浏览器中,改变产品名称,并单击 Update 。此浏览器将回传,而 GridView 将返回到其预编辑模式,显示新编辑记录的新的产品名称。

在第二个浏览器窗口中,更改价格(但是保留产品名称为原始值),并单击 Update 。回传时,网格返回其预编辑模式,但并未记录价格更改。第二个浏览器中显示的值(新产品名称和旧价格)与第一个浏览器中的完全一样。第二个浏览器窗口中所作的更改已经丢失。而且,由于没有显示任何表明出现并发异常的异常或信息,更改丢失得非常平静。

图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 按钮。由于DELETE 语句的 WHERE 子句中的原始值不匹配,因此删除操作毫无声息的以失败告终。

从最终用户的角度看,在第二个用户的浏览器窗口中单击 Update 按钮后,网格将返回到预编辑模式,但是它们所作的修改已经丢失。但是,此处不存在不保持更改的可见回传。理想情况下,如果由于并发冲突的原因导致用户所作更改丢失,我们应该通知他们,或者将网格保持在编辑模式下。下面,我们探讨一下如何完成此操作。

步骤3 :确定出现并发冲突的时间

由于并发冲突拒绝了我们所作的修改,因此,最好能够在出现并发冲突时提醒用户。要想提醒用户,我们需要首先在页面的顶部添加一个名为ConcurrencyViolationMessage 的Web 标签控件,其文本属性显示下列信息:" You have attempted to update or delete a record that was simultaneously updated by another user. Please review the other user's changes and then redo your update or delete."将标签控件的CssClass 属性设置为 “Warning” ,该属性为 Styles.css 中定义的CSS 类,可以红色、斜体、粗体和大字体显示文本。最后,将标签 的Visible 和 EnableViewState 属性设置为False 。除非我们在这些回传中明确将其 Visible 属性设置为 True , 否则将隐藏标签 。

图8 :在页面上添加一个显示警告的标签控件

执行更新或删除操作时,GridView 的 RowUpdated 和 RowDeleted Event Handler 将在其数据源控件完成所需的更新或删除操作后释放。我们可以确定这些 Event Handler 操作所影响的行数。如果影响为零行,我们希望显示ConcurrencyViolationMessage 标签 。

请为 RowUpdated 和 RowDeleted 事件创建 Event Handler ,并添加下列代码:

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

在两个Event Handler 中,选中 e.AffectedRows 属性,如果它等于 0 ,则将 ConcurrencyViolationMessage 标签 的Visible 属性设置为 True 。在 RowUpdated Event Handler 中,我们还可通过将 KeepInEditMode 设置为 true 来指定 GridView 保持编辑模式。通过这种方式,我们需要将数据重新绑定到网格,从而保证其它用户的数据将被加载到编辑界面。此操作可通过调用 GridView 的 DataBind() 方法完成。

如图9 所示,通过使用这两个 Event Handler ,出现并发冲突时,屏幕上将显示一条非常醒目的信息。

图9 :出现并发冲突时显示信息

小结

在多个并发用户可能编辑相同数据的情况下,创建web 应用程序时需要考虑并发控件选项就变得十分重要。在默认情况下, ASP.NET Web 数据控件和数据源控件不使用任何并发控件。正如我们在本教程中所看到的,使用 SqlDataSource 实现并发优化控件非常快捷和便利。SqlDataSource 可以为您处理大多数日常工作—— 向自动生成的 UPDATE 和 DELETE 语句添加扩充 WHERE 子句,但是,正如在 “正确处理NULL 值” 部分所讨论的,几乎不存在处理 NULL 值列的示例。

本教程是最后一篇对 SqlDataSource 进行 探讨的教程。此后的教程中探讨使用 ObjectDataSource 和分层架构处理数据。


 

实现乐观并发 (C#)

作者 :Scott Mitchell

下载 PDF

对于允许多个用户编辑数据的 Web 应用程序,有两个用户可能同时编辑相同数据的风险。 在本教程中,我们将实现乐观并发控制来处理此风险。

介绍

对于仅允许用户查看数据的 Web 应用程序,或者仅包含一个可以修改数据的用户,两个并发用户不会意外覆盖彼此的更改的威胁。 但是,对于允许多个用户更新或删除数据的 Web 应用程序,一个用户的修改可能会与另一个并发用户的修改发生冲突。 如果没有任何并发策略,当两个用户同时编辑单个记录时,提交更改的用户最后一次将覆盖第一个用户所做的更改。

例如,假设两个用户 Jisun 和 Sam 都访问了应用程序中的一个页面,允许访问者通过 GridView 控件更新和删除产品。 两者同时单击 GridView 中的“编辑”按钮。 Jisun 将产品名称更改为“柴茶”,然后单击“更新”按钮。 净结果是 UPDATE 发送到数据库的语句,该语句将产品 的所有 可更新字段设置为 (即使 Jisun 仅更新了一个字段, ProductName) 也是如此。 此时,数据库具有“柴茶”、饮料、供应商异国液体等值。此特定产品。 但是,Sam 屏幕上的 GridView 仍会将可编辑 GridView 行中的产品名称显示为“Chai”。 提交 Jisun 更改几秒后,Sam 会将类别更新为 Condiments 并单击“更新”。 这将导致 UPDATE 发送到数据库的语句,该语句将产品名称设置为“Chai”,以及 CategoryID 相应的饮料类别 ID 等。 已覆盖 Jisun 对产品名称所做的更改。 图 1 以图形方式描绘了这一系列事件。

当两个用户同时更新记录时,可能会有一个用户的更改覆盖另一个用户

图 1:当两个用户同时更新记录时,一个用户的更改可能会覆盖另一个用户 (单击以查看全尺寸图像)

同样,当两个用户访问页面时,一个用户可能会在另一个用户删除记录时更新记录。 或者,当用户加载页面和单击“删除”按钮时,其他用户可能修改了该记录的内容。

有三种可用的 并发控制 策略:

  • 不执行任何操作 - 如果并发用户正在修改同一记录,则让上次提交获胜 (默认行为)
  • 乐观并发 - 假设虽然现在和以后可能会发生并发冲突,但大多数此类冲突都不会出现:因此,如果发生冲突,只需通知用户无法保存其更改,因为其他用户修改了相同的数据
  • 悲观 并发 - 假设并发冲突是常见的,并且用户不会容忍被告知由于其他用户的并发活动而无法保存更改:因此,当用户开始更新记录时,将其锁定,从而阻止任何其他用户编辑或删除该记录,直到用户提交其修改

到目前为止,我们的所有教程都使用了默认并发解析策略,即我们让最后一次写入获胜。 本教程介绍如何实现乐观并发控制。

 备注

本系列教程不会介绍悲观并发示例。 很少使用悲观并发,因为此类锁(如果未正确放弃)可以防止其他用户更新数据。 例如,如果用户锁定记录进行编辑,然后在解锁前一天离开,则其他用户将无法更新该记录,直到原始用户返回并完成其更新。 因此,在使用悲观并发的情况下,通常会有一个超时,如果达到,则会取消锁。 票务销售网站在用户完成订单流程时锁定特定座位位置,是悲观并发控制的示例。

步骤 1:查看如何实现乐观并发

乐观并发控制的工作原理是确保更新或删除的记录与更新或删除进程启动时的值相同。 例如,单击可编辑 GridView 中的“编辑”按钮时,记录的值将从数据库中读取,并在 TextBoxes 和其他 Web 控件中显示。 GridView 保存这些原始值。 稍后,在用户进行更改并单击“更新”按钮后,原始值加上新值将发送到业务逻辑层,然后向下发送到数据访问层。 如果用户开始编辑的原始值与数据库中的值相同,则数据访问层必须发出 SQL 语句,该语句只会更新记录。 图 2 描述了此事件序列。

若要使更新或删除成功,原始值必须等于当前数据库值

图 2:若要使更新或删除成功,原始值必须与当前数据库值相等, (单击以查看全尺寸图像)

可通过各种方法来实现乐观并发 (请参阅 Peter A. Bromberg 的 乐观并发更新逻辑 ,简要了解) 的多个选项。 ADO.NET 类型化数据集提供一个实现,只需勾选复选框的刻度即可配置。 为 Typed DataSet 中的 TableAdapter 启用乐观并发会增强 TableAdapter 的 UPDATE 和 DELETE 语句,以包含子句中 WHERE 所有原始值的比较。 例如,以下 UPDATE 语句仅当当前数据库值等于更新 GridView 中的记录时最初检索的值时,才会更新产品的名称和价格。 @ProductName参数@UnitPrice包含用户输入的新值,而@original_ProductName包含@original_UnitPrice单击“编辑”按钮时最初加载到 GridView 中的值:

SQL
UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

 备注

此 UPDATE 语句已简化为可读性。 实际上,将更涉及子WHERE句中的签入,UnitPrice因为UnitPrice可以包含 NULL s 并检查是否NULL = NULL始终返回 False (,而必须使用IS NULL) 。

除了使用不同的基础 UPDATE 语句,配置 TableAdapter 以使用乐观并发还修改其 DB 直接方法的签名。 回顾第一篇教程“ 创建数据访问层”,DB 直接方法是接受标量值列表作为输入参数 (而不是强类型 DataRow 或 DataTable 实例) 。 使用乐观并发时,DB 直接 Update() 和 Delete() 方法还包括原始值的输入参数。 此外,BLL 中用于使用批处理更新模式的代码 (Update() 接受 DataRows 和 DataTable 的方法重载,而不是) 必须更改标量值。

我们不需要扩展现有的 DAL 的 TableAdapters 来使用乐观并发 (这需要更改 BLL 以适应) ,而是创建名为 NorthwindOptimisticConcurrency的新类型化数据集,我们将向其添加 Products 使用乐观并发的 TableAdapter。 接下来,我们将创建一个 ProductsOptimisticConcurrencyBLL 业务逻辑层类,该类具有适当的修改以支持乐观并发 DAL。 完成此基础工作后,我们将准备好创建 ASP.NET 页面。

步骤 2:创建支持乐观并发的数据访问层

若要创建新的类型化数据集,请 DAL 右键单击文件夹中的文件夹 App_Code ,并添加新名为 NorthwindOptimisticConcurrencyDataSet 的数据集。 正如我们在第一个教程中看到的那样,这样做会将新的 TableAdapter 添加到 Typed DataSet,从而自动启动 TableAdapter 配置向导。 在第一个屏幕中,系统会提示指定要连接到的数据库 - 使用 NORTHWNDConnectionString 设置从 Web.config中连接到同一 Northwind 数据库。

连接到同一 Northwind 数据库

图 3:连接到同一 Northwind 数据库 (单击以查看全尺寸图像)

接下来,我们将提示如何查询数据:通过即席 SQL 语句、新的存储过程或现有存储过程。 由于我们在原始 DAL 中使用了即席 SQL 查询,因此也在此处使用此选项。

指定要使用即席 SQL 语句检索的数据

图 4:指定要使用即席 SQL 语句检索的数据 (单击以查看全尺寸图像)

在以下屏幕上,输入用于检索产品信息的 SQL 查询。 让我们使用原始 DAL 中用于 Products TableAdapter 的完全相同的 SQL 查询,该查询返回所有 Product 列以及产品的供应商和类别名称:

SQL
SELECT   ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
           UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
           (SELECT CategoryName FROM Categories
              WHERE Categories.CategoryID = Products.CategoryID)
              as CategoryName,
           (SELECT CompanyName FROM Suppliers
              WHERE Suppliers.SupplierID = Products.SupplierID)
              as SupplierName
FROM     Products

在原始 DAL 中使用 Products TableAdapter 中的相同 SQL 查询

图 5:使用原始 DAL 中的 TableAdapter 的相同 SQL 查询 Products (单击以查看全尺寸图像)

转到下一个屏幕之前,请单击“高级选项”按钮。 若要让此 TableAdapter 采用乐观并发控制,只需选中“使用乐观并发”复选框即可。

通过检查“使用乐观并发”CheckBox 启用乐观并发控制

图 6:通过选中“使用乐观并发”CheckBox (单击以查看全尺寸图像) 启用乐观并发控制

最后,指示 TableAdapter 应使用填充 DataTable 和返回 DataTable 的数据访问模式;还指示应创建 DB 直接方法。 将返回 DataTable 模式的方法名称从 GetData 更改为 GetProducts,以镜像我们在原始 DAL 中使用的命名约定。

让 TableAdapter 利用所有数据访问模式

图 7:让 TableAdapter 利用所有数据访问模式 (单击以查看全尺寸图像)

完成向导后,数据集设计器将包含强类型 Products DataTable 和 TableAdapter。 花点时间将 DataTable 重命名为ProductsOptimisticConcurrency,可以通过右键单击 DataTable Products 的标题栏并选择上下文菜单中的“重命名”来执行此操作。

已将 DataTable 和 TableAdapter 添加到类型化数据集

图 8:已将 DataTable 和 TableAdapter 添加到类型化数据集 (单击以查看全尺寸图像)

若要查看 TableAdapter ((使用乐观并发) 和DELETE不) 的产品 TableAdapter ()之间的差异UPDATEProductsOptimisticConcurrency,请单击 TableAdapter 并转到okno Vlastnosti。 在DeleteCommandUpdateCommand属性的CommandText子属性中,可以看到在调用 DAL 更新或删除相关方法时发送到数据库的实际 SQL 语法。 对于 TableAdapter,ProductsOptimisticConcurrencyDELETE使用的语句为:

SQL
DELETE FROM [Products]
    WHERE (([ProductID] = @Original_ProductID)
    AND ([ProductName] = @Original_ProductName)
    AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
       OR ([SupplierID] = @Original_SupplierID))
    AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
       OR ([CategoryID] = @Original_CategoryID))
    AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
       OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
    AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
       OR ([UnitPrice] = @Original_UnitPrice))
    AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
       OR ([UnitsInStock] = @Original_UnitsInStock))
    AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
       OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
    AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
       OR ([ReorderLevel] = @Original_ReorderLevel))
    AND ([Discontinued] = @Original_Discontinued))

DELETE而原始 DAL 中 Product TableAdapter 的语句更简单:

SQL
DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

可以看到, WHERE 使用乐观并发的 TableAdapter 语句中的 DELETE 子句包括每个 Product 表的现有列值与 GridView (或 DetailsView 或 FormView) 上次填充时的原始值之间的比较。 由于除 ProductNameDiscontinued 之外ProductID的所有字段都可以包含NULL值,因此包括其他参数和检查,以便正确比较NULL子句中的WHERE值。

我们不会将任何其他 DataTable 添加到本教程中启用乐观并发的 DataSet,因为我们的 ASP.NET 页仅提供更新和删除产品信息。 但是,我们仍然需要将 GetProductByProductID(productID) 方法添加到 ProductsOptimisticConcurrency TableAdapter。

为此,请右键单击 TableAdapter 的标题栏, (Fill 右侧的区域和 GetProducts 方法名称) 并选择上下文菜单中的“添加查询”。 这将启动 TableAdapter 查询配置向导。 与 TableAdapter 的初始配置一样,选择使用即席 SQL 语句创建 GetProductByProductID(productID) 方法 (请参阅图 4) 。 由于该方法 GetProductByProductID(productID) 返回有关特定产品的信息,因此指示此查询是 SELECT 返回行的查询类型。

将查询类型标记为“返回行的 SELECT”

图 9:将查询类型标记为“返回行”SELECT (单击以查看全尺寸图像)

在下一个屏幕上,系统会提示 SQL 查询使用,并预加载 TableAdapter 的默认查询。 扩充现有查询以包含子句 WHERE ProductID = @ProductID,如图 10 所示。

将 WHERE 子句添加到预加载查询以返回特定产品记录

图 10:向预加载的查询添加子 WHERE 句以返回特定产品记录 (单击以查看全尺寸图像)

最后,将生成的方法名称更改为 FillByProductID 和 GetProductByProductID

将方法重命名为 FillByProductID 和 GetProductByProductID

图 11:将方法重命名 (为FillByProductIDGetProductByProductID单击”以查看全尺寸图像)

完成此向导后,TableAdapter 现在包含两种检索数据的方法: GetProducts()返回 所有 产品;返回 GetProductByProductID(productID)指定产品。

步骤 3:为乐观Concurrency-Enabled DAL 创建业务逻辑层

ProductsBLL现有类包含使用批处理更新和 DB 直接模式的示例。 该方法 AddProduct 和 UpdateProduct 重载都使用批处理更新模式,将实例传递到 ProductRow TableAdapter 的 Update 方法。 另一方面,该方法 DeleteProduct 使用 DB 直接模式,调用 TableAdapter Delete(productID) 的方法。

使用新的 ProductsOptimisticConcurrency TableAdapter,DB 直接方法现在要求也传入原始值。 例如,该方法Delete现在需要十个输入参数:原始ProductID、、ProductNameSupplierIDCategoryIDUnitPriceQuantityPerUnitUnitsOnOrderUnitsInStock、、和。 ReorderLevelDiscontinued 它在发送到数据库的语句的子句DELETE中使用WHERE这些附加输入参数的值,仅当数据库的当前值映射到原始记录时,才删除指定的记录。

虽然在批处理更新模式中使用的 TableAdapter Update 方法的方法签名尚未更改,但记录原始值和新值所需的代码已更改。 因此,让我们创建一个新的业务逻辑层类,而不是尝试将已启用乐观并发的 DAL 与现有 ProductsBLL 类配合使用。

将名为ProductsOptimisticConcurrencyBLL文件夹的App_Code类添加到BLL文件夹中。

将 ProductsOptimisticConcurrencyBLL 类添加到 BLL 文件夹

图 12:将 ProductsOptimisticConcurrencyBLL 类添加到 BLL 文件夹

接下来,将以下代码添加到 ProductsOptimisticConcurrencyBLL 类:

C#
using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
    private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
    protected ProductsOptimisticConcurrencyTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
}

请注意类声明开头上方的 using NorthwindOptimisticConcurrencyTableAdapters 语句。 命名空间 NorthwindOptimisticConcurrencyTableAdapters 包含 ProductsOptimisticConcurrencyTableAdapter 提供 DAL 方法的类。 此外,在类声明之前,你将找到该 System.ComponentModel.DataObject 属性,该属性指示 Visual Studio 在 ObjectDataSource 向导的下拉列表中包含此类。

ProductsOptimisticConcurrencyBLLAdapter属性提供对类实例的ProductsOptimisticConcurrencyTableAdapter快速访问,并遵循原始 BLL 类 (ProductsBLLCategoriesBLL中使用的模式,等等) 。 最后,该方法GetProducts()只需调用 DAL GetProducts() 的方法,并返回一个ProductsOptimisticConcurrencyDataTableProductsOptimisticConcurrencyRow对象,该对象使用数据库中每个产品记录的实例填充。

使用具有乐观并发的 DB 直接模式删除产品

对使用乐观并发的 DAL 使用 DB 直接模式时,必须传递新值和原始值。 若要删除,没有新值,因此只需传入原始值。 在 BLL 中,我们必须接受所有原始参数作为输入参数。 让我们 DeleteProduct 让类中的 ProductsOptimisticConcurrencyBLL 方法使用 DB 直接方法。 这意味着此方法需要将所有十个产品数据字段作为输入参数,并将其传递给 DAL,如以下代码所示:

C#
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
    (int original_productID, string original_productName,
    int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued)
{
    int rowsAffected = Adapter.Delete(original_productID,
                                      original_productName,
                                      original_supplierID,
                                      original_categoryID,
                                      original_quantityPerUnit,
                                      original_unitPrice,
                                      original_unitsInStock,
                                      original_unitsOnOrder,
                                      original_reorderLevel,
                                      original_discontinued);
    // Return true if precisely one row was deleted, otherwise false
    return rowsAffected == 1;
}

如果原始值 (上次加载到 GridView (或 DetailsView 或 FormView) 中的值)与用户单击“删除”按钮 WHERE 时数据库中的值不同,该子句不会与任何数据库记录匹配,并且不会影响任何记录。 因此,TableAdapter Delete 的方法将返回 0 ,BLL DeleteProduct 的方法将返回 false

使用具有乐观并发的 Batch 更新模式更新产品

如前所述,无论是否采用乐观并发,TableAdapter Update 的批处理更新模式方法都具有相同的方法签名。 也就是说,该方法 Update 需要 DataRow、DataRows 数组、DataTable 或类型化数据集。 没有用于指定原始值的附加输入参数。 这是可能的,因为 DataTable 会跟踪其 DataRow () 的原始值和修改的值。 当 DAL 发出其 UPDATE 语句时, @original_ColumnName 参数将填充 DataRow 的原始值,而 @ColumnName 参数则用 DataRow 的修改值填充。

ProductsBLL在使用原始的非乐观并发 DAL) 的类 (,使用批处理更新模式更新代码执行以下事件序列时:

  1. 使用 TableAdapter GetProductByProductID(productID) 的方法将当前数据库产品信息读取到ProductRow实例中
  2. 将新值分配给步骤 1 中的 ProductRow 实例
  3. 调用 TableAdapter Update 的方法,传入 ProductRow 实例

但是,由于步骤 1 中填充的内容直接从数据库填充,因此 ProductRow 无法正确支持乐观并发,这意味着 DataRow 使用的原始值是数据库中当前存在的值,而不是在编辑过程开始时绑定到 GridView 的原始值。 相反,在使用启用了乐观并发的 DAL 时,我们需要更改 UpdateProduct 方法重载以使用以下步骤:

  1. 使用 TableAdapter GetProductByProductID(productID) 的方法将当前数据库产品信息读取到ProductsOptimisticConcurrencyRow实例中
  2. 将 原始 值分配给步骤 1 中的 ProductsOptimisticConcurrencyRow 实例
  3. ProductsOptimisticConcurrencyRow调用实例AcceptChanges()的方法,该方法指示 DataRow 的当前值是“原始”值
  4. 将  值分配给 ProductsOptimisticConcurrencyRow 实例
  5. 调用 TableAdapter Update 的方法,传入 ProductsOptimisticConcurrencyRow 实例

步骤 1 读取指定产品记录的所有当前数据库值。 此步骤在重载中 UpdateProduct 是多余的,它更新 所有 产品列 (,因为这些值在步骤 2) 中被覆盖,但对于仅作为输入参数传入列值的子集的重载至关重要。 将原始值赋给ProductsOptimisticConcurrencyRow实例后,AcceptChanges()将调用该方法,该方法将当前 DataRow 值标记为在语句中的UPDATE参数中使用的@original_ColumnName原始值。 接下来,将新参数值分配给 ProductsOptimisticConcurrencyRow 其中,最后调用 Update 该方法,传入 DataRow。

以下代码显示了 UpdateProduct 接受所有产品数据字段作为输入参数的重载。 虽然此处未显示, ProductsOptimisticConcurrencyBLL 但本教程下载中包含的类还包含一个 UpdateProduct 重载,仅接受产品的名称和价格作为输入参数。

C#
protected void AssignAllProductValues
    (NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued)
{
    product.ProductName = productName;
    if (supplierID == null)
        product.SetSupplierIDNull();
    else
        product.SupplierID = supplierID.Value;
    if (categoryID == null)
        product.SetCategoryIDNull();
    else
        product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null)
        product.SetQuantityPerUnitNull();
    else
        product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null)
        product.SetUnitPriceNull();
    else
        product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null)
        product.SetUnitsInStockNull();
    else
        product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null)
        product.SetUnitsOnOrderNull();
    else
        product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null)
        product.SetReorderLevelNull();
    else
        product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(
    // new parameter values
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued, int productID,
    // original parameter values
    string original_productName, int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued,
    int original_productID)
{
    // STEP 1: Read in the current database product information
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products =
        Adapter.GetProductByProductID(original_productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = products[0];
    // STEP 2: Assign the original values to the product instance
    AssignAllProductValues(product, original_productName, original_supplierID,
        original_categoryID, original_quantityPerUnit, original_unitPrice,
        original_unitsInStock, original_unitsOnOrder, original_reorderLevel,
        original_discontinued);
    // STEP 3: Accept the changes
    product.AcceptChanges();
    // STEP 4: Assign the new values to the product instance
    AssignAllProductValues(product, productName, supplierID, categoryID,
        quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel,
        discontinued);
    // STEP 5: Update the product record
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

步骤 4:将原始值和新值从 ASP.NET 页传递到 BLL 方法

完成 DAL 和 BLL 后,剩下的一切都是创建一个 ASP.NET 页,该页可以利用内置于系统的乐观并发逻辑。 具体而言,数据 Web 控件 (GridView、DetailsView 或 FormView) 必须记住其原始值,ObjectDataSource 必须将这两组值传递给业务逻辑层。 此外,必须将 ASP.NET 页配置为正常处理并发冲突。

首先 OptimisticConcurrency.aspx 打开 EditInsertDelete 文件夹中的页面,并将 GridView 添加到设计器,并将其 ID 属性设置为 ProductsGrid。 从 GridView 的智能标记中,选择创建名为 ProductsOptimisticConcurrencyDataSource 的新 ObjectDataSource。 由于我们希望此 ObjectDataSource 使用支持乐观并发的 DAL,因此请将其配置为使用该 ProductsOptimisticConcurrencyBLL 对象。

让 ObjectDataSource 使用 ProductsOptimisticConcurrencyBLL 对象

图 13:让 ObjectDataSource 使用 ProductsOptimisticConcurrencyBLL 对象 (单击以查看全尺寸图像)

GetProducts从向导中的下拉列表中选择和UpdateProductDeleteProduct方法。 对于 UpdateProduct 方法,请使用接受产品所有数据字段的重载。

配置 ObjectDataSource 控件的属性

完成向导后,ObjectDataSource 的声明性标记应如下所示:

ASP.NET
<asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
    DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
    SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
    UpdateMethod="UpdateProduct">
    <DeleteParameters>
        <asp:Parameter Name="original_productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
    </DeleteParameters>
    <UpdateParameters>
        <asp:Parameter Name="productName" Type="String" />
        <asp:Parameter Name="supplierID" Type="Int32" />
        <asp:Parameter Name="categoryID" Type="Int32" />
        <asp:Parameter Name="quantityPerUnit" Type="String" />
        <asp:Parameter Name="unitPrice" Type="Decimal" />
        <asp:Parameter Name="unitsInStock" Type="Int16" />
        <asp:Parameter Name="unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="reorderLevel" Type="Int16" />
        <asp:Parameter Name="discontinued" Type="Boolean" />
        <asp:Parameter Name="productID" Type="Int32" />
        <asp:Parameter Name="original_productName" Type="String" />
        <asp:Parameter Name="original_supplierID" Type="Int32" />
        <asp:Parameter Name="original_categoryID" Type="Int32" />
        <asp:Parameter Name="original_quantityPerUnit" Type="String" />
        <asp:Parameter Name="original_unitPrice" Type="Decimal" />
        <asp:Parameter Name="original_unitsInStock" Type="Int16" />
        <asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
        <asp:Parameter Name="original_reorderLevel" Type="Int16" />
        <asp:Parameter Name="original_discontinued" Type="Boolean" />
        <asp:Parameter Name="original_productID" Type="Int32" />
    </UpdateParameters>
</asp:ObjectDataSource>

可以看到,DeleteParameters集合包含类DeleteProduct方法中ProductsOptimisticConcurrencyBLL十个Parameter输入参数中的每个实例。 同样,集合 UpdateParameters 包含 Parameter 每个输入参数的 UpdateProduct实例。

对于涉及数据修改的前面的教程,我们此时会删除 ObjectDataSource OldValuesParameterFormatString 的属性,因为此属性指示 BLL 方法需要传入旧 (或原始) 值以及新值。 此外,此属性值指示原始值的输入参数名称。 由于我们将原始值传入 BLL, 因此不要 删除此属性。

 备注

属性的值 OldValuesParameterFormatString 必须映射到需要原始值的 BLL 中的输入参数名称。 由于我们命名这些参数original_productNameoriginal_supplierID,等等,因此可以将属性值保留OldValuesParameterFormatString为 original_{0} a0/>。 但是,如果 BLL 方法的输入参数具有名称,old_productNameold_supplierID等等,则需要将OldValuesParameterFormatString属性更新为 old_{0}

需要进行最后一个属性设置,以便 ObjectDataSource 正确将原始值传递给 BLL 方法。 ObjectDataSource 具有一个 ConflictDetection 属性 ,该属性可分配给 以下两个值之一

  • OverwriteChanges - 默认值;不将原始值发送到 BLL 方法的原始输入参数
  • CompareAllValues - 将原始值发送到 BLL 方法;使用乐观并发时选择此选项

花点时间将属性设置为 ConflictDetectionCompareAllValues

配置 GridView 的属性和字段

正确配置 ObjectDataSource 的属性后,让我们注意如何设置 GridView。 首先,由于我们希望 GridView 支持编辑和删除,因此单击 GridView 智能标记中的“启用编辑”和“启用删除”复选框。 这将添加一个 CommandField,其 ShowEditButton 两 ShowDeleteButton 者均设置为 true

绑定到 ProductsOptimisticConcurrencyDataSource ObjectDataSource 时,GridView 包含每个产品数据字段的字段。 虽然可以编辑此类 GridView,但用户体验是可以接受的。 和 CategoryIDSupplierID BoundFields 将呈现为 TextBox,要求用户输入相应的类别和供应商作为 ID 号。 数字字段没有格式设置,也没有验证控件,以确保已提供产品名称,并且单价、库存单位、订单单位和重新排序级别值都是正确的数值,并且大于或等于零。

如我们在“将验证控件添加到编辑和插入接口”和“自定义数据修改接口”教程中所述,可以通过将 BoundFields 替换为 TemplateFields 来自定义用户界面。 我通过以下方式修改了此 GridView 及其编辑界面:

  • 删除了ProductID和 SupplierNameCategoryName BoundFields
  • 将 ProductName BoundField 转换为 TemplateField 并添加了 RequiredFieldValidation 控件。
  • 将 CategoryID And SupplierID BoundFields 转换为 TemplateFields,并调整了编辑界面以使用 DropDownLists 而不是 TextBoxes。 在这些 TemplateFields 中 ItemTemplates, CategoryName 将显示数据字段和数据 SupplierName 字段。
  • UnitPrice和 UnitsInStockUnitsOnOrderReorderLevel BoundFields 转换为 TemplateFields,并添加了 CompareValidator 控件。

由于我们已经了解了如何在前面的教程中完成这些任务,因此我将在此处列出最终声明性语法,并将实现保留为实践。

ASP.NET
<asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
    OnRowUpdated="ProductsGrid_RowUpdated">
    <Columns>
        <asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
        <asp:TemplateField HeaderText="Product" SortExpression="ProductName">
            <EditItemTemplate>
                <asp:TextBox ID="EditProductName" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:TextBox>
                <asp:RequiredFieldValidator ID="RequiredFieldValidator1"
                    ControlToValidate="EditProductName"
                    ErrorMessage="You must enter a product name."
                    runat="server">*</asp:RequiredFieldValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label1" runat="server"
                    Text='<%# Bind("ProductName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditCategoryID" runat="server"
                    DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
                    DataTextField="CategoryName" DataValueField="CategoryID"
                    SelectedValue='<%# Bind("CategoryID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetCategories" TypeName="CategoriesBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label2" runat="server"
                    Text='<%# Bind("CategoryName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
            <EditItemTemplate>
                <asp:DropDownList ID="EditSuppliersID" runat="server"
                    DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
                    DataTextField="CompanyName" DataValueField="SupplierID"
                    SelectedValue='<%# Bind("SupplierID") %>'>
                    <asp:ListItem Value=">(None)</asp:ListItem>
                </asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource"
                    runat="server" OldValuesParameterFormatString="original_{0}"
                    SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
                </asp:ObjectDataSource>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label3" runat="server"
                    Text='<%# Bind("SupplierName") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
            SortExpression="QuantityPerUnit" />
        <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitPrice" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
                <asp:CompareValidator ID="CompareValidator1" runat="server"
                    ControlToValidate="EditUnitPrice"
                    ErrorMessage="Unit price must be a valid currency value without the
                    currency symbol and must have a value greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Currency"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label4" runat="server"
                    Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsInStock" runat="server"
                    Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator2" runat="server"
                    ControlToValidate="EditUnitsInStock"
                    ErrorMessage="Units in stock must be a valid number
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label5" runat="server"
                    Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
            <EditItemTemplate>
                <asp:TextBox ID="EditUnitsOnOrder" runat="server"
                    Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator3" runat="server"
                    ControlToValidate="EditUnitsOnOrder"
                    ErrorMessage="Units on order must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label6" runat="server"
                    Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
            <EditItemTemplate>
                <asp:TextBox ID="EditReorderLevel" runat="server"
                    Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox>
                <asp:CompareValidator ID="CompareValidator4" runat="server"
                    ControlToValidate="EditReorderLevel"
                    ErrorMessage="Reorder level must be a valid numeric value
                        greater than or equal to zero."
                    Operator="GreaterThanEqual" Type="Integer"
                    ValueToCompare="0">*</asp:CompareValidator>
            </EditItemTemplate>
            <ItemTemplate>
                <asp:Label ID="Label7" runat="server"
                    Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label>
            </ItemTemplate>
        </asp:TemplateField>
        <asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
            SortExpression="Discontinued" />
    </Columns>
</asp:GridView>

我们非常接近有一个完全工作的示例。 然而,有一些微妙之处会爬起来,并引起我们的问题。 此外,我们仍然需要一些界面,在发生并发冲突时向用户发出警报。

 备注

为了使数据 Web 控件正确将原始值传递到 ObjectDataSource (,然后传递给 BLL) ,GridView EnableViewState 的属性必须设置为 true (默认) 。 如果禁用视图状态,则原始值在回发时丢失。

将正确的原始值传递给 ObjectDataSource

配置 GridView 的方式存在几个问题。 如果 ObjectDataSource ConflictDetection 的属性设置为CompareAllValues () ,则当 GridView (或 DetailsView 或 FormView) 调用 ObjectDataSource 或Delete()方法时,ObjectDataSource Update() 会尝试将 GridView 的原始值复制到相应的Parameter实例中。 有关此过程的图形表示形式,请参阅图 2。

具体而言,每次将数据绑定到 GridView 时,都会在双向数据绑定语句中分配 GridView 的原始值。 因此,必须通过双向数据绑定捕获所需的原始值,并且它们以可转换格式提供。

若要查看为什么这一点很重要,请花点时间在浏览器中访问我们的页面。 如预期的那样,GridView 列出每个产品,其中最左侧列中带有“编辑和删除”按钮。

产品在 GridView 中列出

图 14:产品列在 GridView (单击以查看全尺寸图像)

如果单击任何产品的“删除”按钮,则会引发 a FormatException 。

尝试删除 FormatException 中的任何产品结果

图 15:尝试在 (单击后删除任何产品结果FormatException以查看全尺寸图像)

FormatException当 ObjectDataSource 尝试读取原始UnitPrice值时,将引发此情况。 ItemTemplate由于格式化为UnitPrice货币 () <%# Bind("UnitPrice", "{0:C}") %> ,因此它包括货币符号,如 $19.95。 当 FormatException ObjectDataSource 尝试将此字符串转换为 .decimal 为了规避此问题,我们有许多选项:

  • 从 . ItemTemplate中删除货币格式。 也就是说,而不是使用 <%# Bind("UnitPrice", "{0:C}") %>,只需使用 <%# Bind("UnitPrice") %>。 其缺点是价格不再设置格式。
  • 在 UnitPrice 格式上 ItemTemplate显示为货币,但使用 Eval 关键字来实现此目的。 回想一下, Eval 执行单向数据绑定。 我们仍然需要为原始值提供UnitPrice值,因此我们仍然需要其中的ItemTemplate双向数据绑定语句,但这可以放置在属性设置为falseVisible标签 Web 控件中。 可以在 ItemTemplate 中使用以下标记:
ASP.NET
<ItemTemplate>
    <asp:Label ID="DummyUnitPrice" runat="server"
        Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
    <asp:Label ID="Label4" runat="server"
        Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
  • Remove the currency formatting from the ItemTemplate, using <%# Bind("UnitPrice") %>. 在 GridView 的事件处理程序中 RowDataBound ,以编程方式访问显示值并将其 UnitPrice 属性设置为 Text 格式化版本的标签 Web 控件。
  • 将 UnitPrice 格式保留为货币。 在 GridView 的事件处理程序中RowDeleting,使用实际十进制Decimal.Parse值替换现有原始UnitPrice值 ($19.95) 。 我们了解了如何在 ASP.NET Page 教程中处理 BLL 和DAL-Level异常的事件处理程序中RowUpdating完成类似操作。

对于我的示例,我选择采用第二种方法,添加隐藏的标签 Web 控件,其 Text 属性是绑定到未格式化 UnitPrice 值的双向数据。

解决此问题后,再次尝试单击任何产品的“删除”按钮。 这一次,你将获得 InvalidOperationException ObjectDataSource 尝试调用 BLL UpdateProduct 方法的时间。

ObjectDataSource 找不到具有要发送的输入参数的方法

图 16:ObjectDataSource 找不到具有要发送的输入参数的方法 (单击以查看全尺寸图像)

查看异常的消息,很明显,ObjectDataSource 想要调用包含original_CategoryNameoriginal_SupplierName输入参数的 BLL DeleteProduct 方法。 这是因为ItemTemplate,s for the CategoryID and SupplierID TemplateFields 当前包含具有和SupplierName数据字段的CategoryName双向 Bind 语句。 相反,我们需要包含Bind包含和数据字段的CategoryIDSupplierID语句。 为此,请将现有 Bind 语句替换为Eval语句,然后添加隐藏的 Label 控件,其Text属性使用双向数据绑定绑定到CategoryIDSupplierID数据字段,如下所示:

ASP.NET
<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummyCategoryID" runat="server"
            Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label2" runat="server"
            Text='<%# Eval("CategoryName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
    <EditItemTemplate>
        ...
    </EditItemTemplate>
    <ItemTemplate>
        <asp:Label ID="DummySupplierID" runat="server"
            Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
        <asp:Label ID="Label3" runat="server"
            Text='<%# Eval("SupplierName") %>'></asp:Label>
    </ItemTemplate>
</asp:TemplateField>

通过这些更改,我们现在能够成功删除和编辑产品信息! 在步骤 5 中,我们将了解如何验证是否检测到并发冲突。 但现在,需要几分钟时间尝试更新和删除一些记录,以确保单个用户的更新和删除按预期工作。

步骤 5:测试乐观并发支持

为了验证是否检测到并发冲突 (而不是导致数据被盲目覆盖) ,我们需要打开此页面的两个浏览器窗口。 在这两个浏览器实例中,单击 Chai 的“编辑”按钮。 然后,在任一浏览器中,将名称更改为“Chai Tea”,然后单击“更新”。 更新应成功,并将 GridView 返回到其预编辑状态,并将“柴茶”作为新产品名称。

但是,在其他浏览器窗口实例中,产品名称 TextBox 仍显示“Chai”。 在此第二个浏览器窗口中,将更新 UnitPrice 为 25.00。 如果没有乐观并发支持,在第二个浏览器实例中单击更新会将产品名称改回“Chai”,从而覆盖第一个浏览器实例所做的更改。 但是,在采用乐观并发的情况下,单击第二个浏览器实例中的“更新”按钮会导致 DBConcurrencyException

检测到并发冲突时,将引发 DBConcurrencyException

图 17:检测到并发冲突时,将引发 (DBConcurrencyException单击以查看全尺寸图像)

DBConcurrencyException仅当使用 DAL 的批处理更新模式时才会引发该模式。 DB 直接模式不会引发异常,它只是表示没有受影响的行。 为了说明这一点,请同时将两个浏览器实例的 GridView 返回到其预编辑状态。 接下来,在第一个浏览器实例中,单击“编辑”按钮,将产品名称从“柴茶”更改为“Chai”,然后单击“更新”。 在第二个浏览器窗口中,单击 Chai 的“删除”按钮。

单击“删除”后,GridView 将调用 ObjectDataSource 的方法,而 ObjectDataSource Delete() 会向下 ProductsOptimisticConcurrencyBLL 调用类 DeleteProduct 的方法,并传递原始值。 第二个浏览器实例的原始 ProductName 值为“Chai Tea”,它与数据库中的当前 ProductName 值不匹配。 DELETE因此,向数据库发出的语句会影响零行,因为该子句满足的数据库WHERE中没有记录。 该方法 DeleteProduct 返回 false ,ObjectDataSource 的数据将反弹到 GridView。

从最终用户的角度来看,在第二个浏览器窗口中单击“删除”按钮会导致屏幕闪烁,回来后,产品仍然存在,尽管现在它被列为“Chai” (第一个浏览器实例) 的产品名称更改。 如果用户再次单击“删除”按钮,“删除”将成功,因为 GridView 的原始 ProductName 值 (“Chai”) 现在与数据库中的值匹配。

在这两种情况下,用户体验远非理想。 我们显然不想在使用批处理更新模式时向用户显示异常的 DBConcurrencyException nitty-gritty 详细信息。 使用 DB 直接模式时的行为有点令人困惑,因为用户命令失败,但没有确切说明原因。

为了修复这两个问题,我们可以在页面上创建标签 Web 控件,该控件提供有关更新或删除失败的原因的说明。 对于批处理更新模式,我们可以确定 GridView 的后级别事件处理程序中是否 DBConcurrencyException 发生异常,并根据需要显示警告标签。 对于 DB 直接方法,我们可以检查 BLL 方法 (的返回值,即 true 如果一行受到影响, false 否则) 并根据需要显示信息性消息。

步骤 6:添加信息性消息,并在出现并发冲突时显示它们

发生并发冲突时,显示的行为取决于 DAL 的批处理更新还是使用 DB 直接模式。 本教程使用这两种模式,以及用于更新的批处理更新模式和用于删除的 DB 直接模式。 若要开始,让我们将两个标签 Web 控件添加到页面,说明尝试删除或更新数据时发生并发冲突。 将“标签”控件Visible和属性设置为 false;这将导致它们在每一页访问中隐藏,但那些以编程方式设置为trueVisible属性的特定EnableViewState页面访问除外。

ASP.NET
<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    Text="The record you attempted to delete has been modified by another user
           since you last visited this 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." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
    EnableViewState="False" CssClass="Warning"
    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." />

除了设置属性VisibleEnabledViewStateText属性外,我还将属性设置为CssClass该属性Warning,这会导致标签以大、红色、斜体、粗体字体显示。 在检查与插入、更新和删除教程关联的事件时,定义了此 CSS Warning 类并将其添加到 Styles.css。

添加这些标签后,Visual Studio 中的设计器应类似于图 18。

已将两个标签控件添加到页面

图 18:已将两个标签控件添加到页面 (单击以查看全尺寸图像)

在这些标签 Web 控件准备就绪后,我们已准备好检查如何确定何时发生并发冲突,此时可以将相应的标签 Visible 的属性设置为 true显示信息性消息。

处理更新时的并发冲突

让我们首先了解如何在使用批处理更新模式时处理并发冲突。 由于此类与批处理更新模式冲突会导致 DBConcurrencyException 引发异常,因此我们需要将代码添加到 ASP.NET 页,以确定更新过程中是否 DBConcurrencyException 发生异常。 如果是这样,我们应该向用户显示一条消息,说明更改未保存,因为其他用户在开始编辑记录时和单击“更新”按钮时修改了相同的数据。

正如我们在 ASP.NET Page 教程的处理 BLL 和DAL-Level异常 中看到的那样,可以在数据 Web 控件的后期事件处理程序中检测和禁止此类异常。 因此,我们需要为 GridView RowUpdated 的事件创建事件处理程序,用于检查是否已 DBConcurrencyException 引发异常。 此事件处理程序将传递对更新过程中引发的任何异常的引用,如以下事件处理程序代码所示:

C#
protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.Exception != null && e.Exception.InnerException != null)
    {
        if (e.Exception.InnerException is System.Data.DBConcurrencyException)
        {
            // Display the warning message and note that the
            // exception has been handled...
            UpdateConflictMessage.Visible = true;
            e.ExceptionHandled = true;
        }
    }
}

面对 DBConcurrencyException 异常,此事件处理程序将显示 UpdateConflictMessage Label 控件,并指示已处理异常。 在此代码中,当更新记录时发生并发冲突时,用户所做的更改将丢失,因为它们会同时覆盖其他用户的修改。 具体而言,GridView 将返回到其预编辑状态,并绑定到当前数据库数据。 这将使用其他用户的更改更新 GridView 行,这些更改以前不可见。 此外, UpdateConflictMessage “标签”控件将向用户说明刚刚发生的情况。 图 19 中详细介绍了此事件序列。

用户更新因并发冲突而丢失

图 19:用户更新因并发冲突而丢失 (单击以查看全尺寸图像)

 备注

或者,我们可以通过将传入GridViewUpdatedEventArgs对象的属性设置为 KeepInEditMode true,而不是将 GridView 返回到预编辑状态,而不是将 GridView 保留为其编辑状态。 但是,如果采用此方法,请务必通过调用 DataBind() 其方法) 将数据重新绑定到 GridView (,以便将其他用户的值加载到编辑界面中。 本教程提供的可供下载的代码在事件处理程序中 RowUpdated 注释掉了这两行代码;只需取消注释这些代码行,使 GridView 在并发冲突后仍处于编辑模式。

在删除时响应并发冲突

使用 DB 直接模式时,如果出现并发冲突,则不会引发异常。 相反,数据库语句只影响任何记录,因为 WHERE 子句与任何记录不匹配。 BLL 中创建的所有数据修改方法都经过设计,以便返回一个布尔值,该值指示它们是否仅影响一条记录。 因此,若要确定删除记录时是否发生了并发冲突,我们可以检查 BLL 方法的 DeleteProduct 返回值。

可以通过传递给事件处理程序的对象的属性,在 ObjectDataSource 的后级事件处理程序 ReturnValue 中检查 BLL 方法的 ObjectDataSourceStatusEventArgs 返回值。 由于我们有兴趣确定方法中的Deleted返回值DeleteProduct,因此我们需要为 ObjectDataSource 的事件创建事件处理程序。 该 ReturnValue 属性的类型 object ,可以是 null 引发异常并且方法在返回值之前中断。 因此,我们首先应确保 ReturnValue 属性不是 null 布尔值,并且是布尔值。 假设此检查通过,我们将显示 DeleteConflictMessage Label 控件(如果为 ReturnValuefalse)。 这可以通过使用以下代码来实现:

C#
protected void ProductsOptimisticConcurrencyDataSource_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.ReturnValue != null && e.ReturnValue is bool)
    {
        bool deleteReturnValue = (bool)e.ReturnValue;
        if (deleteReturnValue == false)
        {
            // No row was deleted, display the warning message
            DeleteConflictMessage.Visible = true;
        }
    }
}

面对并发冲突,将取消用户的删除请求。 GridView 已刷新,显示用户在加载页面和单击“删除”按钮之间针对该记录发生的更改。 当出现此类冲突时, DeleteConflictMessage 将显示标签,说明刚刚发生的情况 (见图 20) 。

用户删除在出现并发冲突时被取消

图 20:用户删除在出现并发冲突 (单击以查看全尺寸图像)

摘要

允许多个并发用户更新或删除数据的每个应用程序中都存在并发冲突的机会。 如果未考虑此类冲突,则当两个用户同时更新在上次写入“获胜”中获取的相同数据时,覆盖其他用户的更改。 或者,开发人员可以实施乐观或悲观并发控制。 乐观并发控制假定并发冲突不频繁,只是不允许更新或删除构成并发冲突的命令。 悲观并发控制假定并发冲突频繁,只是拒绝一个用户的更新或删除命令是不能接受的。 使用悲观并发控制,更新记录涉及锁定记录,从而阻止任何其他用户在锁定记录时修改或删除记录。

.NET 中的类型化数据集提供支持乐观并发控制的功能。 特别是, UPDATE 向数据库发出的语句 DELETE 包括表的所有列,从而确保仅当记录的当前数据与执行更新或删除时用户拥有的原始数据匹配时,才会发生更新或删除。 将 DAL 配置为支持乐观并发后,需要更新 BLL 方法。 此外,必须配置调用 BLL 的 ASP.NET 页,以便 ObjectDataSource 从其数据 Web 控件中检索原始值,并将其向下传递到 BLL。

如本教程所示,在 ASP.NET Web 应用程序中实现乐观并发控制涉及更新 DAL 和 BLL 并在 ASP.NET 页中添加支持。 此添加的工作是否是你的时间和精力的明智投资,取决于你的应用程序。 如果你不经常有并发用户更新数据,或者他们正在更新的数据彼此不同,则并发控制不是关键问题。 但是,如果网站上经常有多个用户处理相同的数据,并发控制可以帮助防止一个用户的更新或删除无意中覆盖另一个用户的更新。

快乐编程!

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍和 4GuysFromRolla.com 创始人,自1998年以来一直在与 Microsoft Web 技术合作。 斯科特是一名独立顾问、教练员和作家。 他的最新书是 山姆斯教自己在24小时内 ASP.NET 2.0。 他可以通过他的博客访问mitchell@4GuysFromRolla.com,也可以通过他的博客找到http://ScottOnWriting.NET

posted on 2023-03-22 10:13  不及格的程序员-八神  阅读(16)  评论(0编辑  收藏  举报