导言
在一个使用了分层体系架构的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
{
4Northwind.ProductsDataTableproducts=Adapter.GetProductByProductID(productID);
5if(products.Count==0)
6//nomatchingrecordfound,returnfalse
7returnfalse;
8
9Northwind.ProductsRowproduct=products[0];
10
11product.ProductName=productName;
12if(unitPrice==null)product.SetUnitPriceNull();
13elseproduct.UnitPrice=unitPrice.Value;
14if(unitsInStock==null)product.SetUnitsInStockNull();
15elseproduct.UnitsInStock=unitsInStock.Value;
16
17//Updatetheproductrecord
18introwsAffected=Adapter.Update(product);
19
20//Returntrueifpreciselyonerowwasupdated,otherwisefalse
21returnrowsAffected==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,GridViewUpdateEventArgse)
2
{
3if(e.NewValues["UnitPrice"]!=null)
4e.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运行时。
图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,EventArgse)
2
{
3ExceptionDetails.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,GridViewUpdatedEventArgse)
2
{
3
4}
5
·
Exception –获取更新操作过程中引发的异常;如果没有抛出异常,该属性的值为
null
·
ExceptionHandled –获取或设置一个值,它指示在更新操作过程中所引发的异常是否已在
RowUpdated事件处理程序中得到处理;如果设为
false(默认值),该异常将被重新引发,漏出到ASP.NET运行时
·
KeepInEditMode – 如果设置为
true,GridView当前编辑行将维持在编辑模式;如果设置为
false(默认值),当前行将恢复到只读模式
那么我们的代码应该检测
Exception是否为
null,不是
null则意味着执行此操作时引发了一个异常。如果是这样,我们则希望:
· 在
ExceptionDetails控件中显示一个对用户友好的提示信息
· 指示异常已经被处理
· 让当前行保持编辑模式
下面的代码实现了上述的目的:
1
protected
void
GridView1_RowUpdated(
object
sender,GridViewUpdatedEventArgse)
2
{
3if(e.Exception!=null)
4{
5//Displayauser-friendlymessage
6ExceptionDetails.Visible=true;
7ExceptionDetails.Text="Therewasaproblemupdatingtheproduct.";
8
9if(e.Exception.InnerException!=null)
10{
11Exceptioninner=e.Exception.InnerException;
12
13if(innerisSystem.Data.Common.DbException)
14ExceptionDetails.Text+="Ourdatabaseiscurrentlyexperiencingproblems.Pleasetryagainlater.";
15elseif(innerisNoNullAllowedException)
16ExceptionDetails.Text+="Thereareoneormorerequiredfieldsthataremissing.";
17elseif(innerisArgumentException)
18{
19stringparamName=((ArgumentException)inner).ParamName;
20ExceptionDetails.Text+=string.Concat("The",paramName,"valueisillegal.");
21}
22elseif(innerisApplicationException)ExceptionDetails.Text+=inner.Message;
23}
24
25//Indicatethattheexceptionhasbeenhandled
26e.ExceptionHandled=true;
27
28//Keeptherowineditmode
29e.KeepInEditMode=true;
30}
31}
在这个事件处理程序中,首先检测
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
{
3Northwind.ProductsDataTableproducts=Adapter.GetProductByProductID(productID);
4if(products.Count==0)
5//nomatchingrecordfound,returnfalse
6returnfalse;
7
8Northwind.ProductsRowproduct=products[0];
9
10//Makesurethepricehasnotmorethandoubled
11if(unitPrice!=null&&!product.IsUnitPriceNull())
12if(unitPrice>product.UnitPrice*2)
13thrownewApplicationException("Whenupdatingaproductprice,"+"thenewpricecannotexceedtwicetheoriginalprice.");
14
15
16product.ProductName=productName;
17if(unitPrice==null)product.SetUnitPriceNull();
18elseproduct.UnitPrice=unitPrice.Value;
19if(unitsInStock==null)product.SetUnitsInStockNull();
20elseproduct.UnitsInStock=unitsInStock.Value;
21
22//Updatetheproductrecord
23introwsAffected=Adapter.Update(product);
24
25//Returntrueifpreciselyonerowwasupdated,otherwisefalse
26returnrowsAffected==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控件到编辑和插入界面。
祝编程快乐!
作者简介