使用 SqlDataSource 实现并发优化
在本教程中,我们将讨论并发优化基础课程,然后探讨如何使用 SqlDataSource 控件。
简介在前一篇教程中,我们探讨了如何向 SqlDataSource 控件添加插入、更新和删除功能。简而言之,要提供这些功能,我们需要在控件的 InsertCommand 、UpdateCommand 或 DeleteCommand 属性指定相应的 INSERT 、UPDATE 、或 DELETE SQL 语句,并在InsertParameters 、UpdateParameters 和DeleteParameters 集合中指定适当的参数。这些属性和集合可以手动指定,Configure Data Source 向导的 Advanced 按钮提供了一个 “ Generate INSERT, UPDATE, and DELETE statements” 复选框,该复选框将根据 SELECT 语句自动生成这些语句。 除了“Generate INSERT, UPDATE, and DELETE statements” 复选框之外,高级SQL 生成选项还包括一个“Use optimistic concurrency” 选项(见图1 )。选中该选项后,如果在用户上一次将数据加载到网格之后没有修改基础数据库数据,则将修改UPDATE 和 DELETE 中自动生成的 WHERE 子句,只执行更新和删除操作。 图1 :可以从 Advanced SQL Generation Options 对话框添加并发优化支持 返回实施并发优化 教程,我们探讨了并发优化的基础知识,以及如何向ObjectDataSource 添加并发优化控件。在本教程中,我们将再次探讨并发优化控件的基础知识,然后探讨如何使用 SqlDataSource 实现。 并发优化简介对于允许多个用户同时编辑或删除相同数据的Web 应用程序来说,某个用户有可能会偶然覆盖其它用户的更改。在实施并发优化 教程中,我将给出下列示例: 假设两个用户Jisun 和 Sam 均在应用程序中访问某一页面,该页面允许访问者通过GridView 控件更新和删除产品。两者在差不多相同的时间均单击了 “Chai” 的 Edit 按钮。Jisun 将产品名称更改为 “Chai Tea” ,并单击了Update 按钮。最终结果是,UPDATE 语句被发送到数据库,该语句设置了产品所有的可更新字段(尽管 Jisun 仅更新了一个字段 ProductName )。 此时,数据库中此产品包含了 “Chai Tea” ,类别 Beverages 和供应商 Exotic Liquids 等值。但是,Sam 屏幕上的 GridView 仍然在可编辑 GridView 行中显示产品名称为 “Chai” 。Jisun 完成更改之后几秒钟 ,Sam 将类别更新为 Condiments ,并单击了Update 。这导致 PDATE 语句将被发送到数据库,该数据库将产品名称设置为 “Chai” ,而CategoryID 则被设置为相应的 Condiments 类别 ID ,其它的与此类似。Jisun 的对产品名称的更改已经被覆盖。 图2 显示了此交互过程。 图2 :两个用户同时更新记录时,一个用户的更改可能覆盖另外一个用户的更改 为了防止这种情况出现,必须执行某种形式的并发控件 。并发优化 作为本教程的重点,将假定尽管偶尔存在并发冲突,但是绝大多数情况下这种冲突并不会出现。因此,如果发生冲突,并发优化控件将通知用户无法保存他们所做的修改,因为其它用户已经修改了同一数据。 注意:对于那些假设会存在很多并发冲突,或者这些冲突是根本不容许的应用来说,可使用保守式控件。有关保守式控件的更详细讨论,请参照实施并发优化 教程。 并发优化控件的作用是确保正在更新或删除的记录保持其更新或删除过程开始的同样的值。例如,在可编辑 GridView 中单击 Edit 按钮时,将从数据库读取记录值,并在文本框和其它 Web 控件中显示记录值。这些原始值由 GridView 保存。随后,在用户完成更改并单击 Update 按钮之后,使用的 UPDATE 语句必须考虑原始值和新值,并且如果开始编辑的原始值与数据库中的值完全相同,则仅更新基础数据库记录。图 3 描述了事件的顺序。 图3 :为使更新或删除成功,原始值必须等于当前数据库的值 有多种方式可执行并发优化(请参阅 Peter A. Bromberg 的并发优化更新逻辑 ,查看多种选择)。SqlDataSource 使用的技术(和数据访问层中使用的 ADO.NET 强类型 DataSets )将添加WHERE 子句,从而包含对所有原始值的比较。例如,只有当目前的数据库值等于更新 GridView 的记录时最初检索到的值时,下面的 UPDATE 语句才更新产品的名称和价格。参数 @ProductName 和 @UnitPrice 包含了由用户输入的新值,而 @original_ProductName 和 @original_UnitPrice 包含了单击 Edit 按钮时最初加载到 GridView 的值:
正如我们将在本教程中看到的一样,使用SqlDataSource 启用并发优化控件就像选中复选框一样简单。 步骤1:创建支持并发优化的SqlDataSource首先,从SqlDataSource 文件夹打开 OptimisticConcurrency.aspx 页面。从工具箱拖拽一个 SqlDataSource 控件,放置到设计器上,将其ID 属性设置为 ProductsDataSourceWithOptimisticConcurrency 。接下来,从控件的智能标记单击 Configure Data Source 链接。在向导中的第一个屏幕上,选择使用NORTHWINDConnectionString ,并单击 Next 。 图4 :选择使用 NORTHWINDConnectionString 在本例中,我们将添加一个能够帮助用户编辑 Products 表的 GridView 。因此,在 “ Configure the Select Statement” 屏幕中,从下拉列表选择 Products列表,并选择 ProductID 、ProductName 、UnitPrice 和 Discontinued 列,如图 5 所示。 图5 :从 Products 表返回 ProductID 、ProductName 、UnitPrice 和 Discontinued 列 在选定列之后,单击Advanced 按钮,显示 Advanced SQL Generation Options 对话框。选择 “Generate INSERT, UPDATE, and DELETE statements” 和 “Use optimistic concurrency” 复选框,单击OK (屏幕快照请参照图 1) 。单击 Next ,然后再单击 Finish 完成向导。 完成 Configure Data Source 向导后,请花点时间检查结果的 DeleteCommand 和 UpdateCommand 属性,以及 DeleteParameters 和 UpdateParameters 集合。完成此操作的最简单的方式是单击左下角的 Source 选项卡,查看页面的声明式语法。在此处将会发现UpdateCommand 值为:
UpdateParameters 集合中有7个参数:
与此类似,DeleteCommand 属性和DeleteParameters 集合应如下面所示:
除了为WHERE 子句增加 UpdateCommand 和DeleteCommand 属性外(以及向各自的参数集添加其它参数外),选择“Use optimistic concurrency” 选项,调整两个其它属性 :
Web 数据 控件调用 SqlDataSource 的 Update() 或者Delete() 方法时,它将传递原始值。如果 SqlDataSource 的 ConflictDetection 属性设置为 CompareAllValues ,命令中将添加这些原始值。OldValuesParameterFormatString 属性提供了这些原始值参数使用的命名形式。Configure Data Source 向导使用 “original_{0}” ,并在UpdateCommand 和 DeleteCommand 属性以及UpdateParameters 和 DeleteParameters 集中命名相应的原始参数。 注意:由于我们未使用 SqlDataSource 控件的插入功能,您可以按照您自己的意愿删除InsertCommand 属性及其InsertParameters 集合。 正确处理 NULL 值遗憾的是,在使用并发优化时,由 Configure Data Source 向导所自动生成的扩充的UPDATE 和 DELETE 语句无法处理包含 NULL 值的记录。要想知道原因,请考虑我们的 SqlDataSource 的 UpdateCommand :
Products 表中的 UnitPrice 列不能包含 NULL 值。如果某条特定记录的UnitPrice 包含了一个 NULL 值,则 WHERE 子句部分“[UnitPrice] = @original_UnitPrice” 将始终等于 False ,这是因为NULL = NULL 始终返回 False 。因此,包含 NULL 值的记录不能进行修改或删除,因为 UPDATE 和 DELETE 语句的 WHERE 子句不会返回需要更新和删除的任何行。 注意:这个问题已于 2004 年 6 月第一次在 SqlDataSource 生成错误的 SQL 语句 中报告给微软,据传将在下一版本的ASP.NET 中解决。 要解决该问题,我们必须在 UpdateCommand 和 DeleteCommand 属性中为可拥有 NULL 值的所有列手动更新WHERE 子句。通常情况下,请将[ColumnName] = @original_ColumnName 更改为:
此修改可直接通过声明式标记完成,您可以从Properties 窗口使用 UpdateQuery 或DeleteQuery 选项,或者在 Configure Data Source 向导中使用 “Specify a custom SQL statement or stored procedure” 中的 UPDATE 和 DELETE 选项卡。同样,必须对可能包含 NULL 值的 UpdateCommand 和 DeleteCommand 的 WHERE 子 句中的每一列 进行修改。 将这种情况应用到我们的例子中将使得UpdateCommand 和 DeleteCommand 值修改:
步骤2 :为 GridView 添加 Edit 和 Delete 选项将SqlDataSource 配置为支持并发优化之后,剩下需要做的事情是向页面中添加使用此并发控件的Web 数据 控件。在本教程中,我们将添加一个既可以支持编辑功能,又可以支持删除功能的 GridView 。要完成此操作,从工具栏中拖拽一个 GridView 到 设计器 中,并将其ID 设置为 Products 。在 GridView 的智能标记中,将其绑定到步骤 1 中添加的 ProductsDataSourceWithOptimisticConcurrency SqlDataSource 控件。最后,从智能标记选中 “Enable Editing” 和 “Enable Deleting” 选项。 图6 :将 GridView 绑定到 SqlDataSource ,并启用编辑和删除功能 在添加GridView 之后,通过删除 ProductID BoundField ,将 ProductName BoundField 的HeaderText 属性更改为 “Product” ,以及更新UnitPrice BoundField 的方式配置其外观,从而其 HeaderText 属性变为 “Price” 。理想情况下,我们应该加强编辑界面,使之包括一个ProductName 值的 RequiredFieldValidator,以及一个 UnitPrice 值的 CompareValidator (保证其数值可保持正确格式)有关定制 GridView 的编辑界面的更详细信息,请参阅自定义数据修改界面 教程。 注意:由于从 GridView 获取的原始值存储在视图状态下,因此必须启用GridView 的视图状态。 完成对 GridView 的 修改后,GridView 和 SqlDataSource 的声明式标记应类似下面所示:
要查看正在执行的并发优化控件,请打开两个浏览器窗口,并在两个窗口中分别加载 OptimisticConcurrency.aspx 页。在两个浏览器中分别单击第一个产品的 Edit 按钮。在第一个浏览器中,改变产品名称,并单击 Update 。此浏览器将回传,而 GridView 将返回到其预编辑模式,显示新编辑记录的新的产品名称。 在第二个浏览器窗口中,更改价格(但是保留产品名称为原始值),并单击 Update 。回传时,网格返回其预编辑模式,但并未记录价格更改。第二个浏览器中显示的值(新产品名称和旧价格)与第一个浏览器中的完全一样。第二个浏览器窗口中所作的更改已经丢失。而且,由于没有显示任何表明出现并发异常的异常或信息,更改丢失得非常平静。 图7 :第二个浏览器中所作的更改毫无声息的丢失了 没有实现对第二个浏览器进行修改的原因在于UPDATE 语句的 WHERE 子句滤掉了所有的记录,因此不会影响任何行。下面,我们重新了解一下 UPDATE 语句:
第二个浏览器更新记录时,在WHERE 子句中指定的原始产品名称未能与现有的产品名称实现匹配(由于它已经被第一个浏览器修改)。因此 ,[ProductName] = @original_ProductName 语句将返回 False ,并且UPDATE 不会影响任何记录。 注意 : 删除的工作方式 相同。打开两个浏览器窗口,首先,编辑某一特定产品,接下来,保存所作更改。在一个浏览器中保存所作更改后,在另外一个浏览器中单击同一产品的 Delete 按钮。由于DELETE 语句的 WHERE 子句中的原始值不匹配,因此删除操作毫无声息的以失败告终。 从最终用户的角度看,在第二个用户的浏览器窗口中单击 Update 按钮后,网格将返回到预编辑模式,但是它们所作的修改已经丢失。但是,此处不存在不保持更改的可见回传。理想情况下,如果由于并发冲突的原因导致用户所作更改丢失,我们应该通知他们,或者将网格保持在编辑模式下。下面,我们探讨一下如何完成此操作。 步骤3 :确定出现并发冲突的时间由于并发冲突拒绝了我们所作的修改,因此,最好能够在出现并发冲突时提醒用户。要想提醒用户,我们需要首先在页面的顶部添加一个名为ConcurrencyViolationMessage 的Web 标签控件,其文本属性显示下列信息:" You have attempted to update or delete a record that was simultaneously updated by another user. Please review the other user's changes and then redo your update or delete."将标签控件的CssClass 属性设置为 “Warning” ,该属性为 Styles.css 中定义的CSS 类,可以红色、斜体、粗体和大字体显示文本。最后,将标签 的Visible 和 EnableViewState 属性设置为False 。除非我们在这些回传中明确将其 Visible 属性设置为 True , 否则将隐藏标签 。 图8 :在页面上添加一个显示警告的标签控件 执行更新或删除操作时,GridView 的 RowUpdated 和 RowDeleted Event Handler 将在其数据源控件完成所需的更新或删除操作后释放。我们可以确定这些 Event Handler 操作所影响的行数。如果影响为零行,我们希望显示ConcurrencyViolationMessage 标签 。 请为 RowUpdated 和 RowDeleted 事件创建 Event Handler ,并添加下列代码:
在两个Event Handler 中,选中 e.AffectedRows 属性,如果它等于 0 ,则将 ConcurrencyViolationMessage 标签 的Visible 属性设置为 True 。在 RowUpdated Event Handler 中,我们还可通过将 KeepInEditMode 设置为 true 来指定 GridView 保持编辑模式。通过这种方式,我们需要将数据重新绑定到网格,从而保证其它用户的数据将被加载到编辑界面。此操作可通过调用 GridView 的 DataBind() 方法完成。 如图9 所示,通过使用这两个 Event Handler ,出现并发冲突时,屏幕上将显示一条非常醒目的信息。 图9 :出现并发冲突时显示信息 小结在多个并发用户可能编辑相同数据的情况下,创建web 应用程序时需要考虑并发控件选项就变得十分重要。在默认情况下, ASP.NET Web 数据控件和数据源控件不使用任何并发控件。正如我们在本教程中所看到的,使用 SqlDataSource 实现并发优化控件非常快捷和便利。SqlDataSource 可以为您处理大多数日常工作—— 向自动生成的 UPDATE 和 DELETE 语句添加扩充 WHERE 子句,但是,正如在 “正确处理NULL 值” 部分所讨论的,几乎不存在处理 NULL 值列的示例。 本教程是最后一篇对 SqlDataSource 进行 探讨的教程。此后的教程中探讨使用 ObjectDataSource 和分层架构处理数据。 |
实现乐观并发 (C#)
对于允许多个用户编辑数据的 Web 应用程序,有两个用户可能同时编辑相同数据的风险。 在本教程中,我们将实现乐观并发控制来处理此风险。
介绍
对于仅允许用户查看数据的 Web 应用程序,或者仅包含一个可以修改数据的用户,两个并发用户不会意外覆盖彼此的更改的威胁。 但是,对于允许多个用户更新或删除数据的 Web 应用程序,一个用户的修改可能会与另一个并发用户的修改发生冲突。 如果没有任何并发策略,当两个用户同时编辑单个记录时,提交更改的用户最后一次将覆盖第一个用户所做的更改。
例如,假设两个用户 Jisun 和 Sam 都访问了应用程序中的一个页面,允许访问者通过 GridView 控件更新和删除产品。 两者同时单击 GridView 中的“编辑”按钮。 Jisun 将产品名称更改为“柴茶”,然后单击“更新”按钮。 净结果是 UPDATE
发送到数据库的语句,该语句将产品 的所有 可更新字段设置为 (即使 Jisun 仅更新了一个字段, ProductName
) 也是如此。 此时,数据库具有“柴茶”、饮料、供应商异国液体等值。此特定产品。 但是,Sam 屏幕上的 GridView 仍会将可编辑 GridView 行中的产品名称显示为“Chai”。 提交 Jisun 更改几秒后,Sam 会将类别更新为 Condiments 并单击“更新”。 这将导致 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
语句已简化为可读性。 实际上,将更涉及子WHERE
句中的签入,UnitPrice
因为UnitPrice
可以包含 NULL
s 并检查是否NULL = NULL
始终返回 False (,而必须使用IS NULL
) 。
除了使用不同的基础 UPDATE
语句,配置 TableAdapter 以使用乐观并发还修改其 DB 直接方法的签名。 回顾第一篇教程“ 创建数据访问层”,DB 直接方法是接受标量值列表作为输入参数 (而不是强类型 DataRow 或 DataTable 实例) 。 使用乐观并发时,DB 直接 Update()
和 Delete()
方法还包括原始值的输入参数。 此外,BLL 中用于使用批处理更新模式的代码 (Update()
接受 DataRows 和 DataTable 的方法重载,而不是) 必须更改标量值。
我们不需要扩展现有的 DAL 的 TableAdapters 来使用乐观并发 (这需要更改 BLL 以适应) ,而是创建名为 NorthwindOptimisticConcurrency
的新类型化数据集,我们将向其添加 Products
使用乐观并发的 TableAdapter。 接下来,我们将创建一个 ProductsOptimisticConcurrencyBLL
业务逻辑层类,该类具有适当的修改以支持乐观并发 DAL。 完成此基础工作后,我们将准备好创建 ASP.NET 页面。
步骤 2:创建支持乐观并发的数据访问层
若要创建新的类型化数据集,请 DAL
右键单击文件夹中的文件夹 App_Code
,并添加新名为 NorthwindOptimisticConcurrency
DataSet 的数据集。 正如我们在第一个教程中看到的那样,这样做会将新的 TableAdapter 添加到 Typed DataSet,从而自动启动 TableAdapter 配置向导。 在第一个屏幕中,系统会提示指定要连接到的数据库 - 使用 NORTHWNDConnectionString
设置从 Web.config
中连接到同一 Northwind 数据库。
图 3:连接到同一 Northwind 数据库 (单击以查看全尺寸图像)
接下来,我们将提示如何查询数据:通过即席 SQL 语句、新的存储过程或现有存储过程。 由于我们在原始 DAL 中使用了即席 SQL 查询,因此也在此处使用此选项。
图 4:指定要使用即席 SQL 语句检索的数据 (单击以查看全尺寸图像)
在以下屏幕上,输入用于检索产品信息的 SQL 查询。 让我们使用原始 DAL 中用于 Products
TableAdapter 的完全相同的 SQL 查询,该查询返回所有 Product
列以及产品的供应商和类别名称:
SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
(SELECT CategoryName FROM Categories
WHERE Categories.CategoryID = Products.CategoryID)
as CategoryName,
(SELECT CompanyName FROM Suppliers
WHERE Suppliers.SupplierID = Products.SupplierID)
as SupplierName
FROM Products
图 5:使用原始 DAL 中的 TableAdapter 的相同 SQL 查询 Products
(单击以查看全尺寸图像)
转到下一个屏幕之前,请单击“高级选项”按钮。 若要让此 TableAdapter 采用乐观并发控制,只需选中“使用乐观并发”复选框即可。
图 6:通过选中“使用乐观并发”CheckBox (单击以查看全尺寸图像) 启用乐观并发控制
最后,指示 TableAdapter 应使用填充 DataTable 和返回 DataTable 的数据访问模式;还指示应创建 DB 直接方法。 将返回 DataTable 模式的方法名称从 GetData 更改为 GetProducts,以镜像我们在原始 DAL 中使用的命名约定。
图 7:让 TableAdapter 利用所有数据访问模式 (单击以查看全尺寸图像)
完成向导后,数据集设计器将包含强类型 Products
DataTable 和 TableAdapter。 花点时间将 DataTable 重命名为ProductsOptimisticConcurrency
,可以通过右键单击 DataTable Products
的标题栏并选择上下文菜单中的“重命名”来执行此操作。
图 8:已将 DataTable 和 TableAdapter 添加到类型化数据集 (单击以查看全尺寸图像)
若要查看 TableAdapter ((使用乐观并发) 和DELETE
不) 的产品 TableAdapter ()之间的差异UPDATE
ProductsOptimisticConcurrency
,请单击 TableAdapter 并转到okno Vlastnosti。 在DeleteCommand
UpdateCommand
属性的CommandText
子属性中,可以看到在调用 DAL 更新或删除相关方法时发送到数据库的实际 SQL 语法。 对于 TableAdapter,ProductsOptimisticConcurrency
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
使用乐观并发的 TableAdapter 语句中的 DELETE
子句包括每个 Product
表的现有列值与 GridView (或 DetailsView 或 FormView) 上次填充时的原始值之间的比较。 由于除 ProductName
Discontinued
之外ProductID
的所有字段都可以包含NULL
值,因此包括其他参数和检查,以便正确比较NULL
子句中的WHERE
值。
我们不会将任何其他 DataTable 添加到本教程中启用乐观并发的 DataSet,因为我们的 ASP.NET 页仅提供更新和删除产品信息。 但是,我们仍然需要将 GetProductByProductID(productID)
方法添加到 ProductsOptimisticConcurrency
TableAdapter。
为此,请右键单击 TableAdapter 的标题栏, (Fill
右侧的区域和 GetProducts
方法名称) 并选择上下文菜单中的“添加查询”。 这将启动 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
现在需要十个输入参数:原始ProductID
、、ProductName
、SupplierID
CategoryID
、UnitPrice
QuantityPerUnit
、UnitsOnOrder
UnitsInStock
、、和。 ReorderLevel
Discontinued
它在发送到数据库的语句的子句DELETE
中使用WHERE
这些附加输入参数的值,仅当数据库的当前值映射到原始记录时,才删除指定的记录。
虽然在批处理更新模式中使用的 TableAdapter Update
方法的方法签名尚未更改,但记录原始值和新值所需的代码已更改。 因此,让我们创建一个新的业务逻辑层类,而不是尝试将已启用乐观并发的 DAL 与现有 ProductsBLL
类配合使用。
将名为ProductsOptimisticConcurrencyBLL
文件夹的App_Code
类添加到BLL
文件夹中。
图 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
对象,该对象使用数据库中每个产品记录的实例填充。
使用具有乐观并发的 DB 直接模式删除产品
对使用乐观并发的 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
。
使用具有乐观并发的 Batch 更新模式更新产品
如前所述,无论是否采用乐观并发,TableAdapter Update
的批处理更新模式方法都具有相同的方法签名。 也就是说,该方法 Update
需要 DataRow、DataRows 数组、DataTable 或类型化数据集。 没有用于指定原始值的附加输入参数。 这是可能的,因为 DataTable 会跟踪其 DataRow () 的原始值和修改的值。 当 DAL 发出其 UPDATE
语句时, @original_ColumnName
参数将填充 DataRow 的原始值,而 @ColumnName
参数则用 DataRow 的修改值填充。
ProductsBLL
在使用原始的非乐观并发 DAL) 的类 (,使用批处理更新模式更新代码执行以下事件序列时:
- 使用 TableAdapter
GetProductByProductID(productID)
的方法将当前数据库产品信息读取到ProductRow
实例中 - 将新值分配给步骤 1 中的
ProductRow
实例 - 调用 TableAdapter
Update
的方法,传入ProductRow
实例
但是,由于步骤 1 中填充的内容直接从数据库填充,因此 ProductRow
无法正确支持乐观并发,这意味着 DataRow 使用的原始值是数据库中当前存在的值,而不是在编辑过程开始时绑定到 GridView 的原始值。 相反,在使用启用了乐观并发的 DAL 时,我们需要更改 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 页传递到 BLL 方法
完成 DAL 和 BLL 后,剩下的一切都是创建一个 ASP.NET 页,该页可以利用内置于系统的乐观并发逻辑。 具体而言,数据 Web 控件 (GridView、DetailsView 或 FormView) 必须记住其原始值,ObjectDataSource 必须将这两组值传递给业务逻辑层。 此外,必须将 ASP.NET 页配置为正常处理并发冲突。
首先 OptimisticConcurrency.aspx
打开 EditInsertDelete
文件夹中的页面,并将 GridView 添加到设计器,并将其 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
集合包含类DeleteProduct
方法中ProductsOptimisticConcurrencyBLL
十个Parameter
输入参数中的每个实例。 同样,集合 UpdateParameters
包含 Parameter
每个输入参数的 UpdateProduct
实例。
对于涉及数据修改的前面的教程,我们此时会删除 ObjectDataSource OldValuesParameterFormatString
的属性,因为此属性指示 BLL 方法需要传入旧 (或原始) 值以及新值。 此外,此属性值指示原始值的输入参数名称。 由于我们将原始值传入 BLL, 因此不要 删除此属性。
备注
属性的值 OldValuesParameterFormatString
必须映射到需要原始值的 BLL 中的输入参数名称。 由于我们命名这些参数original_productName
original_supplierID
,等等,因此可以将属性值保留OldValuesParameterFormatString
为 original_{0}
a0/>。 但是,如果 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
AndSupplierID
BoundFields 转换为 TemplateFields,并调整了编辑界面以使用 DropDownLists 而不是 TextBoxes。 在这些 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 (,然后传递给 BLL) ,GridView EnableViewState
的属性必须设置为 true
(默认) 。 如果禁用视图状态,则原始值在回发时丢失。
将正确的原始值传递给 ObjectDataSource
配置 GridView 的方式存在几个问题。 如果 ObjectDataSource ConflictDetection
的属性设置为CompareAllValues
() ,则当 GridView (或 DetailsView 或 FormView) 调用 ObjectDataSource 或Delete()
方法时,ObjectDataSource Update()
会尝试将 GridView 的原始值复制到相应的Parameter
实例中。 有关此过程的图形表示形式,请参阅图 2。
具体而言,每次将数据绑定到 GridView 时,都会在双向数据绑定语句中分配 GridView 的原始值。 因此,必须通过双向数据绑定捕获所需的原始值,并且它们以可转换格式提供。
若要查看为什么这一点很重要,请花点时间在浏览器中访问我们的页面。 如预期的那样,GridView 列出每个产品,其中最左侧列中带有“编辑和删除”按钮。
图 14:产品列在 GridView (单击以查看全尺寸图像)
如果单击任何产品的“删除”按钮,则会引发 a FormatException
。
图 15:尝试在 (单击后删除任何产品结果FormatException
以查看全尺寸图像)
FormatException
当 ObjectDataSource 尝试读取原始UnitPrice
值时,将引发此情况。 ItemTemplate
由于格式化为UnitPrice
货币 () <%# Bind("UnitPrice", "{0:C}") %>
,因此它包括货币符号,如 $19.95。 当 FormatException
ObjectDataSource 尝试将此字符串转换为 .decimal
为了规避此问题,我们有许多选项:
- 从 .
ItemTemplate
中删除货币格式。 也就是说,而不是使用<%# Bind("UnitPrice", "{0:C}") %>
,只需使用<%# Bind("UnitPrice") %>
。 其缺点是价格不再设置格式。 - 在
UnitPrice
格式上ItemTemplate
显示为货币,但使用Eval
关键字来实现此目的。 回想一下,Eval
执行单向数据绑定。 我们仍然需要为原始值提供UnitPrice
值,因此我们仍然需要其中的ItemTemplate
双向数据绑定语句,但这可以放置在属性设置为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>
- Remove the currency formatting from the
ItemTemplate
, using<%# Bind("UnitPrice") %>
. 在 GridView 的事件处理程序中RowDataBound
,以编程方式访问显示值并将其UnitPrice
属性设置为Text
格式化版本的标签 Web 控件。 - 将
UnitPrice
格式保留为货币。 在 GridView 的事件处理程序中RowDeleting
,使用实际十进制Decimal.Parse
值替换现有原始UnitPrice
值 ($19.95) 。 我们了解了如何在 ASP.NET Page 教程中处理 BLL 和DAL-Level异常的事件处理程序中RowUpdating
完成类似操作。
对于我的示例,我选择采用第二种方法,添加隐藏的标签 Web 控件,其 Text
属性是绑定到未格式化 UnitPrice
值的双向数据。
解决此问题后,再次尝试单击任何产品的“删除”按钮。 这一次,你将获得 InvalidOperationException
ObjectDataSource 尝试调用 BLL UpdateProduct
方法的时间。
图 16:ObjectDataSource 找不到具有要发送的输入参数的方法 (单击以查看全尺寸图像)
查看异常的消息,很明显,ObjectDataSource 想要调用包含original_CategoryName
和original_SupplierName
输入参数的 BLL DeleteProduct
方法。 这是因为ItemTemplate
,s for the CategoryID
and SupplierID
TemplateFields 当前包含具有和SupplierName
数据字段的CategoryName
双向 Bind 语句。 相反,我们需要包含Bind
包含和数据字段的CategoryID
SupplierID
语句。 为此,请将现有 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 返回到其预编辑状态,并将“柴茶”作为新产品名称。
但是,在其他浏览器窗口实例中,产品名称 TextBox 仍显示“Chai”。 在此第二个浏览器窗口中,将更新 UnitPrice
为 25.00
。 如果没有乐观并发支持,在第二个浏览器实例中单击更新会将产品名称改回“Chai”,从而覆盖第一个浏览器实例所做的更改。 但是,在采用乐观并发的情况下,单击第二个浏览器实例中的“更新”按钮会导致 DBConcurrencyException。
图 17:检测到并发冲突时,将引发 (DBConcurrencyException
单击以查看全尺寸图像)
DBConcurrencyException
仅当使用 DAL 的批处理更新模式时才会引发该模式。 DB 直接模式不会引发异常,它只是表示没有受影响的行。 为了说明这一点,请同时将两个浏览器实例的 GridView 返回到其预编辑状态。 接下来,在第一个浏览器实例中,单击“编辑”按钮,将产品名称从“柴茶”更改为“Chai”,然后单击“更新”。 在第二个浏览器窗口中,单击 Chai 的“删除”按钮。
单击“删除”后,GridView 将调用 ObjectDataSource 的方法,而 ObjectDataSource Delete()
会向下 ProductsOptimisticConcurrencyBLL
调用类 DeleteProduct
的方法,并传递原始值。 第二个浏览器实例的原始 ProductName
值为“Chai Tea”,它与数据库中的当前 ProductName
值不匹配。 DELETE
因此,向数据库发出的语句会影响零行,因为该子句满足的数据库WHERE
中没有记录。 该方法 DeleteProduct
返回 false
,ObjectDataSource 的数据将反弹到 GridView。
从最终用户的角度来看,在第二个浏览器窗口中单击“删除”按钮会导致屏幕闪烁,回来后,产品仍然存在,尽管现在它被列为“Chai” (第一个浏览器实例) 的产品名称更改。 如果用户再次单击“删除”按钮,“删除”将成功,因为 GridView 的原始 ProductName
值 (“Chai”) 现在与数据库中的值匹配。
在这两种情况下,用户体验远非理想。 我们显然不想在使用批处理更新模式时向用户显示异常的 DBConcurrencyException
nitty-gritty 详细信息。 使用 DB 直接模式时的行为有点令人困惑,因为用户命令失败,但没有确切说明原因。
为了修复这两个问题,我们可以在页面上创建标签 Web 控件,该控件提供有关更新或删除失败的原因的说明。 对于批处理更新模式,我们可以确定 GridView 的后级别事件处理程序中是否 DBConcurrencyException
发生异常,并根据需要显示警告标签。 对于 DB 直接方法,我们可以检查 BLL 方法 (的返回值,即 true
如果一行受到影响, false
否则) 并根据需要显示信息性消息。
步骤 6:添加信息性消息,并在出现并发冲突时显示它们
发生并发冲突时,显示的行为取决于 DAL 的批处理更新还是使用 DB 直接模式。 本教程使用这两种模式,以及用于更新的批处理更新模式和用于删除的 DB 直接模式。 若要开始,让我们将两个标签 Web 控件添加到页面,说明尝试删除或更新数据时发生并发冲突。 将“标签”控件Visible
和属性设置为 false
;这将导致它们在每一页访问中隐藏,但那些以编程方式设置为true
其Visible
属性的特定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
属性外,我还将属性设置为CssClass
该属性Warning
,这会导致标签以大、红色、斜体、粗体字体显示。 在检查与插入、更新和删除教程关联的事件时,定义了此 CSS Warning
类并将其添加到 Styles.css。
添加这些标签后,Visual Studio 中的设计器应类似于图 18。
图 18:已将两个标签控件添加到页面 (单击以查看全尺寸图像)
在这些标签 Web 控件准备就绪后,我们已准备好检查如何确定何时发生并发冲突,此时可以将相应的标签 Visible
的属性设置为 true
显示信息性消息。
处理更新时的并发冲突
让我们首先了解如何在使用批处理更新模式时处理并发冲突。 由于此类与批处理更新模式冲突会导致 DBConcurrencyException
引发异常,因此我们需要将代码添加到 ASP.NET 页,以确定更新过程中是否 DBConcurrencyException
发生异常。 如果是这样,我们应该向用户显示一条消息,说明更改未保存,因为其他用户在开始编辑记录时和单击“更新”按钮时修改了相同的数据。
正如我们在 ASP.NET Page 教程的处理 BLL 和DAL-Level异常 中看到的那样,可以在数据 Web 控件的后期事件处理程序中检测和禁止此类异常。 因此,我们需要为 GridView RowUpdated
的事件创建事件处理程序,用于检查是否已 DBConcurrencyException
引发异常。 此事件处理程序将传递对更新过程中引发的任何异常的引用,如以下事件处理程序代码所示:
protected 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
“标签”控件将向用户说明刚刚发生的情况。 图 19 中详细介绍了此事件序列。
图 19:用户更新因并发冲突而丢失 (单击以查看全尺寸图像)
备注
或者,我们可以通过将传入GridViewUpdatedEventArgs
对象的属性设置为 KeepInEditMode
true,而不是将 GridView 返回到预编辑状态,而不是将 GridView 保留为其编辑状态。 但是,如果采用此方法,请务必通过调用 DataBind()
其方法) 将数据重新绑定到 GridView (,以便将其他用户的值加载到编辑界面中。 本教程提供的可供下载的代码在事件处理程序中 RowUpdated
注释掉了这两行代码;只需取消注释这些代码行,使 GridView 在并发冲突后仍处于编辑模式。
在删除时响应并发冲突
使用 DB 直接模式时,如果出现并发冲突,则不会引发异常。 相反,数据库语句只影响任何记录,因为 WHERE 子句与任何记录不匹配。 BLL 中创建的所有数据修改方法都经过设计,以便返回一个布尔值,该值指示它们是否仅影响一条记录。 因此,若要确定删除记录时是否发生了并发冲突,我们可以检查 BLL 方法的 DeleteProduct
返回值。
可以通过传递给事件处理程序的对象的属性,在 ObjectDataSource 的后级事件处理程序 ReturnValue
中检查 BLL 方法的 ObjectDataSourceStatusEventArgs
返回值。 由于我们有兴趣确定方法中的Deleted
返回值DeleteProduct
,因此我们需要为 ObjectDataSource 的事件创建事件处理程序。 该 ReturnValue
属性的类型 object
,可以是 null
引发异常并且方法在返回值之前中断。 因此,我们首先应确保 ReturnValue
属性不是 null
布尔值,并且是布尔值。 假设此检查通过,我们将显示 DeleteConflictMessage
Label 控件(如果为 ReturnValue
false
)。 这可以通过使用以下代码来实现:
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 页中添加支持。 此添加的工作是否是你的时间和精力的明智投资,取决于你的应用程序。 如果你不经常有并发用户更新数据,或者他们正在更新的数据彼此不同,则并发控制不是关键问题。 但是,如果网站上经常有多个用户处理相同的数据,并发控制可以帮助防止一个用户的更新或删除无意中覆盖另一个用户的更新。
快乐编程!
关于作者
斯科特·米切尔,七本 ASP/ASP.NET 书籍和 4GuysFromRolla.com 创始人,自1998年以来一直在与 Microsoft Web 技术合作。 斯科特是一名独立顾问、教练员和作家。 他的最新书是 山姆斯教自己在24小时内 ASP.NET 2.0。 他可以通过他的博客访问mitchell@4GuysFromRolla.com,也可以通过他的博客找到http://ScottOnWriting.NET。
南来地,北往的,上班的,下岗的,走过路过不要错过!
======================个性签名=====================
之前认为Apple 的iOS 设计的要比 Android 稳定,我错了吗?
下载的许多客户端程序/游戏程序,经常会Crash,是程序写的不好(内存泄漏?刚启动也会吗?)还是iOS本身的不稳定!!!
如果在Android手机中可以简单联接到ddms,就可以查看系统log,很容易看到程序为什么出错,在iPhone中如何得知呢?试试Organizer吧,分析一下Device logs,也许有用.