在 ASP.NET 页面中处理 BLL 与 DAL 级别的异常

本文档是 Visual Basic 教程 (切换到 Visual C# 教程)

当对 ASP.NET Web 数据控件进行插入、更新、删除的操作前、操作过程中以及操作后,会发生一些事件。在本教程中,我们将详细介绍这些事件的使用。我们还将了解如何定制编辑界面来只更新产品字段的子集。

« 前一篇教程  |  下一篇教程 »

简介

在一个使用了分层应用架构的 ASP.NET web 应用程序中处理数据 , 一般 遵循 以下三个步骤 :

  1. 确定需要调用业务逻辑层 中 的哪个方法 , 需要将哪些参数值传入该层。这些参数值可以通过硬编码设置、通过编程赋值、或者由用户输入。
  2. 调用该方法。
  3. 处理结果。当调用的 BLL 方法有返回数据时 , 该处理可能包括将数据绑定到一个 Web 数据控件上。对于修改数据的 BLL 方法,该处理可能包括基于返回值执行某个操作,或者适当地处理步骤 2 中出现的异常。

我们在前一篇教程 中看到 ,无论 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 类中 :

<System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct _
    (ByVal productName As String, ByVal unitPrice As Nullable(Of Decimal), _
ByVal unitsInStock As Nullable(Of Short), ByVal productID As Integer) As Boolean
    Dim products As Northwind.ProductsDataTable = _
        Adapter.GetProductByProductID(productID)
    If products.Count = 0 Then
        Return False
    End If
    Dim product As Northwind.ProductsRow = products(0)
    product.ProductName = productName
    If Not unitPrice.HasValue Then
        product.SetUnitPriceNull()
    Else
        product.UnitPrice = unitPrice.Value
    End If
    If Not unitsInStock.HasValue Then
        product.SetUnitsInStockNull()
    Else
        product.UnitsInStock = unitsInStock.Value
    End If
    Dim rowsAffected As Integer = Adapter.Update(product)
    Return rowsAffected = 1
End Function

完成这个方法之后 , 我们就可以着手创建允许编辑这四个特定产品字段的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 值。然而,对于本教程,我们允许用户不输入该价格。

Protected Sub GridView1_RowUpdating(ByVal sender As Object, _
    ByVal e As System.Web.UI.WebControls.GridViewUpdateEventArgs) _
    Handles GridView1.RowUpdating
    If e.NewValues("UnitPrice") IsNot Nothing Then
        e.NewValues("UnitPrice") = _
            Decimal.Parse(e.NewValues("UnitPrice").ToString(), _
            System.Globalization.NumberStyles.Currency)
    End If

我们的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 :

Protected Sub Page_Load(sender As Object, e As System.EventArgs) Handles Me.Load
    ExceptionDetails.Visible = False
End Sub

有了这些代码 , 当首次访问页面 , 以及之后的回传时 ,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 类中添加如下代码 :

Protected Sub GridView1_RowUpdated(ByVal sender As Object, _
    ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) _
    Handles GridView1.RowUpdated
End Sub

这个 Event Handler 的第二个输入参数是一个类型为 GridViewUpdatedEventArgs 的对象 , 它有三个属性对异常处理有用 :

  • Exception — 这是对已抛出的异常的一个引用 ; 如果没有抛出异常 , 则该属性的值为 null
  • ExceptionHandled — 这是一个 Boolean 类 型的值 , 它 指示 该异常是否已在 RowUpdated Event Handler 中得到处理 ; 如果值为 false ( 默认值 ), 该异常将被重新抛出 ,漏出 到 ASP.NET 运行时
  • KeepInEditMode — 如果设置为 true , 则 GridView 的当前编辑 行将保持在编辑模式 ; 如果为 false ( 默认 ), 则 GridView 行将恢复到只读模式

那么 , 我们的代码应该检测 Exception 是否为 null , 如果不是 null ,则 意味着在执行此操作时发生了异常。在这种情况下,我们想要:

  • 在 ExceptionDetails Label 中显示一条用户友好的消息
  • 指示该异常已得到处理
  • 将 GridView 当前行保持在编辑模式

下面的代码实现了上述目标:

Protected Sub GridView1_RowUpdated(ByVal sender As Object, _
    ByVal e As System.Web.UI.WebControls.GridViewUpdatedEventArgs) _
    Handles GridView1.RowUpdated
    If e.Exception IsNot Nothing Then
        ExceptionDetails.Visible = True
        ExceptionDetails.Text = "There was a problem updating the product. "
        If e.Exception.InnerException IsNot Nothing Then
            Dim inner As Exception = e.Exception.InnerException
            If TypeOf inner Is System.Data.Common.DbException Then
                ExceptionDetails.Text &= _
                "Our database is currently experiencing problems." & _
                "Please try again later."
            ElseIf TypeOf inner _
             Is System.Data.NoNullAllowedException Then
                ExceptionDetails.Text += _
                    "There are one or more required fields that are missing."
            ElseIf TypeOf inner Is ArgumentException Then
                Dim paramName As String = CType(inner, ArgumentException).ParamName
                ExceptionDetails.Text &= _
                    String.Concat("The ", paramName, " value is illegal.")
            ElseIf TypeOf inner Is ApplicationException Then
                ExceptionDetails.Text += inner.Message
            End If
        End If
        e.ExceptionHandled = True
        e.KeepInEditMode = True
    End If
End Sub

这个 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 。下面是更新后的方法 :

<System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Update, True)> _
    Public Function UpdateProduct(ByVal productName As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal productID As Integer) As Boolean
    Dim products As Northwind.ProductsDataTable = Adapter.GetProductByProductID(productID)
    If products.Count = 0 Then
        Return False
    End If
    Dim product As Northwind.ProductsRow = products(0)
    If unitPrice.HasValue AndAlso Not product.IsUnitPriceNull() Then
        If unitPrice > product.UnitPrice * 2 Then
            Throw New ApplicationException( _
                "When updating a product price," & _
                " the new price cannot exceed twice the original price.")
        End If
    End If
    product.ProductName = productName
    If Not unitPrice.HasValue Then
        product.SetUnitPriceNull()
    Else
        product.UnitPrice = unitPrice.Value
    End If
    If Not unitsInStock.HasValue Then
        product.SetUnitsInStockNull()
    Else
        product.UnitsInStock = unitsInStock.Value
    End If
    Dim rowsAffected As Integer = Adapter.Update(product)
    Return rowsAffected = 1
End Function

修改完后 , 任何新价格如果高于已有价格的两倍 , 就会导致抛出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 ) 而产生异常的可能性。具体而言,我们将看看怎样为编辑与插入界面添加验证控件。

快乐编程 !





下一篇教程