在 ASP.NET 页中处理 BLL 和 DAL 级别的异常 (VB)
在本教程中,我们将了解如何在 ASP.NET 数据 Web 控件的插入、更新或删除操作期间发生异常时显示友好的信息性错误消息。
简介
使用分层应用程序体系结构处理来自 ASP.NET Web 应用程序的数据涉及以下三个常规步骤:
- 确定需要调用业务逻辑层的方法以及要传递的参数值。 参数值可以硬编码、以编程方式分配或用户输入的输入。
- 调用方法。
- 处理结果。 调用返回数据的 BLL 方法时,这可能涉及将数据绑定到数据 Web 控件。 对于修改数据的 BLL 方法,这可能包括基于返回值执行某些操作,或正常处理步骤 2 中出现的任何异常。
正如我们在 上一教程中看到的,ObjectDataSource 和数据 Web 控件都为步骤 1 和 3 提供了扩展点。 例如,GridView 在将字段值分配给其 RowUpdating
ObjectDataSource 的 UpdateParameters
集合之前触发其事件;其 RowUpdated
事件在 ObjectDataSource 完成操作后引发。
我们已经检查了在步骤 1 中触发的事件,并了解了如何使用它们来自定义输入参数或取消操作。 在本教程中,我们将把注意力转向操作完成后触发的事件。 借助这些后级别事件处理程序,我们可以确定操作期间是否发生了异常,并正常处理它,在屏幕上显示友好的信息性错误消息,而不是默认为标准 ASP.NET 异常页面。
为了说明如何使用这些后级别事件,让我们创建一个页面,其中列出了可编辑的 GridView 中的产品。 更新产品时,如果引发异常,ASP.NET 页面将在 GridView 上方显示一条短消息,说明发生了问题。 现在就开始吧!
步骤 1:创建可编辑的产品网格视图
在上一教程中,我们创建了一个只包含两个字段的可编辑 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 页面,以便编辑这四个特定产品字段。 ErrorHandling.aspx
打开 文件夹中的页面EditInsertDelete
,并通过Designer向页面添加 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
属性) 。
在上一教程中,我们了解了如何在只读模式和编辑模式下将 BoundField 格式化 UnitPrice
为货币。 让我们在此处执行相同的操作。 回想一下,这需要将 BoundField 的 属性设置为 {0:c}
,其 HtmlEncode
属性设置为 false
,将 设置为 true
ApplyFormatInEditMode
,如图 2 DataFormatString
所示。
图 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
。
图 3:使 QuantityPerUnit
BoundField Read-Only (单击以查看全尺寸图像)
最后,检查 GridView 的智能标记中的“启用编辑”复选框。 完成这些步骤后,ErrorHandling.aspx
页面Designer应类似于图 4。
图 4:删除除所需边界字段之外的所有,并选中“启用编辑”复选框 (单击以查看全尺寸图像)
此时,我们提供了所有产品的 、、 和 字段的列表;但是,只能ProductName
编辑 、 UnitPrice
和 UnitsInStock
字段。UnitsInStock
UnitPrice
QuantityPerUnit
ProductName
图 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 的文本以红色、斜体、粗体、特大字体显示。
图 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
事件处理程序中检测到),我们将控件 ExceptionDetails
的 Visible
属性设置为 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
事件创建事件处理程序。
图 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.Exception
为 null
。 否则,ExceptionDetails
Label 的 Visible
属性设置为 true
,其 Text
属性设置为“更新产品时出现问题”。引发的实际异常的详细信息驻留在 对象的 InnerException
属性中e.Exception
。 检查此内部异常,如果它属于特定类型,则会将附加的有用消息追加到 ExceptionDetails
Label 的 Text
属性。 最后, ExceptionHandled
和 KeepInEditMode
属性都设置为 true
。
图 9 显示了省略产品名称时此页面的屏幕截图;图 10 显示了输入非法 UnitPrice
值 (-50) 时的结果。
图 9:BoundField ProductName
必须包含值 (单击以查看全尺寸图像)
图 10: UnitPrice
不允许负值 (单击以查看全尺寸图像)
e.ExceptionHandled
通过将 属性设置为 true
,RowUpdated
事件处理程序已指示它已处理异常。 因此,异常不会传播到 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
事件处理程序的代码(如所编写)将正确检测此异常并显示 ApplicationException
的 Message
属性值。 图 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。