"
 
 
 
ASP.NET (snapshot 2017) Microsoft documentation and samples

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:

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.

Previous Next





Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19>  <20>  <21>  <22>  <23
Link to this page: //www.vb-net.com/AspNet-DocAndSamples-2017/aspnet/mvc/overview/older-versions/mvc-music-store/mvc-music-store-part-8.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <CHAT ME>  <ABOUT ME>  < THANKS ME>