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

来源:互联网 发布:c语言入门教学视频 编辑:程序博客网 时间:2024/05/19 14:19

简介

在一个使用了分层应用架构的 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, 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 页面了。打开 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 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 应该只作显示之用,不能被用户编辑。为此,将该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 void Page_Load(object sender, EventArgs e){    ExceptionDetails.Visible = false;}

有了这些代码,当首次访问页面,以及之后的回传时,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 void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e){}

这个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 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;    }}

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

复制代码
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 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 能有选择地独自处理异常。本教程中,我们了解了怎样通过显示友好的错误消息来处理这种异常。

原创粉丝点击