E-Commerce: Customizing and Extending the Commerce Starter Kit: Part 2...

This series of articles is going to examine some of the issues around building a traditional B2C shopping cart e-commerce site using ASP.NET and SQL Server.


By: Chris Sully Date: December 30, 2004

2: The Shopping Cart - CSK Implementation

Introduction

This series of articles is examining some of the issues around building a traditional B2C shopping cart e-commerce site using ASP.NET (VB.NET) and SQL Server using the Commerce Starter Kit (CSK) as a starting code base. In this article we'll take a look at the shopping cart implementation in the CSK. In the next article we'll consider how you might want and/ or need to customise this implementation further, driven by the real world project I'm involved with currently with my company (CW.net), as introduced in the first article of this series.

Shopping Cart

Let's take a more detailed look at how our shopping basket/ cart functionality fits into the architecture of the CSK before continuing to look at how we might extend this functionality.

So our shopper is browsing our site and he selects to add to cart. For the moment I'll assume we have already developed the functionality to display our list of products with whatever accompanying data we see fit. In my case the products are lockers and the data is dimensions, prices and colours (locker door and carcass). Again for the moment, and erroneously in this case, we'll assume all this data is stored against an individual product code and we only want to select one product at a time. Thus we can have an 'Add to basket' button with the product id as an attribute which will take us to the shopping basket aspx page and using this product id we will be able to place all the necessary data into our basket object. The only assumption we initially make here is that the shopper wants just one of the selected items, and that it is sufficient that he or she can change this amount at the next stage in proceedings.

Let's look in more detail at the implementation of the shopping cart/ basket of the Commerce Starter Kit (subsequently CSK) as a starting point, and consider this in terms of the various application tiers: USL, BLL/ DAL and in this instance we'll split out the database (i.e. stored procedures) itself. Note that the logic of the stored procedures may be considered to contribute to the BLL in the logical architectural model.

In the case of the CSK the Add to Cart button of the product detail display passes the ProductId of the product in the querystring to the AddToCart.aspx page. This page adds the identified product to the user's shopping cart, and then immediately redirects to the ShoppingCart page (this avoids problems where a user hits "refresh" and accidentally adds another product to the cart). Let’s look at adding the product within AddToCart.aspx first.

AddToCart.aspx - USL

There is no UI as the entire code for the page is as follows (for the Page_Load event):

If Not Request.Params("ProductID") Is Nothing Then

  Dim cart As ASPNET.StarterKit.Commerce.ShoppingCartDB = New ASPNET.StarterKit.Commerce.ShoppingCartDB

  ' Obtain current user's shopping cart ID
  Dim cartId As String = cart.GetShoppingCartId()

  ' Add Product Item to Cart
  cart.AddItem(cartId, (Request.Params("ProductID")), 1)
End If

Response.Redirect("ShoppingCart.aspx")
AddToCart.aspx – BLL/ DAL

We can see from the above that if a ProductID has been passed to the page three operations are performed:

  1. A new ShoppingCartDB object is created,
  2. The user's current shopping cart ID is obtained and
  3. The passed item is added to the user’s cart, as identified by the cart ID.

Looking at the code for the ShoppingCartDB class (ShoppingCartDB.vb) we see that the class has the following interface:

AddItem(ByVal string, ByVal string, ByVal integer)
EmptyCart(ByVal string)
MigrateCart(ByVal string, ByVal string)
RemoveItem(ByVal string, ByVal string)
UpdateItem(ByVal string, ByVal integer, ByVal integer)
GetItemCount(ByVal string) as integer
GetItems(ByVal string) as SQLDataReader
GetShoppingCartId() as string
GetTotal(ByVal string) as decimal

With the function members prefixed by Get functions and the remainder methods. The first string parameter passed in is the CartID.

Note that there is no object initialisation in the new sub when an object of the class is created. Looking at the members of interest within this particular page, i.e. GetShoppingCartId and AddItem.

