在ASP.NET 2.0中操作数据::在ASP.NET页面中处理BLL/DAL层的异常
导言
在一个使用了分层体系架构的ASP.NET web应用系统里处理数据,一般遵循以下几步:
1. 确定业务逻辑层需要调用哪个方法,并且需要出入哪些参数。这些参数可以通过硬编码设置,程序自动设定,或者由用户输入。
2. 调用此方法。
3. 处理结果。当调用一个返回数据的BLL方法时,这包括绑定数据到Data Web服务器控件。而对于修改数据的BLL方法而言,这包括基于返回值的基础上执行某些动作,或者适当地处理在第二步中引发的异常。
正如我们在前一节里看到的,无论ObjectDataSource控件还是数据Web服务器控件,都为第1和第3步提供了可扩展性。例如GridView控件,触发它的RowUpdating事件之前把它的字段的值赋值到ObjectDataSource的UpdateParameters集合;在ObjectDataSource完成它的操作之后触发RowUpdated事件。
我们已经检测到第1步中触发的事件,并且看过了如何使用它们实现自定义出入参数或者取消操作。这一节我们将把我们的注意力转到操作完成后所触发的事件。通过这些post级的event handler和其它,可以判断在操作过程中是否产生了一个异常,并且适当地处理它,在屏幕中显示友好的错误信息要优于转到ASP.NET的默认错误处理页。
为了举例说明这些post级事件的工作方式,让我们创建一个页面,它在一个可编辑的GridView中列出产品信息。当更新一个产品时,如果引发了一个异常,我们的ASP.NET页面会在GridView控件的上方显示一个简短的信息,说明出现了一个问题。好吧,让我们开始!
第一步: 为产品创建一个可编辑的GridView
这一节里我们创建一个可编辑的GridView,它仅仅包含两个的字段,ProductName和UnitPrice 。这需要为ProductsBLL类的UpdateProduct方法增加一个额外的重载,它仅仅接受3个输入参数(product’s name,unit price,和ID),相对于接受每一个产品的字段的方法。在本节里让我们再一次练习一下这些技巧,创建一个可编辑的GridView,它显示产品的name、quantity per unit、unit price、和units in stock,但仅仅允许name,unit price,和units in stock可编辑。
为了提供这个场景,我们需要对UpdateProduct方法的另一个重载,它接收4个参数: product’s name,unit price,units in stock和ID。在ProductsBLL类中添加下面这个方法:
1 [System.ComponentModel.DataObjectMethodAttribute(System.ComponentModel.DataObjectMethodType.Update, false)]
2 public bool UpdateProduct(string productName, decimal? unitPrice, short? unitsInStock, int productID)
3 {
4 Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
5 if (products.Count == 0)
6 // no matching record found, return false
7 return false;
8
9 Northwind.ProductsRow product = products[0];
10
11 product.ProductName = productName;
12 if (unitPrice == null) product.SetUnitPriceNull();
13 else product.UnitPrice = unitPrice.Value;
14 if (unitsInStock == null) product.SetUnitsInStockNull();
15 else product.UnitsInStock = unitsInStock.Value;
16
17 // Update the product record
18 int rowsAffected = Adapter.Update(product);
19
20 // Return true if precisely one row was updated, otherwise false
21 return rowsAffected == 1;
22 }
23
完成了此方法后,我们可以创建一个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这几列。随意设置一些你认为必要的字段级的格式(例如更改HeaderText属性)。
在之前的章节里我们已经看过了如何在只读和编辑两种模式下格式化UnitPrice绑定列为货币格式。在这里我们同样这样做。这需要设置绑定列的DataFormatString属性为{0:c},它的HtmlEncode属性为false,还有它的ApplyFormatInEditMode属性为true,如图2所示。
图2: UnitPrice绑定列配置为显示一个货币金额
要在编辑界面将UnitPrice格式化为货币,这需要为GridView的RowUpdating事件创建一个事件处理,它将一个货币格式的字符串转换成decimal。回想上一节,RowUpdating事件处理也用来检测并确保用户输入的是一个UnitPrice的值。不过,本节我们可以允许用户忽略price列。
1 protected void GridView1_RowUpdating(object sender, GridViewUpdateEventArgs e)
2 {
3 if (e.NewValues["UnitPrice"] != null)
4 e.NewValues["UnitPrice"] =decimal.Parse(e.NewValues["UnitPrice"].ToString(), System.Globalization.NumberStyles.Currency);
5 }
6
7
我们的GridView包含一个QuantityPerUnit绑定列,但它仅仅用作显示,不能被用户编辑。为了实现这一点,只需要简单地将该绑定列的ReadOnly属性设置为true。
图 3: 设置QuantityPerUnit绑定列为只读
最后,从GridView的智能标记里勾选上“启用编辑”。完成了这些步骤后,ErrorHandling.aspx页面在设计视图里将如图4所示。
图 4: 删除除了必需的绑定列之外的其它列并启用编辑
在这里我们显示产品的所有列,ProductName、QuantityPerUnit、UnitPrice和UnitsInStock;不过仅仅ProductName、UnitPrice和UnitsInStock这几列可以编辑。
图 5: 用户现在可以很方便地编辑Products’ Names、Prices和Units In Stock字段
第二步:适当地处理DAL层异常
这时我们的可编辑的GridView在用户输入合法的product’s name、price和units in stock时表现极佳,输入不合法的值时则导致一个异常。例如,遗漏了ProductName值则引发抛出一个NoNullAllowedException异常,因为ProdcutsRow类的ProductName属性设置了它的AllowDBNull属性为false;如果数据库不正常运作,则在试图连接数据库时通过TableAdapter抛出一个SqlException异常。没有任何的动作,这些异常都会从数据访问层冒出到业务逻辑层,然后到ASP.NET页面,最后到ASP.NET运行时。
取决于你的web应用程序如何配置以及是否从localhost访问该应用,一个未经处理的异常会出现在一类服务器错误处理页,一个详细的错误报表,或者一个对用户友好的web页面。查看Web Application Error Handling in ASP.NET 和 customErrors Element 获得更多的关于ASP.NET页面如何响应一个未捕获的异常的相关信息。
图6展示的是试图不指定ProductName的值更新一个产品时屏幕的状况。这显示的是通过localhost访问时的默认详细错误报表。
图 6: 省略Product’s Name将显示异常明细
虽然这样的异常明细在我们测试应用程序的时候是很有用的,然而当一个最终用户面对这样的异常呈现时却是无所适从的。一个最终用户很可能并不知道NoNullAllowedException是什么,或者它是如何引起的。更好的方法是呈现给用户一个更友好的信息说明试图更新产品时出现了问题。
如果在执行这项操作时出现了一个异常,ObjectDataSource 和数据Web控件的post级事件都提供了发现并不让它出现在ASP.NET运行时的方法。在我们的例子里,让我们为GridView的RowUpdated事件创建一个事件处理程序,它判断是否激发了一个异常,如果是,则在一个Label服务器控件中显示异常详细信息。
首先,添加一个Label控件到ASP.NET页面,设置它的ID属性为ExceptionDetails并清空它的Text属性。为了吸引用户的实现到此信息,设置其CssClass为Warning,这是我们在之前的章节里添加到Styles.css文件的一个CSS类别。记得这个CSS类别让Label的text显示为红色、斜体、加粗的较大的字体。
图 7: 添加一个Label服务器控件到页面
因为我们希望这个Label控件仅在异常出现时显示,在Page_Load事件处理中设置它的Visible属性为false:
1 protected void Page_Load(object sender, EventArgs e)
2 {
3 ExceptionDetails.Visible = false;
4 }
5
通过这些代码,当第一次访问页面和随后的回传后,ExceptionDetails控件的Visible属性都将被设置为false。当在GridView的RowUpdated事件处理程序中检测到一个DAL/BLL层的异常时,我们将设置ExceptionDetails控件的Visible属性为true。因为页面生命周期里Web服务器控件的事件处理出现在Page_Load事件处理之后,该Label将会显示。不过,下一次回传,Page_Load事件处理将重新将Visible属性设置回false,再次隐藏它。
注意: 我们也可以不必在Page_Load里设置ExceptionDetails控件的Visible属性,作为另一种选择,可以在声明语法里设置其Visible属性为false并禁用视图状态(设置它的EnableViewState属性为false)。我们将在以后的章节里使用这种方法。
通过添加这个Label控件,我们下一步是为GridView的RowUpdated事件添加一个事件处理程序。在设计视图中选中GridView控件,打开属性窗口,点击黄色闪电状图标,列出GridView的所有事件。在GridView的RowUpdating事件里我们可以看到已经存在一个入口,因为我们在本节较早的时候已经为此事件创建了一个事件处理程序。为RowUpdated事件创建一个事件处理程序。
图 8: 为GridView的事件创建一个事件处理
注意: 你也可以通过代码隐藏文件顶处的下拉列表创建这个事件处理。从左边的下拉列表中选择这个GridView控件,并从右边的下拉列表中选择RowUpdated事件。
创建这个事件处理将添加下面这些代码到ASP.NET页面的代码隐藏类中:
1 protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e)
2 {
3
4 }
5
这个事件处理程序的第二个输入参数是一个GridViewUpdatedEventArgs类型的对象,它有三个关于处理异常的属性:
· Exception –获取更新操作过程中引发的异常;如果没有抛出异常,该属性的值为null
· ExceptionHandled –获取或设置一个值,它指示在更新操作过程中所引发的异常是否已在RowUpdated事件处理程序中得到处理;如果设为false(默认值),该异常将被重新引发,漏出到ASP.NET运行时
· KeepInEditMode – 如果设置为true,GridView当前编辑行将维持在编辑模式;如果设置为false(默认值),当前行将恢复到只读模式
那么我们的代码应该检测Exception是否为null,不是null则意味着执行此操作时引发了一个异常。如果是这样,我们则希望:
· 在ExceptionDetails控件中显示一个对用户友好的提示信息
· 指示异常已经被处理
· 让当前行保持编辑模式
下面的代码实现了上述的目的:
1 protected void GridView1_RowUpdated(object sender, GridViewUpdatedEventArgs e)
2 {
3 if (e.Exception != null)
4 {
5 // Display a user-friendly message
6 ExceptionDetails.Visible = true;
7 ExceptionDetails.Text = "There was a problem updating the product. ";
8
9 if (e.Exception.InnerException != null)
10 {
11 Exception inner = e.Exception.InnerException;
12
13 if (inner is System.Data.Common.DbException)
14 ExceptionDetails.Text += "Our database is currently experiencing problems. Please try again later.";
15 else if (inner is NoNullAllowedException)
16 ExceptionDetails.Text += "There are one or more required fields that are missing.";
17 else if (inner is ArgumentException)
18 {
19 string paramName = ((ArgumentException)inner).ParamName;
20 ExceptionDetails.Text += string.Concat("The ", paramName, " value is illegal.");
21 }
22 else if (inner is ApplicationException)ExceptionDetails.Text += inner.Message;
23 }
24
25 // Indicate that the exception has been handled
26 e.ExceptionHandled = true;
27
28 // Keep the row in edit mode
29 e.KeepInEditMode = true;
30 }
31 }
32
在这个事件处理程序中,首先检测e.Exception是否为null。如果不是,设置ExceptionDetails控件的Visible属性为true、设置它的Text属性为“There was a problem updating the product.”。当前抛出的异常详细信息则保存在e.Exception对象的InnerException属性里。检查这个内部异常,如果它是特定的类型,则把一些额外的有用的信息附加到ExceptionDetails标签的Text属性。最后,ExceptionHandled和KeepInEditMode属性都设置为true。
图9展示的是遗漏了产品名称时的页面的截屏;图10则显示输入一个不合法的UnitPrice值(-50)时的结果。
图 9: ProductName绑定列必须包含一个值
图 10: UnitPrice值不接受负数
通过设置属性为,事件处理程序指示该异常已经被处理。因此,这个异常不会传送到ASP.NET运行时。
注意: 图9和图10显示了一种得体的方式处理不正确的用户输入所引发的异常。可是,更理想地,这些不正确的输入不应该到达业务逻辑层,因为ASP.NET页面应该在调用ProductsBLL类的UpdateProduct方法之前就确保用户的输入是有效的。我们在下一节里将会看看如何添加validation控件到编辑和插入界面从而保证提交到业务逻辑层的数据遵循业务规则。validation控件不但可以阻止调用UpdateProduct方法直到用户提供有效的数据,还可以为定位数据输入问题提供一个更充满提示性的用户体验。
第三步: 适当地处理BLL层异常
当插入、更新或删除数据时,面对一个数据相关的错误时数据访问层会抛出一个异常。数据库可能未连线,一个必需的数据库表字段可能未指定值,或者违反了某个表间约束。除了确定的数据相关的异常外,业务逻辑层也使用异常指示违反了业务逻辑。在创建一个业务逻辑层 这一节里,作为例子,我们添加了一个业务规则检查最初的UpdateProduct重载。特别地,如果用户标记一个产品为停止供应,我们要求这个产品不能是该供应商唯一供应的产品。如果违反了这个条件,抛出一个ApplicationException异常。
在这一节里,我们给UpdateProduct重载增加一个业务规则:禁止把UnitPrice字段的值设置为超过原来的两倍。为了实现这一点,调整UpdateProduct重载以使它可以执行这个检查并且在违反该规则时抛出一个ApplicationException异常。此更新方法如下:
1 public bool UpdateProduct(string productName, decimal? unitPrice, short? unitsInStock, int productID)
2 {
3 Northwind.ProductsDataTable products = Adapter.GetProductByProductID(productID);
4 if (products.Count == 0)
5 // no matching record found, return false
6 return false;
7
8 Northwind.ProductsRow product = products[0];
9
10 // Make sure the price has not more than doubled
11 if (unitPrice != null && !product.IsUnitPriceNull())
12 if (unitPrice > product.UnitPrice * 2)
13 throw new ApplicationException("When updating a product price," + " the new price cannot exceed twice the original price.");
14
15
16 product.ProductName = productName;
17 if (unitPrice == null) product.SetUnitPriceNull();
18 else product.UnitPrice = unitPrice.Value;
19 if (unitsInStock == null) product.SetUnitsInStockNull();
20 else product.UnitsInStock = unitsInStock.Value;
21
22 // Update the product record
23 int rowsAffected = Adapter.Update(product);
24
25 // Return true if precisely one row was updated, otherwise false
26 return rowsAffected == 1;
27 }
28
通过这个修改,任何超过现有价格两倍的价格更新都回引发一个ApplicationException异常被抛出。就像DAL中引发的异常一样,这个BLL引发的ApplicationException异常可以在GridView的RowUpdated事件处理程序中被侦测并处理。实际上,我们已有的RowUpdated事件处理程序的代码可以正确地发现到这个异常并显示ApplicationException的Message属性的值。图11显示的是当一个用户试图将产品“Chai”的价格更新为$50.00时的截屏,这超过了它原有价格$19.95的两倍。
图 11: 这个业务规则不接受价格增长超出产品现有价格的两倍
注意: 理想化地我们的业务规则不应该在UpdateProduct方法重载里而应该在一个公共的方法中。这留作读者练习。
总结
在插入、更新或删除操作的过程中,数据Web控件和ObjectDataSource控件都包含了pre- 和post-级的事件,它们记录着当前的操作。正如我们在本节和前面的一节里所看到的,当使用一个可编辑的GridView时,GridView的RowUpdating事件在ObjectDataSource的Updating事件之后触发,此时update命令发送到ObjectDataSource的隐含对象。完成了此操作,在GridView的RowUpdated事件之后,触发ObjectDataSource的Updated事件。
我们可以为这些发生在操作之前的事件创建事件处理程序,目的是自定义输入参数;为发生在
操作之后的事件创建事件处理,目的是检测和相应操作的结果。Post-level的事件处理程序通常用作侦测在操作过程中是否出现了一个异常。当面对一个异常时,这些post-level的事件处理程序可以随意地处理该异常。在本节里我们看过了如何处理这样的一个异常,显示一个友好的错误提示信息。
在下一节里我们将看看如何降低因数据格式的问题引起异常的可能性(例如在UnitPrice输入一个负数)。特别地,我们将看看如何添加validation控件到编辑和插入界面。