开发BtoC电子商务系统(ASP.NET)
原文还有两个图
在对ASP.NET Web表单的编程模型有了基本的认识后,通过应用于现实的开发案例来提高对ASP.NET Web表单内在运作机制的了解,以及由此带来的对系统架构的掌控是很有必要的。我们没有为编程而编程的高贵姿态,我们深深懂得能够开发出高效,健壮,强大的应用程序始终是编程的终极。我们下面通过一个完整的BToC电子商务系统的开发流程来展示ASP.NET Web表单是怎样具体搭建面向下一代网络平台的。
这是一个典型的基于B/S(浏览器/服务器) 三层架构的食品,饮料电子商务零售系统——“玉米地零食店”。前端为产品浏览器,为消费者提供浏览/选购商品,下订单购物等各个环节的功能;中间层为销售商的税率,优惠等商务逻辑;后端为与整个零售系统相关的产品,顾客,订单等数据库。我们采用ASP.NET+IIS 5来构建前端和中间层,SQL Server 2000来管理后端数据库,整个系统运行于Windows XP。相关硬件配置只要满足上述软件的基本配置,系统性能便可保证。下面为该网上零售系统的前端界面图示:
在编制Web 表单商业前端和中间层之前,我们有必要对后端数据库做一个简单的介绍。后端数据库 CornfieldGrocer 由4个表组成:产品类别表Categories ,产品细节表 Details ,产品表 Products ,客户信息表Customers。考虑到演示系统的的简洁性,我们没有添加相关的存储过程,视图,规则等,这些在实际的系统的开发中对提高系统的性能是很有必要,尤其是在大数据量的情况下。下面为4个表的字段的图示介绍:
各个表的字段的表义已经相当清楚,我们不在这里赘述。我们下面向大家展示一下整个电子商务零售系统——“玉米地零食店”的物理文件组成及其结构,下图为示意图:
所有的文件位于ASP.NET站点目录CornfieldGrocer下,其中还有Web表单页面用到的图片子目录Images下的文件就不再在这里列出了。
下面我们开始编写前端和中间层代码,为了更清楚地展示Web Form ASP.NET的底层代码构造,我们采用记事本来完成整个代码的编写过程。需要说明的是在真正的工程项目开发实践中,如能借助Visual Studio.Net等可视集成开发工具,开发效率会大大提高。但在ASP.NET代码的底层机制没有谙熟的情况下,笔者强烈建议初期的开发学习不妨放在Windows系统自带的“记事本”这一简单却能够把代码暴露得相当清晰的工具里。
由于篇幅有限,我们不可能将所有的代码都在这里展示给大家。如前所述,web.config为每个站点级的基于XML的配置文件,负责一些ASP.NET的安全认证,编码选择,诊断测试等ASP.NET的配置工作,为浏览器请求ASP.NET Web表单时通过 IIS处理后的第一站。下面为其内容:
<configuration>
<system.web>
<globalization requestEncoding="UTF-8" responseEncoding="UTF-8" />
</system.web>
</configuration>
容易看到这里的配置内容相当简单,仅指定请求/发送的编码为“UTF-8”。我们对此不再赘述。
global.asax文件及其由后端代码文件global.asax.cs编译成的Bin\CornfieldGrocer.dll共同组成该网上零售系统的ASP.NET应用程序定义。我们先来看文件global.asax:
<%@ Application Inherits="CornfieldGrocer.Global" %>
该文件只有一行指示符,它表示ASP.NET应用程序的定义继承自Global类,而Global类正是在global.asax.cs文件中定义:
using System;
using System.Collections;
using System.ComponentModel;
using System.Web;
using System.Web.SessionState;
namespace CornfieldGrocer
{
public class Global : System.Web.HttpApplication
{
protected void Session_Start(Object sender, EventArgs e)
{
if (Session["ShoppingCart"] == null)
{
Session["ShoppingCart"] = new CornfieldGrocer.OrderList();
}
}
}
}
在Global类里,我们定义了区段(Session)意义下的购物卡(ShoppingCart)——这里采用了C#中的索引器。购物卡的类型为CornfieldGrocer命名空间中的OrderList类,在CornfieldGrocer.cs文件中有定义。我们当然也可以在global.asax文件中用脚本语言的形式将上面两个文件的内容合并起来,但那不是ASP.NET推荐的做法,因为脚本语言的第一次执行还要进行动态编译,这回损失一部分性能,而将CS文件提前编译成dll文件则会降低这种代价——当然这里的编译的意思还是指将CS的源代码文件编译成微软中间语言的过程。其次,页面与后端代码分离的原则易于项目管理,是Visual Studio.NET推荐的工程性的做法。
文件Default.aspx为整个网上零售系统的前端页面HTML代码,Default.aspx.cs为其后端控制Web表单行为的CS代码。由于篇幅关系我们这里不再赘述其HTML代码,实际上从前面给出的前端界面图示,我们可以基本了解Default.aspx的HTML代码结构。Style.css文件为Default.aspx文件的页面样式定义文件,定义一些页面元素的颜色,格式,间距等修饰性的东西,我们也不再多言。下面只向大家展示Default.aspx的页面指示符:
<%@ AutoEventWireup="false" Inherits="CornfieldGrocer.MainForm" %>
我们用“Inherits="CornfieldGrocer.MainForm"”来表示我们的页面继承自MainForm类,这样我们就实现了对ASP.NET Web 表单行为的控制代码与页面显示的HTML的分离。其中“AutoEventWireup="false"”表示页面事件非自动使能——页面事件非自动使能的意思是所有页面事件必须经过用户明确的操作才能触发,由于该属性缺省为“true”表示自动使能,但我们的商业逻辑要求非自动使能,故这里的语句很有必要,否则会引起系统处理的混乱。下面我们来看MainForm类:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
namespace CornfieldGrocer
{
public class MainForm: System.Web.UI.Page
{
protected System.Web.UI.WebControls.Label CurrentCategory;
protected System.Web.UI.WebControls.Label Name;
protected System.Web.UI.WebControls.Label SubTotal;
protected System.Web.UI.WebControls.ImageButton Imagebutton1;
protected System.Web.UI.WebControls.Label Description;
protected System.Web.UI.WebControls.Label Company;
protected System.Web.UI.WebControls.Repeater DetailsListing;
protected System.Web.UI.WebControls.DataList ProductListing;
protected System.Web.UI.WebControls.DataList ShoppingCartList;
protected System.Web.UI.HtmlControls.HtmlSelect CategoryList;
protected System.Web.UI.WebControls.Button btnSelect;
protected System.Web.UI.WebControls.Label Tax;
protected System.Web.UI.WebControls.Label Total;
protected System.Web.UI.WebControls.ImageButton Imagebutton4;
protected System.Web.UI.WebControls.ImageButton Imagebutton5;
protected System.Web.UI.WebControls.ImageButton Imagebutton6;
protected System.Web.UI.HtmlControls.HtmlInputText Qty;
protected System.Web.UI.HtmlControls.HtmlGenericControl CheckoutPanel;
protected System.Web.UI.HtmlControls.HtmlImage SelectedProdPicture;
public MainForm()
{
Page.Init += new System.EventHandler(Page_Init);
}
private void Page_Load(object sender, System.EventArgs e)
{
if (!IsPostBack)
{
ProductListing.SelectedIndex = 0;
UpdateProducts();
UpdateShoppingCart();
}
}
private void Page_Init(object sender, EventArgs e)
{
InitializeComponent();
}
private void InitializeComponent()
{
this.btnSelect.Click +=
new System.EventHandler(this.CategoryList_Select);
this.ProductListing.SelectedIndexChanged+=
new System.EventHandler(this.ProductListing_Select);
this.Imagebutton1.Click+=
new ImageClickEventHandler(this.AddBtn_Click);
this.Imagebutton4.Click+=
new ImageClickEventHandler(this.Recalculate_Click);
this.Imagebutton6.Click+=
new ImageClickEventHandler(this.ClearCart_Click);
this.Load +=
new System.EventHandler(this.Page_Load);
}
private void CategoryList_Select(Object sender, EventArgs e)
{
CurrentCategory.Text =
CategoryList.Items[CategoryList.SelectedIndex].Text;
UpdateProducts();
}
private void ProductListing_Select(Object sender, EventArgs e)
{
UpdateProducts();
}
private void AddBtn_Click(Object sender, ImageClickEventArgs e)
{
int productID = Int32.Parse
(ProductListing.DataKeys[ProductListing.SelectedIndex].ToString());
InventoryDB market = new InventoryDB();
DataRow product = market.GetProduct(productID);
CornfieldGrocer.OrderList shoppingCart =
((CornfieldGrocer.OrderList) Session["ShoppingCart"]);
shoppingCart.Add(new CornfieldGrocer.OrderItem(productID,
(String) product["ProductName"],
Double.Parse(product["UnitPrice"].ToString()), 1));
UpdateShoppingCart();
}
private void Recalculate_Click(Object sender, ImageClickEventArgs e)
{
CornfieldGrocer.OrderList shoppingCart =
((CornfieldGrocer.OrderList) Session["ShoppingCart"]);
for (int i=0; i<ShoppingCartList.Items.Count; i++) >
{
HtmlInputText qty =
(HtmlInputText) ShoppingCartList.Items[i].FindControl("Qty");
try
{
shoppingCart[(String) ShoppingCartList.DataKeys][i]].Quantity
= Int32.Parse(qty.Value);
}
catch (Exception)
{
}
}
UpdateShoppingCart();
}
private void ClearCart_Click(Object sender, ImageClickEventArgs e)
{
CornfieldGrocer.OrderList shoppingCart =
((CornfieldGrocer.OrderList) Session["ShoppingCart"]);
shoppingCart.ClearCart();
UpdateShoppingCart();
}
void UpdateProducts()
{
InventoryDB market = new InventoryDB();
int categoryID = Int32.Parse
(CategoryList.Items[CategoryList.SelectedIndex].Value);
ProductListing.DataSource =
market.GetProducts(categoryID).DefaultView;
ProductListing.DataBind();
int productID = Int32.Parse
(ProductListing.DataKeys[ProductListing.SelectedIndex].ToString());
DataRow product = market.GetProduct(productID);
Name.Text = product["ProductName"].ToString();
SelectedProdPicture.Src = product["ImagePath"].ToString();
Description.Text = product["ProductDescription"].ToString();
Company.Text = product["Manufacturer"].ToString();
DetailsListing.DataSource =
market.GetProductCalories(productID).DefaultView;
DetailsListing.DataBind();
}
void UpdateShoppingCart()
{
CornfieldGrocer.OrderList shoppingCart =
((CornfieldGrocer.OrderList) Session["ShoppingCart"]);
SubTotal.Text = String.Format("{0:C}", shoppingCart.SubTotal);
Tax.Text = String.Format("{0:C}", shoppingCart.Tax);
Total.Text = String.Format("{0:C}", shoppingCart.Total);
ShoppingCartList.DataSource=shoppingCart.Values;
ShoppingCartList.DataBind();
}
}
}
MainForm类中共有11个方法,19个保护域。其中的19个保护域和前面给出的前端界面图示的页面元素相对应,这里不再赘述。11个方法中MainForm()为构建器,其添加了页面初始化事件Page_Init(),这是ASP.NET Web表单最先处理的事件,一般进行一些基础的初始化操作。我们可以看到在Page_Init()中进行了初始化组件InitializeComponent()的操作。Page_Load()事件出现在用户发出请求后,页面装载的时候,在这里一般可做一些商业逻辑初始化方面的操作,比如数据库的连接,购物卡的初始化等。我们这里进行了产品展示UpdateProducts()和购物卡的初始化UpdateShoppingCart()的操作。
其他四个方法分别为产品类别的选择ProductListing_Select(),购买产品的添加AddBtn_Click(),购物卡的重新计算Recalculate_Click(),购物卡的清除ClearCart_Click()都是通过对ASP.NET控件的操作来触发相应的事件完成商业逻辑。上面的代码已经展示的相当清楚,我们不再赘述。
最后我们要向大家说明的是中间层商务逻辑的组件。它由三个类构成:库存数据类InventoryDB,订单项目类OrderItem和订单列表类OrderList。它们共同在文件CornfieldGrocer.cs文件中定义。自解释的编程方式已经它们的结构展示的相当清除,我们下面只给出该文件的CS源代码:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections;
namespace CornfieldGrocer
{
public class InventoryDB
{
public DataTable GetProducts(int categoryID)
{
SqlConnection sqlConnect= new SqlConnection
("server=(local);database=CornfieldGrocer;Trusted_Connection=yes");
SqlDataAdapter sqlAdapter1 = new SqlDataAdapter
("Select * from Products where categoryid="+categoryID,sqlConnect);
DataSet products = new DataSet();
sqlAdapter1.Fill(products, "products");
return products.Tables[0];
}
public DataRow GetProduct(int productID)
{
SqlConnection sqlConnect= new SqlConnection
("server=(local);database=CornfieldGrocer;Trusted_Connection=yes");
SqlDataAdapter sqlAdapter1 = new SqlDataAdapter
("Select * from Products where productID=" + productID, sqlConnect);
DataSet product = new DataSet();
sqlAdapter1.Fill(product, "product");
return product.Tables[0].Rows[0];
}
public DataTable GetProductCalories(int productID)
{
SqlConnection sqlConnect = new SqlConnection
("server=(local);database=CornfieldGrocer;Trusted_Connection=yes");
SqlDataAdapter sqlAdapter1 = new SqlDataAdapter
("Select * from Details where productID="+productID,sqlConnect);
DataSet details = new DataSet();
sqlAdapter1.Fill(details, "details");
return details.Tables[0];
}
}
public class OrderItem
{
public int productID;
public int quantity;
public String name;
public double price;
public OrderItem(int productID, String name, double price, int quantity)
{
this.productID = productID;
this.quantity = quantity;
this.name = name;
this.price = price;
}
public int ProductID
{
get { return ProductID; }
}
public int Quantity
{
get { return quantity; }
set { quantity=value; }
}
public String Name
{
get { return name; }
}
public double Price
{
get { return price; }
}
public double Total
{
get { return quantity * price; }
}
}
public class OrderList
{
private Hashtable orders = new Hashtable();
private double taxRate = 0.08;
public double SubTotal
{
get
{
if (orders.Count == 0)
return 0.0;
double subTotal = 0;
IEnumerator items = orders.Values.GetEnumerator();
while(items.MoveNext())
{
subTotal += ((OrderItem) items.Current).Price *
((OrderItem) items.Current).Quantity;
}
return subTotal;
}
}
public double TaxRate
{
get { return taxRate; }
set { taxRate = value; }
}
public double Tax
{
get { return SubTotal * taxRate; }
}
public double Total
{
get { return SubTotal * (1 + taxRate); }
}
public ICollection Values {
get {
return orders.Values;
}
}
public OrderItem this[String name] {
get {
return (OrderItem) orders[name];
}
}
public void Add(OrderItem value)
{
if (orders[value.Name] == null) {
orders.Add(value.Name, value);
}
else
{
OrderItem oI = (OrderItem)orders[value.Name];
oI.Quantity = oI.Quantity + 1;
}
}
public void ClearCart() {
orders.Clear();
}
}
}
需要说明的是我们将三个文件CornfieldGrocer.cs,Default.aspx.cs,Global.asax.cs用编译命令“csc /t:library /out:CornfieldGrocer.dll cornfieldgrocer.cs default.aspx.cs global.asax.cs”将它们全部封装在CornfieldGrocer命名空间里,虽然这并不是必须的。上面的编译器输出CornfieldGrocer.dll文件,我们配置该网上零售站点时只需将该文件拷贝到站点根目录中的Bin目录下即可。
到此为止,我们已经完整的向大家展示了利用ASP.NET Web表单建立一个小型的网上交易系统的编码,配置等工作。当然作为演示案例,它还没有真正系统的完善的性能,安全,界面等各个方面的优化考虑和设计。但它向我们展示的ASP.NET Web表单模型却非常典型且底层,大家不防在此基础上通过不断的修改和扩充来开发适合自己的交易系统。比如对于Default.aspx文件中AutoEventWireup="false"如果设置为“true”或去掉这个语句,在运行页面时会出现什么情况?通过这些练习便会不断的加深我们对ASP.NET底层的认识,最后达到游刃有余的把握。实际上技术的学习,尤其是编程,除了一定的兴趣和悟性外,大量代码实例的锻炼也是很有必要的,这本身就是笔者成长的一个过程,也是本文中笔者竭力要给大家展示的。