通用购物篮






4.76/5 (19投票s)
一个通用的购物车 Web 控件,可以作为您自己开发更高级版本的良好起点。
引言
网络上似乎充斥着各种各样的购物车以及解决此问题的不同方法(PHP、ASP、JScript 等)。我想写一个通用的购物车,作为一个独立的美观的 Web 控件,这会很有趣。整个想法是创建一个包含尽可能多“购物车逻辑”的 Web 控件,这样只需很少的代码即可将其集成到现有或新的 ASP.NET 项目中。我的目标之一是保持代码相对轻量,以便该控件可以作为 C# 控件开发入门教程,甚至可能是一些非常常见的 .NET 和 C# 技术的教程。结果相当不错,我当然确信即使是初级的 .NET 程序员也能理解所有代码。当然,有很多方法可以更改和改进 Web 控件中的一些概念。我将在相关部分提及可能的改进,以便您可以使用此控件作为模板来添加更适合您需求的新功能。Web 控件确实可以实现很多额外的功能,但其中一些超出了本文的范围。那么,让我们开始购物吧……
背景
设计该控件时,我必须做出一些决定,因为“通用”肯定意味着某种开放式设计,同时又要引入某种有限的功能。首先,我不想让我的控件使用 `Viewstate` 在页面之间传递设置,因为我想要一种设计,允许 Web 设计师将购物车放在任何网页上并正确显示其内容。现在有几种方法可以做到这一点,但将购物车保存在会话状态中似乎是最佳的通用解决方案。请注意,.NET 中的会话状态不像普通 ASP 中的那样糟糕。在群集 Web 环境中,现在可以轻松使用会话状态。
.NET 为我提供了将类序列化为二进制格式或 XML 格式的选项。我决定采用 XML 方式,因为它允许开发人员从会话状态读取购物车内容(当然,当您在 ASP.NET 页面上拥有该控件时,您也可以使用该控件来完成此操作)。我没有使用内置序列化,因为它在处理私有成员时存在问题,而我偶尔喜欢自己编写序列化。请注意,如果您想使购物车会话状态更小,可以实现二进制序列化。
第二个重大决定是使用复合控件(包含许多其他子 .NET 控件的控件),但仍然自己处理大部分渲染。根据微软的说法,这会产生一个更快的控件,但实际上我这样做是因为我可以看到控件实际输出的 HTML 是怎样的。当然,这也使得控件的外观和感觉更容易适应。如果您不喜欢它的外观,只需更改 `Render` 代码。宿主控件逻辑使捕获事件(使用 `OnBubbleEvent`)更加容易,`INamingContainer` 接口确保子控件具有唯一的 ID。
使用代码 
使用此控件应该相当直接。只需将其作为 Web 控件添加到 VS2003 工具栏,然后将其拖放到 ASP.NET 网页上。您需要在网页上提供逻辑来添加产品,因为 Web 控件无法知道这些产品来自何处。您需要记住的主要事情是,控件本身负责修改数量、删除产品和清空购物车。此外,该控件提供了一个处理程序,您可以在用户按下购物车上的“结账”按钮时使用。当购物车设置为“禁用”时,它不显示任何按钮,因此此状态可用于仅显示购物车的内容(例如,在结账页面上)。购物车将其所有信息保存在会话状态中,因此您可以在网站的任何页面上使用该购物车,并且它能够读取登录用户的购物车内容。以下是一些示例代码,说明如何在 `aspx.cs` 页面上使用该控件。
protected GenericBasket.Basket Basket1;
// Events will be added in design mode or you can do it manually
// Example : this.Basket1.Checkout += new System.EventHandler(
// this.Page_Checkout);
private void Page_Checkout(object sender, System.EventArgs e)
{
// disable the basket so that no buttons can be pressed
Basket1.Enabled = false;
// do some other stuff to checkout
// add an item that includes the Shipping & Handling fee
// I just add 10% of the total cost for demo purposes
Basket1.Remove("SHIPPING");
Basket1.Add("SHIPPING","+ Shipping & Handling",1,Basket1.TotalPrice/10);
}
private void Page_Load(object sender, System.EventArgs e)
{
if (!IsPostBack)
{
// Add some products
Basket1.Add("1","XBox",2,199);
Basket1.Add("2","GameCube",1,99);
Basket1.Add("3","PS2",1,199);
Basket1.Add("4","Remove me",1,0);
// and remove a product again
Basket1.Remove("4");
// of course you can also use the product class
GenericBasket.Product newProduct = new GenericBasket.Product(
"4","GameBoy Advance",1,140);
Basket1.Add(newProduct );
}
}
}
`GenericBasket.Product` 类
公共方法和属性(即您可以在 ASP.NET 页面上使用的)已保持最少。但是,`Product` 类是公共的,因此您可以使用它作为大多数产品操作的包装器。我已将 `Product` 类保持得非常基础,因为产品可以有很多不同的属性,而且大多数属性都是特定于应用程序的。该控件的设计假定您将自行获取特定于产品的信息(税费、地点、库存等),并在结账周期中对其进行处理。
当然,如果您希望购物车包含更多业务逻辑,您可以轻松地在 `Product` 类中添加一些更具体的内容。`Product` 类实现了 `IComparable` 接口。该接口提供了排序功能,因此我们可以轻松地对 `Product` 元素数组进行排序。
从购物车获取信息
购物车支持(只读)索引器和枚举器,因此它非常像一个普通的数组和/或集合。以下代码是完全有效的
Response.Write(Basket1.Count.ToString());
Response.Write(" Items in Basket");
foreach (GenericBasket.Product itemProduct in Basket1)
{
Reponse.Write("*");
Response.Write(itemProduct.sName);
Response.Write(" ");
}
// Get the product with ID "9"
if (Basket1["9"] != null)
Response.Write("Product ID 9 found");
else
Response.Write("Product ID 9 not found");
实现这些功能相当直接。枚举器不需要太多代码,基本上您可以只返回 `ArrayList` 成员的现有枚举器。不要忘记添加 `IEnumerable` 接口以支持枚举器。同样,支持索引器也轻而易举,只需一些简单的代码即可支持整数索引和字符串索引,就像大多数 .NET 类一样。这是其中一个索引器的代码,字符串版本非常相似
public Product this [int index] // Indexer declaration
{
get
{
if (index >= 0 && index < m_aProducts.Count)
return(m_aProducts[index] as Product);
else
return(null);
}
}
事件
通过事件冒泡,控件可以确保某些事件“冒泡”到父控件。在我们的通用购物车的情况下,我决定只将两个事件冒泡到顶部。要支持事件冒泡,您需要实现 `INamingContainer` 接口。
public event EventHandler Checkout; // Event that is triggered
// when checkout button is pressed
public event EventHandler Changed; // Event that happens when
// control changes *itself*
要正确处理控件触发的所有命令,您需要为创建的所有子控件指定 `CommandArgument` 属性。例如,在 `Button` 上
btnGeneric = new Button();
btnGeneric.Text = "Delete";
btnGeneric.CommandName = "Delete";
btnGeneric.CommandArgument = outputProduct.sID;
btnGeneric.CssClass = CssClass;
Controls.Add(btnGeneric);
然后,这可能会导致冒泡事件处理方法,如下所示
protected override bool OnBubbleEvent(object source, EventArgs e)
{
bool handled = false;
if (e is CommandEventArgs)
{
CommandEventArgs ce = (CommandEventArgs)e;
if (ce.CommandName == "Delete")
{
Remove((string)ce.CommandArgument);
OnChanged(ce);
handled = true;
}
}
return(handled);
}
protected virtual void OnChanged (EventArgs e)
{
if (Changed != null)
{
Changed(this,e);
}
}
复合控件?
我选择采用复合控件(而不是完全由我自己渲染所有 HTML 元素并处理回发结果的控件),因为我认为 .NET 可以很好地处理子控件触发的事件,并且这会使代码更简单。当然,复合控件会稍慢一些,并且有一些小缺点。如果您确实遇到了性能问题,您可能需要完全重新设计控件,使其能够自行处理渲染并支持回发数据处理(实现 `IPostBackDataHandler`)。
我确实遇到了一个严重的问题。显然,.NET 框架在处理子控件触发的事件之前会调用 `CreateChildControls`。这导致了非常奇怪的行为,并且错误控件包含错误信息。解决方案很简单(但并非真正令人满意)。我只是在 `PreRender` 事件中添加了 `CreateChildControls` 调用。为什么不令人满意?好吧,如果您在调试器中单步执行代码,您会看到 `CreateChildControls` 现在被调用了两次。从性能角度来看,这不是很敏感,对吧?我现在暂时搁置了这个问题,因为我怀疑有更优雅的方法来解决它。
公共方法
这是您可以在购物车上使用的方法的列表以及简短的描述
public System.Int32 Add ( GenericBasket.Product newProduct );
// Add a new Product to the basket. Returns index at which
// the value has been added.
public System.Int32 Add ( System.String sProductID ,
System.String sName , System.Int32 iQuantity, System.Single fPrice);
// Add a new product to the basket alternative version.
// Returns index at which the value has been added.
public Basket ( ); // Constructor, nothing interesting to
//say about this one ;)
public void Clear ( ); // Clears the content of the basket
public int Count [ get]; // The number of items in the basket
public string EmptyTitle [ get, set ]; // The title of control
// when it is empty
public System.Collections.IEnumerator GetEnumerator ( );
// returns an enumerator, used for implementing enumeration
public void Remove ( System.String sProductID ); // Removes a product
// from the basket based on product ID
public string SessionKey [ get]; // The "session key" that you
// can use to read the basket session information XML
public const GenericBasket.Product this [ get]; // Indexers,
// one takes int index and the other one takes a product ID string
public string Title [ get, set ]; // The title of your basket
// (when there are items inside)
// New in version 1.1
public float TotalPrice [ get]; // Gets the total price of all items in basket
public System.Globalization.CultureInfo Culture [ get, set ];
// the Culture of the basket, this makes sure the
// basket can display price in correct format
关注点
我必须补充说,这是我为 CodeProject 贡献的第一个 .NET 文章。我大约一年前从 C++ 转向 .NET(当然,我仍然大量使用 C++,但主要是回家后),并很快发现 .NET 框架是一个广阔的新迷宫,有不同的路径来解决不同的问题。在编写此控件的过程中,我不断找到不同的方法,在您开始修改此控件之前,您应该考虑一些新的、更好的方法来改进设计(毕竟,它是一个*通用*控件)。
- 您可以使用更高级的子控件(例如 `DataGrid`)来显示购物车的内容。
- 您甚至可以将 XML 会话数据绑定到 `DataGrid`。这将为您提供一个外观非常漂亮的购物车,并带有一些不错的 `DataGrid` 功能。
- 添加数据库保存/加载也将是一个好主意,因为您的网站访问者可能会喜欢让他们在访问之间保持购物车已填充。这可以在控件中实现,也可以将其作为事件暴露给父控件(就像结账一样)。
- `Product` 类非常基础,可以扩展。可以添加产品页面的 URL 或图片链接吗?
- 可以集成一些业务逻辑。例如,自动添加税费和/或运费。
- 布局有很多可以做的,现在它非常基础。当然,使用 CSS 样式表修改样式是可行的,也许您可以在 `Render` 方法中的某些 HTML 元素中添加额外的 HTML 类标签,或者您可以为控件提供模板化属性,允许页面开发人员自定义控件的用户界面。请记住,目前,控件会将 `CssClass` 属性传播到所有 HTML 元素。这为您提供了一些布局控制,但可能需要更多。
如果您决定在您的网站上使用购物车,我希望本文能为您提供一些想法和有用的代码。如果有人决定改进设计,请告诉我,以便我将其纳入文章的更新版本。购物愉快!
历史
- 版本 1.1
- 修复了索引器中的一个小 bug。
- 添加了价格信息。
- 添加了区域信息,以便购物车知道如何显示价格信息。
- 更改了一些变量名,使其更符合“.NET 规范”。
- 版本 1.2
- 修复了购物车为空时 HTML 输出中的一个小 bug(最后一个 `TD` 标签未关闭)。
- 在表格元素之间添加了一些“ ”以使表格看起来更一致。
- 向一些不应出现在属性窗口中的属性添加了 `
[Browsable(false)]
`。 - 会话键获取器现在是 `
static
`,因此您无需创建购物车实例即可查询购物车会话键信息。 - 将一些 `TD` 标签更改为 `TH` 标签,以允许更好的购物车样式表布局控制。
- 如果未定义颜色,则更改了 HTML 输出中的购物车表格颜色,使其变为白色/黑色。