在 ASP.NET 页面中处理 BLL 与 DAL 级别的异常
本文档是 Visual Basic 教程 (切换到 Visual C# 教程)
当对 ASP.NET Web 数据控件进行插入、更新、删除的操作前、操作过程中以及操作后,会发生一些事件。在本教程中,我们将详细介绍这些事件的使用。我们还将了解如何定制编辑界面来只更新产品字段的子集。
简介在一个使用了分层应用架构的 ASP.NET web 应用程序中处理数据 , 一般 遵循 以下三个步骤 :
我们在前一篇教程 中看到 ,无论 ObjectDataSource 控件 还是Web 数据控件 , 都为步骤 1 和步骤 3 提供了扩展点。例如 ,GridView 在将其字段值赋值给其 ObjectDataSource 的 UpdateParameters 集合之前 , 会触发其RowUpdating 事件 ; 在 ObjectDataSource 完成操作之后 , 会触发 GridView 的 RowUpdated 事件。 我们已经探讨了步骤 1 中触发的事件 , 看到了怎样利用这些事件来定制输入参数或取消操作。在本教程中,我们要将注意力转到操作完成之后触发的事件。通过这些 post 级的 Event Handler,我们可以判断操作过程中是否发生异常,并适当地处理异常,在屏幕上显示友好的、信息丰富的错误消息,而不是转到默认的标准 ASP.NET 异常页面。 为了举例说明怎样使用这些 post 级事件 , 我们来创建一个页面 ,该页面 在一个可编辑的 GridView 中列出产品信息。当更新产品时,如果发生了异常,我们的 ASP.NET 页面将在 GridView 上方显示一个简短的消息,表明出现了问题。让我们开始吧! 步骤1 : 为产品创建一个可编辑的 GridView在之前的教程中 , 我们创建了一个可编辑的 GridView , 它只有两个字段 ,ProductName 与 UnitPrice 。这需要为 ProductsBLL 类的UpdateProduct 方法创建另外一个重载 , 这个重载只接受三个输入参数 ( 产品名称、单价、ID ), 而不是每个产品字段都有一个参数。在本教程中,我们再次练习使用这一技巧,我们创建一个可编辑的 GridView ,它显示产品的名称、每单位数量、单价、库存单位数,但是只有名称、单价、库存单位数才能被编辑。 为了满足这个场景的需要,我们需要 UpdateProduct 方法的另一个重载,这个重载接受四个参数:产品名称、单价、库存单位数、ID 。将下面的方法添加到 ProductsBLL 类中 :
完成这个方法之后 , 我们就可以着手创建允许编辑这四个特定产品字段的ASP.NET 页面了。打开 EditInsertDelete 文件夹中的 ErrorHandling.aspx 页面 , 通过 设计器 将一个 GridView 添加到该页面中。将这个 GridView 绑定到一个新的 ObjectDataSource , 将Select() 方法映射为 ProductsBLL 类的 GetProducts() 方法 , 将 Update() 方法映射为刚刚创建的 UpdateProduct 重载方法。 图1:使用接受四个输入参数的 UpdateProduct 重载方法 这将创建一个ObjectDataSource , 其 UpdateParameters 集合有四个参数 , 还将创建一个 GridView , 它对应每个产品字段都有一个字段。ObjectDataSource 的声明标记会将OldValuesParameterFormatString 属性赋值为 original_{0} , 这将导致异常 , 因为我们的BLL 类并不期待传入一个名为 original_productID 的输入参数。不要忘记从声明语句中完全删除此设置(或将它设为默认值, {0} )。 接下来 , 减少GridView 的绑定字段, 使其仅包含 ProductName 、QuantityPerUnit 、UnitPrice 和 UnitsInStock 这几个 BoundField 。另外 , 可以随意设置一些您认为必要的字段级格式 ( 例如更改HeaderText 属性 ) 。 在之前的教程中 , 我们已看到怎样在只读模式和编辑模式下将UnitPrice BoundField 转化为货币格式。在这里我们同样这样做。我们记得 , 这需要将BoundField 的 DataFormatString 属性设为 {0:c} ,将 它的 HtmlEncode 属性设为 false , 它的ApplyFormatInEditMode 设为true , 如图 2 所示。 图2:将 UnitPrice BoundField 配置为显示一个货币金额 要在编辑界面中将UnitPrice 变为货币格式 , 需要为 GridView 的 RowUpdating 事件创建一个Event Handler , 这个 Event Handler 能将货币格式的字符串解析为 十进制 数值。记得在前一个教程中, RowUpdating Event Handler 还进行了额外的检查,以确保用户提供了 UnitPrice 值。然而,对于本教程,我们允许用户不输入该价格。
我们的GridView 包含一个 QuantityPerUnit BoundField , 但是这个BoundField 应该只作显示之用 ,不能被 用户编辑。为此 , 将该 BoundField 的 ReadOnly 属性设为 true 就可以了。 图3: 将QuantityPerUnit BoundField 设为只读 最后 , 选中GridView 智能标记中的 Enable Editing 复选框。完成这些步骤之后 ,ErrorHandling.aspx 页面的 设计器 应如图4 所示。 图4: 删除所有不需要的BoundField , 并选中Enable Editing 复选框 此时 , 我们可看到所有产品的ProductName 、QuantityPerUnit 、UnitPrice 、UnitsInStock 字段列表 ; 然而 , 只有 ProductName 、UnitPrice 和 UnitsInStock 字段可以编辑。 图5: 用户现在可以很容易地编辑产品的名称、价格、库存单位数字段 步骤 2 : 适当地处理 DAL 层异常当用户为所编辑产品的名称、价格、库存单位数输入合法值时 , 我们的可编辑 GridView 工作良好 , 但是如果输入非法值 , 就会导致异常。例如 , 如果不输入ProductName 值 , 就会导致抛出 NoNullAllowedException , 原因是ProdcutsRow 类中 ProductName 属性的AllowDBNull 属性设为了 false ; 如果数据库宕机时试图连接数据库 , 那么 TableAdapter 会抛出SqlException 。当不采取任何措施时 , 这些异常会从数据访问层冒出到业务逻辑层 , 然后到达 ASP.NET 页面 , 最后到达 ASP.NET 运行时。 根据您 web 应用程序的配置 , 以及您是否是从localhost 访问应用程序 , 未 经 处理的异常可能导致出现一个通用的服务器错误页面 ,一个 详细的错误报告 , 或者一个用户友好的网页。参见 ASP.NET 中 Web 应用程序的错误处理 和 customErrors 元素 ,可以得到 ASP.NET 运行时怎样响应一个未捕获的异常的详细信息。 图 6 是不指定 ProductName 值就试图更新产品时出现的屏幕。这是通过 localhost 访问时显示的默认详细错误报告。 图6: 不输入产品的名称将显示异常细节 虽然这样的异常细节在测试应用程序时很有用 , 但是如果在最终用户使用时发生异常 , 将这样一个屏幕显示给用户就不太理想。最终用户很可能不懂得什么是 NoNullAllowedException 以及它为什么会产生。更好的方法是呈现给用户一个更加友好的消息,说明试图更新产品时出现了问题。 如果在执行这项操作时发生了异常 ,ObjectDataSource 和 Web 数据控件的 post 级事件都提供了发现异常并不让它冒出到ASP.NET 运行时的方法。在我们的例子中,我们为 GridView 的 RowUpdated 事件创建一个 Event Handler,它判断是否触发了一个异常,如果是,则在一个 Web 标签控件中显示异常细节。 首先 , 将一个 Label 控件 添加到ASP.NET 页面 , 将它的 ID 属性设为ExceptionDetails 并清空它的 Text 属性。为了能让这个消息吸引用户的视线 , 将它的 CssClass 属性设为 Warning , 这是一个CSS 类 , 我们在之前的教程中已将其添加到Styles.css 文件。记得这个 CSS 类使 Label 的文本显示为红色、斜体、加粗的超大字体。 图7: 将一个Web 标签 控件添加到页面 因为我们希望此Web 标签 控件 仅 在异常发生之后才立即显示 , 所以在 Page_Load Event Handler 中将它的Visible 属性设为 false :
有了这些代码 , 当首次访问页面 , 以及之后的回传时 ,ExceptionDetails 控件的Visible 属性将被设为 false 。当发生 DAL 层 或BLL 层的异常时 , 我们可以在 GridView 的 RowUpdated Event Handler 中检测到 , 这时我们会将ExceptionDetails 控件的 Visible 属性设为 true 。因为在页面的生命周期中 ,Web 控件的 Event Handler 出现在Page_Load Event Handler 之后 , 所以 Label 将 会显示出来。然而 , 在下次回传时 ,Page_Load 的 Event Handler 会将 Visible 属性变回 false , 使Label 再度隐藏。 注意 : 还有另一种方法 , 我们不必在Page_Load 中设置 ExceptionDetails 控件的Visible 属性 , 取而代之的是在声明语句中将其 Visible 属性赋值为 false , 并禁用其视图状态 ( 将它的 EnableViewState 属性设置为 false ) 。我们会在将来的教程中使用这个替代方法。 添加 Label 控件之后 , 下一步是为 GridView 的 RowUpdated 事件创建Event Handler 。在 设计器 中选择 GridView , 转到Properties 窗口 , 单击闪电状图标 , 列出 GridView 的所有事件。在 GridView 的 RowUpdating 事件处我们可以看到已经存在一个条目,因为我们在本教程的前面已经为这个事件创建了一个 Event Handler。同样为 RowUpdated 事件创建一个 Event Handler 图8: 为GridView 的 RowUpdated 事件创建一个Event Handler 注意 : 也可以通过 code-behind 类文件顶部的下拉列表来创建该 Event Handler 。从左边的下拉列表中选择 GridView ,并 从右边的下拉列表中选择 RowUpdated 事件 。 创建这个Event Handler 会在 ASP.NET 页面的 code-behind 类中添加如下代码 :
这个 Event Handler 的第二个输入参数是一个类型为 GridViewUpdatedEventArgs 的对象 , 它有三个属性对异常处理有用 :
那么 , 我们的代码应该检测 Exception 是否为 null , 如果不是 null ,则 意味着在执行此操作时发生了异常。在这种情况下,我们想要:
下面的代码实现了上述目标:
这个 Event Handler 首先检查 e.Exception 是否为 null 。如果不是 , 将ExceptionDetails Label 的 Visible 属性设为 true , 并将它的 Text 属性设为 “There was a problem updating the product. ” 实际抛出的异常细节则保存在e.Exception 对象的 InnerException 属性中。检查这个内部异常 , 如果它是某种特定的类型 , 则将一条额外的有用的消息附加到 ExceptionDetails Label 的 Text 属性。最后 , 将ExceptionHandled 与 KeepInEditMode 属性都设置为 true 。 图 9 显示没有输入产品名称时此页面的截屏 ; 图 10 则显示 输入非法UnitPrice 值 (-50 ) 时的结果。 图9: ProductName BoundField 必须包含一值 图10: 不允许负的UnitPrice 值 通过将e.ExceptionHandled 属性设为 true ,RowUpdated Event Handler 指示该异常已得到处理。因此 , 这个异常不会上传给 ASP.NET 运行时。 注意 : 图 9 和 10 显示了当用户输入无效时 ,得体地 处理异常的方法。但是在理想情况下 , 这种无效输入首先就不应该到达业务逻辑层 , 因为ASP.NET 页面应该首先确保用户的输入是有效的 , 然后再调用 ProductsBLL 类的UpdateProduct 方法。在下一篇教程中,我们将看到怎样为编辑与插入界面添加验证控件,以确保提交给业务逻辑层的数据符合业务规则。验证控件不仅能在用户提供无效数据时阻止调用 UpdateProduct 方法,而且能提供信息更加丰富的用户体验,以便于查明数据输入的问题。 步骤3 :适当地处理 BLL层异常当插入、更新、删除数据时,如果发生与数据有关的错误,数据访问层就可能抛出异常。数据库可能脱机,可能没有为一个必需的数据库表列指定一值,或者违反了某个表级约束。除了确实与数据相关的异常外,业务逻辑层也可以使用异常来指示违反业务规则的情况。例如,在创建业务逻辑层 教程中,我们在原来的 UpdateProduct 重载中添加了一个业务规则检查。具体地说,如果用户将一个产品标为断货,我们要求这个产品不是其供应商唯一供应的产品。如果违反了这一条件 , 就会抛出ApplicationException 。 对于本教程中创建的 UpdateProduct 重载 , 我们来加入一条业务规则 , 这条规则禁止将UnitPrice 字段设为高于原来 UnitPrice 值的两倍。为此 , 我们要修改UpdateProduct 重载 , 令它进行这一检查 , 如果违反了这个规则 , 就抛出 ApplicationException 。下面是更新后的方法 :
修改完后 , 任何新价格如果高于已有价格的两倍 , 就会导致抛出ApplicationException 。与 DAL 中引发的异常一样 , 这个 BLL 引发的 ApplicationException 可以在 GridView 的 RowUpdated Event Handler 中发现并处理。实际上 , 所编写的 RowUpdated Event Handler 代码能够正确发现这一异常 , 并显示ApplicationException 的Message 属性值。图 11 是一个屏幕截图,显示的是当 Chai 的当前价格为 $19.95 , 而用户试图将它的价格更新为$50.00 时的情形 。 图11: 业务规则不允许产品价格超过原来价格的两倍 注意 : 理想地 , 我们的业务逻辑规则应该 从UpdateProduct 重载方法中分离出来 ,放入 一个公共的方法中。这留作读者练习。 小结在插入、更新、删除操作过程中 ,Web 数据控件和 ObjectDataSource 控件 都包含了 pre 级与 post 级的事件 , 这些事件穿插在实际的操作中。正如我们在本教程和前面教程中看到的 , 当对可编辑的GridView 进行操作时 , 会触发 GridView 的 RowUpdating 事件 , 然后是ObjectDataSource 的 Updating 事件 , 此时更新命令发送给 ObjectDataSource 的底层对象。该操作完成之后 , 会触发 ObjectDataSource 的 Updated 事件 , 之后是 GridView 的 RowUpdated 事件。 我们可以为 pre 级事件创建 Event Handler , 以便定制输入参数 ;还可以 为 post 级事件创建Event Handler , 以便检查操作结果 , 并作出相应地响应。post 级 Event Handler 最常见用法是检查操作过程中是否有异常发生。当有异常时,这些 post 级 Event Handler 能有选择地独自处理异常。本教程中,我们了解了怎样通过显示友好的错误消息来处理这种异常。 在下一教程中 , 我们将了解怎样降低因为数据格式问题 ( 例如输入负的UnitPrice ) 而产生异常的可能性。具体而言,我们将看看怎样为编辑与插入界面添加验证控件。 快乐编程 !
|