Public Function GetShoppingCartId() As String
  ' Obtain current HttpContext of ASP.NET Request
  Dim context As HttpContext = HttpContext.Current
  ' If the user is authenticated, use their customerId as a permanent shopping cart id
  If context.User.Identity.Name <> "" Then
    Return context.User.Identity.Name
  End If

  ' If user is not authenticated, either fetch (or issue) a new temporary cartID
  If Not context.Request.Cookies("ASPNETCommerce_CartID") Is Nothing Then
    Return context.Request.Cookies("ASPNETCommerce_CartID").Value
  Else
    ' Generate a new random GUID using System.Guid Class
    Dim tempCartId As Guid = Guid.NewGuid()

    ' Send tempCartId back to client as a cookie
    context.Response.Cookies("ASPNETCommerce_CartID").Value = tempCartId.ToString()

    ' Return tempCartId
    Return tempCartId.ToString()
  End If
End Function

From the above you can see that this implementation relies on cookies, which is common practice and we won't dwell on this topic at this point but you may want to consider for your particular application whether cookies are a wise choice.

Each user's cart is thus assigned an identifying string, the uniqueness of which is most easily guaranteed via the guid class.

Public Sub AddItem(ByVal cartID As String, ByVal productID As String, ByVal quantity As Integer)

  ' Create Instance of Connection and Command Object
  Dim myConnection As SqlConnection = New SqlConnection(ConfigurationSettings.AppSettings("ConnectionString"))
  Dim myCommand As SqlCommand = New SqlCommand("CMRC_ShoppingCartAddItem", myConnection)

  ' Mark the Command as a SPROC
  myCommand.CommandType = CommandType.StoredProcedure

  ' Add Parameters to SPROC
  Dim parameterProductID As SqlParameter = New SqlParameter("@ProductID", SqlDbType.Int, 4)
  parameterProductID.Value = productID
  myCommand.Parameters.Add(parameterProductID)

  Dim parameterCartID As SqlParameter = New SqlParameter("@CartID", SqlDbType.NVarChar, 50)
  parameterCartID.Value = cartID
  myCommand.Parameters.Add(parameterCartID)

  Dim parameterQuantity As SqlParameter = New SqlParameter("@Quantity", SqlDbType.Int, 4)
  parameterQuantity.Value = quantity
  myCommand.Parameters.Add(parameterQuantity)

  ' Open the connection and execute the Command
  myConnection.Open()
  myCommand.ExecuteNonQuery()
  myConnection.Close()

End Sub

This code is straightforward, simply setting up and calling the stored procedure CMRC_ShoppingCartAddItem, which accepts as parameters product id, cart id and quantity. Note that the SQL connection string is specified by

ConfigurationSettings.AppSettings("ConnectionString"))

as defined within web.config.

AddtoCart.aspx – Data Tier

GetShoppingCartId doesn’t involve the data tier

AddItem does, calling the stored procedure CMRC_ShoppingCartAddItem which is as follows:

CREATE Procedure CMRC_ShoppingCartAddItem
(
  @CartID nvarchar(50),
  @ProductID int,
  @Quantity int
)
As

DECLARE @CountItems int

SELECT
  @CountItems = Count(ProductID)
FROM
  CMRC_ShoppingCart
WHERE
  ProductID = @ProductID
    AND
      CartID = @CartID

IF @CountItems > 0 /* There are items - update the current quantity */

  UPDATE
    CMRC_ShoppingCart
  SET
    Quantity = (@Quantity + CMRC_ShoppingCart.Quantity)
  WHERE
    ProductID = @ProductID
      AND
        CartID = @CartID

ELSE /* New entry for this Cart. Add a new record */

  INSERT INTO CMRC_ShoppingCart
  (
    CartID,
    Quantity,
    ProductID
  )
  VALUES
  (
    @CartID,
    @Quantity,
    @ProductID
  )

Fairly straight forward. At this point you may wonder why the first select is there, particularly as there is also a CMRC_ShoppingCartUpdate stored procedure offering similar functionality but perhaps all will come clear as we investigate the rest of the application.

Turning to ShoppingCart.aspx

ShoppingCart.aspx - USL

The corresponding page is ShoppingCart.aspx with the UI being provided by a datagrid which displays Product ID, Product Name, Model, Quantity, Price, Subtotal and a Remove checkbox.

