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

作者 :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:创建可编辑的产品网格视图

在上一教程中,我们创建了一个只包含两个字段 ProductNameUnitPrice的可编辑 GridView。 这需要为 ProductsBLL 类的 UpdateProduct 方法创建一个额外的重载,该重载只接受三个输入参数 (产品名称、单价和 ID) ,而不是每个产品字段的参数。 对于本教程,让我们再次练习此方法,创建一个可编辑的 GridView,它显示产品名称、每单位数量、单价和库存单位,但仅允许编辑名称、单价和库存单位。

为了适应这种情况,我们需要方法的另一个重载 UpdateProduct ,该方法接受四个参数:产品名称、单价、库存单位和 ID。 将以下方法添加到 ProductsBLL 类:

[System.ComponentModel.DataObjectMethodAttribute(
    System.ComponentModel.DataObjectMethodType.Update, false)]
public bool UpdateProduct(string productName, decimal? unitPrice, short? unitsInStock,
    int productID)
{
    Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    Northwind.ProductsRow product = products[0];
    product.ProductName = productName;
    if (unitPrice == null) product.SetUnitPriceNull();
      else product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null) product.SetUnitsInStockNull();
      else product.UnitsInStock = unitsInStock.Value;
    // Update the product record
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

完成此方法后,我们已准备好创建允许编辑这四个特定产品字段的 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 BoundFields。 还可以随意应用你认为必要的任何字段级格式 (,例如) 更改 HeaderText 属性。

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

将 UnitPrice BoundField 配置为显示为货币

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

UnitPrice在编辑界面中将 设置为货币需要为 GridView RowUpdating 事件创建事件处理程序,该事件处理程序将货币格式的字符串分析为decimal值。 回想一下, RowUpdating 上一教程中的事件处理程序也进行了检查,以确保用户提供了值 UnitPrice 。 但是,在本教程中,让我们允许用户省略价格。

protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e)
{
    if (e.NewValues["UnitPrice"] != null)
        e.NewValues["UnitPrice"] =decimal.Parse(e.NewValues["UnitPrice"].ToString(),
            System.Globalization.NumberStyles.Currency);
}

我们的 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 控件中的异常详细信息。

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

向页面添加标签 Web 控件

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

由于我们希望此 Label Web 控件仅在发生异常后立即可见,因此请在事件处理程序中Page_Load将其Visible属性设置为 false:

protected void Page_Load(object sender, EventArgs e)
{
    ExceptionDetails.Visible = false;
}

使用此代码,在第一页上访问和后续回发, ExceptionDetails 控件的 属性将 Visible 设置为 false。 面对可在 GridView RowUpdated 的事件处理程序中检测到的 DAL 或 BLL 级别的异常,我们将控件 ExceptionDetailsVisible 属性设置为 true。 由于 Web 控件事件处理程序发生在页面生命周期中的 Page_Load 事件处理程序之后,因此将显示 Label。 但是,在下一次回发时Page_Load,事件处理程序会将属性还原Visible回 ,false再次将其从视图中隐藏。

注意

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

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

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

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

注意

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

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

protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
}

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

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

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

  • 在标签中 ExceptionDetails 显示用户友好消息
  • 指示已处理异常
  • 使 GridView 行保持编辑模式

以下代码可实现以下目标:

protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.Exception != null)
    {
        // Display a user-friendly message
        ExceptionDetails.Visible = true;
        ExceptionDetails.Text = "There was a problem updating the product. ";
        if (e.Exception.InnerException != null)
        {
            Exception inner = e.Exception.InnerException;
            if (inner is System.Data.Common.DbException)
                ExceptionDetails.Text +=
                    "Our database is currently experiencing problems." +
                    "Please try again later.";
            else if (inner is NoNullAllowedException)
                ExceptionDetails.Text +=
                    "There are one or more required fields that are missing.";
            else if (inner is ArgumentException)
            {
                string paramName = ((ArgumentException)inner).ParamName;
                ExceptionDetails.Text +=
                    string.Concat("The ", paramName, " value is illegal.");
            }
            else if (inner is ApplicationException)
                ExceptionDetails.Text += inner.Message;
        }
        // Indicate that the exception has been handled
        e.ExceptionHandled = true;
        // Keep the row in edit mode
        e.KeepInEditMode = true;
    }
}

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

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

ProductName BoundField 必须包含值

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

不允许负 UnitPrice 值

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

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

注意

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

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

在插入、更新或删除数据时,数据访问层可能会在遇到数据相关错误时引发异常。 数据库可能处于脱机状态,所需的数据库表列可能未指定值,或者可能违反了表级约束。 除了严格与数据相关的异常外,业务逻辑层还可以使用异常来指示何时违反了业务规则。 例如,在创建业务逻辑层教程中,我们向原始UpdateProduct重载添加了业务规则检查。 具体而言,如果用户将产品标记为已停产,我们要求产品不是其供应商提供的唯一产品。 如果违反此条件, ApplicationException 则会引发 。

对于本教程中创建的 UpdateProduct 重载,让我们添加一个业务规则,禁止将 UnitPrice 字段设置为原始 UnitPrice 值两倍以上的新值。 为此,请调整UpdateProduct重载,使其执行此检查,并在违反规则时引发 ApplicationException 。 更新的方法如下:

public bool UpdateProduct(string productName, decimal? unitPrice, short? unitsInStock,
    int productID)
{
    Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    Northwind.ProductsRow product = products[0];
    // Make sure the price has not more than doubled
    if (unitPrice != null && !product.IsUnitPriceNull())
        if (unitPrice > product.UnitPrice * 2)
          throw new ApplicationException(
            "When updating a product price," +
            " the new price cannot exceed twice the original price.");
    product.ProductName = productName;
    if (unitPrice == null) product.SetUnitPriceNull();
      else product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null) product.SetUnitsInStockNull();
      else product.UnitsInStock = unitsInStock.Value;
    // Update the product record
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

进行此更改后,任何价格更新超过现有价格的两倍都将导致 ApplicationException 引发 。 就像从 DAL 引发的异常一样,可以在 GridView 的事件处理程序中检测和处理此 BLL 引发 ApplicationExceptionRowUpdated 异常。 事实上, RowUpdated 事件处理程序的代码(如所编写)将正确检测此异常并显示 ApplicationExceptionMessage 属性值。 图 11 显示了用户尝试将 Chai 的价格更新为 50.00 美元的屏幕截图,这是其当前 19.95 美元价格的两倍多。

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

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

注意

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

总结

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

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

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

编程快乐!

关于作者

斯科特·米切尔是七本 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放置一行。