实施并发优化21
简介
对于只允许用户浏览数据,或只允许单一用户修改数据的 Web 应用程序,就不会存在这样的风险:两个同时操作的用户碰巧在相互覆盖掉别人所做的修改。然而,对于允许多用户更新或删除数据的Web 应用程序,就存在着这样的可能性:一个用户所做的修改与另一个同时操作的用户的修改相冲突。如果没有适当的并发策略,当两个用户同时编辑同一条记录时,最后提交修改信息的用户就会用其内容覆盖掉第一个用户所做的修改。
例如,假设有两个用户,Jisun 和 Sam ,同时在访问我们应用程序的一个页面,这里允许访问者通过 GridView 控件更新和删除产品信息。两个人几乎是同时单击了 GridView 的 Edit 按钮。 Jisun 把产品名称更改成 Chai Tea ,并单击了 Update 按钮。其最终结果是把一条 UPDATE 语句传递到数据库,数据库则对该产品的所有可更新字段都重新赋值(虽然 Jisun 只更新了一个 ProductName 字段 ) 。这时,数据库中的这个产品的名称为 Chai Tea ,类别为 Beverages ,供应商为 Exotic Liquids ,等等。可是,在 Sam 的屏幕上,在可编辑的 GridView 行中,所显示的产品名称仍然是 Chai 。在 Jisun 的修改提交之后几秒种, Sam 把类别修改为 Condiments ,并单击 Update 。这导致一条 UPDATE 语句被传递到数据库,将产品名称赋值为 Chai ,并把 CategoryID 的值赋给了对应于 Beverages 的类别 ID ,等等。就这样, Jisun 所做的对产品名称的修改被覆盖了。图 1 用图形表示了这一系列事件。
.png)
图1 :当两个用户同时更新一条记录时,存在一个用户所做的修改被另一用户覆盖的潜在风险
类似的情况是,当两个用户同时访问一个页面时,一个用户可能正在更新一条记录,而另一个用户却把它删除了。或者,当一个用户调出了一个页面,正准备单击 Delete 按钮时,另一个用户可能已经修改了这条记录的内容。
有三种 并发控制 策略可供采用:
- Do Nothing 如果并发用户在修改同一条记录,则让后提交者所做的修改信息覆盖前者的修改信息(缺省设置)。
- Optimistic Concurrency 假定虽然可能偶尔会出现并发冲突,但大多数情况下这类冲突不会发生。于是,当冲突确实发生时,就通知用户,他们的修改不能存储,因为另一用户已经修改了相同的数据。
- Pessimistic Concurrency 假定并发冲突经常发生,用户不能容忍被告知他们的修改由于另一用户的并发行为而不能存储。于是,当一个用户开始修改一条记录时,则把这条记录锁住,从而防止任何其他用户再编辑或删除这条记录,直到此用户提交了他的修改内容。
到目前为止,我们所有的教程都用的是缺省的并发解决策略,也就是,让后提交者的修改信息覆盖前者的修改信息。本教程中,我们将探讨如何实施并发优化控制。
注意 :在本系列教程中,我们不讨论封锁式并发的例子。封锁式并发很少使用,因为这样的封锁如果不能适时解除,就会防碍其他用户更新数据。例如,如果一个用户为修改数据而锁住了一条记录,然后在解锁前他又走开了;这样,其他所有用户就都不能再更新这条记录,直到最初锁住它的用户回来并完成其更新。因此,在实施封锁式并发时,一般都要设置超时,一旦超时就解锁。票务销售网站是应用封锁式并发控制的一个例子,它在短期内锁定某些座位,让用户完成订票过程。
步骤1 :探讨如何实施并发优化
并发优化控制的机理,是保证正在修改或删除的记录值与修改或删除过程开始时的值相同。例如,在可编辑的 GridView 控件中单击 Edit 按钮,便从数据库中读出了记录的值并显示在 TextBox 和其他 Web 控件中。 GridView 保存下这些原始值。之后,在用户做了修改并单击了 Update 按钮后,原始值加上新赋的值都被送到业务逻辑层,然后下到数据访问层。数据访问层必须发出一条 SQL 语句,但只有当用户开始编辑时的原始值与数据库里的当前值完全相同时,才真正执行该语句更新记录。图 2 描述了事件的顺序。
.png)
图2 :只有在原始值与数据库当前值相同时更新或删除才能成功
实施并发优化的方法有多种(参阅 Peter A. Bromberg 的 Optmistic Concurrency Updating Logic ,其中有对多个方法的简要介绍)。ADO.NET 的 Typed DataSet 提供了一种只需勾选复选框便可配置的实施方法。对 Typed DataSet 的 TableAdapter 启用并发优化扩展了 TableAdapter 的 UPDATE 和 DELETE 语句,在 WHERE 子句中包含了一个对所有原始值的比较。以下面这句 UPDATE 语句为例,只有当前数据库的值等于最初修改 GridView 的记录时获得的值时,才更新产品名称和价格。其中,@ProductName 和 @UnitPrice 是用户输入的新值,而@original_ProductName 和@original_UnitPrice 是单击Edit 按钮时最初加载到GridView 的值:
UPDATE Products SET
ProductName = @ProductName,
UnitPrice = @UnitPrice
WHERE
ProductID = @original_ProductID AND
ProductName = @original_ProductName AND
UnitPrice = @original_UnitPrice
注意:为便于理解,这条UPDATE 语句是经过简化的。实际应用时,WHERE 子句对 UnitPrice 的检查还要考虑更多的因素,因为 UnitPrice 的值可能为 NULL 。而且,检查 NULL = NULL 是否总是返回 False (这样必须使用 IS NULL 代替)。
除了使用一条不同的基础 UPDATE 语句之外,配置TableAdapter 使用并发优化也要修改其数据库直接方法的签名。在我们的首篇教程(“创建数据访问层 ”)中曾提到,数据库直接方法是将一系列标量值作为输入参数的方法(而不是作为一个强类型的DataRow 或 DataTable 实例)。当使用并发优化时,数据库直接Update() 和 Delete() 方法也包括输入参数的原始值。此外,若使用批量更新模式(接受DataRow 和 DataTable ,而不是标量值的 Update() 方法过载),业务逻辑层中的代码也必须改变。
与其拓展我们现有数据访问层的TableAdapter 来使用并发优化(这也得修改业务逻辑层才能满足要求),还不如让我们创建一个新的Typed DataSet ,名为 NorthwindOptimisticConcurrency ,并向它添加一个使用并发优化的 Products TableAdapter 。然后,创建一个 ProductsOptimisticConcurrencyBLL 业务逻辑层类,并做适当的修改以支持并发优化的数据访问层。一旦奠定了这个基础,我们就可以着手创建ASP.NET 页面了。
步骤2 :创建支持并发优化的数据访问层
为创建一个新的 Typed DataSet ,右键单击App_Code 文件夹内的 DAL 文件夹,并添加一个新的 DataSet ,名为 NorthwindOptimisticConcurrency 。正像我们在首篇教程中学到的,这一操作将向Typed DataSet 添加一个新的 TableAdapter ,并自动启动 TableAdapter Configuration Wizard 。在第一个屏幕中,向导提示我们指定要连接的数据库—— 使用 Web.config 中的 NORTHWNDConnectionString 设置,连接到同一个 Northwind 数据库。
.png)
图3 :连接到同一个 Northwind 数据库
下一步,需要选择如何查询数据:通过一个ad-hoc SQL 语句、新的存储过程,或现有的存储过程。由于我们在原来的数据访问层中使用的是ad-hoc SQL 查询,所以这里仍选择这一选项。
.png)
图4 :指定使用 ad-hoc SQL 语句来检索数据
在下一个屏幕中,输入一条 SQL 查询来检索产品信息。我们使用原来在数据访问层中对Products TableAdapter 用过的完全相同的SQL 查询,返回产品信息的所有列,以及产品的供应商和类别名称:
SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
(SELECT CategoryName FROM Categories
WHERE Categories.CategoryID = Products.CategoryID)
as CategoryName,
(SELECT CompanyName FROM Suppliers
WHERE Suppliers.SupplierID = Products.SupplierID)
as SupplierName
FROM Products
.png)
图5 :使用与原来数据访问层的 Products TableAdapter 相同的 SQL 查询语句
在转到下一个屏幕之前,单击Advanced Options 按钮。为了让这个 TableAdapter 使用并发优化控制,勾选上Use optimistic concurrency 复选框即可。
.png)
图6 :勾选 Use optimistic concurrency 复选框以启用并发优化
最后,选中TableAdapter 应使用既填充 DataTable 又返回 DataTable 的数据访问模式;同时勾选上应创建数据库直接方法。将 Return a DataTable 模式的方法名称从 GetData 改为 GetProducts ,以反映我们在原来数据访问层中使用的命名规则。
.png)
图7 :让 TableAdapter 使用所有数据访问模式
向导运行完之后,DataSet Designer 将包括一个强类型的 Products DataTable 和 TableAdapter 。再花点时间来重新命名DataTable ,由 Products 改为 ProductsOptimisticConcurrency :右键单击DataTable 的标题栏,然后选择上下文菜单中的 Rename 。
.png)
图8 :一个 DataTable 和 TableAdapter 已被添加到Typed DataSet 中
要查看ProductsOptimisticConcurrency TableAdapter ( 使用并发优化 )和 Products TableAdapter (不使用并发优化)之间UPDATE 和 DELETE 查询的区别,单击 TableAdapter 并转至 Properties 窗口。在 DeleteCommand 和 UpdateCommand 属性的 CommandText 子属性中,可以看到调用数据访问层的与更新或删除相关的方法时传递到数据库的真实的 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))
而我们原来的数据访问层的 Product TableAdapter 所用的 DELETE 语句则要简单得多 :
DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))
可以看到,使用并发优化的 TableAdapter 的 DELETE 语句的 WHERE 子句中,将 Product 表中现有的各列的值与 GridView ( 或DetailsView 或 FormView )最后一次所填充的原始值进行比较。因为除了 ProductID 、 ProductName 和 Discontinued 之外的所有字段都可包含 NULL 值,所以在 WHERE 子句中还有附加的参数和检查,用适当方式来比较 NULL 值。
在本教程中,我们不在启用并发优化的 DataSet 中添加任何其他的 DataTable , 因为我们的ASP.NET 页面只提供更改和删除产品信息。不过,我们仍需要将 GetProductByProductID(productID) 方法添加到 ProductsOptimisticConcurrency TableAdapter 中。
为此,右键单击 TableAdapter 的标题栏( Fill 和 GetProducts 方法名称正上方的区域),并在快捷菜单中选择 Add Query 。这将启动TableAdapter 查询配置向导。正如我们的 TableAdapter 的初始配置一样,选择使用 ad-hoc SQL 语句创建 GetProductByProductID(productID) 方法(见图 4 )。由于 GetProductByProductID(productID) 方法返回某一特定产品的信息,所以要指明该查询属于返回行的 SELECT 查询类型。
.png)
图9 :选择 SELECT which returns rows 作为查询类型
下一个屏幕提示我们写上要用的 SQL 查询,预先调入了 TableAdapter 的缺省查询。 向现有的查询中增加子句 WHERE ProductID = @ProductID ,如图 10 所示。
.png)
图10 :增加 WHERE 子句到预先调入的查询中以返回某特定产品的记录
最后,将所生成的方法名称更改为 FillByProductID 和 GetProductByProductID 。
.png)
图11 :将方法重命名为 FillByProductID 和 GetProductByProductID
这个向导运行结束后,TableAdapter 现在包含两个检索数据的方法:返回所有产品的 GetProducts() 和返回特定产品的 GetProductByProductID(productID) 。
步骤3 :为启用并发优化的数据访问层创建业务逻辑层
我们现有的ProductsBLL 类既有使用批量更新模式也有使用数据库直接模式的例子。AddProduct 方法和 UpdateProduct 重 载都使用批量更新模式,把一个 ProductRow 实例传到 TableAdapter 的 Update 方法。另一方面, DeleteProduct 方法使用数据库直接模式,调用 TableAdapter 的 Delete(productID) 方法。
有了新的ProductsOptimisticConcurrency TableAdapter , 数据库直接方法现在要求也要传入原始值。例如, Delete 方法现在需要 10 个输入参数:原始的 ProductID 、 ProductName 、 SupplierID 、 CategoryID 、 QuantityPerUnit 、 UnitPrice 、 UnitsInStock 、 UnitsOnOrder 、 ReorderLevel 和 Discontinued 。在发往数据库的 DELETE 语句的 WHERE 子句中使用这些附加的输入参数值,只有当数据库的当前值与原始值相吻合时,才删除指定的记录。
尽管用在批量更新模式中的 TableAdapter 的 Update 方法的签名并没有改变,但记录原始值和新值的代码却有所改变。所以,与其尝试使用具有我们现有 ProductsBLL 类的启用并发优化的数据访问层,还不如创建一个新的业务逻辑层类,与新的数据访问层一起工作。
把一个名为 ProductsOptimisticConcurrencyBLL 的类添加到 App_Code 文件夹里的 BLL 文件夹中。
.png)
图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();
}
}
注意,在类声明开始部分上面NorthwindOptimisticConcurrencyTableAdapters 语句的使用。NorthwindOptimisticConcurrencyTableAdapters 命名空间包含ProductsOptimisticConcurrencyTableAdapter 类,它提供数据访问层的方法。在类声明之前,您还可以看到 System.ComponentModel.DataObject 特性,它指示 Visual Studio 将这个类包括在 ObjectDataSource 向导的下拉列表中。
ProductsOptimisticConcurrencyBLL 的 Adapter 属性提供了对 ProductsOptimisticConcurrencyTableAdapter 类的实例的快捷访问,并延续了在我们原来的业务逻辑层类(ProductsBLL 、CategoriesBLL 等 )中所使用的模式。最后,GetProducts() 方法只需向下调用数据访问层的 GetProducts() 方法,并返回一个 ProductsOptimisticConcurrencyDataTable 对象,其中填充了数据库中每个产品记录的 ProductsOptimisticConcurrencyRow 实例。
使用具有并发优化功能的数据库直接模式删除产品
在应用并发优化的数据访问层使用数据库直接模式,必须将新值和原始值都传给方法。对于删除过程,由于这里没有新值,所以只需传入原始值。然后,在我们的业务逻辑层中,必须接受所有的原始值作为输入参数。让我们用 ProductsOptimisticConcurrencyBLL 类中的 DeleteProduct 方法应用数据库直接方法。这意味着,这个方法需要将所有 10 个产品数据字段都作为输入参数,并把它们传递到数据访问层。代码如下:
[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 )的那些原始值与用户单击 DELETE 按钮时数据库里的值不符,WHERE 子句将和数据库中的任何记录都匹配不上,不会有记录受到影响。因此,TableAdapter 的 Delete 方法将返回 0 ,而业务逻辑层的 DeleteProduct 方法则返回 false 。
在并发优化下使用批量更新模式修改产品记录
如前所述,无论是否使用并发优化,TableAdapter 的批量更新模式的 Update 方法具有相同的方法签名。也就是说,Update 方法可以接受 DataRow 、 DataRow 数组、 DataTable 或 Typed DataSet 作为参数。这里没有附加的输入参数来指定原始值。之所以能做到这点,是因为 DataTable 始终跟踪它的 DataRow 的原始值和修改值。当数据访问层调用其 UPDATE 语句时,把 DataRow 的原始值存入 @original_ColumnName 参数中,而把 DataRow 的修改值存入 @ColumnName 参数中。
在 ProductsBLL 类(它用的是我们原来的,没有并发优化的数据访问层),当使用批量更新模式更新产品信息时,我们的代码按顺序实现下列事件:
- 使用 TableAdapter 的 GetProductByProductID(productID) 方法读取当前的数据库产品信息到一个ProductRow 实例。
- 将新的值赋给步骤 1 所用的 ProductRow 实例。
- 调用 TableAdapter 的 Update 方法 , 传入该 ProductRow 实例。
但是,以上步骤并不会正确地支持并发优化,因为在步骤1 中放到 ProductRow 中的数据是直接从数据库里读出的,这意味着DataRow 所使用的原始值是当前存在数据库里的值,而不是在编辑过程开始时绑定到 GridView 中的值。取而代之的是,在启用了并发优化的数据访问层应用里,我们需要重载 UpdateProduct 方法,来使用下列步骤:
- 使用 TableAdapter 的 GetProductByProductID(productID) 方法 , 读取当前数据库产品信息到一个 ProductsOptimisticConcurrencyRow 实例。
- 把原始值赋给步骤 1 中所用的 ProductsOptimisticConcurrencyRow 实例。
- 调用 ProductsOptimisticConcurrencyRow 实例的 AcceptChanges() 方法 , 它指示 DataRow : 当前值是 “ 原始 ” 值。
- 把新值赋给 ProductsOptimisticConcurrencyRow 实例。
- 调用 TableAdapter 的 Update 方法 , 传入该 ProductsOptimisticConcurrencyRow 实例。
步骤1 读出指定产品的记录当前在数据库中的所有值。这个步骤对于更新 所有产品列的 UpdateProduct 重载是多余的(因为这些值在步骤 2 中将被覆盖),但对于那些只传入部分列值作为输入参数的过载方法来说,它却是必要的。一旦把原始值赋给 ProductsOptimisticConcurrencyRow 实例, AcceptChanges() 方法即被调用,它标志着把当前 DataRow 中的值作为原始值用在 UPDATE 语句的 @original_ColumnName 参数中。下一步,将新值赋给 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 页面向业务逻辑层方法传递原始值和新值
在讨论完数据访问层和业务逻辑层之后,余下的就是要创建一个 ASP.NET 页面,它能够利用系统中建立的并发优化逻辑。具体点说,数据Web 控件(GridView 、DetailsView 或FormView )必须记住其原始值,且ObjectDataSource 必须把这两套值都传递到业务逻辑层。此外,ASP.NET 页面必须配置为能妥善地处理并发冲突。
首先,打开EditInsertDelete 文件夹中的 OptimisticConcurrency.aspx 页面,把一个GridView 控件添加到 Designer 中,并将它的ID 属性设置为 ProductsGrid 。从GridView 控件的智能标签里,选择创建一个新的 ObjectDataSource ,名为ProductsOptimisticConcurrencyDataSource 。因为我们想要这个 ObjectDataSource 使用支持并发优化的数据访问层,所以将它配置为使用ProductsOptimisticConcurrencyBLL 对象。
.png)
图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 类的DeleteProduct 方法中的 10 个输入参数的每一个,都包含了一个 Parameter 实例。同样,在 UpdateParameters 代码块中,UpdateProduct 里的每一个输入参数也都包含了一个Parameter 实例。
在以前的涉及到数据修改的教程中,我们在这里要删除ObjectDataSource 的 OldValuesParameterFormatString 属性,因为这一属性标志着业务逻辑层方法需要传入老(或原始)值以及新值。此外,这一属性值还标出了原始值的输入参数名称。既然我们要把原始值传递到业务逻辑层,因此 不要删除这一属性。
注意:OldValuesParameterFormatString 属性的值必须与接受原始值的业务逻辑层的输入参数的名称相符。由于我们给这些参数命名为 original_productName 、original_supplierID 等,所以可以让OldValuesParameterFormatString 属性的值为 original_{0} 。但是,如果业务逻辑层方法的参数名称为old_productName 、old_supplierID 等,则需要更改 OldValuesParameterFormatString 的属性为old_{0} 。
最后还有一个属性需要设置,是为了让ObjectDataSource 正确地发送原始值到业务逻辑层方法。ObjectDataSource 有一个ConflictDetection 属性 ,它可以赋予以下 两个值之一 :
- OverwriteChanges :缺省值,不发送原始值到业务逻辑层方法的原始输入参数。
- CompareAllValues :发送原始值到业务逻辑层方法,当使用并发优化时选择这一选项。
请花点时间将 ConflictDetection 属性设置为CompareAllValues 。
配置 GridView 的属性和字段
在正确设置了 ObjectDataSource 的属性之后,让我们转向如何设置 GridView 。首先,因为我们想让 GridView 支持编辑和删除功能,在 GridView 的智能标签中勾选上 Enable Editing 和 Enable Deleting 复选框。这将加入一个 CommandField ,它的ShowEditButton 和 ShowDeleteButton 都设置为 true 。
当与ProductsOptimisticConcurrencyDataSource ObjectDataSource 绑定后,对应每一个产品数据字段,这个GridView 中都有一个列。虽然这样的 GridView 确实可以编辑,但却无法让用户接受。因为CategoryID 和 SupplierID 的 BoundFields 都仅提供为文本框,要求用户输入正确的类别和供应商作为ID 号;对于数字型字段也没有设定格式;没有验证控件来保证已经提供了产品名称;也无法保证单价、库存量、订货量和再订货水平值等都是正确的数字值,且大于或等于零。
正如我们在“向编辑和插入界面添加验证控件与定制数据修改界面”教程中所讨论的,可以通过将BoundFields 转换为 TemplateFields 来自定义用户界面。我已经用下列方式修改了这个GridView 及其编辑界面:
- 删除了 ProductID 、SupplierName 和 CategoryName BoundFields 。
- 将 ProductName BoundFields 转换为 TemplateFields ,并添加 RequiredFieldValidation 控件。
- 将 CategoryID 和 SupplierID 从 BoundFields 转换为 TemplateFields ,并调整编辑界面,使用下拉框,而不是文本框。在这些TemplateFields 的 ItemTemplates 中,已经显示出 CategoryName 和 SupplierName 数据字段。
- 将 UnitPrice 、UnitsInStock 、UnitsOnOrder 和 ReorderLevel 从 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 ( 然后再传递到业务逻辑层 ),很重要的一点是,GridView 的 EnableViewState 属性必须设置为 true ( 缺省值 )。如果您禁用了这个视图状态,原始值就会在回传过程中丢失。
传递正确的原始值到ObjectDataSource
在GridView 的配置方式上,还有几个问题。如果 ObjectDataSource 的 ConflictDetection 属性设为 CompareAllValues ( 同我们的设置一样 ),当 GridView ( 或 DetailsView 或 FormView )调用ObjectDataSource 的 Update() 或 Delete() 方法时,ObjectDataSource 试图复制 GridView 的原始值到它对应的 Paremeter 实例中。在前面的图 2 中,对这一过程做了图解。
特别地,每次向 GridView 绑定数据时,GridView 的原始值的赋值方式都用的是双向数据绑定语句。所以,非常重要的是,所要求的原始值都是通过双向数据绑定方式获得的,并且它们是以可转换的格式提供的。
想了解这一点为什么重要,不妨打开浏览器,用一点时间来访问我们的页面。正如我们所期待的, GridView 列出了每个产品,并在最左侧的一列放置了 Edit 和 Delete 按钮。
.png)
图14 :GridView 列出的产品信息
如果您单击任何一个产品的 Delete 按钮,都会显示一个 FormatException 异常信息页。
.png)
图15 :试图删除任一产品记录时出现的 FormatException 异常
这是当ObjectDataSource 试图读取原始的 UnitPrice 值时,出现了FormatException 异常。由于 ItemTemplate 将 UnitPrice 的格式定义为货币型 (<%# Bind("UnitPrice", "{0:C}") %>) ,它包含一个货币符号,如 $19.95 。当 ObjectDataSource 试图把这个字符串转换为十进位数时,便会出现这个 FormatException 异常。为解决这个问题,有几种选择方式:
- 从 ItemTemplates 中移除货币格式。也就是,不再用 <%# Bind("UnitPrice", "{0:C}") %> ,而用 <%# Bind("UnitPrice") %> 。这样做的缺点是不再能用货币格式显示价格。
- 在 ItemTemplates 中用货币格式显示 UnitPrice ,但用Eval 关键字实现。复习一下,Eval 实行的是单向数据绑定。我们仍然需要提供 UnitPrice 的原始值,所以我们仍然需要在 ItemTemplates 中有一个双向的数据绑定语句,但可以把它放到一个 Label Web 控件中,将该控件的 Visible 属性设为 false 。可以在 ItemTemplates 中使用下列标记:
<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>
- 从 ItemTemplates 中移除货币格式,使用<%# Bind("UnitPrice") %> 。在 GridView 的 RowDataBound 事件处理程序中,用编程方式访问显示 UnitPrice 值的 Label Web 控件,并将该控件的 Text 属性设置为货币格式。
- 保留 UnitPrice 的货币格式。在 GridView 的 RowDeleting 事件处理程序中,用 Decimal.Parse 把现有的原始的 UnitPrice 值 ($19.95) 替换为一个实际的十进位值。 在 ASP.NET 页面中处理 BLL 和 DAL 级别的异常教程的 RowUpdating 事件处理程序中,介绍了类似的处理方法。
在我用的例子中,选择的是第二种方法,加一个隐藏的Label Web 控件,它的 Text 属性是双向数据绑定的,绑定到没有格式的 UnitPrice 值。
在解决完这个问题之后,试着再单击一下任一产品的 Delete 按钮。这一次,您在页面上会看到一个 InvalidOperationException 异常信息,这是当 ObjectDataSource 试图调用业务逻辑层的 UpdateProduct 方法时发生的。
.png)
图16 :ObjectDataSource 找不到具有待发送输入参数的方法
看一下异常信息,显然是因为ObjectDataSource 准备调用的业务逻辑层的DeleteProduct 方法中包括original_CategoryName 和original_SupplierName 输入参数。这是因为 CategoryID 和 SupplierID TemplateFields 的 ItemTemplates 现在包含绑定到 CategoryName 和 SupplierName 数据字段的双向绑定语句。解决的方法是,把包含的绑定语句改为绑定到 CategoryID 和 SupplierID 数据字段。为此,要将现在的绑定语句替换为 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 的 Edit 按钮。然后,仅在一个浏览器窗口中,将名称改为 Chai Tea 并单击 Update 。这个修改应该能够成功,并将 GridView 返回到编辑前的状态,而 Chai Tea 成为了新的产品名称。
但是,在另外一个浏览器窗口中,产品名称的 TextBox 中仍然显示为 Chai 。在这第二个浏览器窗口中,将 UnitPrice 改为 25.00 。如果没有并发优化支持,在第二个浏览器窗口单击 Update 后将把产品名称改回到 Chai ,因而覆盖了在第一个浏览器窗口中所做的修改。但使用了并发优化后,在第二个浏览器窗口中单击 Update 按钮,页面将会显示 DBConcurrencyException 异常信息。
.png)
图17 :探测到并发冲突时显示 DBConcurrencyException 异常信息
只有使用数据访问层的批量更新模式时,这个DBConcurrencyException 异常信息才出现。当使用数据库直接模式时,不出现任何异常信息,只提示没有记录被修改。为了说明这一点,让我们回到两个浏览器窗口的 GridView 的编辑前状态。然后,在第一个浏览器窗口中单击 Edit 按钮并将产品名称从 Chai Tea 改回为 Chai ,并单击 Update 。在第二个浏览器窗口中,单击 Chai 那行的 Delete 按钮。
当单击了Delete 按钮后,页面传回,GridView 调用ObjectDataSource 的 Delete() 方法,而ObjectDataSource 则到下层调用ProductsOptimisticConcurrencyBLL 类的DeleteProduct 方法,传递原始值。第二个浏览器窗口传递的 ProductName 的原始值是 Chai Tea ,它与数据库中当前的 ProductName 的值不符。所以,发到数据库的 DELETE 语句不会影响任何一条记录,因为数据库中没有一条记录能够满足 WHERE 子句中的条件。这时,DeleteProduct 方法返回 false ,并且 ObjectDataSource 的数据重新绑定到 GridView 。
从终端用户的视角看,在第二个浏览器窗口单击了 Chai Tea 的 Delete 按钮后,屏幕闪了一下,然后返回,产品仍然在那里,但现在它显示为 Chai (在第一个浏览器窗口中对产品名称所做的修改)。如果该用户这时再次单击 Delete 按钮,删除操作将会成功,因为 GridView 的原始的 ProductName 值 ( Chai ) 目前与数据库中的值相符。
在这两种情况下,用户体验都还远不够理想。当使用批量更新模式时,我们当然并不想让用户看到 DBConcurrencyException 生硬的详细信息。当使用数据库直接模式时,用户操作失败后的反应也颇令人困惑,并没有对原因做出明确的解释。
为了修补这两个问题,我们可以在这个页面上创建一个 Label Web 控件,对修改或删除操作失败的原因做出解释。在批量更新模式下,我们可以在 GridView 的 post-level 事件处理程序中决定是否显示 DBConcurrencyException 异常信息,并根据需要发出警告提示。在数据库直接模式下,我们可以检查业务逻辑层方法的返回值(如果影响了某一条记录,它返回 true ,否则返回 false ),并根据需要给出提示。
步骤6 :探测到并发冲突时增加并显示警告信息
当并发冲突发生时,其反应方式取决于我们使用的是数据访问层的批量更新模式还是数据库直接模式。在本教程中,这两种模式都使用了:批量更新模式用于更新,数据库直接模式用于删除。首先,让我们添加两个 Label Web 控件到我们的页面中,用来对试图删除或修改数据时发生的并发冲突做出说明。设置 Label 控件的 Visible 和 EnableViewState 属性为 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 、EnabledViewState 和Text 属性之外,我还把 CssClass 属性设置为 Warning ,这使得标签上的字体显示为大号的,红色的,斜体的和加粗的。在以前的 探讨与插入、更新和删除相关的事件教程中,我们定义了这个CSS Warning 类并将其添回到 Styles.css 。
在加入了这些标签之后,在 Visual Studio 中Designer 的显示如图 18 所示。
.png)
图18 :两个 Label 控件被添加到页面中
放置好这两个 Label Web 控件后,我们对处理以下问题就胸有成竹了:怎样知道发生了并发冲突,何时将Label 的 Visible 属性设为 true ,发出警告信息。
更新数据时处理并发冲突
我们首先看在使用批量更新模式时如何处理并发冲突。因为在使用批量更新模式时出现此类冲突会跳出DBConcurrencyException 异常信息,我们需要在 ASP.NET 页面中增加代码,用于决定在数据更新过程中是否显示DBConcurrencyException 异常信息。当这类冲突发生时,我们应该显示一条信息向用户解释,他们所做的修改没有被存储,原因是从他们开始编辑这条记录到单击Update 按钮这段时间内,有另一个用户修改了相同的数据。
正如我们在ASP.NET 页面中处理业务逻辑层和数据访问层级别的异常 教程中所了解的,这类异常是可以探测到的,并可以在数据Web 控件的 post-level 事件处理程序中加以制止。所以,我们需要为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 详细说明了这一系列的事件。
.png)
图19 :并发冲突发生后一个用户的更新数据丢失
注意:还有一个可选的方式,即不返回GridView 的编辑前状态,而是通过把传入的 GridViewUpdatedEventArgs 对象的 KeepInEditMode 属性设为 true ,保留 GridView 的编辑状态。可是,如果您采用这一方法,一定要确认重新绑定数据到 GridView (通过调用其 DataBind() 方法),这样,其他用户修改的值才能加载到编辑界面上。在本教程供下载的代码中,在 RowUpdated 事件处理程序中有这样两行被注释掉的代码;只需取消这两行代码的注释,就可以在并发冲突发生后让 GridView 保留在编辑状态。
删除数据时处理并发冲突
在数据库直接模式下,当并发冲突发生时,并没有异常信息出现。而只是操作数据库的语句并没有影响到任何记录,因为任何记录都无法满足 WHERE 子句中的条件。业务逻辑层中建立的所有数据修改方法都设计为返回布尔值,表明是否它们正好影响了一条记录。因此,在删除数据时要确定是否发生了并发冲突,我们可以检查业务逻辑层的 DeleteProduct 方法的返回值。
可以在 ObjectDataSource 的 post-level 事件处理程序中,通过传递到事件处理程序的ObjectDataSourceStatusEventArgs 对象的ReturnValue 属性,检查业务逻辑层方法的返回值。因为我们对判断 DeleteProduct 方法的返回值感兴趣,所以需要对 ObjectDataSource 的 Deleted 事件创建一个事件处理程序。 ReturnValue 属性属于对象类型,所以当异常信息出现,且方法在返回一个值之前就被迫中断时,它可以是 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 被刷新,显示出从用户调出页面到单击 Delete 按钮这段时间内,这条记录的数据所发生的更改。当这样的冲突发生时,DeleteConflictMessage Label 显示出来,解释刚才发生了什么情况(参见图 20 )。
.png)
图20 :并发冲突发生后一个用户的删除操作被取消
小结
在每一个允许多用户同时修改或删除数据的应用程序中,并发冲突都是可能出现的。如果不在意这样的冲突,当两个用户同时更新同一条数据时,最后写入者“获胜”,覆盖掉另一个用户所做的修改。 开发人员还可以实施优化式或封锁式并发控制。并发优化控制假定,冲突是不经常发生的,对于可能导致并发冲突的修改或删除命令,不允许它们执行即可。封锁式并发控制假定,并发冲突经常发生,拒绝一个用户执行修改或删除命令是不能接受的。在封锁式并发控制下,更新一条记录时即锁住它,从而防止任何其他用户在该记录被锁住时修改或删除它。
.NET 的 Typed DataSet 提供了支持并发优化控制的功能。特别地,发送到数据库的 UPDATE 和 DELETE 语句包括数据表的所有列,所以必须保证,只能在记录的当前数据与用户开始修改或删除时所获得的原始数据相符时,才能真正执行修改或删除操作。一旦将数据访问层配置为支持并发优化控制,就需要更新业务逻辑层的方法。另外,必须配置向下调用业务逻辑层的 ASP.NET 页面,使 ObjectDataSource 从它的数据 Web 控件取回原始值,并把它们下传到业务逻辑层。
我们在本教程中学到,要在一个 ASP.NET Web 应用程序中实施并发优化控制,必须更新数据访问层和业务逻辑层,并在 ASP.NET 页面中加入支持。是否将时间和精力投入到这些附加的工作中要取决于您的应用程序。如果在您的用户不是经常同时更新数据,或是他们所更新的数据各不相干,那么并发控制则不是一个关键因素。但是,如果经常有多用户在您的站点上对相同数据进行工作,采用并发控制就能够防止一个用户在修改或删除过程中无意地覆盖掉另一人所做的工作。
快乐编程 !

浙公网安备 33010602011771号