实现乐观并发 (C#)
对于允许多个用户编辑数据的 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 的 UPDATE
和 DELETE
语句,以包含 子句中 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 直接方法的签名。 回顾我们的第 一个教程创建数据访问层,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 数据库。
图 3:连接到同一个 Northwind 数据库 (单击以查看全尺寸图像)
接下来,系统会提示如何通过即席 SQL 语句、新的存储过程或现有存储过程来查询数据。 由于我们在原始 DAL 中使用了即席 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
图 5:使用 Products
原始 DAL (中的 TableAdapter 中的相同 SQL 查询 单击以查看全尺寸图像)
在转到下一个屏幕之前,单击“高级选项”按钮。 若要使此 TableAdapter 采用乐观并发控制,只需检查“使用乐观并发”复选框。
图 6:通过检查“使用乐观并发”CheckBox (单击以查看全尺寸图像)
最后,指示 TableAdapter 应使用填充 DataTable 和返回 DataTable 的数据访问模式;还指示应创建 DB 直接方法。 将“返回 DataTable 模式”的方法名称从 GetData 更改为 GetProducts,以便镜像原始 DAL 中使用的命名约定。
图 7:让 TableAdapter 利用所有数据访问模式 (单击以查看全尺寸图像)
完成向导后,DataSet Designer将包含强类型 Products
DataTable 和 TableAdapter。 花点时间将 DataTable 从 Products
重命名为 ProductsOptimisticConcurrency
,可以通过右键单击 DataTable 的标题栏并从上下文菜单中选择“重命名”来执行此操作。
图 8:DataTable 和 TableAdapter 已添加到类型化数据集 (单击以查看全尺寸图像)
若要查看使用乐观并发) 的 TableAdapter (和DELETE
ProductsOptimisticConcurrency
不) 的 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
子句中的WHERE
Discontinued
值。 ProductName
对于本教程,我们不会将任何其他 DataTable 添加到已启用乐观并发的 DataSet,因为我们的 ASP.NET 页将仅提供更新和删除产品信息。 但是,我们仍然需要将 方法添加到 GetProductByProductID(productID)
ProductsOptimisticConcurrency
TableAdapter。
为此,请右键单击 TableAdapter 的标题栏 (和 GetProducts
方法名称) 上方Fill
的区域,然后从上下文菜单中选择“添加查询”。 这将启动 TableAdapter 查询配置向导。 与 TableAdapter 的初始配置一样,选择使用即席 SQL 语句创建 GetProductByProductID(productID)
方法 (请参阅图 4) 。 GetProductByProductID(productID)
由于 该方法返回有关特定产品的信息,因此指示此查询是SELECT
返回行的查询类型。
图 9:将查询类型标记为“SELECT
返回行” (单击以查看全尺寸图像)
在下一个屏幕上,系统会提示我们使用 SQL 查询,并预加载 TableAdapter 的默认查询。 扩充现有查询以包含 子句 WHERE ProductID = @ProductID
,如图 10 所示。
图 10:向预加载的查询添加 WHERE
子句以返回特定产品记录 (单击以查看全尺寸图像)
最后,将生成的方法名称更改为 FillByProductID
和 GetProductByProductID
。
图 11:将方法重命名为 FillByProductID
, GetProductByProductID
(单击以查看全尺寸图像)
完成此向导后,TableAdapter 现在包含两种用于检索数据的方法: GetProducts()
返回 所有 产品的 和 GetProductByProductID(productID)
返回指定产品的 。
步骤 3:为乐观 Concurrency-Enabled DAL 创建业务逻辑层
ProductsBLL
现有类包含使用批处理更新模式和 DB 直接模式的示例。 方法和AddProduct
UpdateProduct
重载都使用批处理更新模式,将 ProductRow
实例传递给 TableAdapter 的 Update 方法。 DeleteProduct
另一方面, 方法使用 DB 直接模式,调用 TableAdapter 的 Delete(productID)
方法。
使用新的 ProductsOptimisticConcurrency
TableAdapter 时,DB 直接方法现在还要求传入原始值。 例如, Delete
方法现在需要 10 个输入参数:原始ProductID
的 、ProductName
、、SupplierID
、CategoryID
QuantityPerUnit
、UnitPrice
UnitsInStock
、UnitsOnOrder
、 ReorderLevel
和 Discontinued
。 它使用这些其他输入参数的值在发送到数据库的语句的 DELETE
子句中WHERE
,仅当数据库的当前值映射到原始值时,才会删除指定的记录。
虽然批处理更新模式中使用的 TableAdapter Update
方法的方法签名未更改,但记录原始值和新值所需的代码具有。 因此,让我们创建一个新的业务逻辑层类,而不是尝试将已启用乐观并发的 DAL 与现有 ProductsBLL
类一起使用。
将名为 的 ProductsOptimisticConcurrencyBLL
类添加到 BLL
文件夹中的 App_Code
文件夹中。
图 12:将 ProductsOptimisticConcurrencyBLL
类添加到 BLL 文件夹
接下来,将以下代码添加到 ProductsOptimisticConcurrencyBLL
类:
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 向导的下拉列表中包含此类。
ProductsOptimisticConcurrencyBLL
的 Adapter
属性提供对 类实例的ProductsOptimisticConcurrencyTableAdapter
快速访问,并遵循原始 BLL 类中使用的模式, (ProductsBLL
、 CategoriesBLL
等) 。 最后, GetProducts()
方法只需向下调用 DAL 的 GetProducts()
方法,并返回一个 ProductsOptimisticConcurrencyDataTable
对象,该对象填充了数据库中每个产品记录的 实例 ProductsOptimisticConcurrencyRow
。
使用具有乐观并发的数据库直接模式删除产品
对使用乐观并发的 DAL 使用 DB 直接模式时,必须向方法传递新的值和原始值。 对于删除,没有新值,因此只需传入原始值。 然后,在 BLL 中,我们必须接受所有原始参数作为输入参数。 让我们让 DeleteProduct
类中的 ProductsOptimisticConcurrencyBLL
方法使用 DB 直接方法。 这意味着此方法需要将所有十个产品数据字段作为输入参数,并将其传递给 DAL,如以下代码所示:
[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
。
使用具有乐观并发的批处理更新模式更新产品
如前所述,无论是否采用乐观并发,用于批处理更新模式的 Update
TableAdapter 方法具有相同的方法签名。 也就是说, Update
方法需要 DataRow、DataRows 数组、DataTable 或类型化数据集。 没有用于指定原始值的其他输入参数。 这是可能的,因为 DataTable 会跟踪其 DataRow () 的原始值和已修改的值。 当 DAL 发出其 UPDATE
语句时 @original_ColumnName
,参数将填充 DataRow 的原始值,而 @ColumnName
参数则使用 DataRow 的修改值填充。
在 ProductsBLL
类 (使用原始的非乐观并发 DAL) ,当使用批量更新模式更新产品信息时,我们的代码将执行以下事件序列:
- 使用 TableAdapter 的
GetProductByProductID(productID)
方法将当前数据库产品信息读入ProductRow
实例 - 将步骤 1 中的新值分配给
ProductRow
实例 - 调用 TableAdapter 的
Update
方法,传入ProductRow
实例
但是,这一系列步骤无法正确支持乐观并发,因为 ProductRow
步骤 1 中填充的 是直接从数据库填充的,这意味着 DataRow 使用的原始值是数据库中当前存在的值,而不是在编辑过程开始时绑定到 GridView 的值。 相反,在使用启用了乐观并发的 DAL 时,我们需要更改 UpdateProduct
方法重载以使用以下步骤:
- 使用 TableAdapter 的
GetProductByProductID(productID)
方法将当前数据库产品信息读入ProductsOptimisticConcurrencyRow
实例 - 将 原始 值分配给步骤 1 中的
ProductsOptimisticConcurrencyRow
实例 ProductsOptimisticConcurrencyRow
调用 实例的AcceptChanges()
方法,该方法指示 DataRow 其当前值是“原始”值- 将 新 值分配给
ProductsOptimisticConcurrencyRow
实例 - 调用 TableAdapter 的
Update
方法,传入ProductsOptimisticConcurrencyRow
实例
步骤 1 读取指定产品记录的所有当前数据库值。 此步骤在重载中UpdateProduct
是多余的,因为步骤 2) 中会覆盖所有产品列 (这些值,但对于仅将列值的子集作为输入参数传入的重载来说至关重要。 将原始值分配给 ProductsOptimisticConcurrencyRow
实例后, AcceptChanges()
将调用 方法,该方法将当前 DataRow 值标记为要用于 @original_ColumnName
语句中的 UPDATE
参数的原始值。 接下来,将新参数值分配给 ProductsOptimisticConcurrencyRow
,最后 Update
调用 方法,传入 DataRow。
以下代码显示了 UpdateProduct
接受所有产品数据字段作为输入参数的重载。 虽然此处未显示, ProductsOptimisticConcurrencyBLL
但本教程的下载中包含的类还包含一个 UpdateProduct
重载,该重载仅接受产品名称和价格作为输入参数。
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 页,该页可以利用系统中内置的乐观并发逻辑。 具体而言,gridView、DetailsView 或 FormView (的数据 Web 控件) 必须记住其原始值,并且 ObjectDataSource 必须将这两组值传递给业务逻辑层。 此外,必须将 ASP.NET 页配置为正常处理并发冲突。
首先打开 文件夹中的页面OptimisticConcurrency.aspx
EditInsertDelete
,并将 GridView 添加到Designer,并将其 ID
属性设置为 ProductsGrid
。 在 GridView 的智能标记中,选择创建名为 ProductsOptimisticConcurrencyDataSource
的新 ObjectDataSource。 由于我们希望此 ObjectDataSource 使用支持乐观并发的 DAL,因此请将其配置为使用 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
集合包含类的 方法中十个输入参数中的ProductsOptimisticConcurrencyBLL
每一个Parameter
的DeleteProduct
实例。 同样, UpdateParameters
集合包含 Parameter
中 UpdateProduct
每个输入参数的 实例。
对于之前涉及数据修改的教程,我们将此时删除 ObjectDataSource 的 OldValuesParameterFormatString
属性,因为此属性指示 BLL 方法需要传入旧 (或原始) 值以及新值。 此外,此属性值指示原始值的输入参数名称。 由于我们要将原始值传入 BLL, 因此请不要 删除此属性。
注意
属性的值 OldValuesParameterFormatString
必须映射到 BLL 中需要原始值的输入参数名称。 由于我们将这些参数 original_productName
命名为 、 original_supplierID
等,因此可以将属性值保留 OldValuesParameterFormatString
为 original_{0}
。 但是,如果 BLL 方法的输入参数的名称类似于 old_productName
、 old_supplierID
等,则需要将 OldValuesParameterFormatString
属性更新为 old_{0}
。
需要进行最后一个属性设置,以便 ObjectDataSource 将原始值正确传递给 BLL 方法。 ObjectDataSource 具有 ConflictDetection 属性 ,该属性可分配给 以下两个值之一:
OverwriteChanges
- 默认值;不会将原始值发送到 BLL 方法的原始输入参数CompareAllValues
- 将原始值发送到 BLL 方法;使用乐观并发时选择此选项
花点时间将 ConflictDetection
属性设置为 CompareAllValues
。
配置 GridView 的属性和字段
正确配置 ObjectDataSource 的属性后,让我们将注意力转向设置 GridView。 首先,由于我们希望 GridView 支持编辑和删除,因此请单击 GridView 智能标记中的“启用编辑”和“启用删除”复选框。 这将添加一个 CommandField,其 ShowEditButton
和 ShowDeleteButton
都设置为 true
。
绑定到 ProductsOptimisticConcurrencyDataSource
ObjectDataSource 时,GridView 包含每个产品数据字段的字段。 虽然可以编辑此类 GridView,但用户体验绝不是可以接受的。 CategoryID
和 SupplierID
BoundFields 将呈现为 TextBox,要求用户输入相应的类别和供应商作为 ID 号。 数字字段没有格式设置,也没有验证控件,以确保已提供产品名称,并且单价、库存单位、订单单位和重新订购级别值都是正确的数值,并且大于或等于零。
如将 验证控件添加到编辑和插入接口 和 自定义数据修改接口 教程中所述,可以通过将 BoundFields 替换为 TemplateFields 来自定义用户界面。 我已通过以下方式修改了此 GridView 及其编辑界面:
- 删除了
ProductID
、SupplierName
和CategoryName
BoundFields - 将
ProductName
BoundField 转换为 TemplateField 并添加了 RequiredFieldValidation 控件。 - 将
CategoryID
和SupplierID
BoundFields 转换为 TemplateFields,并调整编辑界面以使用 DropDownLists 而不是 TextBox。 在这些 TemplateFields 的ItemTemplates
中,CategoryName
将显示 和SupplierName
数据字段。 - 已将
UnitPrice
、UnitsInStock
、UnitsOnOrder
和ReorderLevel
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 会列出每个产品,其中最左侧的列中有一个“编辑和删除”按钮。
图 14:产品在 GridView 中列出 (单击以查看全尺寸图像)
如果单击任何产品的“删除”按钮, 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
双向数据绑定语句,但这可以放置在属性设置为false
的Visible
标签 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
。
图 16:ObjectDataSource 找不到具有要发送的输入参数的方法 (单击以查看全尺寸图像)
从异常的消息来看,ObjectDataSource 显然要调用包含 original_CategoryName
和 original_SupplierName
输入参数的 BLL DeleteProduct
方法。 这是因为 ItemTemplate
和 SupplierID
TemplateFields 的 CategoryID
当前包含带有 和 SupplierName
数据字段的CategoryName
双向 Bind 语句。 相反,我们需要将 语句与 和 SupplierID
数据字段一起CategoryID
包含在Bind
内。 为此,请将现有的 Bind 语句 Eval
替换为 语句,然后添加隐藏的 Label 控件,这些控件的属性 Text
使用双向数据绑定绑定到 CategoryID
和 SupplierID
数据字段,如下所示:
<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。
图 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." />
除了设置其 Visible
、 EnabledViewState
和 Text
属性外,我还将 属性设置为 Warning
CssClass
,这会导致标签以红色、斜体、粗体大字体显示。 此 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 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
Label 控件将向用户解释刚刚发生的情况。 此事件序列详见图 19。
图 19:用户汇报在遇到并发冲突时丢失 (单击以查看全尺寸图像)
注意
或者,我们可以通过将传入GridViewUpdatedEventArgs
对象的 属性设置为 KeepInEditMode
true 来使 GridView 保持其编辑状态,而不是将 GridView 返回到预编辑状态。 但是,如果采用此方法,请务必通过调用 GridView DataBind()
方法) 将数据重新绑定到 GridView (,以便将其他用户的值加载到编辑界面中。 本教程中可供下载的代码在事件处理程序中 RowUpdated
注释掉了这两行代码;只需取消注释这些代码行,使 GridView 在发生并发冲突后保持编辑模式。
删除时响应并发冲突
使用 DB 直接模式时,在遇到并发冲突时不会引发异常。 相反,数据库语句只影响任何记录,因为 WHERE 子句与任何记录都不匹配。 在 BLL 中创建的所有数据修改方法都经过设计,以便返回一个布尔值,指示它们是否仅影响一条记录。 因此,若要确定删除记录时是否发生并发冲突,我们可以检查 BLL 方法的 DeleteProduct
返回值。
可以通过传递到事件处理程序中的 对象的 属性ObjectDataSourceStatusEventArgs
,在 ObjectDataSource 的后级别事件处理程序ReturnValue
中检查 BLL 方法的返回值。 由于我们有兴趣确定 方法的返回值 DeleteProduct
,因此我们需要为 ObjectDataSource 的 Deleted
事件创建事件处理程序。 属性 ReturnValue
的类型为 object
,如果引发异常且方法在返回值之前中断,则属性可以为 null
。 因此,我们应首先确保 ReturnValue
属性不是 null
并且 是布尔值。 假设此检查通过,则如果 ReturnValue
为 false
,则显示 DeleteConflictMessage
Label 控件。 这可以通过使用以下代码来实现:
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 页中添加支持。 此添加的工作是否是时间和精力的明智投资取决于应用程序。 如果你不常有并发用户更新数据,或者他们正在更新的数据彼此不同,则并发控制不是一个关键问题。 但是,如果你的网站上经常有多个用户处理相同的数据,并发控制可以帮助防止一个用户的更新或删除在无意中覆盖另一个用户的更新或删除。
编程愉快!
关于作者
Scott Mitchell 是七本 ASP/ASP.NET 书籍的作者, 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0自学。 可以在 上联系 mitchell@4GuysFromRolla.com他, 也可以通过他的博客联系到他,该博客可在 http://ScottOnWriting.NET中找到。