Part 8: Shopping Cart with Ajax Updates
by Jon Galloway
The MVC Music Store is a tutorial application that introduces and explains step-by-step how to use ASP.NET MVC and Visual Studio for web development.
The MVC Music Store is a lightweight sample store implementation which sells music albums online, and implements basic site administration, user sign-in, and shopping cart functionality.
This tutorial series details all of the steps taken to build the ASP.NET MVC Music Store sample application. Part 8 covers Shopping Cart with Ajax Updates.
We’ll allow users to place albums in their cart without registering, but they’ll need to register as guests to complete checkout. The shopping and checkout process will be separated into two controllers: a ShoppingCart Controller which allows anonymously adding items to a cart, and a Checkout Controller which handles the checkout process. We’ll start with the Shopping Cart in this section, then build the Checkout process in the following section.
Adding the Cart, Order, and OrderDetail model classes
Our Shopping Cart and Checkout processes will make use of some new classes. Right-click the Models folder and add a Cart class (Cart.cs) with the following code.
[!code-csharpMain]
1: using System.ComponentModel.DataAnnotations;
2:
3: namespace MvcMusicStore.Models
4: {
5: public class Cart
6: {
7: [Key]
8: public int RecordId { get; set; }
9: public string CartId { get; set; }
10: public int AlbumId { get; set; }
11: public int Count { get; set; }
12: public System.DateTime DateCreated { get; set; }
13: public virtual Album Album { get; set; }
14: }
15: }
This class is pretty similar to others we’ve used so far, with the exception of the [Key] attribute for the RecordId property. Our Cart items will have a string identifier named CartID to allow anonymous shopping, but the table includes an integer primary key named RecordId. By convention, Entity Framework Code-First expects that the primary key for a table named Cart will be either CartId or ID, but we can easily override that via annotations or code if we want. This is an example of how we can use the simple conventions in Entity Framework Code-First when they suit us, but we’re not constrained by them when they don’t.
Next, add an Order class (Order.cs) with the following code.
[!code-csharpMain]
1: using System.Collections.Generic;
2:
3: namespace MvcMusicStore.Models
4: {
5: public partial class Order
6: {
7: public int OrderId { get; set; }
8: public string Username { get; set; }
9: public string FirstName { get; set; }
10: public string LastName { get; set; }
11: public string Address { get; set; }
12: public string City { get; set; }
13: public string State { get; set; }
14: public string PostalCode { get; set; }
15: public string Country { get; set; }
16: public string Phone { get; set; }
17: public string Email { get; set; }
18: public decimal Total { get; set; }
19: public System.DateTime OrderDate { get; set; }
20: public List<OrderDetail> OrderDetails { get; set; }
21: }
22: }
This class tracks summary and delivery information for an order. It won’t compile yet, because it has an OrderDetails navigation property which depends on a class we haven’t created yet. Let’s fix that now by adding a class named OrderDetail.cs, adding the following code.
[!code-csharpMain]
1: namespace MvcMusicStore.Models
2: {
3: public class OrderDetail
4: {
5: public int OrderDetailId { get; set; }
6: public int OrderId { get; set; }
7: public int AlbumId { get; set; }
8: public int Quantity { get; set; }
9: public decimal UnitPrice { get; set; }
10: public virtual Album Album { get; set; }
11: public virtual Order Order { get; set; }
12: }
13: }
We’ll make one last update to our MusicStoreEntities class to include DbSets which expose those new Model classes, also including a DbSet<Artist>. The updated MusicStoreEntities class appears as below.
[!code-csharpMain]
1: using System.Data.Entity;
2:
3: namespace MvcMusicStore.Models
4: {
5: public class MusicStoreEntities : DbContext
6: {
7: public DbSet<Album> Albums { get; set; }
8: public DbSet<Genre> Genres { get; set; }
9: public DbSet<Artist> Artists {
10: get; set; }
11: public DbSet<Cart>
12: Carts { get; set; }
13: public DbSet<Order> Orders
14: { get; set; }
15: public DbSet<OrderDetail>
16: OrderDetails { get; set; }
17: }
18: }
Managing the Shopping Cart business logic
Next, we’ll create the ShoppingCart class in the Models folder. The ShoppingCart model handles data access to the Cart table. Additionally, it will handle the business logic to for adding and removing items from the shopping cart.
Since we don’t want to require users to sign up for an account just to add items to their shopping cart, we will assign users a temporary unique identifier (using a GUID, or globally unique identifier) when they access the shopping cart. We’ll store this ID using the ASP.NET Session class.
Note: The ASP.NET Session is a convenient place to store user-specific information which will expire after they leave the site. While misuse of session state can have performance implications on larger sites, our light use will work well for demonstration purposes.
The ShoppingCart class exposes the following methods:
AddToCart takes an Album as a parameter and adds it to the user’s cart. Since the Cart table tracks quantity for each album, it includes logic to create a new row if needed or just increment the quantity if the user has already ordered one copy of the album.
RemoveFromCart takes an Album ID and removes it from the user’s cart. If the user only had one copy of the album in their cart, the row is removed.
EmptyCart removes all items from a user’s shopping cart.
GetCartItems retrieves a list of CartItems for display or processing.
GetCount retrieves a the total number of albums a user has in their shopping cart.
GetTotal calculates the total cost of all items in the cart.
CreateOrder converts the shopping cart to an order during the checkout phase.
GetCart is a static method which allows our controllers to obtain a cart object. It uses the GetCartId method to handle reading the CartId from the user’s session. The GetCartId method requires the HttpContextBase so that it can read the user’s CartId from user’s session.
Here’s the complete ShoppingCart class:
[!code-csharpMain]
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Web;
5: using System.Web.Mvc;
6:
7: namespace MvcMusicStore.Models
8: {
9: public partial class ShoppingCart
10: {
11: MusicStoreEntities storeDB = new MusicStoreEntities();
12: string ShoppingCartId { get; set; }
13: public const string CartSessionKey = "CartId";
14: public static ShoppingCart GetCart(HttpContextBase context)
15: {
16: var cart = new ShoppingCart();
17: cart.ShoppingCartId = cart.GetCartId(context);
18: return cart;
19: }
20: // Helper method to simplify shopping cart calls
21: public static ShoppingCart GetCart(Controller controller)
22: {
23: return GetCart(controller.HttpContext);
24: }
25: public void AddToCart(Album album)
26: {
27: // Get the matching cart and album instances
28: var cartItem = storeDB.Carts.SingleOrDefault(
29: c => c.CartId == ShoppingCartId
30: && c.AlbumId == album.AlbumId);
31:
32: if (cartItem == null)
33: {
34: // Create a new cart item if no cart item exists
35: cartItem = new Cart
36: {
37: AlbumId = album.AlbumId,
38: CartId = ShoppingCartId,
39: Count = 1,
40: DateCreated = DateTime.Now
41: };
42: storeDB.Carts.Add(cartItem);
43: }
44: else
45: {
46: // If the item does exist in the cart,
47: // then add one to the quantity
48: cartItem.Count++;
49: }
50: // Save changes
51: storeDB.SaveChanges();
52: }
53: public int RemoveFromCart(int id)
54: {
55: // Get the cart
56: var cartItem = storeDB.Carts.Single(
57: cart => cart.CartId == ShoppingCartId
58: && cart.RecordId == id);
59:
60: int itemCount = 0;
61:
62: if (cartItem != null)
63: {
64: if (cartItem.Count > 1)
65: {
66: cartItem.Count--;
67: itemCount = cartItem.Count;
68: }
69: else
70: {
71: storeDB.Carts.Remove(cartItem);
72: }
73: // Save changes
74: storeDB.SaveChanges();
75: }
76: return itemCount;
77: }
78: public void EmptyCart()
79: {
80: var cartItems = storeDB.Carts.Where(
81: cart => cart.CartId == ShoppingCartId);
82:
83: foreach (var cartItem in cartItems)
84: {
85: storeDB.Carts.Remove(cartItem);
86: }
87: // Save changes
88: storeDB.SaveChanges();
89: }
90: public List<Cart> GetCartItems()
91: {
92: return storeDB.Carts.Where(
93: cart => cart.CartId == ShoppingCartId).ToList();
94: }
95: public int GetCount()
96: {
97: // Get the count of each item in the cart and sum them up
98: int? count = (from cartItems in storeDB.Carts
99: where cartItems.CartId == ShoppingCartId
100: select (int?)cartItems.Count).Sum();
101: // Return 0 if all entries are null
102: return count ?? 0;
103: }
104: public decimal GetTotal()
105: {
106: // Multiply album price by count of that album to get
107: // the current price for each of those albums in the cart
108: // sum all album price totals to get the cart total
109: decimal? total = (from cartItems in storeDB.Carts
110: where cartItems.CartId == ShoppingCartId
111: select (int?)cartItems.Count *
112: cartItems.Album.Price).Sum();
113:
114: return total ?? decimal.Zero;
115: }
116: public int CreateOrder(Order order)
117: {
118: decimal orderTotal = 0;
119:
120: var cartItems = GetCartItems();
121: // Iterate over the items in the cart,
122: // adding the order details for each
123: foreach (var item in cartItems)
124: {
125: var orderDetail = new OrderDetail
126: {
127: AlbumId = item.AlbumId,
128: OrderId = order.OrderId,
129: UnitPrice = item.Album.Price,
130: Quantity = item.Count
131: };
132: // Set the order total of the shopping cart
133: orderTotal += (item.Count * item.Album.Price);
134:
135: storeDB.OrderDetails.Add(orderDetail);
136:
137: }
138: // Set the order's total to the orderTotal count
139: order.Total = orderTotal;
140:
141: // Save the order
142: storeDB.SaveChanges();
143: // Empty the shopping cart
144: EmptyCart();
145: // Return the OrderId as the confirmation number
146: return order.OrderId;
147: }
148: // We're using HttpContextBase to allow access to cookies.
149: public string GetCartId(HttpContextBase context)
150: {
151: if (context.Session[CartSessionKey] == null)
152: {
153: if (!string.IsNullOrWhiteSpace(context.User.Identity.Name))
154: {
155: context.Session[CartSessionKey] =
156: context.User.Identity.Name;
157: }
158: else
159: {
160: // Generate a new random GUID using System.Guid class
161: Guid tempCartId = Guid.NewGuid();
162: // Send tempCartId back to client as a cookie
163: context.Session[CartSessionKey] = tempCartId.ToString();
164: }
165: }
166: return context.Session[CartSessionKey].ToString();
167: }
168: // When a user has logged in, migrate their shopping cart to
169: // be associated with their username
170: public void MigrateCart(string userName)
171: {
172: var shoppingCart = storeDB.Carts.Where(
173: c => c.CartId == ShoppingCartId);
174:
175: foreach (Cart item in shoppingCart)
176: {
177: item.CartId = userName;
178: }
179: storeDB.SaveChanges();
180: }
181: }
182: }
ViewModels
Our Shopping Cart Controller will need to communicate some complex information to its views which doesn’t map cleanly to our Model objects. We don’t want to modify our Models to suit our views; Model classes should represent our domain, not the user interface. One solution would be to pass the information to our Views using the ViewBag class, as we did with the Store Manager dropdown information, but passing a lot of information via ViewBag gets hard to manage.
A solution to this is to use the ViewModel pattern. When using this pattern we create strongly-typed classes that are optimized for our specific view scenarios, and which expose properties for the dynamic values/content needed by our view templates. Our controller classes can then populate and pass these view-optimized classes to our view template to use. This enables type-safety, compile-time checking, and editor IntelliSense within view templates.
We’ll create two View Models for use in our Shopping Cart controller: the ShoppingCartViewModel will hold the contents of the user’s shopping cart, and the ShoppingCartRemoveViewModel will be used to display confirmation information when a user removes something from their cart.
Let’s create a new ViewModels folder in the root of our project to keep things organized. Right-click the project, select Add / New Folder.
Name the folder ViewModels.
Next, add the ShoppingCartViewModel class in the ViewModels folder. It has two properties: a list of Cart items, and a decimal value to hold the total price for all items in the cart.
[!code-csharpMain]
1: using System.Collections.Generic;
2: using MvcMusicStore.Models;
3:
4: namespace MvcMusicStore.ViewModels
5: {
6: public class ShoppingCartViewModel
7: {
8: public List<Cart> CartItems { get; set; }
9: public decimal CartTotal { get; set; }
10: }
11: }
Now add the ShoppingCartRemoveViewModel to the ViewModels folder, with the following four properties.
[!code-csharpMain]
1: namespace MvcMusicStore.ViewModels
2: {
3: public class ShoppingCartRemoveViewModel
4: {
5: public string Message { get; set; }
6: public decimal CartTotal { get; set; }
7: public int CartCount { get; set; }
8: public int ItemCount { get; set; }
9: public int DeleteId { get; set; }
10: }
11: }
The Shopping Cart Controller
The Shopping Cart controller has three main purposes: adding items to a cart, removing items from the cart, and viewing items in the cart. It will make use of the three classes we just created: ShoppingCartViewModel, ShoppingCartRemoveViewModel, and ShoppingCart. As in the StoreController and StoreManagerController, we’ll add a field to hold an instance of MusicStoreEntities.
Add a new Shopping Cart controller to the project using the Empty controller template.
Here’s the complete ShoppingCart Controller. The Index and Add Controller actions should look very familiar. The Remove and CartSummary controller actions handle two special cases, which we’ll discuss in the following section.
[!code-csharpMain]
1: using System.Linq;
2: using System.Web.Mvc;
3: using MvcMusicStore.Models;
4: using MvcMusicStore.ViewModels;
5:
6: namespace MvcMusicStore.Controllers
7: {
8: public class ShoppingCartController : Controller
9: {
10: MusicStoreEntities storeDB = new MusicStoreEntities();
11: //
12: // GET: /ShoppingCart/
13: public ActionResult Index()
14: {
15: var cart = ShoppingCart.GetCart(this.HttpContext);
16:
17: // Set up our ViewModel
18: var viewModel = new ShoppingCartViewModel
19: {
20: CartItems = cart.GetCartItems(),
21: CartTotal = cart.GetTotal()
22: };
23: // Return the view
24: return View(viewModel);
25: }
26: //
27: // GET: /Store/AddToCart/5
28: public ActionResult AddToCart(int id)
29: {
30: // Retrieve the album from the database
31: var addedAlbum = storeDB.Albums
32: .Single(album => album.AlbumId == id);
33:
34: // Add it to the shopping cart
35: var cart = ShoppingCart.GetCart(this.HttpContext);
36:
37: cart.AddToCart(addedAlbum);
38:
39: // Go back to the main store page for more shopping
40: return RedirectToAction("Index");
41: }
42: //
43: // AJAX: /ShoppingCart/RemoveFromCart/5
44: [HttpPost]
45: public ActionResult RemoveFromCart(int id)
46: {
47: // Remove the item from the cart
48: var cart = ShoppingCart.GetCart(this.HttpContext);
49:
50: // Get the name of the album to display confirmation
51: string albumName = storeDB.Carts
52: .Single(item => item.RecordId == id).Album.Title;
53:
54: // Remove from cart
55: int itemCount = cart.RemoveFromCart(id);
56:
57: // Display the confirmation message
58: var results = new ShoppingCartRemoveViewModel
59: {
60: Message = Server.HtmlEncode(albumName) +
61: " has been removed from your shopping cart.",
62: CartTotal = cart.GetTotal(),
63: CartCount = cart.GetCount(),
64: ItemCount = itemCount,
65: DeleteId = id
66: };
67: return Json(results);
68: }
69: //
70: // GET: /ShoppingCart/CartSummary
71: [ChildActionOnly]
72: public ActionResult CartSummary()
73: {
74: var cart = ShoppingCart.GetCart(this.HttpContext);
75:
76: ViewData["CartCount"] = cart.GetCount();
77: return PartialView("CartSummary");
78: }
79: }
80: }
Ajax Updates with jQuery
We’ll next create a Shopping Cart Index page that is strongly typed to the ShoppingCartViewModel and uses the List View template using the same method as before.
However, instead of using an Html.ActionLink to remove items from the cart, we’ll use jQuery to “wire up” the click event for all links in this view which have the HTML class RemoveLink. Rather than posting the form, this click event handler will just make an AJAX callback to our RemoveFromCart controller action. The RemoveFromCart returns a JSON serialized result, which our jQuery callback then parses and performs four quick updates to the page using jQuery:
- Removes the deleted album from the list
- Updates the cart count in the header
- Displays an update message to the user
- Updates the cart total price
Since the remove scenario is being handled by an Ajax callback within the Index view, we don’t need an additional view for RemoveFromCart action. Here is the complete code for the /ShoppingCart/Index view:
[!code-cshtmlMain]
1: @model MvcMusicStore.ViewModels.ShoppingCartViewModel
2: @{
3: ViewBag.Title = "Shopping Cart";
4: }
5: <script src="/Scripts/jquery-1.4.4.min.js"
6: type="text/javascript"></script>
7: <script type="text/javascript">
8: $(function () {
9: // Document.ready -> link up remove event handler
10: $(".RemoveLink").click(function () {
11: // Get the id from the link
12: var recordToDelete = $(this).attr("data-id");
13: if (recordToDelete != '') {
14: // Perform the ajax post
15: $.post("/ShoppingCart/RemoveFromCart", {"id": recordToDelete },
16: function (data) {
17: // Successful requests get here
18: // Update the page elements
19: if (data.ItemCount == 0) {
20: $('#row-' + data.DeleteId).fadeOut('slow');
21: } else {
22: $('#item-count-' + data.DeleteId).text(data.ItemCount);
23: }
24: $('#cart-total').text(data.CartTotal);
25: $('#update-message').text(data.Message);
26: $('#cart-status').text('Cart (' + data.CartCount + ')');
27: });
28: }
29: });
30: });
31: </script>
32: <h3>
33: <em>Review</em> your cart:
34: </h3>
35: <p class="button">
36: @Html.ActionLink("Checkout
37: >>", "AddressAndPayment", "Checkout")
38: </p>
39: <div id="update-message">
40: </div>
41: <table>
42: <tr>
43: <th>
44: Album Name
45: </th>
46: <th>
47: Price (each)
48: </th>
49: <th>
50: Quantity
51: </th>
52: <th></th>
53: </tr>
54: @foreach (var item in
55: Model.CartItems)
56: {
57: <tr id="row-@item.RecordId">
58: <td>
59: @Html.ActionLink(item.Album.Title,
60: "Details", "Store", new { id = item.AlbumId }, null)
61: </td>
62: <td>
63: @item.Album.Price
64: </td>
65: <td id="item-count-@item.RecordId">
66: @item.Count
67: </td>
68: <td>
69: <a href="#" class="RemoveLink"
70: data-id="@item.RecordId">Remove
71: from cart</a>
72: </td>
73: </tr>
74: }
75: <tr>
76: <td>
77: Total
78: </td>
79: <td>
80: </td>
81: <td>
82: </td>
83: <td id="cart-total">
84: @Model.CartTotal
85: </td>
86: </tr>
87: </table>
In order to test this out, we need to be able to add items to our shopping cart. We’ll update our Store Details view to include an “Add to cart” button. While we’re at it, we can include some of the Album additional information which we’ve added since we last updated this view: Genre, Artist, Price, and Album Art. The updated Store Details view code appears as shown below.
[!code-cshtmlMain]
1: @model MvcMusicStore.Models.Album
2: @{
3: ViewBag.Title = "Album - " + Model.Title;
4: }
5: <h2>@Model.Title</h2>
6: <p>
7: <img alt="@Model.Title"
8: src="@Model.AlbumArtUrl" />
9: </p>
10: <div id="album-details">
11: <p>
12: <em>Genre:</em>
13: @Model.Genre.Name
14: </p>
15: <p>
16: <em>Artist:</em>
17: @Model.Artist.Name
18: </p>
19: <p>
20: <em>Price:</em>
21: @String.Format("{0:F}",
22: Model.Price)
23: </p>
24: <p class="button">
25: @Html.ActionLink("Add to
26: cart", "AddToCart",
27: "ShoppingCart", new { id = Model.AlbumId }, "")
28: </p>
29: </div>
Now we can click through the store and test adding and removing Albums to and from our shopping cart. Run the application and browse to the Store Index.
Next, click on a Genre to view a list of albums.
Clicking on an Album title now shows our updated Album Details view, including the “Add to cart” button.
Clicking the “Add to cart” button shows our Shopping Cart Index view with the shopping cart summary list.
After loading up your shopping cart, you can click on the Remove from cart link to see the Ajax update to your shopping cart.
We’ve built out a working shopping cart which allows unregistered users to add items to their cart. In the following section, we’ll allow them to register and complete the checkout process.
|