IBuySpy 网上商店的设计和实现
简述:本文说明IbuySpy网上商店的设计和结构。HTTP://WWW.ASP.NET 网站提供了若干个基于.NET技术的实例,IbuySpy网上商店是其中一个例子,它是用ASP.NET技术构建网站的例子,可以供大家共享和借鉴。HTTP://WWW.ASP.NET提供了IBuySpy 网上商店的VB.NET和C#.NET两个语言版本。本文分析C#版本的实现。
读者可以从HTTP://WWW.ASP.NET下载英文版本的全部代码,执行所提供的安装程序,就可以在自己的机器上安装IBuySpy 网上商店实例。为了能够访问她的每个页面,要求读者的机器预先安装了支持.NET的IIS,还要安装了SQL SERVER。
在编写本文的过程中,阅读了IbuySpy白皮书,逐个阅读了IbuySpy的每张页面的文档说明,为了向自己的学生提供中文对照说明,将原来的英语文档全部翻译为中文文档。读者看到的内容就是根据HTTP://WWW.ASP.NET提供的英语文档编辑而成。
本文从3层架构的三个逻辑层面分别深入分析了IBuySpy 的实现,说明了ASP.NET关键特性和技术的应用方法。
图1 是IBuySpy 网上商店的页面布局。
图2 是IBuySpy 网上商店的首页(经过作者中文化后的页面)。
图2 IBuySpy 网上商店首页(default.aspx)。
IBuySpy 网上商店展示了ASP.NET 技术提供的许多特性。ASP.NET 技术提供的特性有:
· 支持Netscape和IE浏览器。网页显示风格在不同的浏览器保持一致。根据客户端浏览器的功能,自动实现程序逻辑在客户端和服务端分配和执行。
· 代码和HTML标签的分离。页面文件使用HTML标签、服务器控件标签、用户控件标签设计界面和布局。服务器控件丰富了网页可以选用的控件种类和功能。网页相关的代码放在分离的文件中,代码文件存放事件响应逻辑,预先把代码文件编译为程序集文件(.DLL文件),在服务端执行程序集中的代码,对客户端事件进行处理。
· 使用输出缓冲。加快页面提取的速度。.NET缓冲可以用于存放被经常提取的整个HTML网页文件,或者网页中一个组件的HTML内容,也可以保存从数据库提取的数据记录集。如果从缓冲区能够得到,就可以减少对网页编译、呈现,以及访问数据库所花费的重复时间。
· 使用3层架构设计网站。特别强调了在中间层和数据层采用ADO.NET 数据访问和SQL存储过程的技术。
· 使用数据库支持窗体(Forms)认证。对用户名/口令进行核对,认证客户的真实性。在客户端保存认证后的凭证信息。保证某些窗体只允许认证通过的客户访问。
· 用SOAP XML Web 服务支持直接通过客户端程序下订单和查询订购状态。
传统的分布式Web应用被设计成3层逻辑结构:
1. 数据访问层(Database Access Layer -DAL )
2. 商业逻辑层(Business Logic Layer -BLL)
3. 表现层(Presentation Layer)
数据访问层(DAL)与数据库有关。商业逻辑层(BLL) 与应用中的所有商业逻辑的操作有关。表现层与Web应用的页面有关,它是与客户交户界面。
使用.NET技术的分布式应用仍然依靠完善设计的三层逻辑架构。IBuySpy 网上商店使用了同样的逻辑结构。
l 数据层包括了数据库的模式(例如表格、表格与表格的联系的定义),和记录数据。
l 商业逻辑层主要为了减少应用程序的复杂性。IbuySpy的商业逻辑层更多地用数据库存储过程来实现。一组C#编写的类方法实现了与存储过程接口。这样实现的好处是:
如果表结构发生变化,只需要对存储过程作出修改。
如果访问数据的商业规则发生变化,只需要对存储过程作出修改,不必改写C#程序代码和重新编译。
数据访问代码与数据库之间交换的数据量精简到最少。
l 表现层是一组协同工作的由标签编写的页面文件。与页面有关的逻辑代码采用C#语言编写,处理客户的事件,并预先编译到一个名字空间的程序集(IbuySpy.DLL)中。
图3 说明了IBuySpy Store 总体架构。
商业逻辑层: |
表示层: |
数据层: |
.NET组件,实现了中间层类和方法。使用ADO.NET访问数据库(许多方法与存储过程直接接口),完成表示层与数据层的数据交换。 |
SQL SERVER 数据库存储过程完成中间商业逻辑过程。存储过程直接访问数据表。 |
创建数据模型
按照软件工程生命周期的划分,设计一个Web数据库应用程序,在概念和逻辑设计阶段,可以先从数据模式开始设计,首先识别所需要的表格、表格的字段结构、表格之间的联系,确定每张表格的主键和必要的外键。它将为后续的代码设计铺平道路。在数据库逻辑设计阶段,IBuySpy Store 确定了7张表格的结构。
为了设计数据库模式,首先从需求阶段的案例分析入手。在IBUYSPY应用的需求中,要实现如下功能:具有多种查阅商品的方式,能够分类列表商品清单;客户面对商品列表,选中某个商品放入购物车;客户确认购物车,最后下订单;客户对指定商品发表评论,其他的客户都可以看到这个评论等等。提供某些附加的商品信息:例如“在所有客户的订单中,购买这件商品的同时还买了哪些商品”;“这一周哪些商品最好卖”。
根据以下的用户案例中的名词(下划线部分)识别出对象,进一步确定数据库的主要表。根据动词(双下划线部分)识别出存储过程:
1. 新 客户 的 登记 向网上用户开放。
2. 客户 可以 添加 指定的 商品 到 购物车。
3. 客户 可以 发表 对指定 商品 的 评论。
4. 客户 可以从分类目录 查阅 商品清单。
5. 验证 客户 的身份,确认 购物车,提交 订单。
6. 客户 可以修改 购物车 中选购商品的种类和数量。
7. 客户 可以查询 购物车 中的内容。
8. 验证 通过后的 客户,允许 查询 订单的历史。
从这些案例中的主要的名词,识别出这样一些对象:
· 客户
· 商品
· 购物车
· 评论
· 分类目录
· 订单
经过数据设计阶段后,以上对象作为表格,从而产生数据库的主要模式结构。每个表的结构,每个表的主键,表的外键参照关系,如图4所示:
图4 数据库模式
从上述案例中的主要的动词,识别出这样一些操作方法(仅列举部分):
· 登记 客户
· 客户 登录
· 添加 商品到购物车
· 发表 对指定商品的评论
· 查阅 分类目录
· 根据购物车 提交 订单
IBuySpy 使用存储过程封装了所有对表格数据的操作。存储过程清楚地划分了数据库表格和中间数据访问组件的界线。这样实现带来的好处是维护的方便,数据库模式的改变只需对存储过程代码修改,对中间层数据访问组件是看不见的。此外使用存储过程还带来性能的提升,因为第一次运行存储过程时进行了优化,可供以后运行时重复调用;而且数据层与中间层组件之间的数据交换精简到必须传递的那一部分。
IBuySpy Store的“ShoppingCartAddItem” 存储过程是其中一个存储过程的例子。ShoppingCart表保存客户购物车数据。该存储过程负责向客户购物车(用客户购物车标识码CartID表示)追加一条记录,登记客户订购的一件商品的编号ProductID和数量,如果该商品已经在表格中,那么把新购买数量加到原有数量上。
CREATE Procedure ShoppingCartAddItem
(
@CartID nvarchar(50),
@ProductID int,
@Quantity int
)
As
Declare @CountItems int
SELECT @CountItems= Count(ProductID)
FROM ShoppingCart
WHERE ProductID= @ProductID AND CartID= @CartID
IF @CountItems > 0 /* 商品已经在该购物车- 修改数量*/
UPDATE ShoppingCart
SET Quantity= (@Quantity + ShoppingCart.Quantity)
WHERE ProductID= @ProductID AND CartID= @CartID
ELSE /* 向购物车新增一项。增加一条新记录*/
INSERT INTO ShoppingCart
(CartID, Quantity, ProductID)
VALUES
(@CartID, @Quantity, @ProductID)
GO
程序1 ShoppingCartAddItem存储过程
IBuySpy Store 使用中间组件在ASP.NET Web 页面与SQL Server 数据库之间通信。本节说明数据访问组件的实现。
中间组件(IBUYSPY名字空间) |
表示层: |
数据层: |
中间层组件的类一般与表对应。类方法调用存储过程,通过存储过程访问表。 |
SQL SERVER 数据库存储过程完成中间商业逻辑过程。存储过程直接访问数据表。 |
IBuySpy 名字空间
设计数据访问组件的基本思想是以数据为中心,首先创建数据库模式。通过数据访问组件访问数据库的数据。数据访问组件是类的集合,这些类的方法直接调用存储过程,存储过程再直接访问表。按照组件访问的表划分为如下类。这些类放在各自的源代码文件中。
· CustomersDB .CS文件定义CustomersDB类(CustomersDetails类)-访问Customers表
· ProductsDB .CS文件定义ProductsDB类(ProductsDetails类)-访问ProductsDB表
· ShoppingCartDB .CS文件定义ShoppingCartDB类-访问ShoppingCartDB表
· ReviewsDB .CS文件定义ReviewsDB类-访问ReviewsDB表
· OrdersDB .CS文件定义OrdersDB类(OrderDetails类)-访问Orders表
在 Microsoft .NET,可以把组件组织到名字空间中。IBuySpy store,创建了包含所有上述类的名字空间“IbuySpy”, “IBuySpy” 名字空间包含的组件结构显示在图6:
图6 IBuySpy 名字空间中组件的类
名字空间中的各个类不必在一个文件内定义。在 IbuySpy 中为每一个功能专用的类创建了一个源文件,并且把所有的源代码文件编译为一个程序集 IBuySpy.dll,该文件放在虚拟/BIN子目录中。
为了与SQL Server 建立连接,中间层组件需要与数据库的必要连接信息。连接字符串用来提供与数据库的连接信息。每次访问数据库前,都需要用连接信息与数据库建立连接。一种做法是在组件中用一个字符串常数保持这个连接串。如果连接串是变化的 (例如,数据库文件物理位置和口令变化等等) ,为了避免对组件重新编译,连接串可以放在Web应用配置文件web.config 中,这个文件按文本格式保存,可以随时修改。IBuySpy store把所有组件共用的连接串保存在web.config 文件的AppSettings 节 (web.config 文件位于Web虚根目录)。 图7是web.config 文件的AppSettings 节中保存的连接串。
<configuration>
<appSettings>
<add key="ConnectionString"
value="server=localhost;uid=sa;pwd=;database=store" />
</appSettings>
</configuration>
图7 web.config 文件的AppSettings 节
使用System.Configurations 名字空间的ConfigurationSettings类访问这个配置信息。 例如:
m_ConnectionString =
(String) ConfigurationSettings.AppSettings["ConnectionString"];
数据访问的公共模式
IBuySpy Store 使用SQL Server 的新的驱动程序提升性能。数据访问组件大多数按照非常相似的模式工作。这些模式化的过程如下:
1. 创建与数据库的连接对象;
2. 创建命令对象;
3. 设置命令对象的命令类型属性为存储过程;
4. 创建参数对象,并设置参数值。所有的参数对象加入命令对象的参数集合中;
5. 执行命令对象中的存储过程调用命令;
6. 从返回参数中取得返回值,或者取得返回的记录集合;
7. 关闭连接
例如:ProductsDB 类中的GetProducts() 方法如程序2所示:
using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
//ProductsDB 类:封装了对 IBuySpy Product 数据库表操作和查询。
namespace IBuySpy {
public class ProductsDB {
// GetProducts 方法返回 DataReader 对象,它提供指定商品类别的商品列表。
// SQLDataReader对象还保持 SQL 连接,DataReader 中数据全部绑定到控件后自动关闭连接。
public SqlDataReader GetProducts(int categoryID) {
// 创建连接和命令对象实例
SqlConnection myConnection = new
SqlConnection(ConfigurationSettings.AppSettings["ConnectionString"]);
SqlCommand myCommand = new SqlCommand("ProductsByCategory", myConnection);
// 标志命令类型是存储过程
myCommand.CommandType = CommandType.StoredProcedure;
// 创建存储过程的参数对象,添加到命令对象的参数集合中。
SqlParameter parameterCategoryID = new SqlParameter("@CategoryID",
SqlDbType.Int, 4);
parameterCategoryID.Value = categoryID;
myCommand.Parameters.Add(parameterCategoryID);
// 执行命令
myConnection.Open();
SqlDataReader result = myCommand.ExecuteReader
(CommandBehavior.CloseConnection);
// 返回 datareader 结果
return result;
}
//类的其它方法定义
……
}
程序2 ProductsDB 类中的GetProducts()方法
需要说明的是myCommand.ExecuteReader (CommandBehavior.CloseConnection ) 执行时,返回只读记录集合,该记录集合保持连接,记录指针只能向前进。当数据集合绑定到页面控件的DataSource属性后(数据读完后),记录指针到达尾部,自动关闭连接。这使得打开记录集合和读数据的操作在逻辑上变得简单,不必操心何时应该关闭连接。
该方法调用的存储过程:
CREATE Procedure ProductsByCategory
(
@CategoryID int
)
AS
SELECT
ProductID, ModelName, UnitCost, ProductImage
FROM
Products
WHERE
CategoryID = @CategoryID
ORDER BY
ModelName,
ModelNumber
GO
程序3 GetProducts()方法调用的存储过程ProductsByCategory
ADO.NET提供了两种客户端数据集合对象:DataReader 和 DataSet。数据访问最常见的问题之一是DataReader 和 DataSet有什么差异,“什么时候应该使用DataReader,什么时候又应该使用DataSet?”。
DataReader 提供数据的仅向前浏览的只读客户端视图。而且读数据期间保持连接。因为 DataReaders 仅前进,读数据时非常快,因此用于许多场合。
读数据期间保持连接以及仅前进读数据也带来了一些限制。例如,不增加代码,用DataReader无法对数据分页和前后翻页操作,也无法对数据排序。如果希望使用ASP.NET 缓存保存数据集合,供多个页面使用,就不能使用DataReader,此时应该使用DataSet。此外,如果需要在组件层之间传递数据集合,或者使用数据前需要对包含的数据修改,必须使用 DataSet。
IBuySpy 的页面普遍使用DataReader。因为,IBuySpy store中大多数页面以只读的方式使用数据,有时对访问数据的速度更感兴趣。但是,DataReader的只进功能对网页功能有一些限制。
五 Web 窗体设计
使用ASP.NET Web 窗体基类构造IBuySpy Store 的表示层。本节说明如何使用ASP.NET 实现IBuySpy Store 的主要页面功能。
大多数Web 应用,用户认证都是一项基本功能。ASP.NET 提供了多种用户认证模式。如以下的Login.aspx源代码所示 ,IBuySpy Store使用了窗体认证(FormsAuthentication)。
“安全和部署” 一节对安全认证的配置详细说明。
程序4 显示了Login.aspx.CS文件完成客户认证的代码。
namespace IBuySpy {
public class Login : System.Web.UI.Page {
……
// LoginBtn_Click 事件处理程序把客户提供的用户名/口令与数据库中的比较,验证客户。
//
// 如果用户名/口令与数据库的一致,把用户信息加入客户的Cookie 可以让主页显示对客户的欢迎信息。
// 把客户的临时购物车的选购商品信息转移到永久购物车(客户永久购物车用客户ID标识),然后重定向
// 到原来的页面。
// 未登陆客户使用临时购物车,临时购物车用临时创建的GUID标识。
// 客户永久购物车使用客户编号标识。
private void LoginBtn_Click(object sender, System.Web.UI.ImageClickEventArgs e) {
// 如果页面上的所有输入项有效,开始验证。
if (Page.IsValid == true) {
// 保存购物车标识(ShoppingCartID)。
IBuySpy.ShoppingCartDB shoppingCart = new IBuySpy.ShoppingCartDB();
String tempCartID = shoppingCart.GetShoppingCartId();
// 使用 CustomersDB 验证用户的Email地址和口令。
IBuySpy.CustomersDB accountSystem = new IBuySpy.CustomersDB();
String customerId = accountSystem.Login(email.Text, password.Text);
if (customerId != null) {
// 把临时购物车的商品转移到永久购物车。
shoppingCart.MigrateCart(tempCartID, customerId);
// 查询客户的账户细节信息,取得客户名字。
IBuySpy.CustomerDetails customerDetails=accountSystem.GetCustomerDetails(customerId);
// 在客户cookie中保存客户名字。
Response.Cookies["IBuySpy_FullName"].Value = customerDetails.FullName;
// 如果客户选择了 "保存我的登录信息" 复选框,在 cookie 中保存登录信息一个月。
if (RememberLogin.Checked == true) {
Response.Cookies["IBuySpy_FullName"].Expires = DateTime.Now.AddMonths(1);
}
// 转向到原来的页面。同时,context.User.Identity.Name记录customerId。
// 如果RememberLogin.Checked为真,还在客户cookie中加密形式保存customerId。
FormsAuthentication.RedirectFromLoginPage(customerId, RememberLogin.Checked);
}
else {
Message.Text = "请再一次检查您的登录信息!";
}
}
……
}
}
程序4 Login.aspx.CS中Login .LoginBtn_Click()方法
Login .LoginBtn_Click()方法调用ShoppingCartDB.GetShoppingCartId() 方法,取得客户购物车标识。
程序5 显示ShoppingCartDB.GetShoppingCartId() 方法源代码。
namespace IBuySpy {
// ShoppingCartDB 类 封装了对IBuySpy 购物车添加/删除/更新/购买的操作方法。
public class ShoppingCartDB {
// GetShoppingCartId 方法返回客户购物车的ID 键。
// ShoppingCartID 的值,可能是用户标识(如果是登录过的和认证过的用户),
// 随机的 GUID用于标识没有登录的客户的购物车。
public String GetShoppingCartId() {
// 得到HTTP请求的上下文HttpContext 。
System.Web.HttpContext context = System.Web.HttpContext.Current;
// 如果客户已经登录,使用客户的customerId 作为永久购物车标识ID。
if (context.User.Identity.Name != "") {
return context.User.Identity.Name;
}
// 如果客户没有登录,使用临时购物车标识ID。
if (context.Request.Cookies["IBuySpy_CartID"] != null) {
// 客户临时购物车标识已经产生。
return context.Request.Cookies["IBuySpy_CartID"].Value;
}
else {
// 使用System.Guid 类创建一个随机GUID。
Guid tempCartId = Guid.NewGuid();
// 把临时CartId 保存到客户的 cookie。
context.Response.Cookies["IBuySpy_CartID"].Value=tempCartId.ToString();
// 返回 tempCartId
return tempCartId.ToString();
}
}
}
}
程序5 ShoppingCartDB.GetShoppingCartId() 方法源代码。
Login.LoginBtn_Click()方法调用CustomersDB. Login() 方法,使用客户的电子邮件地址和口令查询数据库表格。如果找到记录,返回该客户编号。
程序6 显示CustomersDB. Login() 方法源代码。
namespace IBuySpy
{
// CustomerDetails 类 封装了IBuySpy Customer数据库表中客户信息。
public class CustomerDetails {
public String FullName;
public String Email;
public String Password;
}
// CustomersDB 类 封装了向Customer 数据库表添加/登记/查询客户的操作。
public class CustomersDB {
// CustomersDB.Login() 方法:拿客户数据库中保存的信息验证 email地址/口令 对。
// 如果 email地址/口令对有效,该方法返回客户的 "CustomerId"
// 否则返回null值。
public String Login(string email, string password) {
// 创建连接和命令实例
SqlConnection myConnection=new SqlConnection(ConfigurationSettings.AppSettings["ConnectionString"]);
SqlCommand myCommand = new SqlCommand("CustomerLogin", myConnection);
// 命令类型是存储过程
myCommand.CommandType = CommandType.StoredProcedure;
// 向命令对象添加存储过程的参数对象
SqlParameter parameterEmail = new SqlParameter("@Email",SqlDbType.NVarChar, 50);
parameterEmail.Value = email;
myCommand.Parameters.Add(parameterEmail);
SqlParameter parameterPassword = new SqlParameter("@Password", SqlDbType.NVarChar, 50);
parameterPassword.Value = password;
myCommand.Parameters.Add(parameterPassword);
// 向命令对象添加存储过程的返回参数对象
SqlParameter parameterCustomerID = new SqlParameter("@CustomerID", SqlDbType.Int, 4);
parameterCustomerID.Direction = ParameterDirection.Output;
myCommand.Parameters.Add(parameterCustomerID);
// 打开连接,执行命令。
myConnection.Open();
myCommand.ExecuteNonQuery();
myConnection.Close();
// 取得返回值
int customerId = (int)(parameterCustomerID.Value);
if (customerId == 0) {
return null;
}
else {
return customerId.ToString();
}
}
}
}
程序6 CustomersDB. Login() 方法源代码
登录过程首先保存购物车标识号,以便可以把合法登录用户的临时购物车内容放入永久购物车中。然后,用户的登录信息就与数据库内的用户信息进行比较,如果是注册用户,用户的详细信息就被提取了出来,并保存到cookie 供面向个人服务。然后,把客户临时购物车的信息转移到永久购物车。如果客户指定“remember login”, cookie 中的用户信息就有一个月有效期,否则这次会话结束后cookie中的信息作废。完成这些步骤后,调用RedirectFromLoginPage 方法,向客户端发认证凭据,并转向客户要访问的页面。认证凭据是客户编号的加密形式,保存在客户端cookie,HTTP请求的上下文HttpContext取得该客户的认证凭据,放在context.User.Identity.Name中,作为访问其它受保护页面的通行证。
数据库 |
IBUYSPY名字空间(IBUYSPY.DLL) |
LOGIN.ASPX页面窗体 |
存储过程:CustomerLogin |
LOGIN.ASPX.CS:Login.LoginBtn_Click(Sender,e) |
ShoppingCartDB.CS:
|
CustomersDB.CS:
|
ShoppingCartDB.CS: ShoppingCartDB.
|
CustomersDB.CS: CustomersDB. |
存储过程:CustomerDetail |
存储过程:ShoppingCartMigrate |
表:ShoppingCart |
表:Customers |
ASP.NET 服务器控件集合用来提高代码封装性和编码效率。有三类服务器控件:HTML 控件、 窗体控件和数据绑定列表控件。可以如下容易地定义服务器控件的实例:
<asp:Label id="MyLabel" class="MyLabelText" runat="server" />
“id” 属性标识服务器控件,“runat” 属性说明它是服务器控件。Class表示服务器控件使用CSS来定制显示格式。
HTML 控件用在原来使用HTML标记开发的页面,可以指定在服务器端运行事件处理代码。窗体和数据绑定列表控件在新的Web页面中使用。
商品列表页面是使用数据绑定控件的例子。在这个页面中有一个DataList 控件,显示数据源即商品的信息。DataList 含有一些模版,使开发者可以控制数据在控件中显示的样式。
除了简单显示源数据外,DataList 提供了丰富的功能,增强编程效率。在商品列表页面,设置DataList的 RepeatColumns属性为2,用两列显示商品。ProductsList.aspx 中DataList 的页面代码如下:
<%@ Page Language="c#" CodeBehind="ProductsList.aspx.cs" AutoEventWireup="false" EnableViewState="false" Inherits="IBuySpy.ProductsList" %>
// 根据查询串参数值CategoryID不同,各页面在缓存区保留100分钟,
// 这样减少了提取页面时访问数据库的次数。
<%@ OutputCache Duration="6000" VaryByParam="CategoryID" %>
……
<asp:datalist id="MyList" runat="server" RepeatColumns="2">
<ItemTemplate>
<TABLE width="300" border="0">
<TR><TD vAlign="middle" align="right" width="100">
<A href='ProductDetails.aspx?productID=
<%#DataBinder.Eval(Container.DataItem, "ProductID") %>'>
<IMG height=75 src='ProductImages/thumbs/<%#
DataBinder.Eval(Container.DataItem, "ProductImage") %>'
width=100 border=0>
</A></TD>
<TD vAlign="middle" width="200">
<A href='ProductDetails.aspx?productID=<%#
DataBinder.Eval(Container.DataItem, "ProductID") %>'>
<SPAN class="ProductListHead">
<%# DataBinder.Eval(Container.DataItem, "ModelName") %></SPAN><BR>
</A>
<SPAN class="ProductListItem"><B>单价: </B>
<%# DataBinder.Eval(Container.DataItem, "UnitCost", "{0:c}") %>
</SPAN><BR>
<A href='AddToCart.aspx?productID=
<%# DataBinder.Eval(Container.DataItem, "ProductID") %>'>
<SPAN class="ProductListItem">
<FONT color="#9d0000"><B>加入购物车<B></FONT></SPAN>
</A></TD></TR>
</TABLE>
</ItemTemplate>
</asp:datalist>
程序7 ProductsList.aspx 中DataList 控件代码
ProductsList.aspx.cs中的Page_Load代码执行页面装载的初始化,从数据库商品表格提取指定类别的商品清单。
namespace IBuySpy {
public class ProductsList : System.Web.UI.Page {
protected System.Web.UI.WebControls.DataList MyList;
……
// Page_Load 事件处理程序从查询字符串取得商品类别参数。
// 从数据库取得指定商品类别中的所有商品清单。
// 然后把商品列表绑定到模板化控件 asp:datalist。
private void Page_Load(object sender, System.EventArgs e) {
// 从查询字符串取得商品类别参数(categoryId)
int categoryId = Int32.Parse(Request.Params["CategoryID"]);
// 取得商品记录集合,绑定到asp:datalist 控件。
IBuySpy.ProductsDB productCatalogue = new IBuySpy.ProductsDB();
MyList.DataSource = productCatalogue.GetProducts(categoryId);
MyList.DataBind();
}
……
}
}
程序8 ProductsList.aspx.cs中的Page_Load源代码
ProductsDB.GetProducts()方法调用存储过程,从数据库商品表格提取指定类别的商品清单,以只读数据集和SqlDataReader对象返回。
namespace IBuySpy {
//ProductsDB 类:封装了对 IBuySpy Product 数据库表操作和查询逻辑。
public class ProductsDB {
//ProductsDB.GetProducts()方法:返回DataReader对象,它提供只读前进的指定类别的商品列表。
public SqlDataReader GetProducts(int categoryID) {
// 创建连接和命令对象实例
SqlConnection myConnection=new SqlConnection(
ConfigurationSettings.AppSettings["ConnectionString"]);
SqlCommand myCommand = new SqlCommand("ProductsByCategory", myConnection);
// 标志命令类型是存储过程
myCommand.CommandType = CommandType.StoredProcedure;
// 添加存储过程的参数
SqlParameter parameterCategoryID = new SqlParameter("@CategoryID",
SqlDbType.Int, 4);
parameterCategoryID.Value = categoryID;
myCommand.Parameters.Add(parameterCategoryID);
// 执行命令
myConnection.Open();
SqlDataReader result = myCommand.ExecuteReader(
CommandBehavior.CloseConnection);
// 返回 datareader 结果
return result;
}
}
}
程序9 ProductsDB.GetProducts()方法源代码
数据库 |
IBUYSPY名字空间(IBUYSPY.DLL) |
ProductsList.aspx页面窗体,窗体装载事件。 |
ProductsList.aspx.cs:ProductsList.Page_Load(Sender,e) |
ProductsDB.CS: ProductsDB.GetProducts(categoryId)
|
存储过程:ProductsByCategory |
表:Products |
用户控件是在页面中嵌入的模块,它封装了页面标签代码,通过标签源代码重用提高页面编程效率。用户控件与传统的服务端Includes相似,而且功能更强和更灵活。用户控件可以包含属性和封装的代码逻辑,并暴露给所在的Web 窗体。开发者可以在不同的页面中重用用户控件。
IBuySpy Store 有一些在整个应用的各个页面中重用的用户控件。菜单用户控件就是其中之一。菜单用来显示商品类别的列表。我们使用DataList 绑定控件实现菜单用户控件,客户点击某个菜单项,超链接到显示指定类别中的商品清单的页面,链接串为:“ProductsList.aspx?categoryId=商品类别编号”。
程序10是_Menu.ascx文件标签源代码。
<%@ Control Language="c#" CodeBehind="_Menu.ascx.cs" AutoEventWireup="false" Inherits="IBuySpy.C_Menu" %>
<%@ OutputCache Duration="3600" VaryByParam="selection" %>
<%--
本用户控件创建数据库中商品类别菜单。它构成了页面左边的商品目录超链接。
--%>
<table cellspacing="0" cellpadding="0" width="145" border="0">
……
<asp:DataList id="MyList" runat="server" cellpadding="3" cellspacing="0" width="142px" SelectedItemStyle-BackColor="dimgray" EnableViewState="false" Height="65px">
<SelectedItemStyle BackColor="DimGray"></SelectedItemStyle>
<SelectedItemTemplate>
<asp:HyperLink id=HyperLink2 runat="server" NavigateUrl='<%# "productslist.aspx?CategoryID=" + DataBinder.Eval(Container.DataItem, "CategoryID") + "&selection=" + Container.ItemIndex %>' Text='<%# DataBinder.Eval(Container.DataItem, "CategoryName") %>' cssclass="MenuSelected">
</asp:HyperLink>
</SelectedItemTemplate>
<ItemTemplate>
<asp:HyperLink id=HyperLink1 runat="server" NavigateUrl='<%# "productslist.aspx?CategoryID=" + DataBinder.Eval(Container.DataItem, "CategoryID") + "&selection=" + Container.ItemIndex %>' Text='<%# DataBinder.Eval(Container.DataItem, "CategoryName") %>' cssclass="MenuUnselected">
</asp:HyperLink>
</ItemTemplate>
</asp:DataList>
……
</table>
程序10 _Menu.ascx文件标签源代码
数据库 |
IBUYSPY名字空间(IBUYSPY.DLL) |
<%@ Control Language="c#" CodeBehind="_Menu.ascx.cs" Inherits="IBuySpy.C_Menu" %> …… |
_Menu.ascx.cs:C_Menu.Page_Load(sender, e) |
ProductsDB.CS:ProductsDB.GetProductCategories() (categoryId)
|
存储过程:ProductCategoryList |
表:Categories |
向Web 窗体使用Register指令添加用户控件。Register指令有三个属性:TagPrefix,指定页面中用户控件的唯一名字空间;TagName,指定用户控件的唯一名字;Src,指定用户控件文件名的虚路径。添加菜单用户控件的Register指令如下:
<%@ Register TagPrefix="IBuySpy" TagName="Menu" Src="_Menu.ascx" %>
向页面添加了Register指令后,接下来需要指定用户控件出现在页面的位置。通过向Web 窗体界面中添加用户控件标签即可。菜单控件的标签如下:
<IBuySpy:Menu id="Menu1" runat="server" />
购物车
DataGrid 控件具有丰富功能,其中包括在线编辑的功能。在DataGrid 控件中使用TemplateColumn ,并用另外一个ASP.NET 服务器控件(TextBox 或 CheckBox)嵌入其中。 图11显示了IBuySpy Store 购物车页面中使用DataGrid 控件的例子。
DataGrid 默认添加数据源(DataSource)的每一个列。设置DataGrid 的AutoGenerateColumns 属性为false,可以程序定制Columns 集合。购物车的 DataGrid 实现如下:
<%@ Page Language="c#" CodeBehind="ShoppingCart.aspx.cs" AutoEventWireup="false"
Inherits="IBuySpy.ShoppingCart" %>
……
<asp:DataGrid id="MyList" runat="server" Font-Names="Verdana" BackColor="White"
BorderWidth="1px" BorderStyle="None" BorderColor="#CC9966"
cellpadding="4" Font-Name="Verdana" Font-Size="8pt" ShowFooter="True"
HeaderStyle-CssClass="CartListHead" FooterStyle-CssClass="CartListFooter" ItemStyle-CssClass="CartListItem"
AlternatingItemStyle-CssClass="CartListItemAlt" DataKeyField="Quantity"
AutoGenerateColumns="False" HorizontalAlign="Center">
<SelectedItemStyle Font-Bold="True" ForeColor="#663399" BackColor="#FFCC66"></SelectedItemStyle>
<AlternatingItemStyle CssClass="CartListItemAlt"></AlternatingItemStyle>
<ItemStyle ForeColor="#330099" CssClass="CartListItem" BackColor="White"></ItemStyle>
<HeaderStyle Font-Bold="True" HorizontalAlign="Center" ForeColor="#FFFFCC" CssClass="CartListHead"
BackColor="#990000"></HeaderStyle>
<FooterStyle ForeColor="#330099" CssClass="CartListFooter" BackColor="#FFFFCC"></FooterStyle>
<Columns>
<asp:TemplateColumn HeaderText="商品 ID"><ItemTemplate>
<asp:Label id="ProductID" Text='<%# DataBinder.Eval(Container.DataItem, "ProductID") %>' runat="server">
</asp:Label></ItemTemplate></asp:TemplateColumn>
<asp:BoundColumn DataField="ModelName" HeaderText="商品名称"></asp:BoundColumn>
<asp:BoundColumn DataField="ModelNumber" HeaderText="商品型号"></asp:BoundColumn>
<asp:TemplateColumn HeaderText="选购数量"><ItemTemplate>
<asp:TextBox id=Quantity runat="server" Text='<%# DataBinder.Eval(Container.DataItem, "Quantity") %>'
Columns="4" MaxLength="3" width="40px"></asp:TextBox></ItemTemplate></asp:TemplateColumn>
<asp:BoundColumn DataField="UnitCost" HeaderText="单价" DataFormatString="{0:c}"></asp:BoundColumn>
<asp:BoundColumn DataField="ExtendedAmount" HeaderText="小计" DataFormatString="{0:c}"></asp:BoundColumn>
<asp:TemplateColumn HeaderText="删除">
<ItemTemplate><CENTER><asp:CheckBox id="remove" runat="server"></asp:CheckBox></CENTER></ItemTemplate>
</asp:TemplateColumn>
</Columns>
<PagerStyle HorizontalAlign="Center" ForeColor="#330099" BackColor="#FFFFCC"></PagerStyle>
</asp:DataGrid>
程序11 ShoppingCart.aspx中DataGrid 控件代码
购物车页面发生的客户事件,导致ShoppingCart.aspx.cs文件中ShoppingCart类方法的执行,调用ShoppingCartDB类方法调用存储过程,然后通过存储过程访问数据库的表格。
跨浏览器支持
典型的Web应用程序需要广泛支持各种浏览器,例如Internet Explorer 和 Netscape,以及低版本浏览器(例如,在Internet Explorer 4.0 和更高的版本上运行的应用程序)。用户界面要在Internet Explorer 和 Netscape Navigator 等浏览器中都保持一致是非常困难的。
需要记住几点:
1. 使用 CSS
2. 小心区分CSS class 名字的大小写– Navigator 6 在乎
3. Navigator要求在 inputs标签中使用Size属性
在所有目标浏览器上测试您开发的页面是很重要的。
性能
使用ASP.NET 的缓存可以大大提高性能。在 ASP.NET中有三种缓存类型,页面输出缓存、部分缓存和数据缓存。第一种,页面输出缓存,就是在服务器缓存中保存页面内容 (已经呈现为HTML的页面),直接从缓存中把页面返回给客户,在一段时间内不需要重新分析和呈现操作。 第二种,部分缓存,可以缓存页面中指定的一部分,并保存一段时间。实现的主要方法就是缓存用户控件的呈现结果。最后,数据缓存就是基于SESSION 和APPLICATION对象缓存从数据库提取的数据集合。
Products.aspx 使用页面缓存。它根据查询串参数CategoryID的不同取值,缓存不同的页面实例,缓存时间最多6000秒。在Products.aspx页面顶行,会看到下列激活页面缓存的指令。
<%@ OutputCache Duration="6000" VaryByParam="CategoryID" %>
Duration 属性要求Products.aspx页面实例缓存 6000 秒。
IBuySpy Store 的用户控件使用部分缓存。菜单用户控件是部分缓存的一个例子。在 _menu.ascx 页面顶行,会看到下列激活用户控件部分缓存的指令。
<%@ OutputCache Duration="3600" VaryByParam="Selection" %>
VaryByParam 允许缓存用户控件的不同实例。使用客户在商品类别列表中选择的序号确定不同实例,即可以根据公共属性的取值缓存用户控件的不同实例。这里,VaryByParam 设置为“Selection”,就是缓存菜单控件的多个实例 – 每个实例由该控件的公共属性Selection的值确定。
ASP.NET 提供了内置的基于窗体的认证系统,在web.config 的authentication节,指定login 页面。IBuySpy Store的authentication 节如下:
<system.web>
<authentication mode="Forms">
<forms name="IBuySpyStoreAuth"
loginUrl="login.aspx"
protection="All" path="/" />
</authentication>
</system.web>
以上配置确定了使用窗体安全认证,负责客户认证的页面文件是login.aspx。
然后为需要认证的每一页面加一个location 节,以下是“Checkout.aspx” 页面的例子:
<location path="Checkout.aspx">
<system.web>
<authorization>
<deny users="?" />
</authorization>
</system.web>
</location>
path 属性指定页面文件名,告诉系统只有拥有认证凭证的客户才可以访问该页面文件。<deny users=”?” /> 不允许所有匿名用户访问该页面。当未认证的用户试图访问该页面,立即被重定向到上面定义的login 页面。当用户提供了必要的登录信息后,login 页面对用户验证,通过后生成认证凭据保存在客户的上下文环境中,然后重定向到用户打算访问的页面:
FormsAuthentication.RedirectFromLoginPage(customerId,
RememberLogin.Checked);
程序12 login.aspx.CS中完成认证后,生成凭据并重定向页面的语句
Microsoft .NET Framework 提供的部署过程是引人注意的一大改进。过去,花费不多的工作可以部署数据库层和表现层,但是,需要在服务器登记才能部署组件。这带来许多麻烦,当您需要用到那些依赖于组件版本的应用和模块时,就会遭遇“DLL 地狱”问题。
Microsoft .NET 取消所有组件注册的办法,解决了这个麻烦。现在,组件(更正确地称为程序集)是自描述的,不需要在注册表保存程序集的描述信息。就是说,安装.NET 应用时,在应用的bin目录内简单地复制组件程序集即可。程序集内的元数据提供了所有自描述信息。
IBuySpy Store 应用的许多部分支持“web 农场”。就是说可以把应用安装在几台服务器上,用集群方式互相分配客户访问负载。此时,只需要构造配置文件的authentication节。
当您登录后,ASP.NET 认证系统创建客户凭证,保存在客户浏览器方cookie,供服务器读写验证。一旦通过了认证,如果您浏览了需要认证的页面,服务器从cookie中读到了认证凭证,您就被允许浏览页面。
从Web农场的场景看,用户可以在一个服务器上登录 (那个服务器在客户端创建cookie ),然后从另一个需要认证的服务器提取页面(该服务器还没有创建客户端cookie)。根据会话的负载平衡的设置,每当点击不同的Web服务器都需要重新认证。为了避免重新认证,必须修改IBuySpy Store 配置,以便在多个服务器之间共享cookie 中保存的认证凭据(或使用服务端上下文,转入另一个亲近的会话)。
默认情况下,对cookie中保存的认证凭据的解密键设置为 “autogenerate”,意味着根据服务器,自动随机产生解密键。即,服务器之间不能使用彼此的cookie。可以在Web 农场上的每一个服务器,设置配置文件web.config 中的decryptionKey 属性,都取相同键值。
<machineKey decryptionKey="213454ABE333321D" />
只要每个服务器的配置文件(web.config)设置的解密键一样,就可以实现认证凭据的共享。基于窗体认证的缺省保护(protection)级别也要从“All”改为 “None”。
<authentication mode="Forms">
<forms name="IBuySpyStoreAuth"
loginUrl="login.aspx" protection="None" />
</authentication>
Microsoft .NET 技术使分布式Web应用的开发大大简化。ASP.NET Web 窗体,缓存能力、简化的数据访问,加快了从商业需求转化为中间层组件和完整应用代码开发的过程。除了展示以上.NET 新技术之外,IBuySpy Store 还可以作为构造电子商务应用的代码模板。