在学习了三层架构的相关知识,并实现了Web站点主页的一小部分之后,接下来就应该开始为BalloonShop创建商品目录了。
由于商品目录是由许多构件组成的,因而创建的过程将花整整两章来讲解。要注意本章学习强度很大,特别是对于初学者而言,因为其中涉及许多新理论。充分理解这些理论是很重要的,所以在读懂并理解这些基础理论知识之前,不要仓促地开始做练习。不必犹豫,大胆使用诸如Beginning ASP.NET 2.0 in C#:From Novice to Professional(Apress,2006)等其他参考材料,进一步学习本书中没有提及的相关知识。
在本章中,你将会创建第一张数据库表、第一个存储过程,实现通用的数据访问代码,学习如何进行错误处理,并将错误的细节通过电子邮件发给管理员,了解ASP.NET配置文件web.config的使用方法,业务逻辑的实现方法等,最后将通过业务逻辑机制从数据库中取出数据,为访问者动态地构造网站内容。
在本章中将介绍的主要议题包括:
l 分析商品目录的结构及所提供的功能。
l 为商品目录创建数据库结构和数据层。
l 实现支持商品目录功能的业务层对象,并实现一个基本的、但可用的错误处理策略。
l 为商品目录实现一个功能性的UI。
3.1 向访问者展示商品
在诸多网上商店中,一个最基本的功能需求就是让访问者能够轻松地浏览他们所需的商品。试想如果Amazon.com没有卓越的商品目录那将会怎样!
无论是想寻找某个特定商品的访问者还是偶尔浏览一下的来宾,确保使他经历一次愉快的体验是很重要的。如果访问者想要查找某个商品或商品门类,应使他尽可能容易找到。这就是为什么要在网站中加入查找功能,并寻找一种巧妙地将商品组织到目录中的方法,以便访问者们能够快而准地寻找到他们所需要的。
根据商店规模的不同,或许只要几个分类就足够对商品进行分组了,但如果有大量的商品,就需要用更多的方法来组织它们。
在本章中,将要实现的首要目标之一就是为商品目录制定结构。谨记,如果想用专业的方法,必须在开始编码前编写项目需求文档时就应确定这些细节,如附录B所述。但是,针对本书的写作目的,宁可一次处理一件事。
在制定了商品目录的结构之后,就可以开始编写目录代码,使其按预期的方式工作。
3.1.1 商品目录的界面
现在的网民对网站的要求要远高于过去。他们希望随时能找到其想要的商品或服务资料,如果没能找到,那么在给这个网站第二次机会之前,他们很可能就已去了竞争对手的网站。当然,你并不希望这些事情发生在你的访问者身上,所以目录结构要尽可能地直观、好用。
网上商店在开张之初通常大约只有100个左右的商品,但在将来或许会更多,因此仅用“分类”来对其分组是不够的。该网上商店包含许多门类,每个门类又包含许多分类。每个分类下面又包含着许多其所属的商品。
注解 在本书后续章节中,你还将创建网站管理页面,俗称控制面板(Control Panel),允许客户更新门类、分类和商品数据。到目前为止,你只能手动将数据添加到数据库中(或用Apress网站[http://www.apress.com]源码区中的SQL脚本)。
还有一个特别重要的细节是,需要考虑一个分类是否可以存在于多个门类中,一个商品是否可以存在于多个分类中。这个决定将影响到商品目录的实现方式(更重要的是,它影响到数据库的设计),因此还必须和客户商讨这个问题。
对于BalloonShop的商品目录而言,每个分类只能存在于一个门类中,但一个商品可以存在于多个分类中。如商品“Today,Tomorrow & Forever”将同时出现在Love & Romance与Birthdays两个分类中。(并非作者天性浪漫,这个例子足以阐明上述观点。)
最后,除了要有商品分类外,我们也希望有特色商品。对于这个网站,一个特色商品既可以出现在首页中也可以出现在商品门类页面中。下面我们通过几个屏幕截图来解释。
3.1.2 预览商品目录
虽然要到第4章的最后才会有完整的商品目录功能,不过现在看看能够帮助你更好地了解上面的内容。图3-1展示了BalloonShop网站的首页,以及四个特色商品介绍。
图3-1 BalloonShop网站首页与部分特色商品
注意页面左上角的Choose a Department列表。这个列表是根据数据库中的数据动态生成的;在本章中将实现该门类列表。
当访问者在门类列表中点击某个门类时,就会进入该门类的主页。这时商店特色商品目录列表的页面,将会被包含所选门类详细信息的页面(包含了该门类的特色商品列表)所取代。在图3-2中展示了在点击Anniversary Balloons门类之后所显示的页面。
图3-2 Anniversary Balloons门类和四个特色商品
在门类列表的下方,现在可以看到属于已选定门类的分类列表。在屏幕的右边,可以看到所选门类的名称、描述和特色商品。在门类页面中,我们决定只列出特色商品,因为全部的列表太长。特色商品列表上方是所选门类的文字描述,这也就意味着需要在数据库中保存每个门类的名称及描述。
在该页面中,当选择了一个分类时,该分类下的所有商品将被一一列出,同时更新相关的标题与描述。图3-3展示了在选择Birthdays分类时所呈现的样子。另外,当商品列表中的商品数量大于既定数量时,将会出现分页控件。
图3-3 Birthdays分类
在每个商品页面中,可以通过点击商品名称或图片来查看商品详细信息(如图3-4所示)。在随后的章节中,将在页面上添加更多的功能,诸如商品推荐等。
图3-4 商品详细信息页面
3.2 本章路线图
在本章将会涉及很多方面的内容。为了确保你不走弯路,让我们先从全景来看看本章的内容。
商品门类列表将是本网站第一个动态生成的数据,即门类名称将从数据库中取出。本章将用Web用户控件把门类列表展现在界面中,同时还将分析这些控件的工作原理。只有了解了门类列表组件的工作原理,才能在第4章中更好地实现其他功能组件。
在第2章中,我们描述了将要在Web应用程序中使用的三层架构。作为网站的一部分,商品目录也不例外,该组件(包括门类列表)将分成三个逻辑层。如图3-5所示,本章将完成门类列表功能组件的每一层实现。
我们将从数据库开始一直到表示层,完成门类列表的开发:
1) 在数据库中创建Department表。用于存放与商品门类相关的数据。在创建表之前,需要了解关系型数据库的基本概念。
2) 在库中添加GetDepartments存储过程,它(像你将编写的所有其他存储过程一样)是应用程序中数据层的程序逻辑。在这一步中,将学会如何使用SQL语言操作关系型数据库。
3) 在业务层创建门类列表组件。学习如何通过存储过程与数据库通信,并把结果返回给表示层。
4) 最后,开发Web用户控件DepartmentsList.ascx,为访问者动态显示门类列表,这是本章的重点。在第4章中将实现商品目录中其余的功能,下面我们就从数据库开始。
3.3 存储目录信息
绝大多数的Web应用程序、电子商务网站都是基于其管理的数据运转的。分析和理解所需存储与处理的数据是顺利完成项目必不可少的步骤。
对这一类应用程序,所采用的数据存储方式主要是关系型数据库。但这也并不是唯一的方式,可以自己创建数据层并且选择其他的数据存储方式来支持应用程序。
注解 在一些特定场合里,把数据存放在文本文件或XML文件而不是数据库中可能是一个更可取的方式,但是这些方法通常并不适合于类似BalloonShop的应用程序,所以本书并未提及。不过需要知道它是一个可选项。
虽然这不是一本介绍数据库与关系型数据库设计的书,但还是可以从中学到创建商品目录并使它运行起来的相关方法。想了解更多有关SQL Server数据库设计方面的知识,可以阅读诸如Beginning SQL Server 2005 Programming(Wiley,2005)之类的书籍。
从本质上看,关系型数据库是由数据表及它们之间的关系组成的。由于在本章只需创建一个数据表,因此我们只涉及与单表相关的数据库理论。在下一章中,当添加了更多的表时,将对表之间是如何关联的,以及SQL Server是如何处理它们之间的关系等方面进行分析,以更深入地说明关系型数据库后面的理论知识。
注解 在现实世界中,可能会在一开始就对整个数据库进行设计(至少是与要实现功能相关的所有表)。不过我们将开发过程分成两章来讲述,就是为了更好地在理论与实践之间寻找一个平衡点。
那么我们现在先学习一些基础理论,然后再创建Department数据表及其结构。
3.3.1 理解数据表
本节是一个关于数据库的速成课程,讲述了在设计一个简单的数据表时所需要掌握的主要信息。我们将简要地描述组成数据库表的各个主要部分:
l 主键;
l 唯一列;
l SQL Server数据类型;
l 可为空的列和默认值;
l 标识列;
l 索引。
注解 如果你已经对SQL Server有足够的使用经验,则可以跳过本节直接看3.3.2节。
一个数据表是由列和行组成的。列通常指的就是字段,行有时也称为记录。尽管如此,在关系型数据库中,一个简单的数据行列表后面隐藏着大量的逻辑。
1.Department表
商品目录的数据库元素是由表、表关系和存储过程组成的。由于本章只涉及门类列表,因此仅需创建Department这一个数据表就可以了。该表将存储商品门类数据,同时它也是最简单的一个表。
采用Visual Studio .NET或Visual Web Developer开发工具,如果知道要存储什么类型的数据,创建一个数据表就是件很容易的事。在设计一张表时,要考虑它包含哪些字段以及每个字段的数据类型。除了字段的数据类型外,还有几个属性也需要考虑,我们将在接下来的几页中学习它。
在确定Department表需要哪些字段前,先在纸上写几个将存储在表中记录实例。从上一幅图到这里,关于商品门类的信息并不多,只有名称与描述。包含门类数据记录的表格如图3-6所示。
图3-6 Department表中的数据
从该表中获取的名称将出现在页面左上角的列表中,而描述则作为特色商品列表的标题。
2.主键
在关系型数据库中处理数据表与在纸上处理有所不同,在关系型数据库中,一个基本要求就是表中每一数据行都必须能唯一标识。这点很重要,因为通常把记录保存在数据库中是为了稍后能重新查询到它们;但如果每一行没有一个唯一标识,就无法做到了。例如,假设在如图3-6所示的Department表中添加另一条记录,使其内容如图3-7所示。
图3-7 两个名称相同的门类
现在看这个表,告诉我门类Balloons for Children的描述。是的,我们有麻烦了!有两个同名的门类。所有针对Name列的查询:将新商品添加到Balloons for Children门类中、更改门类名称,或是别的什么操作,都将获得两个结果!
为了解决这个问题,应该使用主键,它能在许多行中唯一地标识出某一行。从技术上说,PRIMARY KEY(主键)是应用于列的约束,用来确保该列的值在整个表内都不会出现重复。
注解 在某个字段上应用RPIMARY KEY约束,默认会自动生成一个唯一索引。索引是能提高许多数据库操作性能的对象,能使Web应用程序运行更快(随后将会更详细说明索引)。
一个表只能有一个PRIMARY KEY约束,可以由一个或几个列组成。要注意主键并不是列本身,它是应用于一个或几个已有列的约束。约束是数据库用于维护数据完整性的一种方法。数据库维护它自己的完整性并确保这些规则不被破坏。例如,当往一个有主键约束的表中插入两条标识值相同的记录时,数据库将拒绝此操作并产生一个错误信息。我们将在本章的后面部分做一些这样的试验。
注解 虽然PRIMARY KEY不是列本身,而是一个应用于列的约束,但为了方便,在我们提到主键时,通常是指应用了PRIMARY KEY约束的列。
回到例子中,把Department表中的Name列设为主键,就可以解决由于两种门类不能拥有相同的名称所引发的问题。如果Name是Department表的主键,那么查找一个指定名称的门类时,结果要么有一条记录,要么就没有记录。
另一种可选的方法是在表中添加一个ID列作为主键。添加了ID列之后,Department表将如图3-8所示。
图3-8 在Department表中添加一个作为主键的ID列
把主键列命名为DepartmentID。在此后创建的所有表中,将采用相同的命名规范对来主键命名。
为什么另建一个数值型的列做主键要比用Name(或是表中其他列)列做主键更好呢,原因有两点:
l 性能:数据库引擎在处理数值类型的排序和查询操作时要比字符串类型快得多。同样,这对相关表之间频繁进行的联接操作(这将在第4章中讲述)会产生很大影响 。
l 门类名称更改:如果需要依赖一定时间内稳定的ID值,那么可以采用人工键来解决这个问题,因为此类ID不太可能需要修改。
在图3-8中,主键只是由一个列组成的,但这不是必需的。如果主键由多个列组成,那么这些列(作为整体)将共同保证它的唯一性(但是它们中的每个列在同一个表中是可以有重复值的)。在第4章中,将有一个多列主键的例子。目前,只要了解这些就可以了。
3.唯一列
唯一列是对列的另一种约束。它与PRIMARY KEY相似,也是不允许在一列中有重复数据。不过它们还是有差别的。每个表只能有一个PRIMARY KEY约束,可是却可以有多个UNIQUE约束。
当在表中已经有了主键,但还有一些列的值是必须唯一的时候,对列应用UNIQUE约束就很有用。如果已经将DepartmentID列设置为Department表中的主键,而又想使Name字段不出现重复值,则可以将Name列设置为唯一列(本书并没有使用UNIQUE约束,但为了保持知识的完整性,在此还是简要描述了它)。我们决定允许出现同名的门类名,因为只有网站管理员有权修改和改变门类数据。
实际上,关于UNIQUE约束需要记住的是:
l UNIQUE约束禁止一个字段中两个一样的值。
l 每个数据表可以有多个UNIQUE约束。
l 不同于主键,它不能应用于多个字段中。
l UNIQUE字段是接受NULL值的,但只允许有一个NULL值。
l UNIQUE和PRIMARY KEY列都将会自动创建索引。
4.列和数据类型
每个列都有一个特定的数据类型。看前面图3-8中的Department表,显然DepartmentID是数值类型,而Name与Description则是文本类型。
了解SQL Server所支持的数据类型是很重要的,它能让你在创建表时做出正确的决定。虽然表3-1没有列出全部的SQL Server数据类型,但对于目前的项目来说已经够用了。你可以查阅SQL Server 2005的在线帮助,其网址是http://msdn.microsoft.com/sql/,在上面可以自由地查看、下载更多的详细信息。
注解 虽然表3-1是针对SQL Server 2005的,不过它也适用于SQL Server 2000,甚至在SQL Server 7中也适用。不同版本SQL Server的差异主要在于一些细节问题上,列如字符型数据的最大长度。
为了保持表的简洁,我们只在数据类型标题下面列出了最常用的类型,相似的数据类型在“描述与说明标题”下面作了阐述。你不一定要记住这个列表,只要知道有哪些数据类型可用就可以了。
表3-1 SQL Server 2005数据类型
数据类型 |
字节大小 |
描述与说明 |
Int |
4 |
存储从—2 147 483 648~2 147 483 647之间的整数。可以把它们用在ID列和其他需要整数类型时的情况。相关的类型有SmallInt和TinyInt。还有Bit数据类型,它可以存储0和1两个值 |
Money |
8 |
存储从—263到263—1之间,小数精度为4位的货币数据。可以用来存储商品价格、购物车金额小计等。SQL Server也支持Float数据类型,用于存储浮点数,但不要用Float存储货币信息,因为精度不够。Money的变体是SmallMoney,它存储范围更小,但精度相同 |
DateTime |
8 |
支持从1753年1月1日到9999年12月31日之间的日期和时间数据,可精确到1/300秒。SmallDateTime类型,范围从1900年1月1日到2079年6月6日,精确到分。可以用这个数据类型存储购买日期之类的信息 |
UniqueIdentifier |
16 |
存储全局唯一标识符(GUDI)数字。GUID必定是唯一的;这个属性在某些情况下非常有用。在书中,我们更喜欢用其他方法产生唯一标识符,但还是可以了解一下的 |
VarChar,NVarChar |
可变的 |
存储可变长的字符数据。NVarChar存储最大长度为4000个字符的Unicode数据,VarChar存储最大长度为8000个字符的非Unicode数据。它们最好用于存储没有固定长度的短字符串(注意它们有长度限制) |
Char,NChar |
固定的 |
存储固定长度的字符数据。值的长度比定义的小就用空格来填充。NChar是Unicode版本,最多可以存储4000个字符,而char最多可以存储8000个字符。当字符串的长度相对比较固定时,采用Char会比采用VarChar更有效率 |
Text,NText |
固定的 |
存储大字符数据。NText是Unicode版本,最多可以存储1 073 741 823个字符。而Text可存储的字符个数是NText的两倍。使用这些数据类型会使数据库变慢,因此采用Char,VarChar,NChar或NVarChar类型来替代。当添加Text或NText类型字段时,其长度固定为16,它表示指向实际存储文本地址的指针大小,而不是本身大小。Text数据类型可以用于存储诸如段落、比较长的商品描述等大字符数据。我们在本书中没有使用它 |
Binary,VarBinary |
固定的/可变的 |
存储最大长度为8000字节的二进制数据 |
Image |
可变的 |
存储最大长度为231—1字节的二进制数据。尽管看字面好像只能存储图片,实际它可以存储任何二进制数据。在多数场合中,最好是把文件存放在OS文件系统中而把它们的名称存放在数据库中,这是一种理性的存储二进制数据到数据库中的做法。在BalloonShop中,就是把商品图片存在文件系统中的 |
注解 SQL Server 2005数据类型名称是不区分大小的,但大多数的程序员们还是把它们全部写成大写或小写。我们完全是为了可读性才区分它们的。
现在让我们回过头来看看Department表并决定要采用哪些数据类型。不要为现在数据库中还没有表而感到担心,我们稍后就将创建它。现在首先要了解的是,如何在SQL Server中使用数据类型。
如果知道这些数据类型的含义,图3-9的内容就无需说明了。DepartmentID是Int类型,Name和Description是VarChar类型。左边的金色钥匙表示DepartmentID列是Department表的主键。
还可以看出VarChar类型字段的长度。注意,“长度”对于不同的数据类型是不同的。对于数值数据类型,长度常是固定的(所以在有些设计器中不显示,就像图3-9所示的)并且它存储一条记录所占的字节数,而字符串数据类型(不含Text和NText)存储一条记录所占的字符数。这是一个微妙但很重要的差别,因为对于Unicode文本数据(NChar、NVarChar和NText),存储每个字符实际需要2字节空间。
我们将门类的name列设置为50个字符,description列设置为1000个字符。有些人喜欢用NVarChar代替VarChar,实际上当需要存储Unicode字符时(如汉字文本),就应是这样子的。 否则,通常首选非Unicode版本,因为它们占的空间是Unicode的一半。在大型数据库中,这种区别是很明显的。
5.可为空的列和默认值
观察Department表设计窗口中的Allow Nulls列,有些字段选中了这个选项,有些则没有。如果选中,那么说明该列允许存储NULL值。
对于NULL最好且最短的定义是“未定义”。在Department表中,只有DepartmentID和Name是必需的,而Description是可选的,这意味着,可以添加一行没有描述的门类。添加一新行数据时,如果没有给允许为空的列指定值,它将自动使用NULL值。
特别对于字符数据,NULL值与“空”值有着很微妙的差别。如果添加一个Description字段为空的商品信息,就表示实际上已经设了一个值,即空字符串,而不是未定义值(NULL)。
主键字段永远不允许NULL值。其他列是否允许为空,取决于你决定哪些字段可为空,哪些字段不可为空。
在有些时候,更喜欢用默认值去代替NULL值。这样,当创建一新行时,如果没有指定值,那么将由默认值代替。默认值可以是一个字面值(如,Salary列默认值为0,Description列默认值为Unknown),也可以是一个系统值(如,返回当前日期的GETDATE函数)。在第10章中,将有个名为DateCreated的列,它就可以采用GETDATE函数默认值。
6.标识列
标识列是“自动编号”列。这种行为类似于Microsoft Access中的AutoNumber列。当某列被设置为标识列时,在往表中添加新记录时,SQL Server会自动为其提供值;默认情况下,数据库不允许人为地为标识列指定值。
SQL Server保证所生成的值总是唯一的,这使得它们当和PRIMARY KEY约束一起使用时特别有用。现已知道用在列上的主键可以唯一标识表中的每一行。将主键列设置为标识列,SQL Server就会在添加新行时自动填充该列的值(换句话说,生成新的ID),并保证了值的唯一性。
当你设置标识列时,还必须指定一个标识种子,它是SQL Server将为该列提供的第一个值,以及标识递增量,即在连续两条记录之间要递增的指定数量。
默认情况下,标识种子和标识递增量都设为1,也就是说第一个值为1,在它之后的值是前一个值加1。你无需指定其他的值,因为你不用关心其生成的值。
虽然在前面的图3-9中没有显示,实际上DepartmentID是Department表的标识列。稍后在创建Department表时,将学习如何设置标识列。
注解 标识列所生成的值在表的生命周期中是唯一的。一旦一个值产生之后,它再也不会产生第二次了,即使你删除了表中的所有行。如果想让SQL Server重新从初始值开始,必须删除并重新创建表或是用SQL命令TRUNCATE去截断(truncate)表。截断表跟删除并重新创建表的效果一样。
7.索引
索引是与SQL Server性能调校相关的,所以我们只是简要地描述一下。有关SQL Server索引的更多信息请阅读专门讲解SQL Server 2005的书籍。
索引是数据库对象,用来提高数据库操作的整体速度。索引的设计是基于“大部分数据库操作都是读操作”这样一个假定的。索引能够提高查询操作的速度,但会降低插入、删除和更新操作的速度。不过通常使用索引都是利大于弊的。
在一个表中可以创建多个索引,每个索引可以包含一个或多个列。当一个表按某个列创建了索引后,不是行被索引,就是根据该列的值和索引类型被物理索引。这使得在该列上的查询操作非常快。例如,如果按DepartmentID创建了索引,那么再查询ID为934的门类时,就会执行得非常快。插入新行或更新原有行则会稍慢点,这是因为每次操作都会引发索引(或重新整理表中的行)。
对于索引,应该谨记以下内容:
l 索引可以加快数据库查询操作,但同时会减慢改变数据库的操作(删除、更新和插入操作)。
l 太多索引会降低数据库性能。通常的做法是,对于常用在WHERE、ORDER BY和GROUP BY子句中的列,常用于表连接、与其他表进行外键关联的列,应该创建索引。
l 默认情况下,主键和唯一表列会自动创建索引。
可以使用专门的压力测试工具来测试有索引与未索引条件下的性能;事实上,一位认真的数据库管理员会在决定采用什么样的索引组合之前进行这些测试。也可以使用Database Tuning Advisor工具,它可以通过SQL Server Management Studio访问(然而,在Express版本中没有包括)。可以考虑找一本专门讲解SQL Server的书籍来了解关于这些主题的更多信息。
在这个应用程序中,我们将依赖主键列自动创建的索引,对于我们这种Web网站来说是一种安全的组合。
3.3.2 创建Department表
在第2章中,已经创建了BalloonShop数据库。在接下来的练习中,你将在该数据库中添加一个Department表。
我们建议采用下面练习中的步骤来创建Department表。另外,你也可从Apress网站(http://www. apress .com/)的Source Code区下载SQL脚本创建Department表。创建Department表的脚本文件为Create Department.sql,用SQL Server Express Manager可以执行脚本(参阅附录A查看安装使用说明)。
练习:创建Department表
1) 在Visual Web Developer中,用Database Explorer(数据库资源管理器)窗口打开在第2章中创建的BalloonShop数据连接。记住,如果Database Explorer不可用,用View→Database Explorer或使用默认快捷键Ctrl+Alt+S激活它。
2) 展开BalloonShop数据库连接节点,右击Tables(表)节点,然后选择Add New Table。另外你也可以在连接数据库后,选择Data→Add New→Table。
3) 显示一个可以给新表添加列的窗体。使用这个窗体,添加三列,其属性如表3-2所示。
表3-2 Department表设计
字段名称 |
数据类型 |
其他属性 |
DepartmentID |
int |
主键,标识列 |
Name |
varchar(50) |
不允许为NULL值 |
Description |
varchar(1000) |
可为NULL值 |
注解 要把一个列设置为主键,可以先在其上点击鼠标右键,然后在弹出的菜单中点击Set Primary Key(设置主建)命令。要把一个列设为标识列,先在列的属性窗体中展开Identity Specification项,并设置它为Yes(Is Identity)。如果不想用标识递增量和标识种子的默认值,也可以设置它们的值。
在添加了这些字段后,Visual Studio中的窗体将如图3-10所示。
图3-10 Department表的三个字段
4) 现在一切就绪,可以保存新创建的表。按下Ctrl+S键或是选择File(文件)→Save Table1(保存表1),当询问时,输入Department作为表名。
5) 在数据库中完成表的创建操作之后,就可以打开它以便添加一些数据了。要打开Department表进行编辑,在Database Explorer中右击它,然后选择弹出菜单中的Show Table Data(显示表数据)项。(你也可以在数据库资源管理器中选择该表,然后选择Database→Show Table Data。)使用集成的编辑器就可以开始添加行了。因为DepartmentID是标识列,不能人为地编辑它的值,SQL Server会自动填充该字段,它使用在创建表时指定的标识种子和标识递增量。
6) 添加两个门类,如图3-11所示。
图3-11 在Department表中添加两行例子记录
注解 为了确保和Apress网站上Source Code区中的脚本保持一致(并使你的工作更加简单),确认门类ID正如图3-11所示,值分别为1和2。因为DepartmentId是一个标识列,其ID值只产生一次,即使在这个过程中有记录从表中删除。要重置标识值的生成器只有一种方法,那就是删除并重建表,或者截断表。截断表的最快方法是启动SQL Server Express Manager,联接到本地SQL Server Express实例上(默认情况下,名称为:localhost” SqlExpress),并执行下面的SQL命令:
解析:数据库表
你刚刚创建了第一张数据库表!同时设置了主键、标识列,也在表中填入了一些数据。正如你所见,一旦有了清晰的表结构后,用Visual Web Developer和SQL Server实现就很简单了。
让我们继续学习如何通过SQL代码程序性地访问和操作数据。
3.4 与数据库通信
现在已经有了一张包含数据的表,就让我们用它做一些有用的事。使用该表的最终目标就是用C#代码从数据库中获取门类名称的列表。
为了从数据库中获取数据,首先要知道如何与数据库通信。SQL Server可以接受Transact-SQL(T-SQL)语言。通常,与SQL Server通信是编写T-SQL命令,把它送给SQL Server,然后返回结果。然而,这些命令也可以直接通过业务层发送给SQL Server(不需要中间的数据层),或是集中保存在数据库的存储过程里。
存储过程是数据库对象,用于存储用T-SQL编写的程序。和大多数的函数功能一样,存储过程接受输入、输出参数并可以有返回值。
注解 如第2章所提到的,SQL Server 2005第一次提出了托管存储过程概念,即用.NET语言编写程序,在SQL Server中执行。编写托管存储过程是一个高级的话题,它超出了本书的范围,但知道它的存在也是一个好事。
如果只是执行数据库操作,就不需要使用存储过程。可以通过外部的应用程序直接把SQL命令发给SQL Server。当用存储过程来取代将要执行的SQL代码时,只要传递存储过程名称以及所需的参数值即可。使用存储过程操作数据有如下优点:
l 把SQL代码放在存储过程里会有更好的性能,因为SQL Server在它第一次执行时就生成并缓存了存储过程的执行计划。
l 使用存储过程可以更好地维护访问和操作数据的代码,它集中存放在一个地方,能够使三层架构的实现更加简单(存储过程将组成数据层)。
l 可以更好地控制安全,因为SQL Server允许为每一个存储过程设置不同的安全许可。
l 在C#代码中创建的SQL查询,更容易受到SQL注入攻击,这是一个很严重的安全威胁。因特网上有许多资源提到这个安全主题,如在http://www.sitepoint.com/article/sql-injection- attacks-safe中的文章。
l 这或许是个人的爱好,把SQL从C#代码中分离出来,可以使C#代码更易管理和更加清晰;通过存储过程名称调用比用连接字符串传递给数据库来创建一个SQL查询来的好些。
本节的目标是编写GetDepartments存储过程,不过首先让我们了解一下SQL语言。
3.4.1 数据库语言
SQL(结构化查询语言)是用于与现代关系数据库管理系统(RDBMS)通信的语言。大多数数据库都支持某种SQL,如SQL Server支持T-SQL,Oracle支持PL/SQL(SQL的过程化语言扩展)。因为对T-SQL进行详细说明是一个很庞大的主题,在此只是简要地介绍一下,以便让你能够理解存储过程中的代码。
提示 如果你对SQL感兴趣,在此推荐一本我们编写的书The Programmer’s Guide to SQL(Apress,2003)。它涵盖了SQL标准和在SQL Server、Oracle、DB2、MySQL及Access中的实现。
最基本和最重要的SQL命令是SELECT、INSERT、UPDATE及DELETE。它们的名称就已经说明其功能了,它们可以用来在数据库中执行最基本的操作。
你可以使用SQL Server Express Manager在新建的Department表中测试这些命令。启动SQL Server Express Manager,连接到本地SQL Server Express实例(默认情况下,名称为:localhost” SqlExpress)上,然后在连接上的BalloonShop数据库中执行下列命令(要执行该命令,可以通过工具栏上的执行按钮,或在菜单上选择Query→Execute,或按下快捷键F5):
执行完这个命令后,你应该得到一个“命令已成功完成(Command(s) completed successfully)”消息。在连接上数据库之后,就可准备测试你要学习的SQL命令了。
由于每个SQL命令有许多可选参数,它们就会变得比现在更复杂些。尽管如此,为了简明扼要表述,我们将只学习最重要、最常用的参数,详细内容将在本书后面讲述。
1.SELECT语句
SELECT语句用于查询数据库,返回符合指定条件的数据。其基本结构如下所示:
注解 虽然SQL不区分大小写,在本书中,为了一致性和清晰性,SQL命令和查询都采用大写字母。其中在方括号中的WHERE子句是可选的。
在BalloonShop数据库中,可以执行的最简单SELECT命令是:
如果在练习中你有在Department表中填充数据,那将得到如图3-12所示的结果。
在SQL查询语句中通配符“*”表示“所有列”。多数情况下,除非有很重要的原因,否则应避免使用通配符,而是手动指定想要取回的列,就像:
下面的命令返回DepartmentID为1的门类名称。此处,返回值是Anniversary Balloons,但如果没有ID为1的门类,将不会返回任何结果。
2.INSERT语句
INSERT语句用于插入或添加一条数据行。语法如下:
下面的INSERT命令将在Department表中添加一个名为Mysterious Department门类的记录:
提示 INTO关键字是可选的,有了它会使语句可读性更好。
我们没有指定任何值给Description字段,因为它在Department表中标记为可以为空。这就是为什么可以不指定值,只要你想这么做。但是,Name字段是必须有值的,如果指定了描述却没有指定名称,那就会得到一个错误:
错误消息如下:
还要注意,不能指定DepartmentID的值。因为DepartmentID被设置为标识列,不允许手动为它指定一个值。SQL Server会保存一个唯一值,但是仅在没有干涉它时才行。
因而,如果不能为DepartmentID指定值,那要如何知道SQL Server自动生成了什么值?为此,可以用一个名为@@IDENTITY的特殊变量。可以使用SELECT语句获取该值。下面两个SQL命令是给Department表添加一条记录,并返回刚添加行的DepartmentID值:
3.UPDATE语句
UPDATE语句用于修改已存在的数据,语法如下:
下面的查询语句将把ID为43的类型名称改为Cool Department。如果ID等于该值的门类有多个,则它们将全部被修改,但由于DepartmentID是主键,就不会存在有更多相同ID的门类。
小心UPDATE语句,因为它很容易导致整个表产生混乱。如果忽略了WHERE子句,表中的每条记录都会被改变,这通常是不希望发生的。SQL Server可以改变所有的记录;即使表中所有门类的名称与描述都一样,它们仍然被认为是不同的实体,因为它们有DepartmentID。
4.DELETE命令
实际上DELETE命令的语法很简单:
FROM关键字是可选的、可被忽略的。我们一般都会使用它,这是因为它可以使查询听起来更符合英语语法。
多数情况下,你会想用WHERE子句来删除某一行:
和UPDATE一样,要小心这个命令,因为如果忘记了加WHERE子句,将会删除指定表中的所有行。下面的语句删除Department表中所有的记录。但是,表本身不会被DELETE命令删除。
提示 和INSERT语句的INTO关键字一样,FROM关键字也是可选的。加上了它会使语句可读性更好。
3.4.2 创建存储过程
你需要创建GetDepartments存储过程,用来从Department表中取回门类信息。存储过程是数据层的一部分,业务层将使用它。最后的目标是把数据显示在用户控件上。
查询所需数据的SQL代码,也就是需要保存在GetDepartments存储过程中的语句如下:
这条命令将返回所有门类信息。
注意 除非有特殊原因要这么做,否则当你只需要其中部分列时千万不要查询所有的列(使用*通配符)。这会增加数据库服务器的通信量,降低其性能。而且,即使确实需要表中的所有列,安全做法是明确指出它们,以防止应用程序未来改变列数或者列号。
把查询保存为存储过程
和数据表一样,当你知道了表结构以后,实现存储过程就是一件轻松的事了。当你知道SQL代码后,这一工具可以帮助你很容易地将查询保存为存储过程。
没有输入或输出参数的存储过程,其语法如下:
如果这个存储过程已经存在,只是想修改其代码,可以使用ALTER PROCEDURE来代替CREATE PROCEDURE。
存储过程可以有输入或输出参数。只是GetDepartments没有参数,所以现在不用关心它们。在第4章中将会学习如何使用输入和输出参数。
接下来的练习是,把GetDepartments存储过程添加到数据库中去。
注解 另一种选择是在BalloonShop数据库中执行GetDepartments.sql脚本文件,以创建GetDepartments存储过程。
练习:编写存储过程
1) 确保在Database Explorer(数据库资源管理器)中展开并选中BalloonShop数据库的数据连接。选择Data→Add New→Stored Procedure。或是,在Database Explorer中右击存储过程节点,选择Add New Stored Procedure。
2) 将默认文本替换为你的GetDepartments存储过程:
3) 按Ctrl+S保存存储过程。与表不同,这时不会再要求指定一个名称,因为数据库已经可以从GetDepartments存储过程中知道名称了。
注解 保存存储过程实际上就是执行你输入的SQL代码,它将在数据库中创建存储过程。在保存完程序之后,CREATE关键字将变成了ALTER,它是用来修改已存在过程的代码的SQL命令。
4) 现在,测试一下第一个存储过程,看看它的实际工作。在Database Explorer中,找到GetDepartments存储过程节点,然后选择Execute(执行),如图3-13所示。
5) 在运行了存储过程之后,在输出窗口中可以看到执行结果(如图3-14所示)。可以通过选择View→Other Windows→Output或是按下Ctrl+Alt+O打开输出窗口。
图3-13 在Visual Web Developer中执行一个存储过程 |
图3-14 显示结果的输出窗口 |
解析:GetDepartments存储过程
你已经完成了读取门类列表的数据层的编码工作了!
在输出窗口显示结果证明了存储过程能够按预期工作。你也可以用SQL Express Manager来测试存储过程,执行存储过程的方法如下所示:
3.5 为网站添加业务
业务层(中间层)被认为是应用程序的精髓,因为它掌控着应用程序的业务逻辑。然而,对于简单的任务,例如从数据层中获取门类列表,业务层并没有太多的逻辑要实现。它只是从数据库中取数,并把它传递给表示层。
对于门类列表的业务层,将要实现三个类:
l GenericDataAccess:实现公共的功能,在随时需要访问数据库时可以重用它。将这些通用功能单独封装在一个类中,可以减少按键的次数,还能在长时间运行时避免bug。
l CatalogAccess:包含特定于商品目录的功能,例如用来从数据库中获取门类列表的GetDepartments方法。
l BalloonShopConfiguration和Utilities:包含各种其他的功能,比如发送邮件,它在BalloonShop的许多地方都将被重用。
在第4章中,将继续在这些类中添加新方法以完成新的功能。
3.5.1 连接到SQL Server
在此主要的挑战是理解如何通过代码访问数据库。在.NET中,使C#代码可以访问数据库的技术是ADO.NET。ADO.NET包含了所有与访问数据库相关的.NET类。这是最现代化的Microsoft数据访问技术,可用于任何.NET语言。
ADO.NET是一个复杂的主题,它本身就需要一本专门的书进行论述,因此,我们只介绍一些有助于理解业务层工作原理的内容。有关ADO.NET的更多信息,请参考Beginning ASP.NET 2.0 Databases: From Novice to Professional(Apress,2005)。
要编写的名为GenericDataAccess的数据访问类,将要用到大量的ADO.NET功能,包括一些ADO.NET 2.0的新特性(在适当的时候我们会强调这些特性)。GenericDataAccess类涉及数据库访问、执行存储过程以及获得返回数据。它是业务层的一部分,为业务层的其他类提供公共的功能。
每个数据库操作都包含三个步骤:
1) 打开一个SQL Server数据库连接。
2) 执行所需的数据库操作并返回结果。
3) 关闭数据库连接。
在开发包含这三个步骤的GenericDataAccess类之前,先逐个看看每个步骤。
提示 总是应该使第2)步(执行命令)执行得尽可能地快。一个数据库连接打开太久,或者同时打开了太多个数据库连接,对应用程序的性能是一个很大的损耗。其黄金法则是尽可能晚地打开数据库连接,执行完必要的操作后马上关闭该连接。
用于连接SQL Server的类是SqlConnection。每当创建一个新的数据库连接时,总是要指定至少三个重要的数据:
l 将要连接的SQL Server实例名。
l 访问服务器的用户授权信息。
l 所要操作的数据库。
这些连接数据组成连接字符串,也就是要传给SqlConnection对象的字符串。下面的代码片段示范了如何创建和打开一个数据库连接:
这段代码十分简单明了:首先创建一个SqlConnection对象,接着设置ConnectionString属性,最后打开连接。在做任何操作之前都要先打开连接。
了解了连接字符串的重要性后,当程序在连接数据库时出现问题,就可以通过“修复”连接字符串来解决问题(前提是SQL Server配置正确并可以访问它)。
连接字符串包含三个重要元素。第一个是将要连接的SQL Server实例的名字。对于SQL Server 2005 Express版本而言,默认实例名字是(local)”SqlExpress。如果SQL Server实例有其他的名字,可以更改它。也可以用机器名替代(local)。当然,如果是连接远程的SQL Server实例,就需要指定完整的网络路径来替代(local)。
在指定了服务器后,就需要提供登录到服务器所需的安全信息。要登录到SQL Server,可以用SQL Server认证模式(在这种情况下,需要提供SQL Server的用户名与密码,如前面的代码片段所示),或者Windows认证模式(也叫Windows Integrated Security)。在用Windows Integrated Security时,不用提供同户名与密码,因为SQL Server将使用当前登录用户的Windows登录信息。
用Windows认证登录时,需要用Integrated Security=True(或是Integrated Security=SSPI)来代替User ID=username; Password=password字符串。连接字符串的最后一部分是指定所要操作的数据库。
除了在创建SqlConnection对象之后再设置连接字符串的方法外,也可以在创建SqlConnection对象时就提供连接字符串:
最后,要注意的是连接字符串中的几个可以互换的同义字;如可以使用Data Source或是Data Server代替Server,用Initial Catalog代替Database。这个列表很长,完整的信息可以参照SQL Server 2005的在线帮助。
配置SQL Server安全
由于SQL Server连接问题很常见,因此有很多读者都会问一些关于解决连接错误的问题。接下来就讨论一下如何配置SQL Server,使其能接受来自网站的连接,假设你已经按附录A中的方法完成了安装过程。如果你使用的是外部SQL Server实例,诸如由Web主机托管公司提供的,那么就需要由系统管理员或是主机托管公司提供详细的连接字符串。
配置的细节可能会令人厌烦,如果你愿意也可以跳过这节内容。等BalloonShop项目在执行中出现连接异常时,再回过头来看是哪里出错了也不迟。
SQL Server可以配置为工作于Windows Authentication模式或是工作于Mixed模式。在Mixed模式中,SQL Server可以接受通过Windows Authentication或SQL Server Authentication的连接。你不能将SQL Server设置为只接受通过SQL Server Authentication的连接。
如果在安装时没有指定,那么SQL Server将默认工作于Windows Authentication模式中,SQL Server将通过在Windows中的登录信息来识别用户。这就是为什么不需要指定任何额外的信任信息,就可以从Visual Web Developer中访问SQL Server或是使用SQL Express Manager连接数据库的原因。
可是,在IIS中运行的ASP.NET应用程序,其所属用户是名为ASPNET的特定账户(在Windows 2003 Server系统中,这个账户名为NetWork Service),其默认权限是无法访问SQL Server,更不可能访问BalloonShop数据库了。因此要在IIS中访问SQL Server,就需要给ASPNET账户赋予相应的权限,使其可以访问BalloonShop数据库,以便应用程序能够正确运行。和Visual Web Developer一起发布的集成的Web服务器则是由当前登录用户启动的,因此你基本不用操心(不需要设置任何安全选项,默认情况下,网站拥有访问BalloonShop数据库所需的权限)。
解决连接问题的另一方法是,在IIS中启用SQL Server验证,然后在连接字符串中使用用户ID与密码;或是当ASP.NET应用程序以另一个Windows用户而非ASPNET运行时,使用ASP.NET impersonation方法。不过在这里就不论述这些方法的细节了。
为了使ASPNET账户可以访问BalloonShop数据库,需要完成以下步骤:
1) 启动SQL Express Manager,指定SQL Server实例名(默认为localhost”SqlExpress),以Windows验证模式登录。
2) 用grantlogin存储过程把Windows的用户添加到SQL Server数据库中。这个命令将赋予ASPNET账户连接SQL Server的权限。注意要用本机的主机名代替命令中的MachineName。
3)在为ASPNET账户赋予了连接SQL Server的权限后,还需要为其赋予访问BalloonShop数据库的权限:
4) 最后,需要赋予ASPNET访问BalloonShop数据库内部对象的权限,如执行存储过程、读取和修改表等。最简单的方法是为ASPNET账户分配一个BalloonShop数据库的db_owner角色。如果在前面的步骤中(用USE BalloonShop)已连接到了BalloonShop数据库,输入下面的命令:
就这样,现在你可以从Web应用程序以Windows验证模式连接数据库了。
3.5.2 提交命令与执行存储过程
在打开一个数据库连接后,通常需要创建一个SqlCommand对象去执行操作。在使用SqlCommand对象时有许多诀窍,我们将逐步对其进行介绍。
1.创建SqlCommand对象
在开发数据访问代码时,SqlCommand是最好的朋友。这个类可以将要与数据库交互的信息保存起来,它存放的是要执行的SQL查询或是存储过程名字。SqlCommand也能支持存储过程的参数,这将在第4章中进一步学习,因为本章中的存储过程(GetDepartments)没有任何参数。
下面是标准的创建和初始化SqlCommand对象的方法:
其实,这段代码也没有什么神秘的地方。首先创建一个SqlCommand对象,然后设置一些属性。要设置的属性中最重要的是Connection,因为每一个命令都是在特定的连接上执行的。另一个重要的属性是CommandText,即需要执行的指定命令。它可以是SQL查询,如SELECT*FROM Department,不过在本应用系统中却总是使用存储过程名称。
在默认情况下,CommandText属性接受SQL查询。因为你提供的是存储过程名称而非SQL查询,就需要告诉SqlCommand对象,也就是把CommandType属性设为CommandType.StoredProcedure。
在前面的代码片段中,展示了一种简单、结构化的创建和设置SqlCommand对象的方法。可是,也可以用更少的代码达到同样的结果,也就是在创建Command对象时就传给它一些信息:
2.执行命令和关闭连接
这是值得引以为豪的时刻,在创建完连接以及SqlCommand对象,并设置了各种参数后,就可以准备执行命令了。在执行完所需的数据库操作后,就应马上关闭连接,这总是很重要的,因为打开的连接会消耗服务器资源,如果管理得不好,最终会降低性能的。
基于不同的需求,有很多种执行命令的方法。是否会返回信息?如果会,那是什么信息,什么格式?稍后在你把理论应用于实践时,我们将分析各种情况,不过现在先让我们看一下SqlCommand类的三种Execute方法:ExecuteNonQuery、ExecuteScalar和ExecuteReader。
ExecuteNonQuery用于执行不返回任何记录的SQL语句或存储过程。在数据库中执行如更新、插入、删除操作时,应使用这个方法。ExecuteNonQuery返回一个整数值,用来说明受查询语句影响的行数,如果你需要这个值,这是很有效的,例如,上一个删除操作一共删除了多少行。当然,本例中不需要知道这个数字,所以可以简单地忽略这个返回值。下面这段简单的代码展示了如何打开连接,再用ExecuteNonQuery执行命令,然后马上关闭连接:
ExecuteScalar与ExecuteNonQuery一样,其返回的也是单个值,但该返回值是从数据库中读出来的,而不是受影响的行数。它常用于选择一个值的SQL语句。如果SELECT返回多行或多列,则使用该方法仅返回第一行第一列的数据。
ExecuteReader被用于返回多条记录的SELECT语句(包含任意个字段)。ExecuteReader将返回一个包含查询结果的SqlDataReader对象。一个SqlDataReader对象是以顺序向前且只读的形式逐个读取并返回结果的。SqlDataReader有利的一面是它是从数据库中取数最快的对象,不利的一面是操作前需要一个已打开的连接,在它被关闭之前,无法使用同一个连接执行其他任何数据库操作。在我们的解决方案中,通过SqlDataReader取回所有的记录,并把它们存入DataTable对象中(它可以存储离线数据而不需要一个打开的连接),这样就可以马上关闭数据库连接了。
DataTable类可以存放本地的结果集而不需要一个打开的SQL Server连接,和其他的ADO.NET对象一样,它也不是特定于某种数据提供程序的(诸如名字是以SQL开始的都是特定于SQL Server数据库的)。
提示 DataTable对象的父类是DataSet,它是一个非常强大的对象,扮演着不同的角色,像一个“内存中”的数据库。DataSet能够存储数据表、它们的数据类型、表间关系等。正是因为其复杂性,DataSet会消耗许多内存,所以应尽可能地避免使用它。在创建BalloonShop中,没有用到任何的DataSet。
下面是一个简单的例子,它将从数据库中读取一些记录并把它们保存在DataTable中:
3.5.3 实现通用的数据访问代码
到目前为止,我们在例子中所使用的类的名称都是以Sql开头的:SqlConnection,SqlCommand和SqlDataReader。它们和其他所有以Sql开头的对象都是针对SQL Server和SQL Server托管数据提供器的。SQL Server托管数据提供器是介于数据库与应用程序间的底层接口。这些数据提供程序所使用的ADO.NET对象被组织在System.Data.SqlClient命名空间中,因此,当你要直接访问这些类时,需要引入System.Data.SqlClient这个命名空间。
.NET框架中自带的数据提供器包括:SQL Server托管数据提供器(System.Data.SqlClient命名空间),Oracle(System.Data.Oracle),OLE DB(Sytem.Data.OleDb),以及ODBC(System.Data.Odbc)。
为了使应用程序尽可能和后台数据库无关,我们使用了一个技巧,即避免使用特定于某数据库的类,诸如SqlConnection等等。相反,我们将让应用程序在运行时决定使用哪个提供器,这取决于提供的连接字符串。此外,因为这是ADO.NET 2.0的一个全新特性,因此在使用这个技巧时根本不会影响应用程序的性能!
提示 如果你熟悉面向对象编程(OOP)理论,会对进一步了解相关信息感兴趣。在我们的代码中,使用数据库无关的类,例如使用DbConnection和DbCommand来代替SqlConnection和SqlCommand。在执行时,这些类的对象将通过多态的方法,包含特定于某数据库的实例。其结果是,当调用DbConnection类的一个方法时,就会执行SqlConnection的相应方法。使用这个技巧,即使你改变了后端数据库的类型,已编译的代码也根本不用修改就可以运行了,只要在新的数据库中也实现了那些存储过程即可。在我的个人网站http://www.CristianDarie.ro中可以下载与“基于C#的OOP”相关的免费资料。
虽然使用特定于SQL Server的类可以使例子保持简单性,但在实践中我们仍然使用了一种方法,以使C#代码(至少在理论上)不依赖于特定的数据库服务器商品。
ADO.NET 2.0提供了一些用于通用数据访问功能开发的新的类(这些在ADO.NET 1.0或1.1中是没有的),如DbConnection、DbCommand等,这些都被组织在System.Data.Common命名空间中。
要开发数据库无关的数据访问代码,首先应使用DbProviderFactory类创建一个新的数据库提供器工厂对象:
在这段代码中,因为我们传入的参数是System.Data.SqlClient,因此生成的factory对象将包括SQL Server数据库提供器工厂(术语“工厂”是指为你构建其他类的类)。在实践中,System.Data.SqlClient字符串参数是保存在配置文件中的,以使你编写的C#代码并不知道使用的是什么数据库。
数据库提供器工厂类能够通过它的CreateConnection)方法创建一个特定的数据库连接对象。因此,可以继续使用通用DbConnection来代替特定连接对象:
因此实际上,如果后端的数据库是SQL server,那连接对象实际包含的是SqlCommand对象,如果后端的数据库是Oracle,那就是OracleCommand。然而,我们的程序并非是基于SqlCommand或OracleCommand对象编写的,只是简单使用DbCommand,让它在运行时自己决定在后台要创建哪种类型的对象。
在创建完连接对象后,就可以按你熟悉的方法简单地设置其属性,就像你拥有的是“常规”连接对象那样:
好的,现在已经有了连接对象,但如何执行命令呢?正好连接对象中有一个名为CreateCommand的方法,可以返回数据库命令对象。和连接对象一样,CreateCommand)返回特定于某种数据库的命令对象,但还是可以用数据库无关的对象来代替特定于某种数据库的对象。下面这行代码就将完成该任务:
现在,你已经拥有了一个连接对象和一个命令对象,就可以像以前一样运行它了。这边有一个相对完整(差不多能运行)的ADO.NET 2.0代码段,不用知道是和哪种数据库打交道,就能把门类列表取到DataTable中:
3.5.4 捕获及处理异常
创建网站的法则当然是使网站能够永远运行正常、不会发生任何问题。但在开发的过程中这些法则经常会发生异常,甚至在已投产的系统中也会发生。先不用说那些你控制范围之外的因素,诸如硬件故障、软件崩溃以及导致软件不能按预期运行的病毒都是常见的。甚至在某种情况下你都知道会发生错误,例如用户输入了不良的(或非预期)的数据组合,正好击中了应用程序逻辑的缺陷。
在访问数据库或执行存储过程时出现的错误很常见也特别危险。引起这种错误的原因太多了,它可能导致向访问者展现荒唐的错误信息,或是造成数据库资源被锁,这将会对此时访问网站的所有访问者造成影响。
对于面向对象语言而言,“异常”(exception)是截取和处理运行时错误的现代方法。当代码出现运行时错误时,执行就会被中断,并产生(引发)异常。如果引发异常的代码没有对其进行处理,则异常将沿着栈的出栈方向向外传。如果都没有对其进行处理,最后将被.NET框架捕获,并显示错误信息。如果错误发生在客户端请求某个ASP.NET页面时,ASP.NET会向访问者显示一个错误页面,其中包含调试信息。(所幸的是在这种情况下,能让ASP.NET显示一个自定义错误页面以取代默认的页面,在本章最后你将完成该操作。)
另一方面,如果异常在代码中处理了,则程序将继续执行,当处理页面请求时,访问者根本就不知道有错误发生。通常,对于运行时异常的处理策略为:
l 如果错误不严重,就在代码中处理,让代码继续正常运行,并且访问者永远都不会知道有错误发生。
l 如果错误严重,就需要使用代码进行处理并尽可能地减少负面影响,并将错误传递到表示层,向访问者展示一个类似于“Houston,这里发生了错误”的友好页面。
l 对于意料之外的错误,最后的防线仍然是在表示层,客气地请访问者稍后再访问。
对于任何类型错误,最好是通知网站管理员(或专职技术人员)。可以把错误的细节保存到自定义数据库表中或是写入Windows事务日志中,还可以发邮件。在本章的最后,将学习如何在一个错误发生时,把相关细节通过电子邮件发给网站管理员。
在我们的数据访问代码中,将把所有错误视为严重错误。其结果是直接关闭数据库连接、记录错误日志并把它传递给表示层,这样可以将潜在的危险减到最小。
注解 如你所见,在业务层中的代码逻辑可以对经过它的异常进行控制。一些由数据访问代码产生的异常,可以被捕捉并在业务层中处理。万一业务层无法处理,则异常将传递到表示层上,也将被再一次记录(因此管理员知道它是一个严重的错误),同时向访问者展现一个友好的错误提示信息,并请他稍后再访问。
因此,对于那些在接触访问者之前处理的数据访问错误,只被记录一次(在数据访问代码中)。而影响访问者浏览体验(显示错误信息)的严重错误将被记录两次,第一次是在数据访问代码中抛出时,第二次在显示错误信息给访问者时。
这种理论听起来不错,但我们在实践中要如何做呢?首先,要学习与异常相关的知识。在C#代码中,是用try-catch-finally结构处理异常,其最简单的版本类似于:
把可能产生错误的代码放在try区里。如果有异常发生,马上跳转到catch区中执行。如果在try区中没有异常发生,catch区将被忽略。最后,不管异常有没有发生,finally区都会被执行。
finally区很重要,因为不管发生什么,都保证它的执行。如果在try区中执行一些数据库操作,那么标准做法是在finally区中放置关闭数据库连接的代码,以保证在数据库服务器中不会遗留不使用的已打开的连接。这样做很有好处,因为打开的连接会消耗数据库服务器资源,甚至在其他操作者同时运行数据库活动时会使数据库资源锁定,引发问题。
finally和catch区是可选的,但(显然)要使整个结构有意义,至少它们两个要出现一个。如果没有catch区(仅有try和finally),异常是不会被处理的;代码将会停止执行,异常则被传递到比类更高一层的代码中,但这要在执行完finally区之后(前面提过了,无论如何都会执行)。
运行时的异常将从其产生点开始向外传递,沿着程序的调用栈。因此,如果在数据库的存储过程中产生一个异常,它马上会传递给数据访问代码。如果数据层有try-catch结构处理错误,那么什么事情都好办,业务层和表示层永远也不知道有一个错误发生过。如果数据层没有处理异常,那么异常将被传递到业务层,如果业务层没有处理,那么异常将被传递到表示层。如果异常在表示层也没有处理,那么异常最后传递给ASP.NET运行环境,其处理的方法就是向访问者展现一个错误页面。
我们有时希望能够捕捉异常,并对其做出一些响应,然后再通过调用栈去传播它。例如,在BalloonShop数据访问代码中,就需要捕捉异常并记录它们,然后把它传递到更高层的类中,因为它们才知道如何更好地处理该情形和确定错误的严重程度。其方法就是在catch区中捕捉它并使用throw语句重新引发一个错误:
正如你在这个代码片段中所见到的,在.NET代码中异常是通过Exception类表示的。在.NET框架中,包含许多由特定事件产生的特定异常类,甚至可以创建自定义的异常类。不过,这些议题都超出了本书的范畴。
关于try-catch-finally结构的详细用法可以查阅C#语言参考书。在本章中,你将看到在数据访问代码中,该结构捕捉潜在的数据访问错误并报告给管理员。
3.5.5 发送电子邮件
说到报告错误,在BalloonShop中,将通过电子邮件把错误报告给网站管理员(或是给指定处理错误的人)。另一种解决方式是把错误写入Windows事件日志,或者保存到数据库中,还可以保存在一个文本文件中。
要发送邮件,需要使用System.Web.Mail命名空间中的SmtpClient和MailMessage类。
MailMessage中有四个重要的属性,在你发送一封邮件之前应当设置:From,To,Subject和Body。也可以通过MailMessage的构造函数(以参数的形式)设置这些属性。在MailMessage对象正确设置后,就可以用SmtpMail类来发送邮件。
在使用SmtpClient时,可以通过设置它的Host属性来使用外部SMTP(简单邮件传输协议)服务器;否则,邮件将通过本地的Windows SMTP服务器发送。注意,在使用前,在Windows中必须已经安装了SMTP服务。这个服务是IIS中的一个组件,你可以按照附录A中的说明进行安装。
标准的电子邮件发送代码如下面的代码片段所示(需要把代码中的斜体字替换成自己的数据):
如果你使用的是本地SMTP服务器,则通过IIS控制面板确保服务已启动。另外,可能需要对本机启用邮件中继。要完成该任务,应打开IIS控制面板,展开计算机节点,右键点击默认SMTP虚拟服务器,选择属性项,找到访问页,点击“中继”按钮,添加127.0.0.1到列表中,最后重启SMTP服务器。
如果仍然有问题,则在尝试用Google搜索答案之前,先看一下http://www.systemwebmail.com/ default.aspx,虽然这是针对.NET 1.1设计的,但该网站仍可能包含有问题的解决方法。
3.5.6 编写业务层代码
现在是通过一些新代码更新BalloonShop的解决方法的时候了。下面的练习在开发业务层代码时运用了很多前面所讲的理论。你将在应用程序中添加以下C#类:
l GenericDataAccess:包含通用数据库访问代码,实现基本的错误处理和日志功能。
l CatalogAccess:包含与商品目录相关的业务逻辑。
l BalloonShopConfiguration:提供一种访问各种配置信息(通常是从web.config中读取)的简单方法,诸如数据库连接字符串等。
l Utilities:包含各种杂项功能,例如发送电子邮件,它将在BalloonShop的许多不同地方中使用。
按照下面练习中所述的步骤把这些类添加到你的项目中。
练习:实现数据访问代码
1) 打开web.config配置文件(在Solution Explorer中双击该文件),将connectionStrings元素更新为:
注解 你可能需要修改连接字符串以适应特定的SQL Server配置。同样,你将要在一行中输入<add>元素的所有信息,而不能像前面代码片段中那样分成多行。
2) 在web.config中<appSettings>节点下面添加其他必要的配置数据,例如:
注解 确认用一个可用的服务器地址代替localhost,用一个有效的邮件账号代替errors@ yourballoonshopxyz.com,如果打算使用邮件记录日志特性的话。否则,只要设置EnableErrorLogEmail为false。
3) 在Solution Explorer中,在项目名称上点击鼠标右键,然后选择Add New Item。
4) 选择Class(类)模板,将其名字设置为BalloonShopConfiguration.cs,再点击Add按钮。
5) 向导将询问你是否把该类添加到App_Code目录中。这是ASP.NET 2.0的专用目录,选择Yes。
6) 将BalloonShopConfiguration类的内容修改为:
7) 在Solution Explorer中,右击项目名称,然后在右键快捷菜单中选择Add New Item命令。
8) 选择Class模板,将其名字设置为Utilities.cs,再点击Add按钮。当向导询问是否把该类添加到App_Code目录中时,选择Yes。
9) 在Utilities.cs中写入以下代码(注意,我们去除了不必要的using语句):
10) 在Solution Explorer中,右击项目名称,然后选择Add New Item。然后选择Class模板,将名字设置为GenericDataAccess.cs,最后点击Add按钮。当向导询问是否把类添加到App_Code目录时,选择Yes。
11) 在GenericDataAccess.cs中写入以下代码:
12) 在Solution Explorer中,右击App_Code目录,然后选择Add New Item。在出现的窗体中,创建一个名为CatalogAccess的类(存放在名为CatalogAccess.cs的文件中)。然后将以下新代码添加到文件中:
解析:业务层
让我们花些时间去理解刚才所写的代码。
首先,在web.config配置文件中添加了一些配置信息。这是由ASP.NET管理的外部XML配置文件。这是一个复杂而又强大的文件,包含了许多应用程序安全、性能、行为等选项。
把数据保存在web.config是有好处的,因为它可以独立于C#代码进行修改,现在改变邮件服务器地址或数据库连接字符串,都不要重新编译。这些再加上数据库类型无关的数据访问代码,使得整个数据访问代码更加强大。
然后,添加了BalloonShopConfiguration类,包含一些从web.config中返回数据的简单静态属性集合。使用这个类后,就不再需要不时地读取web.config文件,对长期运行更有利。同时也改善了性能,因为这个类能够将从web.config中读取的值缓存起来,不必每一次请求都重新读取。第一次使用BalloonShopConfiguration类是在Utility类中,现在该类只包含实现发送电子邮件的代码。
接着,实现了GenericDataAccess类,其目的是存放一系列公共的数据库访问代码,避免在其他地方一再地输入。现在它只包含两个方法:
l CreateCommand:创建一个DbCommand对象,设置一些标准属性,并返回配置后的对象。如果宁愿使用特定数据库命令对象,例如SqlCommand,那么代码会变得简单些,但在这个例子中,我们更喜欢数据库无关的访问代码,正如在本章前面部分所介绍的。CreateCommand方法使用本章前面部分说明的步骤,去创建一个特定于某种数据库的命令对象,把这个实例包进一个通用DbCommand引用中,并返回这个引用。外部类可以调用CreateCommand以获取一个已配置,并包含一个准备好的数据库连接的DbCommand对象。
l ExecuteSelectCommand:本质上就是对DbCommand的ExecuteReader方法的一个封装,只不过它返回的是一个DataTable而不是DataReader。使用DataTable是为了确保数据库连接的保留时间尽可能短。在本方法中,还实现了一种错误处理机制,万一出现异常时,错误信息将会通过电子邮件发送给管理者(如果应用程序是这样配置的),并正确地关闭数据库连接,重新抛出该错误。我们决定让错误能够传播,因为这个类处于较低的水平上,不知道如何正确地处理错误。这时,我唯一感兴趣的就是确保数据库安全(通过关闭连接)和报告任何可能的错误。关于客户端如何使用GenericDataAccess与BalloonShop数据库交互的最好例子是在CatalogAccess类中的GetDepartments方法。
所有要添加的类都是静态类,它都是由静态成员构成的。注意,对基本的OOP术语(类、对象、构造函数、方法、属性、字段、实例成员和静态成员、公共数据和私有数据等)有些了解,是学习本书的重要前提。在因特网上有很多有关这方面的文章,例如可以自由下载的http://www. cristiandarie.ro/ downloads.html
注解 静态类成员(如静态属性和静态方法)可不用先创建该类的实例就可以在类的外面调用它;作为替代,它们可以直接用类名调用。最好的例子就是Math类,它包含许多不同操作的静态方法,如Math.Cos等。静态类成员是在一个全局类实例上被调用的,其在执行完后不会被GC(垃圾收集器)销毁。在类的静态成员第一次被调用时,全局的类实例就被自动创建,静态构造函数也将被执行。因为在每个应用程序的生命周期中静态构造函数仅被调用一次,全局类实例一直没被销毁,我们可以保证在静态构造函数中(如读取数据库连接字符串)执行的任何初始化仅被执行一次,而静态成员则被保持。一个静态类成员可以直接调用或访问另一个静态类成员。可是,如果需要从静态类成员中访问一个实例化类成员(不是静态类成员),将不得不要创建一个类的实例,甚至是从这个类的方法中去访问其成员。
我们选择使用静态类成员主要是为了改善性能。因为静态类和静态成员仅被初始化一次,以后每次新的访问者有新的请求时都不需要再初始化了,相反,使用的是它们的“全局”实例。在表示层中,你将通过一个如下所示的调用展示门类列表的内容:
如果GetDepartments是一个实例的方法,那么就需要创建一个单独的CatalogAccess类实例,以取代使用静态实例,这样就显然会对性能产生较大的影响:
在BalloonShopConfiguration中,还使用一个额外的技巧来改善性能,即用静态字段缓存连接字符串数据(dbConnectionString和dbProviderName),这些数据是类的静态构造函数从web.config读取出来的。因为在每个应用程序的生命周期中静态构造函数仅被调用一次,因此,web.config文件不会在每次数据库操作时都被读取一次,仅是在类被初始化时。
3.6 显示门类列表
现在,其他层都准备好了,要做的事只剩下创建表示层了,这是从一开始你就瞄准的最后目标。正如在本章最开始给出的图中所看到的,当在Web浏览器中加载该网站时,门类列表应如图3-15所示。
你将实现一个名为DepartmentsList的Web用户控件,方法和第2章中的Header控件类似,接着还要把用户控件加到母版页中,以使其在网站所有页面上都可以使用。
门类列表需要基于数据库中现有的数据动态产生。幸运的是,.NET框架提供了一些有用的Web控件,可以帮助解决这个问题,而不需要编写很多代码。例如,DataList控件,它可以设置为把DataTable对象作为输入参数,依据其中的数据生成列表。
在实际编写该用户控件前,让我们为BalloonShop准备一个CSS文件。
3.6.1 准备工作:主题、外观和样式
CSS文件是一个用于存储字体和格式化信息的标准贮藏室,它可以被很容易地应用于网站的各个部分。例如,可以通过为其CssClass属性设置一个已存在的样式,来代替对某个Label控件的字体、颜色和尺寸进行设置。CSS文件是用于客户端的,但这并不意味着在ASP.NET应用程序的服务器端没有任何处理。下面就是一个典型的CSS样式定义:
ASP.NET 2.0提出主题和外观的概念。外观(skin)就像CSS文件,包含有各种属性,但它们是基于控件类型的,允许设置CSS中不可访问的属性,是运用于服务器端的。外观定义保存在扩展名为.skin(这些文件可以保存一个或多个外观定义)的文件中,看起来就像ASP.NET控件的定义,一个典型的外观定义看起来像:
该外观是一个已命名的外观,因为它有一个SkinID。此时如果要添加一个实现该外观的图片,就需要将它们的SkinID属性设置为BalloonShopLogo。如果在创建一个外观时没有指定SkinID,那么这个外观就成为控件类型的默认外观。
正像CSS文件那样,当希望在更多控件中重用某种特定的控件格式时就需要使用外观。由于DepartmentsList.ascx只包含其中一分类型,所以为只有一个实例的控件使用外观并不会带来多大的好处。因此,我们在本章没有创建任何外观,但在本书的后面将会遇到,在那些值得使用它们的地方。
主题(theme)是许多CSS文件、外观和图片的集合。可以将多个主题添加到一个网站中,这样当需要修改网站的外观时,可以通过在设计时甚至在运行时改变活动的主题来实现。
在下面这个练习中,将创建一个新的名为BalloonShopDefault的主题,然后在主题中添加一个CSS文件,用来显示门类列表。
练习:准备样式
1) 在Solution Explorer中右击根目录,并选择Add Folder(添加ASP.NET文件夹)→Theme Folder(主题)。将新文件夹的名字设置为BalloonShop Default,如图3-16所示。
2) 在Solution Explorer中右击BalloonShopDefault,并选择Add New Item。在模板窗口中,选择Style Sheet并命名为BalloonShop. css。点击Add按钮。
3) 在Solution Explorer中双击BalloonShop.css打开它。删除里面的内容,并把这些样式添加到里面:
4) 最后,打开web.config,启用默认主题:
解析:使用主题
用一个集中的地方来存储样式信息,可以使你更容易改变网站的外观,而且不用修改任何一行代码。
此时,BalloonShop.css包含少许样式,是为了显示门类列表所需的。这些样式涉及门类名称在未选择时(未选择但鼠标划过它们时)和已选择时的样子。CSS文件和外观文件都将添加到默认主题中,并在web.config中启用它,因此在网站的任何地方都引用它们。
在此,介绍一个有价值的东西,Visual Web Developer中有内建的CSS文件编辑器。当Balloon- Shop.css在编辑模式打开时,右击其中的一个样式,点击Build Style(创建样式)菜单选项。将看到如图3-17所示的对话框,提供了一种可视化的样式编辑方式。
图3-17 在Visual Web Developer中编辑样式
3.6.2 显示门类
现在万事俱备,只欠DepartmentsList用户控件这个东风了。这个用户控件中包含了用来生成门类列表的DataList控件。
在这个练习中,将用Visual Web Developer设计视图实现大部分功能,同时可以看到控件所产生的HTML代码。而在其他的练习中,都将直接在源码视图中进行操作。
练习:创建DepartmentsList.ascx
1) 首先,在UserControls文件夹中创建一个新的Web用户控件。右击UserControls文件夹,选择Add New Item,在模式中选择Web User Control(Web用户控件)并命名为DepartmentsList. ascx(或只写DepartmentsList)。然后钩上Place Code in separate file(将代码放在单独的文件中)可选框,确认语言是Visual C#,点击Add。
2) 将DepartmentsList.ascx切换到设计视图。确保工具箱是可见的(Ctrl+Alt+X),打开Data标签页,双击DataList实体。这样就把DataList控件添加到了DepartmentsList.ascx中。
3) 使用Properties(属性)窗口(如图3-18所示)修改DataList中如表3-3所示的属性。
表3-3 设置DataList属性
属性名称 |
值 |
(ID) |
list |
Width |
200px |
CssClass |
DepartmentListContent |
HeaderStyle-CssClass |
DepartmentListHead |
4) 在设计视图中打开DepartmentsList.ascx,在DataList上点击鼠标右键,选择Edit Template(编辑模板)→Header and Footer Templates(页眉和页脚模板)。
5) 在Header Template中输入Choose a Department。
6) 在DataList控件上点击鼠标右键,然后选择Edit Template(编辑模板)→Item Templates(项模板)。
7) 从Toolbox(工具箱)中的Standard(标准)标签选HyperLink控件,添加到ItemTemplate中。
8) 将HyperLink的Text属性设置为空字符串。然后这个List控件看起来就如图3-19所示。
图3-18 改变DataList控件的名称 |
图3-19 编辑DataList模板 |
9) 切换到源代码视图,在此需要对HyperLink控件做几处修改。以下就是与DataList控件相关的全部代码:
10) 打开用户控件的后台代码文件(DepartmentsList.ascx.cs),然后将Page_Load事件处理程序修改为:
11) 在设计视图中打开BalloonShop.master。从Solution Explorer中将DepartmentsList.ascx拖到文本信息“List of Departments”边上。然后从该单元将文本删除,只留下用户控件,如图3-20所示。
12) 练习的最后将创建Catalog.aspx,它是门类列表中链接所引用的页面。在Solution Explorer中右击项目名称并选择Add New Item。选择Web Form模板,设其名称为Catalog.aspx,确认钩上了Place Code in separate file(将代码放在单独的文件中)和Select Master Page(选择母版页)两个可选框,并点击Add按钮。当询问使用哪个母版页文件时,选择Balloonshop.master。
13) 在源码视图中打开Catalog.aspx,将它的标题修改为“BalloonShop – The Product Catalog”:
图3-20 将门类列表添加到母版页
14) 按F5键执行项目(如图3-21所示)。然后选择其中一种门类。
图3-21 执行BalloonShop项目
注解 如果这时有错误出现,那么不是键入了不正确的代码就是问题出在了连接SQL Server上。回顾3.5.1节的“配置SQL Server安全”。
解析:DepartmentsList用户控件
DepartmentsList Web用户控件的核心是DataList控件,就是用于生成门类列表的。为了让DataList工作,至少需要编辑它的ItemTemplate属性。也可编辑它的HeaderTemplate属性。
模板既可以在设计视图模式也可以在源码视图模式中编辑。在设计视图中编辑会简单些,但直接编辑HTML的功能更强大,可以更改在设计视图中无法更改的事。
生成门类列表的DataList控件,是灵活可配置的。配置控件最重要的步骤是设置它的ItemTemplate属性。当DataList绑定了一个数据源时,ItemTemplate会为数据源的每一行生成一个数的数据列表。在本例中,DataList对象在ItemTemplate中包含有一个HyperLink控件,因此在从数据库返回的每条记录中都会有一个超链接。理解数据是如何绑定过程是很重要的。让我们看一下HyperLink的代码:
这段代码主要将针对数据源中获取的每一行数据,生成一个形如http://webserver/Catalog. aspx?DepartmentID=XXX的链接。在我们的例子中,数据源就是一个DataTable,它包含每个门类的DepartmentID、Name和Description。这些详细信息是使用Eval)函数进行解析的。例如,Eval(”Name”)将返回由DataList处理的每行的Name字段值。
提示 在ASP.NET的以前版本中,代码看起来会更多些,如:原来要用DataBinder.Eval(Container.DataItem,”Name”),现在则可用Eval(”Name”)。引用这种新格式在编程方面会更简单些。
或许在这段创建超链接的代码中,最有趣的细节是设置CssClass的方法。如果你对三元操作熟悉,那这段代码的意思就不难理解了。如果从数据源读取的行中DepartmentID值与查询字符串的DepartmentID值是一样,则将把CssClass设置为DepartmentSelected,否则设置为DepartmentUnselected。这样当访问者点击了某门类后,页面将采用一个新的查询字符串重新加载,这时在列表中所选的门类会使用和其他门类不一样的样式。
提示 三元操作格式为condition ? value1 : value2。如果条件为真,将返回value1,否则,返回value2。这个表达式也可以这样写:
回到DataList控制,最为重要的是要知道它可以通过更多的模板来定制它的外观,它使用的XML模式如下所示:
最后一个可能存在疑问的就是写在DepartmentsList.ascx.cs中,实现门类列表的C#代码。当加载数据列表时会触发Page_Load事件,即从业务层获取门类列表,并将该列表绑定到DataList中:
令人兴奋的是,有了前面创建的如此强大的业务层代码,现在只需几行代码就能够生成一个DataList控件,不是吗?记住,现在是在表示层中工作。CatalogAccess类和GetDepartments方法是如何实现的并不用关心。现在只要知道GetDepartments返回的是(DepartmentID,Name)值对列表。在表示层上,不必关心Catalog.GetDepartments是如何实现预期功能的。
3.7 添加自定义错误页面
现在,在本章最后一节中,将为BalloonShop添加最后一个错误处理功能。
到此为止,唯一实现错误处理的代码是在数据层中。数据访问代码不知道错误是否严重,或是否可以简单地忽略,因此,其唯一的目标是保证数据库连接能正常关闭,并把错误报告给管理员。然后重新抛出异常,并把它传递到体系架构的上一层,以决定异常要如何处理。
问题是,如果这个错误在哪里都没有被正确处理,它将会产生一个丑陋的错误信息展现给访问者,你并不希望它发生。
在接下来的练习中,你将
l 为网站添加一个自定义错误页面,当发生一个未处理的错误时,访问者将看到这个错误页面。该页面将会友好地请访问者稍后再访问。
l 再一次报告错误,这样管理员知道访问者看到了严重错误,应尽快进行处理。
添加自定义错误页面是非常简单的任务,只需创建一个简单Web窗体并在web.config中把它配置为默认的错误页面。报告未处理的错误相当简单,只需使用一个类名为Global Application的类。按下面练习的步骤,将一切付诸实践。
练习:添加自定义错误页面及报告未处理错误
1) 在Solution Explorer中右击项目,然后选择Add New Item。
2) 选择Global Application Class(全局应用程序类)模板并点击Add按钮。
3) 将Application_Error修改为:
4) 在Solution Explorer中双击web.config,把下面的元素作为<system.web>元素的子元素加上:
注解 这样改变后,当出现未处理的异常抛出时,将传送Oooops.aspx给远程客户。但是,在本机中,仍然会收到错误信息的细节。如果想在本地也看到与访问者同样的错误信息,把mode设置为On而不是RemoteOnly。
5) 在应用程序根节点中添加一个新的Web窗体,并命名为Oooops.aspx,并使用BalloonShop. master母版页。
6) 在源码视图中,修改页面的标题,并将以下内容添加到内容占位符中:
解析:错误报告
立刻运行网站,没有出现任何问题,看起来一切正确。假设遇到一个错误,将不再显示默认的错误页面(当然,这对于顾客而言不够漂亮),而是显示一个漂亮的错误信息页面。为了进行一个简单的测试,配置应用程序,使其向你显示的也是相同的错误页面,不仅仅只针对访问者(在练习中已经说过应该如何做)。记住要去掉这个选项的设置,因为在创建和调试应用程序时需要异常的细节。
现在,执行这个项目,点击一个门类,添加一些字符给DepartmentID,像这样:
试图加载这个页面就会产生一个异常,因为department ID是数值型,在送给数据库前业务层代码会试图把它转换成整数(这是一个防止把假数据传给数据库的好办法)。
这时将会显示如图3-22所示的自定义错误页面。
图3-22 Oooops!
首先,异常会在数据层被捕获,但只是简单地报告一下,然后重抛出它。在数据层的报告生成的电子邮件内容类似于:
电子邮件中包含了所有与错误相关且有意义的细节。不过,错误还是被重新抛出,因为它在别处没有处理,只是最后在表示层被捕获到,显示友好错误页面。第二封邮件产生时,这时就要认真了,因为这个错误引发了访问者看到一个错误信息:
3.8 小结
当你在想已经学习了多少理论,并且有多少应用到BalloonShop项目时,这个冗长的章节是非常有价值的!在本章,完成了以下内容:
l 创建Department表,并添加了数据。
l 给数据库添加了一个存储过程,添加了从中间层中访问这个存储过程的相关代码,它使用一个特定的数据访问类。
l 给web.config添加了一些配置选项,如数据库连接字符串,使以后改变这些选项将变得更简单。
l 编写错误处理和报告代码,以使管理员知道网站发生的错误。
l 给网站添加了一个名为DepartmentList的Web用户控件。
在下一章中,将继续开发网站,以使其包含更多的功能。