Sitecore is not very often used as a real webshop, but as it is covered in the MusicStore MVC tutorial, we will also add in our Sitecore site.
First things first, we need to create three models in which we will store the shopping cart data:
[SitecoreType(true, "{A588DFDE-C3B0-4CF9-AFFA-F214BF8C4C4C}", TemplateName = "Cart")] public class Cart : IBaseEntity { [SitecoreField("{4E77B7F8-C958-4B90-80EB-5B5C82F21821}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Cart Id", FieldSortOrder = 100)] public virtual string CartId { get; set; } [SitecoreField("{64AE9140-F1DA-4DA1-BC38-57EA1A31BE01}", SitecoreFieldType.Number, "Data", FieldName = "Count", FieldSortOrder = 200)] public virtual int Count { get; set; } [SitecoreField("{381DA688-E03B-4EB5-B497-D0DE514B865C}", SitecoreFieldType.GroupedDroplink, "Data", FieldName = "Album", FieldSource = "DataSource=/sitecore/content/MusicStore/Content/Albums&IncludeTemplatesForSelection=Album", FieldSortOrder = 300)] public virtual Album Album { get; set; } }
[SitecoreType(true, "{A035A8DD-AE80-4F7A-AA65-104BB361E92D}", TemplateName = "Order")] public class Order : IBaseEntity { [SitecoreField("{83F9C976-44E3-4C76-9706-3384B7E45E2C}", SitecoreFieldType.SingleLineText, "Data", FieldName = "First Name", FieldSortOrder = 100)] public virtual string FirstName { get; set; } [SitecoreField("{74E7AADC-9982-4538-BC23-1D955F5D0C28}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Last Name", FieldSortOrder = 200)] public virtual string LastName { get; set; } [SitecoreField("{B99E32BE-741F-4AA9-BCA2-AEE69819727A}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Address", FieldSortOrder = 300)] public virtual string Address { get; set; } [SitecoreField("{248DF270-0738-4F1D-BEDA-00B77517184C}", SitecoreFieldType.SingleLineText, "Data", FieldName = "City", FieldSortOrder = 400)] public virtual string City { get; set; } [SitecoreField("{174C18D8-22D3-4525-957E-9154151DF46E}", SitecoreFieldType.SingleLineText, "Data", FieldName = "State", FieldSortOrder = 500)] public virtual string State { get; set; } [SitecoreField("{4AD6BC63-4B05-4BE0-BB60-6AB76BCF6F70}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Postal Code", FieldSortOrder = 600)] public virtual string PostalCode { get; set; } [SitecoreField("{18093A26-791E-461D-8FC7-FD2AEA443224}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Country", FieldSortOrder = 700)] public virtual string Country { get; set; } [SitecoreField("{CCE69F32-17C3-45FA-AEED-C152A766B878}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Phone", FieldSortOrder = 8100)] public virtual string Phone { get; set; } [SitecoreField("{666378F3-2559-4E2F-A585-9BB848E73B4A}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Email", FieldSortOrder = 900)] public virtual string Email { get; set; } [SitecoreField("{46B0BB8A-DB00-4F44-97C7-6DCD34263A9D}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Total", FieldSortOrder = 1000)] public virtual decimal Total { get; set; } [SitecoreField("{F01396D3-0E21-4666-84ED-5FB0145E5291}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Order Details", FieldSortOrder = 1100)] public virtual List<OrderDetail> OrderDetails { get; set; } }
[SitecoreType(true, "{EC63B874-CC54-4F6D-9EB4-50B6B45B9263}", TemplateName = "Order Detail")] public class OrderDetail : IBaseEntity { [SitecoreField("{20578F12-139C-425C-8B35-6E6B75AA8FB2}", SitecoreFieldType.GroupedDroplink, "Data", FieldName = "Album", FieldSource = "DataSource=/sitecore/content/MusicStore/Content/Albums&IncludeTemplatesForSelection=Album", FieldSortOrder = 100)] public virtual Album Album { get; set; } [SitecoreField("{914B84BA-8F14-4338-A4F1-2CB93F0CC089}", SitecoreFieldType.Number, "Data", FieldName = "Quantity", FieldSortOrder = 200)] public virtual int Quantity { get; set; } [SitecoreField("{54C1D82F-6F6D-46B8-8885-7860DFA3641E}", SitecoreFieldType.Number, "Data", FieldName = "Unit Price", FieldSortOrder = 300)] public virtual decimal UnitPrice { get; set; } }
Next to these basic models they also create the ShoppingCart model, which actually isn't really a model but handles most of the business logic for the shopping cart.
public partial class ShoppingCart { string ShoppingCartId { get; set; } public const string CartSessionKey = "CartId"; private readonly ICartService _cartService = IoC.Resolver.Resolve<ICartService>(); private readonly IAlbumService _albumService = IoC.Resolver.Resolve<IAlbumService>(); private readonly IOrderService _orderService = IoC.Resolver.Resolve<IOrderService>(); private readonly IOrderDetailService _orderDetailService = IoC.Resolver.Resolve<IOrderDetailService>(); public static ShoppingCart GetCart(HttpContextBase context) { var cart = new ShoppingCart(); cart.ShoppingCartId = cart.GetCartId(context); return cart; } // Helper method to simplify shopping cart calls public static ShoppingCart GetCart(Controller controller) { return GetCart(controller.HttpContext); } public void AddToCart(Album album) { // Get the matching cart and album instances var cartItem = _cartService.GetCartItemByAlbum(ShoppingCartId, album.Id); if (cartItem == null) { // Create a new cart item if no cart item exists cartItem = new Cart { Album = _albumService.GetAlbum(album.Id.ToString()), CartId = ShoppingCartId, Count = 1 }; _cartService.AddCart(cartItem); } else { // If the item does exist in the cart, then add one to the quantity cartItem.Count++; _cartService.UpdateCart(cartItem); } } public int RemoveFromCart(string id) { // Get the cart var cartItem = _cartService.GetCartItem(id); int itemCount = 0; if (cartItem != null) { if (cartItem.Count > 1) { cartItem.Count--; itemCount = cartItem.Count; } else { _cartService.RemoveCartItem(cartItem.Id); } } return itemCount; } public void EmptyCart() { var cartItems = _cartService.GetCartItems(ShoppingCartId); foreach (var cartItem in cartItems) { _cartService.RemoveCartItem(cartItem.Id); } } public List<Cart> GetCartItems() { return _cartService.GetCartItems(ShoppingCartId).ToList(); } public int GetCount() { return GetCartItems().Count; } public decimal GetTotal() { // Multiply album price by count of that album to get // the current price for each of those albums in the cart // sum all album price totals to get the cart total decimal? total = (from cartItems in _cartService.GetCartItems(ShoppingCartId) select (int?)cartItems.Count * cartItems.Album.Price).Sum(); return total ?? decimal.Zero; } public string CreateOrder(Order order) { decimal orderTotal = 0; var cartItems = GetCartItems(); // Iterate over the items in the cart, adding the order details for each foreach (var item in cartItems) { var orderDetail = new OrderDetail { Album = item.Album, UnitPrice = item.Album.Price, Quantity = item.Count }; // Set the order total of the shopping cart orderTotal += (item.Count * item.Album.Price); _orderDetailService.AddOrderDetail(orderDetail, order); } // Set the order's total to the orderTotal count order.Total = orderTotal; // Save the order _orderService.UpdateOrder(order); // Empty the shopping cart EmptyCart(); // Return the OrderId as the confirmation number return order.Id.ToString(); } // We're using HttpContextBase to allow access to cookies. public string GetCartId(HttpContextBase context) { if (context.Session[CartSessionKey] == null) { if (!string.IsNullOrWhiteSpace(context.User.Identity.Name)) { context.Session[CartSessionKey] = context.User.Identity.Name.Contains('\\') ? context.User.Identity.Name.Replace("\\", "-") : context.User.Identity.Name; } else { // Generate a new random GUID using System.Guid class Guid tempCartId = Guid.NewGuid(); // Send tempCartId back to client as a cookie context.Session[CartSessionKey] = tempCartId.ToString(); } } return context.Session[CartSessionKey].ToString(); } }
Read the code carefully and understand what each method does:
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.
Our ShoppingCart controller will communicate through the use of ViewModels. Create the ViewModels folder in the project and add the following ViewModels:
public class ShoppingCartViewModel { public List<Cart> CartItems { get; set; } public decimal CartTotal { get; set; } }
public class ShoppingCartRemoveViewModel { public string Message { get; set; } public decimal CartTotal { get; set; } public int CartCount { get; set; } public int ItemCount { get; set; } public string DeleteId { get; set; } }
Now we can create the ShoppingCart controller, which contains components to show the content of the cart and functionalities(accessible by Ajax) to add and remove albums from the cart.
public class ShoppingCartController : Controller { private readonly IAlbumService _albumService; public ShoppingCartController(IAlbumService albumService) { _albumService = albumService; } public ActionResult Index() { var cart = ShoppingCart.GetCart(this.HttpContext); // Set up our ViewModel var viewModel = new ShoppingCartViewModel { CartItems = cart.GetCartItems(), CartTotal = cart.GetTotal() }; // Return the view return View(viewModel); } public void AddToCart(string id) { // Retrieve the album from the database var addedAlbum = _albumService.GetAlbum(id); // Add it to the shopping cart var cart = ShoppingCart.GetCart(this.HttpContext); cart.AddToCart(addedAlbum); // Go back to the main store page for more shopping Response.Redirect("/ShoppingCart"); } [HttpPost] public ActionResult RemoveFromCart(string id) { // Remove the item from the cart var cart = ShoppingCart.GetCart(this.HttpContext); // Get the name of the album to display confirmation string albumName = _albumService.GetAlbum(id).Title; // Remove from cart int itemCount = cart.RemoveFromCart(id); // Display the confirmation message var results = new ShoppingCartRemoveViewModel { Message = Server.HtmlEncode(albumName) + "The album has been removed from your shopping cart.", CartTotal = cart.GetTotal(), CartCount = cart.GetCount(), ItemCount = itemCount, DeleteId = id }; return Json(results); } [ChildActionOnly] public ActionResult CartSummary() { var cart = ShoppingCart.GetCart(this.HttpContext); ViewData["CartCount"] = cart.GetCount(); return PartialView("CartSummary"); } }
Only the Index and CartSummary methods need views.
The Index view contains a full overview of the items in the cart, and has a link next to each entry which triggers an Ajax call to remove the item from the cart. This Ajax call is instead of an ActionLink like we used in the Album Details view.
@model MusicStore.Web.ViewModels.ShoppingCartViewModel <script src="/Scripts/jquery-1.4.4.min.js" type="text/javascript"></script> <script type="text/javascript"> $(function() { // Document.ready -> link up remove event handler $(".RemoveLink").click(function() { // Get the id from the link var recordToDelete = $(this).attr("data-id"); if (recordToDelete != '') { // Perform the ajax post $.post("/api/sitecore/ShoppingCart/RemoveFromCart", { "id": recordToDelete }, function(data) { // Successful requests get here // Update the page elements if (data.ItemCount == 0) { $('#row-' + data.DeleteId).fadeOut('slow'); } else { $('#item-count-' + data.DeleteId).text(data.ItemCount); } $('#cart-total').text(data.CartTotal); $('#update-message').text(data.Message); $('#cart-status').text('Cart (' + data.CartCount + ')'); }); } }); }); function handleUpdate() { // Load and deserialize the returned JSON data var json = context.get_data(); var data = Sys.Serialization.JavaScriptSerializer.deserialize(json); // Update the page elements if (data.ItemCount == 0) { $('#row-' + data.DeleteId).fadeOut('slow'); } else { $('#item-count-' + data.DeleteId).text(data.ItemCount); } $('#cart-total').text(data.CartTotal); $('#update-message').text(data.Message); $('#cart-status').text('Cart (' + data.CartCount + ')'); } </script> <h3> <em>Review</em> your cart: </h3> <p class="button"> <a href="/Checkout">Checkout >></a> </p> <div id="update-message"> </div> <table> <tr> <th> Album Name </th> <th> Price (each) </th> <th> Quantity </th> <th></th> </tr> @foreach (var item in Model.CartItems) { <tr id="row-@item.Id"> <td> <a href="/Store/Details?id=@item.Album.Id">@item.Album.Title</a> </td> <td> @item.Album.Price </td> <td id="item-count-@item.Id"> @item.Count </td> <td> <a href="#" class="RemoveLink" data-id="@item.Id">Remove from cart</a> </td> </tr> } <tr> <td> Total </td> <td></td> <td></td> <td id="cart-total"> @Model.CartTotal </td> </tr> </table>
And the CartSummary view only displays the amount of items inside the cart:
<a href="/ShoppingCart" id="cart-status">Cart (@ViewData["CartCount"])</a>
Now create the Index rendering for the ShoppingCartController in Sitecore and add it to the /Home/ShoppingCart page in the 'main' placeholder.
Next up is testing our new functionalities. First go to an album detail page and add it to the cart.
By pressing the button, the Shopping Cart Index should open and show the product in the cart.
By pressing the Remove from cart link the product should be removed from the cart.
In the next and last part of this series, we will add the checkout form and order processing.