Sitecore MVC MusicStore – Part 7

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:

Cart.cs
		[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; }
		}
	
Order.cs
		[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; }
		}
	
OrderDetail.cs
		[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.

ShoppingCart.cs
		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:

ShoppingCartViewModel.cs
		public class ShoppingCartViewModel
		{
			public List<Cart> CartItems { get; set; }
			public decimal CartTotal { get; set; }
		}
	
ShoppingCartRemoveViewModel.cs
		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.

ShoppingCartController.cs
		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.

Index.cshtml
		@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:

CartSummary.cshtml
		<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.

Go to Part 8

Leave a Reply

Your email address will not be published. Required fields are marked *