Introduction
The web seems to be teeming with different shopping baskets and different ways (PHP, ASP, JScript..) to solve the problem. I thought it would be fun to write a generic shopping basket as a nice, standalone web control. To whole idea was to make a web control that includes as much "shopping basket logic" as possible so that it needed minimal coding to include in an existing or new ASP.NET project. One of my aims was also to keep the code relatively light so that the control could be used as a kind of introduction to developing controls in C# and maybe even as a tutorial on some very common .NET and C# techniques. The result turned out pretty good and I am certainly convinced that even beginning .NET programmers will be able to understand all of the code. Of course, there are many ways to change and improve some of the concepts in the web control. I will mention possible improvements in the relevant sections so you can use this control as a template to add new functionality that better suits your needs. There really is a whole lot of extra you can do with web controls but some of these are beyond the scope of the article. So let's start shopping...
Background
When I designed the control, I had to make certain decisions since "generic" certainly implies some kind of open design while at the same time introducing some kind of limited functionality. First of all, I did not want my control to use Viewstate
to pass settings from page to page because I wanted a design that enabled a web designer to drop the basket on any webpage and have it display its content correctly. Now there are a couple of ways to do this but saving the basket in a session state looked like the best generic solution. Note that session state in .NET is not so evil anymore as it was in normal ASP. Session state can now easily be used in clustered web environments.
.NET gives me the option to serialize classes in a binary format or in an XML format. I decided to go the XML way because it would enable developers to read the basket content from session state (of course, you can also use the control to do that when you have it available on your ASP.NET page). I did not use built-in serialization because it has trouble handling private members and I prefer to roll my own serialization from time to time. Note that you can implement binary serialization if you want to make the basket session state smaller.
The second big decision was to use a composite control (a control that contains a host of other child .NET controls) but still handle a big part of the rendering myself. This results (according to MS) in a faster control but I really did it because I could see how the actual HTML was being spit out by the control. Of course, this also makes the look and feel of the control a bit easier to adapt. If you don't like the way it looks, just change the Render
code. The host control logic made it a bit easier to catch events (with OnBubbleEvent
), and the INamingContainer
interface makes sure that children have unique IDs.
Using the code
Using this control should be pretty straightforward. Just add it as a web control to the VS2003 toolbar and drop it on an ASP.NET webpage. You should provide logic on your webpage to add products because there is no way the web control can know where these products come from. The main things you need to remember is that the control itself handles modifying quantities, removing products and emptying the basket. Also, the control provides a handler that you can use when the user pushes the Checkout button of the basket. The basket does not display any button when it is set to "disabled", so this state can be used to just display the content (on a checkout page, for example) of the basket. The basket saves all its information in session state, so you can use the basket on any page in your website and it will be able to read the content of the shopping basket of the logged on user. Here is some sample code that illustrates how you could use the control on an aspx.cs page.
protected GenericBasket.Basket Basket1;
private void Page_Checkout(object sender, System.EventArgs e)
{
Basket1.Enabled = false;
Basket1.Remove("SHIPPING");
Basket1.Add("SHIPPING","+ Shipping & Handling",1,Basket1.TotalPrice/10);
}
private void Page_Load(object sender, System.EventArgs e)
{
if (!IsPostBack)
{
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);
Basket1.Remove("4");
GenericBasket.Product newProduct = new GenericBasket.Product(
"4","GameBoy Advance",1,140);
Basket1.Add(newProduct );
}
}
}
The GenericBasket.Product
class
The methods and properties that are public (i.e. that you can use on ASP.NET pages) have been kept to a minimum. The Product
class, however, is made public so you can use it as a wrapper for most of your product manipulations. I have kept the Product
class very basic because there are so many different properties that you could attach to a product and most of them are application specific. The design of the control assumes that you will get the product specific information (tax, location, stock....) yourself and do something with that on the checkout cycle.
Of course, you can easily add some more specific stuff to the Product
class if you want the shopping basket to contain more business logic. The Product
class implements the ICompareable
interface. This interface provides sorting so we can easily sort an array of Product
elements.
Getting information out of the basket
The basket supports (read-only) indexers and enumerators so it behaves very much like a normal array and/or collection. The following code is quite valid:
Response.Write(Basket1.Count.ToString());
Response.Write(" Items in Basket");
foreach (GenericBasket.Product itemProduct in Basket1)
{
Reponse.Write("*");
Response.Write(itemProduct.sName);
Response.Write(" ");
}
if (Basket1["9"] != null)
Response.Write("Product ID 9 found");
else
Response.Write("Product ID 9 not found");
Implementing these was quite straightforward. The enumerator does not need a lot of code, basically you can just return the existing enumerator from the ArrayList
member. Don't forget to add the IEnumerable
interface to support enumerators. Likewise, supporting indexers was a breeze as well, with just some simple code to support an integer index and a string index like most .NET classes. Here is the code of one indexer, the string version is very similar:
public Product this [int index]
{
get
{
if (index >= 0 && index < m_aProducts.Count)
return(m_aProducts[index] as Product);
else
return(null);
}
}
Events
With event bubbling, the control can make sure some events "bubble" up to the parent control. In the case of our generic basket, I decided to bubble only two events to the top. To support event bubbling, you need to implement the INamingContainer
interface.
public event EventHandler Checkout;
public event EventHandler Changed;
To correctly process all the commands that are fired by the control, you need to specify the CommandArgument
attribute to all child controls that you create. For example, on a Button
:
btnGeneric = new Button();
btnGeneric.Text = "Delete";
btnGeneric.CommandName = "Delete";
btnGeneric.CommandArgument = outputProduct.sID;
btnGeneric.CssClass = CssClass;
Controls.Add(btnGeneric);
And this can then result in a bubbled event processing method like this:
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);
}
}
Composite control?
I chose to go the way of the composite control (as opposed to a control where I render all HTML elements myself and handle the postback results) because I think that .NET handles events fired from children fairly well and this results in easier code. Of course, a composite control is a bit slower and has some small pitfalls. If you really experience performance issues, you might have to completely redesign the control so that it takes care of its own rendering and support postback data processing (implement IPostBackDataHandler
).
I did run into one serious problem. Apparently the .NET framework calls CreateChildControls
before it processes the events fired by the child controls. This resulted in very weird behavior and wrong controls containing wrong information. The solution was quite simple (but not really satisfactory). I just added the CreateChildControls
call in the PreRender
event. Why not satisfactory? Well, if you step through the code with the debugger, you will see that CreateChildControls
is now called twice. Not very sensitive performance-wise, is it? I have left this for now because I suspect there is a more elegant way of solving this problem.
Public methods
Here is a list of the methods you can use on the basket and a short description:
public System.Int32 Add ( GenericBasket.Product newProduct );
public System.Int32 Add ( System.String sProductID ,
System.String sName , System.Int32 iQuantity, System.Single fPrice);
public Basket ( );
public void Clear ( );
public int Count [ get];
public string EmptyTitle [ get, set ];
public System.Collections.IEnumerator GetEnumerator ( );
public void Remove ( System.String sProductID );
public string SessionKey [ get];
public const GenericBasket.Product this [ get];
public string Title [ get, set ];
public float TotalPrice [ get];
public System.Globalization.CultureInfo Culture [ get, set ];
Points of Interest
I must add that this is my first .NET contribution to CodeProject. I made the switch to .NET from C++ about a year ago (of course, I still use C++ a lot but mainly at home) and discovered quickly that the .NET framework is a vast new maze with different paths to solve different problems. During the writing of this control, I constantly found different approaches and before you start modifying this control, you should think about some new and better ways to improve on the design (it is a generic control after all).
- You can use more advanced child controls (
DataGrid
, for example) to display the contents of the basket.
- You could even bind the XML session data to the
DataGrid
. This would give you a very good looking basket with some nice DataGrid
functionality.
- Adding database saving/loading would also be good since visitors of your website might like it if their basket stays filled between visits. This can be implemented in the control or it can be exposed as an event to the parent (just like checkout).
- The
Product
class is pretty basic and could be extended. What about an URL to the product page or maybe a link to a picture?
- Some business logic could be incorporated. For example, automatically adding tax and/or shipping costs.
- There is a lot that can be done with the layout that is pretty basic right now. Modifying styles with a CSS stylesheet is of course very doable, maybe you could add extra HTML class tags to some of the HTML elements in the
Render
method, or maybe you can give the control templated properties which allow a page developer to customize the UI for the control. Remember that the control, at the moment, propagates the CssClass
attribute to all of the HTML elements. This gives you some layout control, but more might be needed.
If you decide to use a shopping basket on your website, I hope this article gave you some ideas and useful code. If anybody decides to improve the design, let me know so I can incorporate them in newer versions of the article. Have fun shopping!
History
- Version 1.1:
- Fixed a minor bug in the indexer.
- Added price information.
- Added culture information, so that the basket will know how to display price information.
- Changed some variable names to make them more ".NET compliant".
- Version 1.2
- Fixed a small bug in the HTML output when basket was empty (the last
TD
tag was not closed).
- Added some " " between table elements to make table look consistent.
- Added
false)>
to some properties that shouldn't really appear in the Properties window.
- Session key getter is now
static
so you can query basket session key info without creating basket instance.
- Changed some
TD
tags into TH
tags to allow better stylesheet layout control of basket.
- Changed the basket table color in HTML output if no color was defined, to white/black.