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

作者 :斯科特·米切尔

下载 PDF

在本教程中,我们将学习如何在进行 ASP.NET 数据 Web 控件的插入、更新或删除操作时,显示友好且详细的错误消息。

介绍

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

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

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

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

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

步骤 1:创建可编辑的 GridView 产品

在上一教程中,我们创建了一个仅包含两个字段的可编辑 GridView, ProductName 以及 UnitPrice。 这需要为 ProductsBLLUpdateProduct 的方法创建额外的重载,即只接受三个输入参数(产品名称、单价和 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 ,并通过设计器向页面添加 GridView。 将 GridView 绑定到新的 ObjectDataSource,将Select()方法映射到类ProductsBLL的方法GetProducts(),并将Update()方法映射到刚刚创建的UpdateProduct重载。

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

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

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

接下来,将 GridView 精简为仅包含 ProductNameQuantityPerUnitUnitPriceUnitsInStock 这几个 BoundFields。 还可以随意应用你认为必要的任何字段级格式(如更改 HeaderText 属性)。

在前面的教程中,我们介绍了如何在只读模式和编辑模式下将 BoundField 格式 UnitPrice 设置为货币。 让我们在这里进行相同的操作。 回想一下,这需要将 BoundField 的属性DataFormatString设置为{0:c},将HtmlEncode属性设置为false,并将其设置为ApplyFormatInEditModetrue如图 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 页面的设计视图应与图 4 相似。

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

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

此时,我们已经列出了所有产品的ProductNameQuantityPerUnitUnitPriceUnitsInStock字段;但是,只有ProductNameUnitPriceUnitsInStock字段可以编辑。

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

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

步骤 2:优雅地处理 DAL-Level 异常

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

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

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

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

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

虽然此类异常详细信息在测试应用程序时很有用,但在遇到异常时,向最终用户显示此类屏幕并不理想。 终端用户可能不知道什么是 NoNullAllowedException 或为什么会发生这种情况。 更好的方法是向用户显示一条更友好的消息,说明尝试更新产品时出现问题。

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

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

向页面添加标签 Web 控件

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

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

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

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

注释

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

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

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

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

注释

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

创建此事件处理程序会将以下代码添加到 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 标签的 Visible 属性会被设置为 true,而其 Text 属性被设置为“更新产品时出现问题”。被引发的实际异常的详细信息驻留在 e.Exception 对象的 InnerException 属性中。 检查此内部异常,如果它属于特定类型,则会将附加有用的消息追加到 ExceptionDetails Label Text 的属性中。 最后,属性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 引发的异常一样,BLL 引发的ApplicationException异常可在 GridView 的RowUpdated事件处理程序中被检测和处理。 事实上, RowUpdated 事件处理程序的代码将正确检测此异常并显示 ApplicationExceptionMessage 属性值。 图 11 显示了当用户尝试将 Chai 的价格更新至 50.00 美元时的屏幕截图,此价格超过了当前 19.95 美元价格的两倍。

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

图 11:业务规则禁止价格上涨超过产品价格的两倍(单击查看全尺寸图像

注释

理想情况下,我们的业务逻辑规则应重构出 UpdateProduct 方法重载,转移到一个通用方法中。 留给读者自己练习。

概要

在插入、更新和删除操作期间,数据网页控件和 ObjectDataSource 都会触发包围实际操作的前置事件和后置事件。 正如我们在本教程和前一个教程中看到的,当使用可编辑的 GridView 时,首先触发的是 GridView 的RowUpdating事件,然后是 ObjectDataSource 的Updating事件,这时更新命令就会作用于 ObjectDataSource 的底层对象。 操作完成后,ObjectDataSource 的 Updated 事件将触发,随后是 GridView 的 RowUpdated 事件。

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

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

快乐编程!

关于作者

斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams Teach Yourself ASP.NET 2.0 in 24 Hours》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。

特别致谢

本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Liz Shulok。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请给我写信。mitchell@4GuysFromRolla.com