在 ASP.NET 页中处理 BLL 和 DAL 级别的异常 (VB)

作者 :Scott Mitchell

下载 PDF

在本教程中,我们将了解如何在 ASP.NET 数据 Web 控件的插入、更新或删除操作期间发生异常时显示友好的信息性错误消息。

简介

使用分层应用程序体系结构处理来自 ASP.NET Web 应用程序的数据涉及以下三个常规步骤:

  1. 确定需要调用业务逻辑层的方法以及要传递的参数值。 参数值可以硬编码、以编程方式分配或用户输入的输入。
  2. 调用方法。
  3. 处理结果。 调用返回数据的 BLL 方法时,这可能涉及将数据绑定到数据 Web 控件。 对于修改数据的 BLL 方法,这可能包括基于返回值执行某些操作,或正常处理步骤 2 中出现的任何异常。

正如我们在 上一教程中看到的,ObjectDataSource 和数据 Web 控件都为步骤 1 和 3 提供了扩展点。 例如,GridView 在将字段值分配给其 RowUpdating ObjectDataSource 的 UpdateParameters 集合之前触发其事件;其 RowUpdated 事件在 ObjectDataSource 完成操作后引发。

我们已经检查了在步骤 1 中触发的事件,并了解了如何使用它们来自定义输入参数或取消操作。 在本教程中,我们将把注意力转向操作完成后触发的事件。 借助这些后级别事件处理程序,我们可以确定操作期间是否发生了异常,并正常处理它,在屏幕上显示友好的信息性错误消息,而不是默认为标准 ASP.NET 异常页面。

为了说明如何使用这些后级别事件,让我们创建一个页面,其中列出了可编辑的 GridView 中的产品。 更新产品时,如果引发异常,ASP.NET 页面将在 GridView 上方显示一条短消息,说明发生了问题。 现在就开始吧!

步骤 1:创建可编辑的产品网格视图

在上一教程中,我们创建了一个只包含两个字段的可编辑 GridView: ProductNameUnitPrice。 这需要为 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 页面,以便编辑这四个特定产品字段。 ErrorHandling.aspx打开 文件夹中的页面EditInsertDelete,并通过Designer向页面添加 GridView。 将 GridView 绑定到新的 ObjectDataSource,将 Select() 方法映射到 ProductsBLL 类的 GetProducts() 方法,将 Update() 方法映射到 UpdateProduct 刚刚创建的重载。

使用接受四个输入参数的 UpdateProduct 方法重载

图 1:使用 UpdateProduct 接受四个输入参数的方法重载 (单击以查看全尺寸图像)

这将创建一个 ObjectDataSource,其中包含一个包含四个 UpdateParameters 参数的集合和一个 GridView,其中包含每个产品字段的字段。 ObjectDataSource 的声明性标记为 属性分配 OldValuesParameterFormatStringoriginal_{0},这将导致异常,因为我们的 BLL 类不希望传入名为 的 original_productID 输入参数。 不要忘记从声明性语法 (完全删除此设置,或将其设置为默认值, {0}) 。

接下来,向下分析 GridView 以仅 ProductName包括 、 QuantityPerUnitUnitPriceUnitsInStock BoundField。 还可以随意应用你认为必要的任何字段级格式 (,例如更改 HeaderText 属性) 。

在上一教程中,我们了解了如何在只读模式和编辑模式下将 BoundField 格式化 UnitPrice 为货币。 让我们在此处执行相同的操作。 回想一下,这需要将 BoundField 的 属性设置为 {0:c},其 HtmlEncode 属性设置为 false,将 设置为 trueApplyFormatInEditMode ,如图 2 DataFormatString 所示。

将 UnitPrice BoundField 配置为显示为货币

图 2:将 UnitPrice BoundField 配置为显示为货币 (单击以查看全尺寸图像)

在编辑界面中将 UnitPrice 设置为货币需要为 GridView 的 RowUpdating 事件创建事件处理程序,以便将货币格式的字符串解析为值 decimal 。 回想一下, RowUpdating 上一教程中的事件处理程序也进行了检查,以确保用户提供了 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 应仅用于显示目的,用户不应编辑。 若要对此进行排列,只需将 BoundFields 的 ReadOnly 属性设置为 true

将 QuantityPerUnit BoundField 设为只读

图 3:使 QuantityPerUnit BoundField Read-Only (单击以查看全尺寸图像)

最后,检查 GridView 的智能标记中的“启用编辑”复选框。 完成这些步骤后,ErrorHandling.aspx页面Designer应类似于图 4。

删除除所需边界字段之外的所有字段并选中“启用编辑”复选框

图 4:删除除所需边界字段之外的所有,并选中“启用编辑”复选框 (单击以查看全尺寸图像)

