实现乐观并发 (VB)

作者 :Scott Mitchell

下载 PDF

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

简介

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

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

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

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

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

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

  • 不执行任何操作 - 如果并发用户正在修改同一条记录,请让最后一个提交赢得 (默认行为)
  • 乐观并发 - 假设虽然时不时会出现并发冲突,但绝大多数情况下不会发生此类冲突;因此,如果确实发生冲突,只需通知用户无法保存其更改,因为其他用户修改了相同的数据
  • 悲观并发 - 假设并发冲突司空见惯,并且用户不会容忍被告知由于其他用户的并发活动而未保存更改;因此,当一个用户开始更新记录时,请将其锁定,从而阻止任何其他用户编辑或删除该记录,直到用户提交其修改

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

注意

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

步骤 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 语句已得到简化。 实际上,子句中的UnitPriceWHERE检查将更加复杂,因为UnitPrice可以包含 NULL ,并且检查是否NULL = NULL始终返回 False (而必须使用 IS NULL) 。

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

与其扩展现有的 DAL 的 TableAdapters 以使用乐观并发 (这需要更改 BLL 以适应) ,不如创建一个名为 NorthwindOptimisticConcurrency的新 Typed DataSet,我们将向其添加 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 中 TableAdapter Products 完全相同的 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:使用 Products 原始 DAL (中的 TableAdapter 中的相同 SQL 查询 单击以查看全尺寸图像)

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

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

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

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

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

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

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

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

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

若要查看使用乐观并发) 的 TableAdapter (和DELETEProductsOptimisticConcurrency不) 的 Products TableAdapter (查询之间的差异UPDATE,请单击 TableAdapter 并转到属性窗口。 DeleteCommand在 和 UpdateCommand 属性的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))

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

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

为此,请右键单击 TableAdapter 的标题栏 (和 GetProducts 方法名称) 上方Fill的区域,然后从上下文菜单中选择“添加查询”。 这将启动 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 方法现在需要 10 个输入参数:原始ProductID的 、ProductName、、SupplierIDCategoryIDQuantityPerUnitUnitPriceUnitsInStockUnitsOnOrderReorderLevelDiscontinued。 它使用这些其他输入参数的值在发送到数据库的语句的 DELETE 子句中WHERE,仅当数据库的当前值映射到原始值时,才会删除指定的记录。

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

将名为 的 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 使用 DB 直接模式时,必须向方法传递新的值和原始值。 对于删除,没有新值,因此只需传入原始值。 然后,在 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

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

如前所述,无论是否采用乐观并发,用于批处理更新模式的 Update TableAdapter 方法具有相同的方法签名。 也就是说, 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 实例

但是,这一系列步骤无法正确支持乐观并发,因为 ProductRow 步骤 1 中填充的 是直接从数据库填充的,这意味着 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 值标记为要用于 @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 页,该页可以利用系统中内置的乐观并发逻辑。 具体而言,gridView、DetailsView 或 FormView (的数据 Web 控件) 必须记住其原始值,并且 ObjectDataSource 必须将这两组值传递给业务逻辑层。 此外,必须将 ASP.NET 页配置为正常处理并发冲突。

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

让 ObjectDataSource 使用 ProductsOptimisticConcurrencyBLL 对象

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

从向导的 GetProducts下拉列表中选择 、 UpdateProductDeleteProduct 方法。 对于 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集合包含类的 方法中十个输入参数中的ProductsOptimisticConcurrencyBLL每一个ParameterDeleteProduct实例。 同样, UpdateParameters 集合包含 ParameterUpdateProduct每个输入参数的 实例。

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

注意

属性的值 OldValuesParameterFormatString 必须映射到 BLL 中需要原始值的输入参数名称。 由于我们将这些参数 original_productName命名为 、 original_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 将呈现为 TextBox,要求用户输入相应的类别和供应商作为 ID 号。 数字字段没有格式设置,也没有验证控件,以确保已提供产品名称,并且单价、库存单位、订单单位和重新订购级别值都是正确的数值,并且大于或等于零。

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

  • 删除了 ProductIDSupplierNameCategoryName BoundFields
  • ProductName BoundField 转换为 TemplateField 并添加了 RequiredFieldValidation 控件。
  • CategoryIDSupplierID BoundFields 转换为 TemplateFields,并调整编辑界面以使用 DropDownLists 而不是 TextBox。 在这些 TemplateFields 的 ItemTemplates中, CategoryName 将显示 和 SupplierName 数据字段。
  • 已将 UnitPriceUnitsInStockUnitsOnOrderReorderLevel BoundField 转换为 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 属性设置为 CompareAllValues () ,当 GridView (、Delete()DetailsView 或 FormView) 调用 ObjectDataSource Update() 的 或 方法时,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 关键字 (keyword) 来实现此目的。 回想一下, Eval 执行单向数据绑定。 我们仍需要提供UnitPrice原始值的值,因此我们仍然需要 中的ItemTemplate双向数据绑定语句,但这可以放置在属性设置为 falseVisible标签 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>
  • 使用 <%# Bind("UnitPrice") %>ItemTemplate中删除货币格式。 在 GridView 的 RowDataBound 事件处理程序中,以编程方式访问在其中显示值的标签 Web 控件 UnitPrice ,并将其 Text 属性设置为格式化版本。
  • UnitPrice 格式保留为货币。 在 GridView 的RowDeleting事件处理程序中,使用 Decimal.Parse将现有原始UnitPrice值 ($19.95) 替换为实际的十进制值。 在 ASP.NET 页中处理 BLL 和 DAL-Level 异常教程中,我们了解了如何在事件处理程序中完成类似RowUpdating操作。

在我的示例中,我选择使用第二种方法,添加隐藏的 Label Web 控件,该控件的 Text 属性是绑定到未格式化 UnitPrice 值的双向数据。

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

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

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

从异常的消息来看,ObjectDataSource 显然要调用包含 original_CategoryNameoriginal_SupplierName 输入参数的 BLL DeleteProduct 方法。 这是因为 ItemTemplateSupplierID TemplateFields 的 CategoryID 当前包含带有 和 SupplierName 数据字段的CategoryName双向 Bind 语句。 相反,我们需要将 语句与 和 SupplierID 数据字段一起CategoryID包含在Bind内。 为此,请将现有的 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 返回到其预编辑状态,以“Chai Tea”作为新产品名称。

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

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

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

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

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

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

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

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

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

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

<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 属性外,我还将 属性设置为 WarningCssClass ,这会导致标签以红色、斜体、粗体大字体显示。 此 CSS Warning 类已定义并添加到Styles.css检查 与插入、更新和删除关联的事件 教程中。

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

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

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

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

处理更新时的并发冲突

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

正如我们在 ASP.NET 页中处理 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:用户汇报在遇到并发冲突时丢失 (单击以查看全尺寸图像)

注意

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

删除时响应并发冲突

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

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

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:用户删除在面对并发冲突 (单击以查看全尺寸图像)

总结

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

.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 技术。 Scott 担任独立顾问、培训师和作家。 他的最新一本书是 山姆斯在 24 小时内 ASP.NET 2.0。 可以在 上mitchell@4GuysFromRolla.com联系他,也可以通过他的博客(可在 中找到http://ScottOnWriting.NET)。