导言
对于那些仅仅允许用户查看数据,或者仅有一个用户可以修改数据的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展示了这些连续的事件。
图 1: 当两个用户同时更新一条记录,则存在一个用户的更改覆盖另一个的更改的可能性
类似地,当两个用户同时访问一个页面,一个用户可能更新的事另一个用户已经删除的记录。或者,在一个用户加载页面跟他点击删除按钮之间的时间里,另一个用户修改了这条记录的内容。
有下面三中并发控制策略可供选择:
1.什么都不做 –如果并发用户修改的是同一条记录,让最后提交的结果生效(默认的行为)
2.开放式并发(Optimistic Concurrency) - 假定并发冲突只是偶尔发生,绝大多数的时候并不会出现; 那么,当发生一个冲突时,仅仅简单的告知用户,他所作的更改不能保存,因为别的用户已经修改了同一条记录
3.保守式并发(Pessimistic Concurrency) – 假定并发冲突经常发生,并且用户不能容忍被告知自己的修改不能保存是由于别人的并发行为;那么,当一个用户开始编辑一条记录,锁定该记录,从而防止其他用户编辑或删除该记录,直到他完成并提交自己的更改
注意:在本节里,我们不讨论保守式并附的例子。保守式并发控制很少使用,因为锁定如果没有完全释放,会妨碍其他用户进行数据更新。例如,如果一个用户为了编辑而锁定某一条记录,但在解锁之前就离开了,那么其他任何用户都不能更新这条记录,直到最初的用户返回并完成他的更新。因此,使用保守式并发控制的地方,相应地会作一个时间限制,如果到达这个时间限制,则取消锁定。例如订票网站,当用户完成他的订票过程时会锁定某个特定的座位,这就是一个使用保守式并发控制的例子。
第一步:如何实现开放式并发控制
开放式并发控制能够确保一条记录在更新或者删除时跟它开始这次更新或修改过程时保持一致。例如,当在一个可编辑的GridView里点击编辑按钮时,该记录的原始值从数据库中读取出来并显示在TextBox和其他Web控件中。这些原始的值保存在GridView里。随后,当用户完成他的修改并点击更新按钮,这些原始值加上修改后的新值发送到业务逻辑层,然后到数据访问层。数据访问层必定发出一个SQL语句,它将仅仅更新那些开始编辑时的原始值根数据库中的值一致的记录。图二描述了这些事件发生的顺序。
图2: 为了更新或删除能够成功,原始值必须与数据库中相应的值一致
有多种方法可以实现开放式并发控制(查看Peter A. Bromberg的文章 Optmistic Concurrency Updating Logic,从摘要中看到许多选择)。ADO.NET类型化数据集提供了一种应用,这只需要在配置时勾选上一个CheckBox。使用开发式并发的目的是使类型化数据集的TableAdapter的UPDATE和DELETE语句可以检测自该记录加载到DataSet中以来数据库中的值是否被更改。例如下面的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语句是为了易读而简单化了。实际上,在WHERE子句中检测UnitPrice会比较棘手,这是因为UnitPrice可以包含空值,而NULL = NULL则总是返回False(相应地你必须用IS NULL)。
除了使用一个不同的UPDATE语句之外,配置TableAdapter使用开放式并发控制还需要修改它直接发送到数据库的方法。回到我们的第一节,创建一个数据访问层,这些发送到数据库的方法接收一列标量的值作为输入参数(不仅仅是强类型DataRow或DataTable的实例)。当使用开放式并发,直接发送到数据库的Update() 和 Delete()方法就包含了对应原始值的输入参数。而且,业务逻辑层中批量方式更新的代码(Update()的重载,它不仅接受标量值,也接受DataRows 和 DataTables)也要做出相应的更改。
与其扩展我们现有得数据访问层表适配器使用开放式并发(同时也必须修改业务逻辑层以协调),不如让我们创建一个新的类型化数据集NorthwindOptimisticConcurrency,在它里面我们添加一个使用开放式并发的Products表适配器。然后,我们将在业务逻辑层中创建类ProductsOptimisticConcurrencyBLL,它为了支持开放式并发的DAL会有适当的更改。一旦这些基础工作都已完成,我们就可以创建ASP.NET页面。
第二步: 创建一个支持开放式并发的数据访问层
为了创建一个新的类型化数据集,在App_Code文件夹里的DAL文件夹上右键点击,选择添加一个新的数据集并命名为NorthwindOptimisticConcurrency。正如我们在第一节中看到过的那样,系统会自动添加一个表适配器(TableAdapter)到当前的类型化数据集众,并自动地进入TableAdapter配置向导。在第一屏中,向导提示我们选择数据库连接 – 连接到同样的数据库Northwind并使用Web.config里设置好的连接字符串NORTHWNDConnectionString。
图 3: 连接到同一个数据库Northwind
下一步,向导提示我们选择如何访问数据库:通过一个指定的SQL语句,创建新的存储过程,或者使用一个现有的存储过程。既然我们最初的DAL是使用的是指定SQL查询语句,这里我们还是使用它。
图4: 使用指定SQL语句的方式访问数据库
下一步,进入查询分析器,返回产品信息。让我们使用在最初的DAL中产品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
图5:使用在最初的DAL中产品TableAdapter相同的SQL查询
在我们进入下一步之前,点击“高级选项”按钮。要让这个TableAdapter使用开放式并发,仅仅需要勾选上“使用开放式并发”。
图6:勾选“使用开放式并发”启用开放式并发控制
最后,需要指出的是,该TableAdapter应该同时使用“填充DataTable”和“返回DataTable”两种要生成的方法;并且,勾选“创建方法以将更新直接发送到数据库(GenerateDBDirectMethods)”。将返回DataTable的方法名称从GetData改为GetProducts,使之与我们最初的DAL中的命名规则匹配。
图7:让这个TableAdapter利用所有的数据访问方式
完成了配置向导后,该数据集设计器将包含一个强类型的Products DataTable和TableAdapter。让我们花些时间把该DataTable的名称Products改为ProductsOptimisticConcurrency,方法是右键点击DataTable的标题栏,从菜单中选择“重命名”。
图8:一个DataTable和TableAdapter已经添加到类型化数据集
为了看看ProductsOptimisticConcurrency TableAdapter(使用开放式并发)和Products TableAdapter(不使用并发控制)的UPDATE 和 DELETE查询之间有什么不同,选中该TableAdapter并转到属性窗口。在DeleteCommand 和 UpdateCommand 这两个属性的 CommandText 子属性里,我们可以看到调用DAL的update或者delete关联的方法时发送到数据库的实际的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))
相反,最初的DAL的Products TableAdapter所使用的DELETE语句则简单得多:
DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))
正如你所看到的,启用了开发式并发的TableAdapter所使用的DELETE语句里的WHERE子句包含了对表Product每一个字段现有的值与GridView(或者DetailsView,FormView)最后一次加载时的原始值的对比。因为除了ProductID,ProductName, 和Discontinued之外,其他所有字段都可能为NULL值,所以WHERE子句里还包含了额外的参数以及与NULL值恰当的比较。
在这一节里,我们不会在启用了开放式并发的数据集里增加其他的DataTable了,因为我们的ASP.NET页面将仅提供更新和删除产品信息的功能。然而,我们仍然需要在ProductsOptimisticConcurrency TableAdapter里添加GetProductByProductID(productID) 方法。
为了实现这一点,在TableAdapter的标题栏(在Fill和GetProducts方法名的上方)上右键并从菜单里选择“添加查询”。这将启动TableAdapter查询配置向导。在TableAdapter的最初配置的基础上,选择指定SQL语句来创建GetProductByProductID(productID)方法(见图四)。因为GetProductByProductID(productID)方法返回指定产品的信息,因此需要指定SQL查询类型为“SELECT(返回行)”。
图9:标记SQL查询类型为“SELECT(返回行)”
进入下一步,向导提示我们指定SQL语句,并且与载入TableAdapter默认查询语句。在现有的查询语句的基础上添加WHERE ProductID = @ProductID子句,如图10:
图10:在预载入的查询语句上添加WHERE子句从而返回特定的产品记录
最后,把生成的方法重命名为FillByProductID和GetProductByProductID。
图11:把生成的方法重命名为FillByProductID和GetProductByProductID
完成这个向导之后,现在这个TableAdapter包含两个访问数据的方法:GetProducts(),它返回所有 的产品;和GetProductByProductID(productID),它返回特定的产品。
第三步: 创建一个支持启用了开放式并发的DAL的业务逻辑层
我们现有的ProductsBLL类包含批量更新和直接发送数据库的模式的例子。AddProduct方法和 UpdateProduct重载都使用了批量更新模式,通过一个ProductRow实例发送到TableAdapter的Update方法。另一方面,DeleteProduct方法则使用直接发送到数据库的模式,调用TableAdapter的Delete(productID)方法。在新的ProductsOptimisticConcurrency TableAdapter里,发送到数据库的方法现还要求传入原始的值。例如,Delete方法
现在要求十个输入参数:原始的ProductID、ProductName、SupplierID、CategoryID、QuantityPerUnit、UnitPrice、UnitsInStock、UnitsOnOrder、ReorderLevel和Discontinued。它在发送到数据库的DELETE语句的WHERE子句里使用这些额外的输入参数,仅仅删除当前数据库的值与原始值一致的指定记录。
使用批量更新模式时,如果标记给TableAdapter的Update使用的方法没有更改,那么代码就需要同时记录原始值和新的值。然而,与其在我们现有的ProductsBLL类的基础上试图使用启用了开放式并发的DAL,不如让我们重新创意一个业务逻辑类支持我们新的DAL。在App_Code文件夹下的BLL子文件夹里,添加一个名为ProductsOptimisticConcurrencyBLL的新类。
图 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的GetProdcuts()方法并返回一个ProductsOptimisticConcurrencyDataTable对象,该对象由对应数据库里每一个产品记录的ProductsOptimisticConcurrencyRow实例组成。
使用支持开放式并发的发送到数据库的模式删除一个产品记录
当使用支持开放式并发的DAL发送到数据库的模式,方法必须传入新值和原始值。对删除来说,这没有新的值,所以仅仅需要传入原始值。那么,在我们的BLL里,我们必须接收所有原始值所为输入参数。让ProductsOptimisticConcurrencyBLL类的DeleteProduct方法使用这个发送到数据的方法。这意味着此方法必须接受所有的十个产品数据字段作为输入参数,并传送这些参数到DAL,如下面的代码所示:
[System.ComponentModel.DataObjectMethodAttribute (System.ComponentModel.DataObjectMethodType.Delete, true)] public bool DeleteProduct (int original_productID, string original_productName, int"htmlcode">protected void AssignAllProductValues (NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product, string productName, int"//img.jbzj.com/file_images/article/201605/20160507112647113.png" src="/UploadFiles/2021-04-02/20160507112647113.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方法的每一个输入参数的Parameter实例。同样地,UpdateParameters集合也包含了对应UpdateProduct每一个输入参数的Parameter实例。在先前的那些关于数据修改的教程中,我们在这里都会移除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并勾选上“删除”和“编辑/更新”。这将增加一个CommandField,它的ShowEditButton和ShowDeleteButton属性都已设置为true。当绑定ProductsOptimisticConcurrencyDataSource ObjectDataSource,该GridView对应每一个产品数据字段都包含一列。
虽然这样的一个GridView可以被编辑,但用户的体验将是不可接受的。这没有对数字栏作格式化处理,也没有validation控件以确保提供product's name并且unit price、units in stock、units on order、和reorder level的值都是大于零的数字。
跟我们在之前的给编辑和新增界面增加验证控件 这一节里所论述的一样,用户界面可以通过将绑定列(BoundFields)替换为模板列(TemplateFields)实现自定义。我已经通过以下方式修改了这个GridView和它的编辑界面:
1.删除ProductID、SupplierName、和CategoryName这几个绑定列;
2.将ProductName绑定列替换为模板列并添加一个RequiredFieldValidation控件;
3.将CategoryID和SupplierID绑定列替换为模板列,并调整编辑界面,使用DropDownList而不是TextBox。在这些模板列的ItemTemplates里,显示CategoryName和SupplierName字段;
4.将UnitPrice、UnitsInStock、UnitsOnOrder、和ReorderLevel绑定列替换为模板列并添加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),将GirdView的EnableViewState属性设置为true(默认值)是至关重要的。如果禁用了视图状态,这些原始值在postback的时候将会丢失。
传送正确的原始值到ObjectDataSource完成了GridView的配置,还有几个问题。如果这个ObjectDataSource的ConflictDetection 属性设置为CompareAllValues (正如我们所做的),它会尝试复制GridView的原始值到它的Parameter实例。回到图2查看这个过程的图解。
特别需要指出的是,这个GridView的原始值是被指定为双向绑定的。因此,这些必需的原始值是通过双向绑定获取的,并且它们是规定为可改变的格式,这一点很重要。为了看看为什么这一点非常重要,花些时间通过浏览器访问我们的页面。正如所预料那样,GridView列出每一个产品,并且每行最左边的一列都显示编辑和删除按钮。
图14: GridView列出所有的产品信息
如果你点击任意一行的删除按钮,则抛出一个FormatException异常。
图15: 尝试删除任意一个产品导致FormatException异常
当ObjectDataSource试图读取原始的UnitPrice值引发了一个FormatException异常。因为该模板列将UnitPrice的值限制为货币格式(<%# Bind("UnitPrice", "{0:C}") %>),它包含一个货币符号,例如$19.95。该FormatException异常发生在ObjectDataSource试图将字符产转换成小数。为了绕过此问题,我们有许多种选择:
1.从模板列里删除货币格式限制。就是说,取代<%# Bind("UnitPrice", "{0:C}") %>,简单地使用<%# Bind("UnitPrice") %>。下方的价格就是没有格式化的。
2.在模板列中显示UnitPrice时格式化为货币,但是使用Eval关键字实现绑定。记得Eval是实现单向绑定的。我们仍然需要提供UnitPrice的值作为原始的值,因此在模板列里我们依旧需要一个双向绑定的声明,但这可以放在一个Visible属性设置为false的Label服务器控件里。在模板列里我们可以使用下面的标记:<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>3.从模板列里删除货币格式限制,使用 <%# Bind("UnitPrice") %>。在GridView的RowDataBound事件处理里,编码访问显示UnitPrice的值的Label服务器控件并设置其Text属性为格式化的版本。
4.让UnitPrice保留货币格式化。在GridView的RowDeleting事件处理里,将现存的UnitPrice的原始($19.95)替换为实际的小数值(使用Decimal.Parse)。在前面的 在ASP.NET页面中处理BLL/DAL异常这一节教程里我们也已经看过如何RowUpdating事件处理里实现类似的功能。 在我的例程里我选择第二种方法,添加一个隐藏的Label服务器控件,并将它的Text属性双向绑定到无格式的UnitPrice值。解决了这个问题之后,再次点击任意一个产品的删除按钮。这一次,当ObjectDataSource尝试调用BLL的UpdateProduct方法时我们得到一个InvalidOperationException异常。图 16: ObjectDataSource找不到具有它要发送的输入参数的方法
仔细看看异常信息,明显地ObjectDataSource希望调用一个BLL的DeleteProduct方法,此方法包含original_CategoryName和original_SupplierName输入参数。这是因为CategoryID和SupplierID模板列的ItemTemplate当前是双向绑定到CategoryName和SupplierName数据字段。作为替换,我们需要包含对CategoryID和SupplierID数据字段的Bind声明。为了实现这一点,把现有的Bind声明更改为Eval声明,并且添加隐藏的Label服务器控件,这些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>通过这些更改,现在我们可以成功地删除和编辑产品信息了!在第五步里,我们将看看如何验证删除时发生并发冲突。但是现在,花几分钟尝试更新和删除一些记录,确认在单用户的情况下更新和删除能够正常运作。
第五步: 测试开放式并发支持
为了验证并发冲突是否能够被发现(而不是导致数据被盲目改写),我们需要打开两个浏览器窗口来访问这个页面。在两个浏览窗口里,都点击产品“Chai”的编辑按钮。然后,在其中一个窗口修改其名称为“Chai Tea”并点击更新。这个更新应该会成功并且GridView回到预编辑状态,并且该产品的名称已经改为“Chai Tea”。
而在另一个浏览器窗口里,产品名称域依旧显示的是“Chai”。在这个浏览器窗口,将UnitPrice的值更新为25.00。如果没有开放式并发支持的话,点击第二个浏览器窗口的更新按钮将把产品名称改回“Chai”,从而覆盖了第一个浏览器窗口里所作的修改。然而现在有了开发式并发,当点击第二个窗口中的更新按钮时导致了一个DBConcurrencyException异常。图 17: 发现并发冲突,抛出一个DBConcurrencyException异常
这个DBConcurrencyException异常仅当利用DAL的批量更新模式时会被抛出。直接发送到数据库的模式则不会引发异常,它仅仅会提示没有行受到影响。为了举例说明这个,两个浏览器窗口里的GridView都回到预编辑的状态。然后,在第一个窗口里,点击编辑按钮,把产品名称从“Chai”改为“Chai Tea”并点击更新。在第二个窗口里,点击产品“Chai”的删除按钮。点击删除按钮,页面会传,GridView调用ObjectDataSource的Delete()方法,然后ObjectDataSource调用ProductsOptimisticConcurrencyBLL类的DeleteProduct方法,传入原始的值。在第二个浏览器窗口里原始的ProductName值是“Chai Tea”,这个值与当前数据库中相应的ProductName值是不一致的。因此,发送到数据库的DELETE语句影响0行,因为数据库中没有记录能够满足WHERE子句。DeleteProduct方法返回false并且ObjectDataSource的数据重新绑定到GridView控件。
从最后一个用户的观点来看,在第二个浏览器窗口里点击了产品“Chai Tea”的删除按钮导致屏幕闪烁,恢复后该产品依旧在,虽然现在它的名称是“Chai”(在第一个浏览器窗口里修改了产品名称)。如果用户再次点击删除按钮,这次就能成功删除,因为GridView的原始的ProductName值(“Chai”)现在能够与数据库中相应的值匹配。在这些例子里,用户的体验跟理想的状况还有颇远的距离。显然我们在使用批量更新模式时不希望用户看到DBConcurrencyException异常生硬的详细信息。并且使用直接发送到数据库模式的行为也会让用户有些疑惑,因为用户操作失败了但是没有准确的提示说明为什么。
为了补救这两个小问题,我们可以在页面上放置一个Label服务器控件,它用来提供为什么更新或删除失败的说明。在批量更新模式,我们可以在GridView的post级事件处理里判定是否引发了一个DBConcurrencyException异常,显示必要的警告标签。对于直接发送到数据库的方法,我们可以检测BLL方法(它对一行或多行产生影响返回true,否则false)的返回值并显示必要的提示信息。
第六步: 添加提示信息并且在发生并发冲突时显示
当一个并发冲突出现时,展现出来的行为取决于是使用DAL的批量更新还是直接发送到数据库的模式。我们这一节的教程两种模式都用了,用批量更新模式实现修改,用直接发送到数据库的方式实现删除。首先,我们添加两个Label服务器控件到页面,它们用来解释更新或删除数据时出现的并发冲突。设置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设计器里看起来应该类似于图18:
图 18: 两个Label控件添加到页面
这些Label服务器控件放置到适当的位置后,我们准备好检测当并发冲突发生时如何判定,在哪个时间点把适当的Label的Visible属性设置为true并显示提示信息。
更新时处理并发冲突
让我们首先看看当使用批量更新模式是如何处理并发冲突。因为批量更新模式下的这些冲突导致抛出一个DBConcurrencyException异常,我们需要在ASP.NET页面中添加代码来判定更新过程中出现的是否DBConcurrencyException异常。如果是,我们则显示一个信息向用户解释他们的更改没有被保存,由于别的用户在他开始编辑和点击更新按钮之间的时间里修改了同样的数据记录。
正如我们在在ASP.NET页面中处理BLL/DAL异常 这一节里看过的那样,这样的异常可以在数据Web服务器控件的post级事件处理里被发现和排除。因此,我们需要创建一个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: 面对并发冲突,一个用户的更改丢失了
注意:作为另一种选择,与其让GridView回到预编辑状态,我们还不如让GridView停留在编辑状态,通过设置传入的GridViewUpdatedEventArgs对象的KeepInEditMode属性为true。如果你接受这种方法,那么,必须重新绑定数据到GridView(通过调用它的DataBind()方法)从而将其他用户更改后的值栽入到编辑界面。在这一节的可下载的代码里,RowUpdated事件处理里有这两行注悉掉的代码;仅仅需要启用这两行代码就可以让GridView在发生了并发冲突之后保留编辑模式。
响应删除时的并发冲突
对于直接发送到数据库的模式,面对并发冲突时并不会引发异常。然而,数据库语句不影响任何记录,因为WHERE子句不能匹配任何记录。所有在BLL里创建的修改数据的方法都被设计为返回一个布尔值指示它们是否正好影响了一条记录。因此,为了确定删除记录时是否发生了并发冲突,我们可以检查BLL的DeleteProduct方法的返回值。
BLL方法的返回值可以在ObjectDataSource的post级事件处理中通过传入事件处理的ObjectDataSourceStatusEventArgs对象的ReturnValue属性被检测。因为我们感兴趣的是判断从DeleteProduct方法返回的结果,我们需要创建一个ObjectDataSource的Deleted事件的事件处理程序。该ReturnValue属性是object类型的,并且如果在方法可以返回一个值之前引发了异常并且方法被中断的情况下,它的值也可能为null。所以,我们应该首先确保ReturnValue属性非空并是个布尔值。若能通过这个检查,如果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 Label控件,说明发生了什么(见图20)。
图 20: 面对并发冲突,一个用户的删除请求被取消了
总结
并发冲突可能存在于所有允许多用户同时更新或删除数据的应用程序里。如果不解决这样的冲突,当两个用户同时更新同一条数据,无论谁最后得到“胜利”,都将覆盖掉另一个用户所做的更改。作为另一种选择,开发者可以实现开放式并发控制(optimistic concurrency control),或者保守式并发控制(pessimistic concurrency control)。开放式并发控制假定并发冲突很少发生,简单地否决一个会提起并发冲突的更新或者删除命名。保守式并发控制则假定并发冲突频繁地发生,简单地拒绝某个用户的更新或者删除命令是不可接受的。在保守式并发控制下,编辑一条记录涉及到锁定它,从而该记录被锁定时预防其他用户的修改或删除。
.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年以来一直应用微软Web技术。Scott是个独立的技 术咨询顾问,培训师,作家,最近完成了将由Sams出版社出版的新作,24小时内精通ASP.NET 2.0。他的联系电邮为mitchell@4guysfromrolla.com,也可以通过他的博客http://ScottOnWriting.NET与他联系。
免责声明:本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除!
RTX 5090要首发 性能要翻倍!三星展示GDDR7显存
三星在GTC上展示了专为下一代游戏GPU设计的GDDR7内存。
首次推出的GDDR7内存模块密度为16GB,每个模块容量为2GB。其速度预设为32 Gbps(PAM3),但也可以降至28 Gbps,以提高产量和初始阶段的整体性能和成本效益。
据三星表示,GDDR7内存的能效将提高20%,同时工作电压仅为1.1V,低于标准的1.2V。通过采用更新的封装材料和优化的电路设计,使得在高速运行时的发热量降低,GDDR7的热阻比GDDR6降低了70%。
更新日志
- 凤飞飞《我们的主题曲》飞跃制作[正版原抓WAV+CUE]
- 刘嘉亮《亮情歌2》[WAV+CUE][1G]
- 红馆40·谭咏麟《歌者恋歌浓情30年演唱会》3CD[低速原抓WAV+CUE][1.8G]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[320K/MP3][193.25MB]
- 【轻音乐】曼托凡尼乐团《精选辑》2CD.1998[FLAC+CUE整轨]
- 邝美云《心中有爱》1989年香港DMIJP版1MTO东芝首版[WAV+CUE]
- 群星《情叹-发烧女声DSD》天籁女声发烧碟[WAV+CUE]
- 刘纬武《睡眠宝宝竖琴童谣 吉卜力工作室 白噪音安抚》[FLAC/分轨][748.03MB]
- 理想混蛋《Origin Sessions》[320K/MP3][37.47MB]
- 公馆青少年《我其实一点都不酷》[320K/MP3][78.78MB]
- 群星《情叹-发烧男声DSD》最值得珍藏的完美男声[WAV+CUE]
- 群星《国韵飘香·贵妃醉酒HQCD黑胶王》2CD[WAV]
- 卫兰《DAUGHTER》【低速原抓WAV+CUE】
- 公馆青少年《我其实一点都不酷》[FLAC/分轨][398.22MB]
- ZWEI《迟暮的花 (Explicit)》[320K/MP3][57.16MB]