此时,我们提供了所有产品的 、、 和 字段的列表;但是,只能ProductName编辑 、 UnitPriceUnitsInStock 字段。UnitsInStockUnitPriceQuantityPerUnitProductName

用户现在可以在“库存”字段中轻松编辑产品名称、价格和单位

图 5:用户现在可以在库存字段中轻松编辑产品名称、价格和单位 (单击以查看全尺寸图像)

步骤 2:正常处理 DAL-Level 异常

虽然当用户输入已编辑产品的名称、价格和库存单位的合法值时,我们的可编辑 GridView 效果非常出色,但输入非法值会导致异常。 例如,省略 ProductName 该值会导致引发 NoNullAllowedException ,因为 ProductName 类中的 ProductsRow 属性已将其 AllowDBNull 属性设置为 false;如果数据库关闭, SqlException 则 TableAdapter 在尝试连接到数据库时将引发 。 如果不采取任何操作,这些异常会从数据访问层浮升到业务逻辑层,然后浮升到 ASP.NET 页,最后浮升到 ASP.NET 运行时。

根据 Web 应用程序的配置方式以及是否从 localhost访问应用程序,未经处理的异常可能会导致泛型服务器错误页、详细错误报告或用户友好的网页。 有关 ASP.NET 运行时如何响应未捕获异常的详细信息,请参阅 ASP.NET 中的 Web 应用程序错误处理customErrors 元素

图 6 显示了在未指定 ProductName 值的情况下尝试更新产品时遇到的屏幕。 这是通过 localhost时显示的默认详细错误报告。

省略产品名称将显示异常详细信息

图 6:省略产品名称将显示异常详细信息 (单击以查看全尺寸图像)

虽然此类异常详细信息在测试应用程序时很有用,但在遇到异常时向最终用户显示此类屏幕并不理想。 最终用户可能不知道 什么是 NoNullAllowedException ,也不知道其原因。 更好的方法是向用户显示一条更方便用户的消息,说明尝试更新产品时出现问题。

如果在执行操作时发生异常,ObjectDataSource 和数据 Web 控件中的后级别事件将提供一种方法来检测该异常,并取消从浮升到 ASP.NET 运行时的异常。 对于我们的示例,让我们为 GridView 的 RowUpdated 事件创建一个事件处理程序,用于确定是否触发了异常,如果是,则显示标签 Web 控件中的异常详细信息。

首先,将 Label 添加到 ASP.NET 页,将其 ID 属性设置为 ExceptionDetails 并清除其 Text 属性。 为了吸引用户对此消息的注意,请将其 CssClass 属性设置为 Warning,这是我们在上一教程中添加到 Styles.css 文件中的 CSS 类。 回想一下,此 CSS 类会导致 Label 的文本以红色、斜体、粗体、特大字体显示。

向页面添加标签 Web 控件

图 7:将标签 Web 控件添加到页面 (单击以查看全尺寸图像)

由于我们希望此标签 Web 控件仅在发生异常后立即可见,因此请在事件处理程序中Page_Load将其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 事件处理程序中检测到),我们将控件 ExceptionDetailsVisible 属性设置为 true。 由于 Web 控件事件处理程序发生在页面生命周期中的事件处理程序之后 Page_Load ,因此将显示 Label。 但是,在下一次回发时,Page_Load事件处理程序会将属性还原Visible回 ,false再次将其隐藏在视图中。

注意

或者,我们可以通过在声明性语法中分配控件Visible的 属性false并禁用其Visible视图状态 (将其属性设置为 false) ,来消除在 中Page_Load设置ExceptionDetails控件EnableViewState属性的必要性。 我们将在以后的教程中使用此替代方法。

添加 Label 控件后,下一步是为 GridView 的 RowUpdated 事件创建事件处理程序。 在Designer中选择 GridView,转到属性窗口,然后单击闪电图标,列出 GridView 的事件。 GridView RowUpdating 的事件应已存在一个条目,因为我们在本教程前面已为此事件创建了一个事件处理程序。 同时为 RowUpdated 事件创建事件处理程序。

为 GridView 的 RowUpdated 事件创建事件处理程序

图 8:为 GridView 的事件 RowUpdated 创建事件处理程序

注意

还可以通过代码隐藏类文件顶部的下拉列表创建事件处理程序。 从左侧的下拉列表中选择 GridView, RowUpdated 从右侧的下拉列表中选择事件。

创建此事件处理程序会将以下代码添加到 ASP.NET 页的代码隐藏类:

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

此事件处理程序的第二个输入参数是 GridViewUpdatedEventArgs 类型的对象,它具有三个用于处理异常的属性:

  • Exception 对引发的异常的引用;如果未引发异常,则此属性的值为 null
  • ExceptionHandled 一个布尔值,指示是否在事件处理程序中 RowUpdated 处理了异常;如果 false (默认) ,则会重新引发异常,并一直渗透到 ASP.NET 运行时
  • KeepInEditMode 如果设置为 true 编辑的 GridView 行仍处于编辑模式,则为 ;如果 false (默认) ,GridView 行将恢复为只读模式

