实现乐观并发 (VB)

作者 :斯科特·米切尔

下载 PDF

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

介绍

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

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

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

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

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

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

  • 什么都不做 -if 并发用户正在修改同一记录,最后一次提交获胜(默认行为)
  • 乐观并发控制 - 假设虽然有时会出现并发冲突,但绝大多数情况下不会发生此类冲突;因此,如果发生冲突,只需通知用户无法保存他们的更改,因为其他用户已修改了相同的数据。
  • 悲观并发控制 - 假设并发冲突很常见,并且用户不会容忍由于其他用户的并发操作而导致其更改未能保存;因此,当用户开始更新记录时,进行加锁,从而阻止其他任何用户编辑或删除该记录,直到用户提交其修改。

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

注释

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

步骤 1:研究乐观并发的实现方式

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

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

图 2:若要使更新或删除成功,原始值必须等于当前数据库值(单击以查看完整大小的图像

实施乐观并发有多种方法(请参阅 Peter A. Bromberg乐观并发更新逻辑 ,大致了解多个选项)。 ADO.NET 类型化数据集提供一个实现,只需勾选复选框即可进行配置。 为 Typed DataSet 中的 TableAdapter 启用乐观并发会增强 TableAdapter 的 UPDATEDELETE 语句,以包含子句中 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

注释

UPDATE 语句已简化为可读性。 实际操作中,在UnitPrice子句中的WHERE检查会更复杂,因为UnitPrice可以包含NULL,并且检查NULL = NULL是否始终返回False(相反你必须使用IS NULL)。

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

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

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

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

连接到同一 Northwind 数据库

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

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

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

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

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

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 中的 Products 相同 SQL 查询(单击可查看全尺寸图像

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

通过选中“使用乐观并发”复选框来启用乐观并发控制

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

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

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

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

完成向导后,数据集设计器将包含一个强类型 Products 数据表和一个表适配器 (TableAdapter)。 建议您花点时间将 DataTable 从 Products 重命名为 ProductsOptimisticConcurrency,可以通过右键单击 DataTable 的标题栏,然后在上下文菜单中选择“重命名”来完成此操作。

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

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

若要查看使用乐观并发的UPDATE TableAdapter和未使用乐观并发的Products TableAdapter之间的DELETEProductsOptimisticConcurrency查询差异,请单击TableAdapter并转到“属性”窗口。 在DeleteCommandUpdateCommand属性的CommandText子属性中,可以看到在调用 DAL 更新或删除相关方法时发送到数据库的实际 SQL 语法。 ProductsOptimisticConcurrency对于 TableAdapter,DELETE使用的语句为:

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 的语句要简单得多。

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

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

我们不会在这个教程中向启用乐观并发的数据集添加任何额外的数据表,因为我们的 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 句以返回特定产品记录(单击可查看全尺寸图像

最后,将生成的方法名称更改为 FillByProductIDGetProductByProductID

将方法重命名为 FillByProductID 和 GetProductByProductID

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

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

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

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

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

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

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

将 ProductsOptimisticConcurrencyBLL 类添加到 BLL 文件夹

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

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

Imports NorthwindOptimisticConcurrencyTableAdapters
<System.ComponentModel.DataObject()> _
Public Class ProductsOptimisticConcurrencyBLL
    Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing
    Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter
        Get
            If _productsAdapter Is Nothing Then
                _productsAdapter = New ProductsOptimisticConcurrencyTableAdapter()
            End If
            Return _productsAdapter
        End Get
    End Property
    <System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Select, True)> _
    Public Function GetProducts() As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable
        Return Adapter.GetProducts()
    End Function
End Class

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

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

使用乐观并发的直接模式从数据库中删除产品

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

<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteProduct( _
    ByVal original_productID As Integer, ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean) _
    As Boolean
    Dim rowsAffected As Integer = 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
End Function

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

使用具有乐观并发的批处理更新模式更新产品

如前所述,TableAdapter 的 Update 方法在批量更新模式下,无论是否使用乐观并发,其方法签名都是相同的。 也就是说,Update 方法期望是一个 DataRow、一个 DataRow 数组、一个 DataTable 或一个类型化 DataSet。 没有用于指定原始值的附加输入参数。 这是可能的,因为 DataTable 会跟踪其 DataRow 的原始值和修改的值。 当 DAL 发出其 UPDATE 语句时, @original_ColumnName 参数将填充 DataRow 的原始值,而 @ColumnName 参数则用 DataRow 的修改值填充。

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

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

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

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

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

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

Protected Sub AssignAllProductValues( _
    ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean)
    product.ProductName = productName
    If Not supplierID.HasValue Then
        product.SetSupplierIDNull()
    Else
        product.SupplierID = supplierID.Value
    End If
    If Not categoryID.HasValue Then
        product.SetCategoryIDNull()
    Else
        product.CategoryID = categoryID.Value
    End If
    If quantityPerUnit Is Nothing Then
        product.SetQuantityPerUnitNull()
    Else
        product.QuantityPerUnit = quantityPerUnit
    End If
    If Not unitPrice.HasValue Then
        product.SetUnitPriceNull()
    Else
        product.UnitPrice = unitPrice.Value
    End If
    If Not unitsInStock.HasValue Then
        product.SetUnitsInStockNull()
    Else
        product.UnitsInStock = unitsInStock.Value
    End If
    If Not unitsOnOrder.HasValue Then
        product.SetUnitsOnOrderNull()
    Else
        product.UnitsOnOrder = unitsOnOrder.Value
    End If
    If Not reorderLevel.HasValue Then
        product.SetReorderLevelNull()
    Else
        product.ReorderLevel = reorderLevel.Value
    End If
    product.Discontinued = discontinued
End Sub
<System.ComponentModel.DataObjectMethodAttribute( _
System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean, ByVal productID As Integer, _
    _
    ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean, _
    ByVal original_productID As Integer) _
    As Boolean
    'STEP 1: Read in the current database product information
    Dim products As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
        Adapter.GetProductByProductID(original_productID)
    If products.Count = 0 Then
        ' no matching record found, return false
        Return False
    End If
    Dim product As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = 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
    Dim rowsAffected As Integer = Adapter.Update(product)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function

步骤 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方法、UpdateProduct方法和DeleteProduct方法。 对于 UpdateProduct 方法,请使用接受所有产品数据字段的重载版本。

配置 ObjectDataSource 控件的属性

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

<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集合包含了类Parameter中的ProductsOptimisticConcurrencyBLL方法的DeleteProduct实例,每个实例对应十个输入参数。 同样,UpdateParameters 集合中包含一个 Parameter 实例用于 UpdateProduct 的每个输入参数。

对于那些以前涉及数据修改的教程, 在这一阶段,我们会删除 ObjectDataSource 的 OldValuesParameterFormatString 属性,因为此属性表明 BLL 方法需要传入旧值(或原始值)以及新值。 此外,此属性值指示原始值的输入参数名称。 由于我们要将原始值传入 BLL, 因此请勿 删除此属性。

注释

BLL 中输入参数名称必须与属性 OldValuesParameterFormatString 的值相映射,这些参数需要原始值。 由于我们命名了这些参数original_productNameoriginal_supplierID,依此类说,因此可以将属性值保留OldValuesParameterFormatStringoriginal_{0}。 但是,如果 BLL 方法的输入参数的名称类似 old_productNameold_supplierID依此类说,则需要将 OldValuesParameterFormatString 属性更新为 old_{0}

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

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

花点时间将 ConflictDetection 属性设置为 CompareAllValues

配置 GridView 的属性和字段

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

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

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

  • 删除了ProductIDSupplierNameCategoryName BoundFields
  • ProductName BoundField 转换为 TemplateField 并添加了 RequiredFieldValidation 控件。
  • CategoryIDSupplierID BoundFields 转换为 TemplateFields,并调整了编辑界面以使用 DropDownLists 而不是 TextBoxes。 在这些 TemplateFields 中,将显示 ItemTemplatesCategoryName 数据字段。
  • UnitPriceUnitsInStockUnitsOnOrderReorderLevel BoundFields 转换为 TemplateFields 并添加了 CompareValidator 控件。

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

<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(按原样),当 ObjectDataSource CompareAllValuesUpdate()Delete()方法由 GridView(或 DetailsView 或 FormView)调用时,ObjectDataSource 将尝试将 GridView 的原始值复制到其相应的Parameter实例中。 有关此过程的图形表示形式,请参阅图 2。

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

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

产品在 GridView 中列出

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

如果单击任何产品的“删除”按钮,就会引发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,但这可以放置在属性设置为Visiblefalse标签 Web 控件中。 可以在 ItemTemplate 中使用以下标记:
<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>
  • 使用ItemTemplate删除<%# Bind("UnitPrice") %>中的货币格式。 在 GridView 的 RowDataBound 事件处理程序中,以编程方式访问其中显示 UnitPrice 值的 Web 控件标签,并将其 Text 属性设置为格式化的版本。
  • 保留 UnitPrice 的货币格式。 在 GridView 的 RowDeleting 事件处理程序中,将现有的原始值 UnitPrice ($19.95) 替换为 Decimal.Parse 所用的实际十进制值。 我们在《在 ASP.NET 页面中处理 BLL 和 DAL-Level 异常》教程中看到如何在事件处理程序中实现类似的功能。

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

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

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

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

查看异常的消息,ObjectDataSource 显然想要调用包含DeleteProductoriginal_CategoryName输入参数的 BLL original_SupplierName 方法。 这是因为 ItemTemplateCategoryIDSupplierID 的 TemplateFields 当前包含与 CategoryNameSupplierName 数据字段的双向 Bind 语句。 相反,我们需要将Bind语句与CategoryIDSupplierID数据字段一起包含。 为此,请将现有 Bind 语句替换为Eval语句,然后添加隐藏的 Label 控件,其Text属性使用双向数据绑定绑定到CategoryIDSupplierID数据字段,如下所示:

<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”。 在此第二个浏览器窗口中,将更新 UnitPrice25.00。 如果没有乐观并发支持,在第二个浏览器实例中单击更新会将产品名称更改回“Chai”,从而覆盖第一个浏览器实例所做的更改。 但当使用乐观并发时,如果在第二个浏览器实例中点击“更新”按钮,就会导致DBConcurrencyException发生。

检测到并发违规时,将抛出 DBConcurrencyException

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

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

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

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

在这两种情况下,用户体验远非理想。 我们显然不想在使用批量更新模式时向用户显示具体的DBConcurrencyException异常细节。 使用 DB 直接模式时的行为有点混乱,因为用户命令失败,但没有确切说明原因。

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

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

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

<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." />

除了设置它们的Visible属性EnabledViewStateText属性,我还将属性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 异常。 此事件处理程序将传递对更新过程中引发的任何异常的引用,如下面的事件处理程序代码所示:

Protected Sub ProductsGrid_RowUpdated _
        (ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
        Handles ProductsGrid.RowUpdated
    If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
        If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
            ' Display the warning message and note that the exception has
            ' been handled...
            UpdateConflictMessage.Visible = True
            e.ExceptionHandled = True
        End If
    End If
End Sub

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

用户更新在发生并发冲突时丢失

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

注释

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

在删除时响应并发冲突

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

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

Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
        (ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
        Handles ProductsOptimisticConcurrencyDataSource.Deleted
    If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
        Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
        If deleteReturnValue = False Then
            ' No row was deleted, display the warning message
            DeleteConflictMessage.Visible = True
        End If
    End If
End Sub

面对并发冲突,将取消用户的删除请求。 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 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams Teach Yourself ASP.NET 2.0 in 24 Hours》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。