作者 :斯科特·米切尔
在本教程中,我们将学习如何在进行 ASP.NET 数据 Web 控件的插入、更新或删除操作时,显示友好且详细的错误消息。
介绍
使用分层应用程序体系结构处理来自 ASP.NET Web 应用程序的数据涉及以下三个常规步骤:
- 确定需要调用业务逻辑层的方法以及要传递哪些参数值。 参数值可以硬编码、以编程方式分配或由用户输入。
- 调用该方法。
- 处理结果。 调用返回数据的 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
。 这需要为 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
,并通过设计器向页面添加 GridView。 将 GridView 绑定到新的 ObjectDataSource,将Select()
方法映射到类ProductsBLL
的方法GetProducts()
,并将Update()
方法映射到刚刚创建的UpdateProduct
重载。
图 1:使用 UpdateProduct
接受四个输入参数的方法重载(单击可查看全尺寸图像)
这将创建一个包含四个参数的集合的 ObjectDataSource,以及一个 GridView,其中包含每个产品字段的相应字段。 ObjectDataSource 的声明性标记分配 OldValuesParameterFormatString
值 original_{0}
,这将导致异常,因为 BLL 类不希望传入名为 original_productID
的输入参数。 不要忘记完全从声明性语法中删除此设置(或将其设置为默认值)。 {0}
接下来,将 GridView 精简为仅包含 ProductName
、QuantityPerUnit
、UnitPrice
和 UnitsInStock
这几个 BoundFields。 还可以随意应用你认为必要的任何字段级格式(如更改 HeaderText
属性)。
在前面的教程中,我们介绍了如何在只读模式和编辑模式下将 BoundField 格式 UnitPrice
设置为货币。 让我们在这里进行相同的操作。 回想一下,这需要将 BoundField 的属性DataFormatString
设置为{0:c}
,将HtmlEncode
属性设置为false
,并将其设置为ApplyFormatInEditMode
,true
如图 2 所示。
图 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
。
图 3:制作QuantityPerUnit
BoundField Read-Only(单击查看完整大小图像)
最后,选中 GridView 智能标记中的“启用编辑”复选框。 完成这些步骤后,ErrorHandling.aspx
页面的设计视图应与图 4 相似。
图 4:删除除所需边界字段外的所有内容,并选中“启用编辑”复选框(单击可查看全尺寸图像)
此时,我们已经列出了所有产品的ProductName
、QuantityPerUnit
、UnitPrice
和UnitsInStock
字段;但是,只有ProductName
、UnitPrice
和UnitsInStock
字段可以编辑。
图 5:用户现在可以轻松编辑库存字段中的产品名称、价格和单位(单击可查看全尺寸图像)
步骤 2:优雅地处理 DAL-Level 异常
虽然当用户为编辑的产品的名称、价格和库存单位输入法律值时,可编辑的 GridView 效果非常出色,但输入非法值会导致异常。 例如,省略ProductName
该值会导致引发 NoNullAllowedException,因为ProductName
类ProductsRow
中的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 类导致标签的文本以红色、斜体、粗体、特大字体显示。
图 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
事件处理程序中检测到该异常,我们将控件ExceptionDetails
Visible
的属性设置为 true。 由于 Web 控件事件处理程序在页面生命周期中的事件处理程序之后 Page_Load
发生,因此将显示标签。 但是,在下一次回发时,Page_Load
事件处理程序将 Visible
属性还原为 false
,再次将其隐藏起来。
注释
或者,我们可以通过在声明性语法中分配控件的ExceptionDetails
属性为Visible
,并禁用其视图状态(将Page_Load
属性设置为Visible
),来消除需要在false
中设置EnableViewState
控件的false
属性的必要性。 我们将在将来的教程中使用此替代方法。
添加标签控件后,下一步是为 GridView 的事件 RowUpdated
创建事件处理程序。 在设计器中选择 GridView,转到“属性”窗口,然后单击闪电图标,列出 GridView 的事件。 网格视图 RowUpdating
的事件应该已经有一个条目,因为我们在本教程前面为此事件创建了事件处理程序。 同时为 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.Exception
为 null
。 如果不是,ExceptionDetails
标签的 Visible
属性会被设置为 true
,而其 Text
属性被设置为“更新产品时出现问题”。被引发的实际异常的详细信息驻留在 e.Exception
对象的 InnerException
属性中。 检查此内部异常,如果它属于特定类型,则会将附加有用的消息追加到 ExceptionDetails
Label Text
的属性中。 最后,属性ExceptionHandled
KeepInEditMode
都设置为 true
。
图 9 显示省略产品名称时此页面的屏幕截图;图 10 显示输入非法 UnitPrice
值时的结果(-50)。
图 9: ProductName
BoundField 必须包含值(单击以查看全尺寸图像)
图 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
。 更新的方法如下:
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
事件处理程序的代码将正确检测此异常并显示 ApplicationException
其 Message
属性值。 图 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