然后,我们的代码应检查以查看是否Exception不是 null,这意味着在执行操作时引发了异常。 如果是这种情况,我们希望:

  • 在标签中 ExceptionDetails 显示用户友好的消息
  • 指示已处理异常
  • 将 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

此事件处理程序首先检查 是否 e.Exceptionnull。 否则,ExceptionDetailsLabel 的 Visible 属性设置为 true ,其 Text 属性设置为“更新产品时出现问题”。引发的实际异常的详细信息驻留在 对象的 InnerException 属性中e.Exception。 检查此内部异常,如果它属于特定类型,则会将附加的有用消息追加到 ExceptionDetails Label 的 Text 属性。 最后, ExceptionHandledKeepInEditMode 属性都设置为 true

图 9 显示了省略产品名称时此页面的屏幕截图;图 10 显示了输入非法 UnitPrice 值 (-50) 时的结果。

ProductName BoundField 必须包含值

图 9:BoundField ProductName 必须包含值 (单击以查看全尺寸图像)

不允许负 UnitPrice 值

图 10UnitPrice 不允许负值 (单击以查看全尺寸图像)

e.ExceptionHandled通过将 属性设置为 trueRowUpdated事件处理程序已指示它已处理异常。 因此,异常不会传播到 ASP.NET 运行时。

注意

图 9 和图 10 显示了一种处理因用户输入无效而引发的异常的正常方法。 但是,理想情况下,此类无效输入一开始永远不会到达业务逻辑层,因为 ASP.NET 页应确保在调用 ProductsBLL 类的 UpdateProduct 方法之前,用户的输入有效。 在下一教程中,我们将了解如何将验证控件添加到编辑和插入接口,以确保提交到业务逻辑层的数据符合业务规则。 验证控件不仅防止 UpdateProduct 调用 方法,直到用户提供的数据有效,而且还提供更丰富的用户体验来识别数据输入问题。

步骤 3:正常处理 BLL-Level 异常

插入、更新或删除数据时,数据访问层可能会在出现与数据相关的错误时引发异常。 数据库可能处于脱机状态,所需的数据库表列可能未指定值,或者可能违反了表级约束。 除了严格与数据相关的异常外,业务逻辑层还可以使用例外来指示违反业务规则的时间。 例如,在创建业务逻辑层教程中,我们向原始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 引发的异常一样,可以在 GridView 的RowUpdated事件处理程序中检测和处理此 BLL 引发ApplicationException的异常。 事实上, RowUpdated 事件处理程序的代码(如所编写)将正确检测此异常并显示 ApplicationExceptionMessage 属性值。 图 11 显示了用户尝试将 Chai 的价格更新为 50.00 美元时的屏幕截图,这是当前价格 19.95 美元的两倍多。

业务规则禁止价格上涨超过产品价格的两倍

图 11:业务规则禁止将产品的价格提高一倍以上 (单击以查看全尺寸图像)

注意

理想情况下,业务逻辑规则会从 UpdateProduct 方法重载重构为通用方法。 这是留给读者的练习。

总结

在插入、更新和删除操作期间,数据 Web 控件和 ObjectDataSource 都涉及触发预定实际操作的级别前和后期事件。 正如我们在本教程和前面的教程中看到的,在使用可编辑的 RowUpdating GridView 时,GridView 的事件会触发,后跟 ObjectDataSource 的 Updating 事件,此时对 ObjectDataSource 的基础对象发出 update 命令。 操作完成后,将触发 ObjectDataSource 的事件 Updated ,然后触发 GridView 的事件 RowUpdated

我们可以为预级别事件创建事件处理程序以自定义输入参数,也可以为后级别事件创建事件处理程序,以便检查和响应操作的结果。 级别后事件处理程序最常用于检测操作期间是否发生异常。 面对异常时,这些后级别事件处理程序可以选择自行处理异常。 在本教程中,我们了解了如何通过显示友好的错误消息来处理此类异常。

在下一教程中,我们将了解如何降低数据格式设置问题 (出现异常的可能性,例如输入负 UnitPrice) 。 具体而言,我们将了解如何将验证控件添加到编辑和插入接口。

编程愉快!

关于作者

Scott Mitchell 是七本 ASP/ASP.NET 书籍的作者, 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0自学。 可以在 上联系 mitchell@4GuysFromRolla.com他, 也可以通过他的博客联系到他,该博客可在 http://ScottOnWriting.NET中找到。

特别感谢

本教程系列由许多有用的审阅者查看。 本教程的首席审阅者是 Liz Shulok。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处放置一行 mitchell@4GuysFromRolla.com。