The developers of the CSK have chosen to enforce the distinction between ProductID and model, despite the fact that the corresponding field, ModelNumber is unique in the database, is an alternate key and could therefore act as a primary key for the products table. One possible reason is that using an integer rather than the nvarchar(50) ModelNumber will conserve resources, particularly when indexes on the database are considered. In my current application the product database is already defined with the products table primary key being varchar(11).

The Quantity columns are textboxes so you may update the value. The Remove column has a checkbox for each row of the grid, selection not surprisingly indicating the row is to be deleted.

Finally there are two image buttons, update and checkout.

Update causes the following subs to be called

UpdateShoppingCartDatabase()
PopulateShoppingCartList()

and checkout similarly calls

UpdateShoppingCartDatabase()

and then redirects to checkout.aspx if the cart is not empty, as follows:

Dim cart As ASPNET.StarterKit.Commerce.ShoppingCartDB = New ASPNET.StarterKit.Commerce.ShoppingCartDB()

' Calculate shopping cart ID
Dim cartId As String = cart.GetShoppingCartId()

' If the cart isn't empty, navigate to checkout page
If cart.GetItemCount(cartId) <> 0 Then
  Response.Redirect("Checkout.aspx")
Else
  MyError.Text = "You can't proceed to the Check Out page with an empty cart."
End If

With the cartID being obtained as per AddToCart.aspx.

The UpdateShoppingCartDatabase() method validates any quantity input and identifies any changed data, i.e. quantity value changed or a remove checkbox ticked. If quantity has been changed to 0 or a checkbox checked cart.RemoveItem is called. If the quantity value has been changed to a different non-zero value cart.UpdateItem is called.

ShoppingCart.aspx – BLL/ DAL/ Data Tier

GetShoppingCartId is used to obtain the user’s current CartId to create a new one, as before.

GetItemCount returns the number of items in a Cart via the CMRC_ShoppingCartItemCount stored procedure.

UpdateItem in turn executes the CMRC_ShoppingCartUpdate stored procedure, in a similar fashion to AddItem previously discussed.

Similarly, RemoveItem in turn executes the CMRC_ShoppingCartRemoveItem stored procedure.

This should give you a pretty good idea of how the ShoppingBasket application works without going into the detail of all the code.

The three members we haven’t met in the code yet are:

EmptyCart(ByVal string)
MigrateCart(ByVal string, ByVal string)
GetTotal(ByVal string) as decimal

GetTotal we can predict will be used as part of the CheckoutProcess to calculate the total for the order. Similarly, presumably we will want to call EmptyCart when we have stored the corresponding Order details. What about MigrateCart? The MigrateCart method migrates the items from one cartId to another. This is used during the login and/or registration process to transfer a user's temporary cart items to a permanent account, i.e. associating a cart with a user when they have been identified via login.

If you think about this last area this can cause issues in itself with the current implementation. Say a temporary cart a user has been using contains the same product as the cart that exists in the database associated with their user id from the previous time they were browsing when logged in. What happens if the user now logs in? Is the code clever enough to merge the two carts together and simply add the number of items of one product to the number of items of the other, i.e. consolidate the two carts contents successfully. Let's see: if we look at the MigrateCart code we see that it calls the sproc CMRC_ShoppingCartMigrate with old and new cart ids as parameters. The code therein is:

UPDATE
  CMRC_ShoppingCart
SET
  CartID = @NewCartId
WHERE
  CartID = @OriginalCartId

So, no ... it doesn't cater for situations where the ProductIDs might be the same in the two carts. However, this doesn't cause a primary key violation in the shopping cart table as productID is not the primary key, the artificial recordid is. So maybe this is not an issue. We will need to investigate further however, as this may cause issues with our Locker scenario, as we explain in the next article.

Summary and what's next

In this second article in the series we've looked at the CSK implementation of the shopping cart. In the next article we'll continue our focus on this area of functionality, but look at how we need to alter the base implementation to support the more complex data requirements of the example Lockers project.

References

Commerce Whitepaper
www.asp.net

Beginning ASP.NET E-Commerce
Darie and Watson
Wrox