Sitecore MVC MusicStore – Part 8

Standard

In the final part of this series we will add the possibility to checkout the items from your cart.
As you probably would have noticed in Part 7, we already added the checkout button in the Shopping Cart Index page.

In this series I will not cover the possibility of registering and login in. I will cover this in another tutorial/walkthrough. So the users can now just checkout without needing to sign in to an account.

First create the CheckoutController which will have the following methods:

AddressAndPayment (GET method) will display a form to allow the user to enter their information.
AddressAndPayment (POST method) will validate the input and process the order. If the input is valid, an order item will be created in Sitecore and the order will be confirmed by redirecting to the completion page.
Complete will be shown after a user has successfully finished the checkout process. This view will include the user’s order number, as confirmation.

CheckoutController.cs
		public class CheckoutController : Controller
		{
			private readonly IOrderService _orderService;
			const string PromoCode = "FREE";
		  
			public CheckoutController(IOrderService orderService)
			{
				_orderService = orderService;
			}
		  
			public ActionResult AddressAndPayment()
			{
				return View();
			}
		  
			[HttpPost]
			public ActionResult AddressAndPayment(FormCollection values)
			{
				var order = new Order();
				var viewModel = new OrderViewModel();
		  
				try
				{
					if (string.Equals(values["PromoCode"], PromoCode,
						StringComparison.OrdinalIgnoreCase) == false)
					{
						return View(viewModel);
					}
						 
					order.FirstName = values["FirstName"];
					order.LastName = values["LastName"];
					order.Address = values["Address"];
					order.City = values["City"];
					order.State = values["State"];
					order.PostalCode = values["PostalCode"];
					order.Country = values["Country"];
					order.Phone = values["Phone"];
					order.Email = values["Email"];
		  
					//Save Order
					_orderService.AddOrder(order);
					//Process the order
					var cart = ShoppingCart.GetCart(this.HttpContext);
					cart.CreateOrder(order);
		  
					Response.Redirect("/Checkout/Complete?id=" + order.Id);
				}
				catch
				{
					//Invalid - redisplay with errors
					return View(viewModel);
				}
				return View(viewModel);
			}
		  
			public ActionResult Complete(string id)
			{
				// Validate customer owns this order
				bool isValid = _orderService.GetOrder(id) != null;
		  
				if (isValid)
				{
					return View(model: id);
				}
				else
				{
					return View("Error");
				}
			}
		}
	

As you can see, this controller uses an OrderService to create and update orders in Sitecore.
Note that I use a 'IBaseEntityService' in this class. This is a service I created just like the Album and Genre services which only has a get method, which gets the Sitecore item based on the ID.

OrderService.cs
		public class OrderService : BaseModelService<Order>, IOrderService
		{
			private readonly IBaseEntityService _baseEntityService;
		  
			public OrderService(IModelValidator validator, IEventAggregator eventAggregator, IRepository<Order> repository, IBaseEntityService baseEntityService) : base(validator, eventAggregator, repository)
			{
				_baseEntityService = baseEntityService;
			}
		  
			public Order AddOrder(Order order)
			{
				order.Name = HttpContext.Current.Session["CartId"].ToString().Replace("\\", "-") + " - " + DateTime.Now.ToDashedDateFormat();
				order.Parent = _baseEntityService.Get("{D0C819DA-BA63-4766-8E5B-603D83DD7D30}");
				using (new global::Sitecore.SecurityModel.SecurityDisabler())
				{
					using (UnitOfWork.BeginUnitOfWork())
					{
						return Insert(order);
					}
				}
			}
		  
			public Order UpdateOrder(Order order)
			{
				using (new global::Sitecore.SecurityModel.SecurityDisabler())
				{
					using (UnitOfWork.BeginUnitOfWork())
					{
						return SaveOrUpdate(order);
					}
				}
			}
		  
			public Order GetOrder(object id)
			{
				using (UnitOfWork.BeginUnitOfWork())
				{
					return Get(id);
				}
			}
		}
	

Don't forget to also create the interface for this service and to add it to the SetDependencies class.

As you can see the AddOrder method creates a new Order item in Sitecore. But to do this it needs a parent item, in this case I use an Sitecore ID to get a folder I created in Sitecore.
Using an Sitecore ID like this is not recommended though! Normally I would create a setting in Sitecore in which the web admin can specify a folder where the orders will be stored. But because this is only a simple tutorial, I used the direct ID to this folder.

Now we need to create the views for the methods we added in the Checkout Controller.

AddressAndPayment.cshtml
		@model MusicStore.Web.ViewModels.OrderViewModel
  
		@{
			ViewBag.Title = "Address And Payment";
		}
		  
		<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
		<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
		  
		@using (Html.BeginForm())
		{
		  
			<h2>Address And Payment</h2>
			<fieldset>
				<legend>Shipping Information</legend>
		  
				@Html.EditorForModel()
			</fieldset>
			<fieldset>
				<legend>Payment</legend>
				<p>We're running a promotion: all music is free with the promo code: "FREE"</p>
		  
				<div class="editor-label">
					@Html.Label("Promo Code")
				</div>
				<div class="editor-field">
					@Html.TextBox("PromoCode")
				</div>
			</fieldset>
		  
			<input type="submit" value="Submit Order" />
		}
	
Complete.cshtml
		@model string
  
		<h2>Checkout Complete</h2>
		  
		<p>Thanks for your order! Your order number is: @Model</p>
		  
		<p>
			How about shopping for some more music in our <a href="/">store</a>
		</p>
	

The complete method also refers to a "Error" view. This view is not placed in the Checkout folder in the Views folder of the project as conventions would want the others, but because it's a direct call to the view, we need to add it to the Shared folder.

Error.cshtml
		<h2>Error</h2>
  
		<p>
			We're sorry, we've hit an unexpected error.
			<a href="javascript:history.go(-1)">Click here</a>
			if you'd like to go back and try that again.
		</p>
	

Finally we ofcourse need to create the proper renderings in Sitecore again.

The AddressAndPayment rendering should be added to the /Home/Checkout page and the Complete rendering to the /Home/Checkout/Complete page.

That concludes this series. We should now have created a website where we can browse through genres and albums, and finally order those albums.

Sitecore MVC MusicStore – Part 7

Standard

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

Sitecore MVC MusicStore – Part 6

Standard

When creating the detail view component for the genres and albums, we want the URL to be pretty and SEO friendly. So an id in the URL will not suffice.
Sitecore already has some base functionalities, like the creation of wildcard pages. The item name of these pages must be '*' and must be the first item of that level. 
Once you open a page on that site level, it either searches for a specified item with that page name or otherwise opens the wildcard page.

First thing we need, is to create UrlProviders which will take care of finding the item which is mentioned in the URL.
Take for example the following GenreUrlProvider:

GenreUrlProvider.cs
		public class GenreUrlProvider : IGenreUrlProvider
		{
			private readonly IGenreService _genreService;
		  
			public GenreUrlProvider(IGenreService genreService)
			{
				_genreService = genreService;
			}
		  
			public Genre GetCurrentGenre()
			{
				var httpContext = HttpContext.Current;
		  
				if (httpContext == null || httpContext.Request.Url == null)
					return null;
		 
				if (!string.IsNullOrEmpty(httpContext.Request.Params["genreid"]))
				{
					return _genreService.GetGenre(httpContext.Request.Params["genreid"]);
				}
		  
				var displayName = HttpUtility.UrlDecode(httpContext.Request.Url.Segments.Last().Split('?').First());
				return _genreService.GetGenre(displayName);
			}
		}
	

The GetCurrentGenre method, first checks if the genre id is specified in the request parameters. If this isn't the case it takes the last segment of the URL and gets the genre by that display name.

Same thing should be done for the AlbumUrlProvider:

AlbumUrlProvider.cs
		public class AlbumUrlProvider : IAlbumUrlProvider
		{
			private readonly IAlbumService _albumService;
		 
			public AlbumUrlProvider(IAlbumService albumService)
			{
				_albumService = albumService;
			}
		  
			public Album GetCurrentAlbum()
			{
				var httpContext = HttpContext.Current;
		 
				if (httpContext == null || httpContext.Request.Url == null)
					return null;
		 
				if (!string.IsNullOrEmpty(httpContext.Request.Params["albumid"]))
				{
					return _albumService.GetAlbum(httpContext.Request.Params["albumid"]);
				}
		  
				var displayName = HttpUtility.UrlDecode(httpContext.Request.Url.Segments.Last().Split('?').First());
				return _albumService.GetAlbum(displayName);
			}
		}
	

Next up is creating the components. These components are fairly simple. The request the current item from the UrlProvider and returns it to the view.
These components should be created in the StoreController. The Details method is used for the album details page, while the Browse method is used by the genre detail page.

StoreController.cs
		public class StoreController : Controller
		{
			private readonly IAlbumService _albumService;
			private readonly IGenreService _genreService;
			private readonly IAlbumUrlProvider _albumUrlProvider;
			private readonly IGenreUrlProvider _genreUrlProvider;
		  
			public StoreController(IAlbumService albumService, IGenreService genreService, IAlbumUrlProvider albumUrlProvider, IGenreUrlProvider genreUrlProvider)
			{
				_albumService = albumService;
				_genreService = genreService;
				_albumUrlProvider = albumUrlProvider;
				_genreUrlProvider = genreUrlProvider;
			}
		  
			public ActionResult Index()
			{
				return View(_genreService.GetAll());
			}
		  
			public ActionResult Details(string id)
			{
				var album = _albumUrlProvider.GetCurrentAlbum() ?? _albumService.GetAlbum(id);
				return View(album);
			}
		  
			public ActionResult Browse(string id)
			{
				Genre genre = _genreUrlProvider.GetCurrentGenre() ?? _genreService.GetGenre(id);
				genre.Albums = _albumService.GetAlbumsByGenre(genre.Id).ToList();
		  
				return View(genre);
			}
		}
	

As you can see, before the current genre is returned to the view it also fills the albums list with related albums from that genre using the AlbumService.

As for the views, they are also pretty simple and should look like this:

Details.cshtml
		@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<MusicStore.Web.Models.Album>
  
		<h2>@Editable(a => a.Title)</h2>
		<p>
			@RenderImage(a => a.AlbumArt, isEditable: true)
		</p>
		<div id="album-details">
			<p>
				<em>Genre:</em>
				@Model.Genre.Name
			</p>
			<p>
				<em>Artist:</em>
				@Model.Artist.Name
			</p>
			<p>
				<em>Price:</em>
				@String.Format("{0:F}",Model.Price)
			</p>
			<p class="button">
				@Html.ActionLink("Add to cart", "AddToCart", "ShoppingCart", new { id = Model.Id.ToString() }, "")
			</p>
		</div>
	

As you can see the Details view already contains an action link to a method and controller which we did not create yet. This will be added in the next part of this series and will eventually add the current album to the cart.

Browse.cshtml
		@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<MusicStore.Web.Models.Genre>
		@{
			ViewBag.Title = "Browse";
		}
		<div class="genre">
			<h3><em>@Model.Name</em> Albums</h3>
		  
			<ul id="album-list">
				@foreach (var album in Model.Albums)
				{
					<li>
						<a href="/Store/Details?id=@album.Id">
							@RenderImage(a => album.AlbumArt)
							<span>@album.Title</span>
						</a>
					</li>
				}
			</ul>
		</div>
	

Next thing we should obviously do, is create the renderings in Sitecore.

And finally create the detail page for both the album details(under /Home/Store/Details/*) and the genre details(under /Home/Store/Browse/*) and add the newly created renderings in the 'main' placeholder.

 

Finally we should have two new pages which should look like this:

The Detail page.

The Browse page.

Go to Part 7

Sitecore MVC MusicStore – Part 5

Standard

Now that we have some content in Sitecore we can show this in an overview of all the genres and albums.

First thing we need to do is create a service which will provide the data from Sitecore so that we can use this in a component.
The BoC module provides us with some very handy base services and functionalities to provide this content.
In the service we are creating, we will make use of the BaseModelService. This BoC service provides methods to get all or find a certain content item based on the GlassMapper model we created.

GenreService.cs
		public class GenreService : BaseModelService<Genre>, IGenreService
		{
			public GenreService(IModelValidator validator, IEventAggregator eventAggregator, IRepository<Genre> repository) : base(validator, eventAggregator, repository)
			{
			}
		  
			public IEnumerable<Genre> GetAll()
			{
				using (DataContext.BeginDataContext())
				{
					return ListAll();
				}
			}
		 
			public Genre GetGenre(string name)
			{
				using (DataContext.BeginDataContext())
				{
					return Find(g => g.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
				}
			}
		}
	

In above service we created two methods, one to get all genres from Sitecore, and one to get a single genre based on the name of this item.

To provide the album content, we do the same thing but add another method to get an album based on its Sitecore id or display name.

AlbumService.cs
		public class AlbumService : BaseModelService<Album>, IAlbumService
		{
			public AlbumService(IModelValidator validator, IEventAggregator eventAggregator, IRepository<Album> repository) : base(validator, eventAggregator, repository)
			{
			}
		  
			public Album GetAlbum(object id)
			{
				using (DataContext.BeginDataContext())
				{
					int externalId;
					Album album = null;
					if (id is Guid)
						album = Get(id.ToString());
					else if (ID.IsID(string.Concat(id)))
						album = Get(string.Concat(id));
					else if (int.TryParse(string.Concat(id), out externalId))
						album = Find(a => a.ExternalId.Equals(string.Concat(id), StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
					else
						album = Find(a => a.DisplayName.Equals(string.Concat(id), StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
		  
					return album;
				}
			}
		 
			public IEnumerable<Album> GetAlbumsByGenre(Guid id)
			{
				using (DataContext.BeginDataContext())
				{
					return Find(a => a.Genre.Id == id);
				}
			}
		  
			public IEnumerable<Album> GetAll()
			{
				using (DataContext.BeginDataContext())
				{
					return ListAll();
				}
			}
		}
	

To use these new services in combination with the IoC container, we will need to add it to the container.
To do this create a new class in the App_Start folder of the project. This class will have to derive from the IContainerInitializer interface from the BoC.InversionOfControl package.
Inside the Execute method you will need to add both services to the resolver.

SetDependencies.cs
		public class SetDependencies : IContainerInitializer
		{
			private readonly IDependencyResolver _resolver;
		 
			public SetDependencies(IDependencyResolver resolver)
			{
				_resolver = resolver;
			}
		 
			public void Execute()
			{
				_resolver.RegisterType<IAlbumService, AlbumService>();
				_resolver.RegisterType<IGenreService, GenreService>();
			}
		}
	

Now that we have the means to get the content, we can create overview components to show this content.

Lets first create the StoreController. In this controller's index method we will show the genre overview, which displays all genres available in Sitecore.

StoreController.cs
		public class StoreController : Controller
		{
			private readonly IGenreService _genreService;
		 
			public StoreController(IGenreService genreService)
			{
				_genreService = genreService;
			}
		 
			public ActionResult Index()
			{
				return View(_genreService.GetAll());
			}
		}
	

This method uses the service we just created to get all the genres from Sitecore and returns them to the view.

Index.cshtml
		@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<IEnumerable<MusicStore.Web.Models.Genre>>
		@{
			ViewBag.Title = "Store";
		}
		<h3>Browse Genres</h3>
		<p>
			Select from @Model.Count()
			genres:
		</p>
		<ul>
			@foreach (var genre in Model)
			{
				<li><a href="/Store/Browse?genre=@genre.Name">@genre.Name</a></li>
			}
		</ul>
	

And finally we will need to add this view as a rendering to Sitecore:

Now it is ready to be used on any page you like. For this tutorial you will at least have to place it on the Browse page which you should create under the homepage.

Note: If you don't see any content on your site, make sure everything is published and that you have rebuild the content indexes.

It would also be nice if your customers could see which albums are ordered the most in the store. To provide this we will update the homepage index rendering to show the top 5 albums.

HomeController.cs
		public class HomeController : Controller
		{
			private readonly IAlbumService _albumService;
		  
			public HomeController(IAlbumService albumService)
			{
				_albumService = albumService;
			}
		  
			public ActionResult Index()
			{
				List<Album> albums = _albumService.GetAll().OrderBy(a => Globals.LinkDatabase.GetReferenceCount(Database.GetDatabase("master").Items[new ID(a.Id)])).Take(5).ToList();
				return View(albums);
			}
		}
	

The OrderBy Linq query, will sort the albums based on how many times each album is referenced. Later on in this series we will create the orders, and in these order we will add a reference to each album ordered.

The view will look like this:

Index.cshtml
		@inherits Glass.Mapper.Sc.Web.Mvc.GlassView<List<MusicStore.Web.Models.Album>>
		@{
			ViewBag.Title = "ASP.NET MVC Music Store";
		}
		<div id="promotion">
		</div>
		  
		<h3><em>Fresh</em> off the grill</h3>
		  
		<ul id="album-list">
			@foreach (var album in Model)
			{
				<li>
					<a href="/Store/Details?id=@album.Id">
						@RenderImage(a => album.AlbumArt)
						<span>@album.Title</span>
					</a>
				</li>
			}
		</ul>
	

In the next part of this series, we will create a genre detail page by using wildcard functionalities.

Go to Part 6

Sitecore MVC MusicStore – Part 4

Standard

Before we can actually create the real store functionalities, we will have to add the models and data.
Because we added the Glass.Mapper extension to this solution we can create code first models which will automatically be created in Sitecore as templates.

Creating the models

Normal MVC conventions suggest to add your models in the “Models” folder of your solution, so we will do the same, though it isn’t required.

Let’s first add the Genre model. Add the following “Genre.cs” to the “Models” folder:

Genre.cs
		using System.Collections.Generic;
		using Glass.Mapper.Sc.Configuration;
		using Glass.Mapper.Sc.Configuration.Attributes;
		  
		namespace MusicStore.Web.Models
		{
			[SitecoreType(true, "{A326A413-C7E0-4B59-968D-CC7076EEB983}", TemplateName = "Genre")]
			public class Genre : IBaseEntity
			{
				[SitecoreField("{9789128F-A10B-440E-AADE-B1F34132386D}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Name", FieldSortOrder = 100)]
				public virtual string Name { get; set; }
		  
				[SitecoreField("{F8BF9715-7693-485E-AE88-AB4F04096EFA}", SitecoreFieldType.RichText, "Data", FieldName = "Description", FieldSortOrder = 200)]
				public virtual string Description { get; set; }
		  
				public List<Album> Albums { get; set; }
			}
		}
	

As you can see the class and its attributes have data annotations which Glass.Mapper uses to create the template and its fields. Next to that the class extends the BaseCommon class from the eFocus.Sitecore.Glass module. This class contains some default Sitecore attributes, like the Sitecore path and Sitecore item ID.

Each class and attribute has to have its own unique GUID. You can also give the templates and fields custom names instead of the code name. And there are multiple field types you can choose from.
For more information and tutorials, see the Glass.Mapper website(http://glass.lu/Mapper/Sc/Tutorials)

As you can also see, we already added a List with albums. This attribute will later be used for the albums overview. But we haven’t created the Album model yet, so let’s add the following class:

Album.cs
		using Glass.Mapper.Sc.Configuration;
		using Glass.Mapper.Sc.Configuration.Attributes;
		using Glass.Mapper.Sc.Fields;
		  
		namespace MusicStore.Web.Models
		{
			[SitecoreType(true, "{1483783D-CF92-499B-B45F-47D90E9A17A9}", TemplateName = "Album")]
			public class Album : IBaseEntity
			{
				[SitecoreField("{03C3753B-F325-4D56-BFC9-AA00EF110445}", SitecoreFieldType.SingleLineText, "Data", FieldName = "ExternalId", FieldSortOrder = 50)]
				public virtual string ExternalId { get; set; }
		  
				[SitecoreField("{A4B5B52B-FC82-413F-B5D0-D3DFF2CBE5BE}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Title", FieldSortOrder = 100)]
				public virtual string Title { get; set; }
		  
				[SitecoreField("{0A721A79-C81A-4019-833A-B4B3C58185BD}", SitecoreFieldType.Droplink, "Data", FieldName = "Genre", FieldSource = "DataSource=/sitecore/content/MusicStore/Content/Genres&IncludeTemplatesForSelection=Genre", FieldSortOrder = 200)]
				public virtual Genre Genre { get; set; }
		  
				[SitecoreField("{F53A93B9-C6DF-4F87-8984-96E10633617E}", SitecoreFieldType.Droplink, "Data", FieldName = "Artist", FieldSource = "DataSource=/sitecore/content/MusicStore/Content/Artists&IncludeTemplatesForSelection=Artist", FieldSortOrder = 300)]
				public virtual Artist Artist { get; set; }
		  
				[SitecoreField("{EAEA340A-EA56-47F7-A1B5-807F7F7A02FC}", SitecoreFieldType.Number, "Data", FieldName = "Price", FieldSortOrder = 400)]
				public virtual decimal Price { get; set; }
		  
				[SitecoreField("{E7FCE177-5AA7-4CF6-A982-0270435FCBE7}", SitecoreFieldType.Image, "Data", FieldName = "Album Art", FieldSortOrder = 500)]
				public virtual Image AlbumArt { get; set; }
			}
		}
	

A new thing we see in this class is the virtual reference to the Genre, Artist and Image models. This attribute is automatically filled with the correct data of the Sitecore item selected in the Album item.

We can also see there is a “FieldSource” added to the Genre and the Artist field. This means the user can only select a droplink value from below that Sitecore path.

Last but not least we will have to add the Artist model before we can build. So add the following model to the project:

Artist.cs
		using Glass.Mapper.Sc.Configuration;
		using Glass.Mapper.Sc.Configuration.Attributes;
		  
		namespace MusicStore.Web.Models
		{
			[SitecoreType(true, "{4A80F39F-7A31-43FE-BC3E-0F83E5C482D5}", TemplateName = "Artist")]
			public class Artist : IBaseEntity
			{
				[SitecoreField("{1D66D9D8-2711-4C70-95C2-9DDE6F7C1E4F}", SitecoreFieldType.SingleLineText, "Data", FieldName = "Name", FieldSortOrder = 100)]
				public virtual string Name { get; set; }
			}
		}
	

Creating functionalities

Next up is creating the StoreController. In our StoreController we will want to have a Details method which shows a View with album details. This method will know which album to show the details from by using the wildcard principal. This means we will create a wildcard page on which the last part of the page URL will be the album id.

But before we create the Controller itself we want to have a service which can provide us with the album data. The BoC package provides some basic repository functionalities which we can use to provide data from Sitecore. But because these functionalities are very basic, we need to create a shell around them. So let’s create an AlbumService which derives from the BoC.Service.BaseModelService<TModel> class(where TModel should obviously be implemented as Album). This BaseModelService uses Content Search to provide some basic repository functionalities, like getting but also inserting and updating data.
Then create a method GetAlbum with a string parameter id. This method will use the BaseModelService It’s Get method to get the specific album model from Sitecore.

AlbumService.cs
		using System;
		using System.Linq;
		using BoC.EventAggregator;
		using BoC.Persistence;
		using BoC.Services;
		using BoC.DataContext;
		using BoC.Validation;
		using MusicStore.Web.Models;
		using Sitecore.Data;
		  
		namespace MusicStore.Web.Services
		{
			public class AlbumService : BaseModelService<Album>, IAlbumService
			{
				public AlbumService(IModelValidator validator, IEventAggregator eventAggregator, IRepository<Album> repository) : base(validator, eventAggregator, repository)
				{
				}
		  
				public Album GetAlbum(object id)
				{
					using (DataContext.BeginDataContext())
					{
						int externalId;
						Album album = null;
						if (id is Guid)
							album = Get(id.ToString());
						else if (ID.IsID(string.Concat(id)))
							album = repository.Get(string.Concat(id));
						else if (int.TryParse(string.Concat(id), out externalId))
							album = Find(a => a.ExternalId.Equals(string.Concat(id), StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
						else
							album = Find(a => a.Name.Equals(string.Concat(id), StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
		  
						return album;
					}
				}
			}
		}
	

Now we can create the StoreController with the Details method. The method receives a string parameter with the id(which will be provided through the WildcardValueProvider from the BoC)

Create the content

Now build the solution, deploy it, and open the Sitecore Content Editor.

The generated templates created by Glass.Mapper are placed in the /sitecore/templates/GlassTemplates folder. Move the templates we just created to a more specific project location like /sitecore/templates/User Defined/MVC MusicStore.

Now you can start adding the content for the Artists, Genres and Albums.
Make sure the FieldSource value of the Album fields is still correct. Either create the same structure, or change the value of the FieldSource.

Go to Part 5

Sitecore MVC MusicStore – Part 3

Standard

Now that we have Sitecore running, we can start creating our site.

With a normal MVC site, the controller and method name would be part of your site's URL. But because this is a Sitecore site, we don’t want this same routing. So we will have to edit the configured routing in App_Start/RouteConfig.cs. Change the Default route to this:

RouteConfig.cs
		routes.MapRoute(
			name: "Default",
			url: "musicstore_api/{controller}/{action}/{id}",
			defaults: new { id = UrlParameter.Optional }
		);
	

Creating the homepage

Create a HomeController under the Controllers folder of you Web project(this controller probably already exists because it was created when you created the MVC Web project).
Then add(or replace) a method called Index, which returns a simple view showing a string:

HomeController.cs
		public ActionResult Index()
		{
			return View(model: "This is the homepage");
		}
	

And create a View for the method we just created under Views/Home

The homepage will still show a default Sitecore page, but if we go to /musicstore_api/Home/Index we will see the result of our newly created controller and method. This is the URL which Sitecore will also use under water to show the result of a created component.

Before we can add this component to a page on our site, we’ll have to add a placeholder to our view so that we can actually add components to the pages.
Open or create the _Layout.cshtml under Views/Shared and edit it to look like the following:

_Layout.cshtml
		@using Sitecore.Mvc
		<!DOCTYPE html>
		<html>
		<head>
			<meta charset="utf-8" />
			<title>@ViewBag.Title</title>
		</head>
		<body>
			<div id="main">
				@Html.Sitecore().Placeholder("main")
			</div>
		</body>
		</html>
	

Now build your solution and open the Content Editor in Sitecore.

First we will add a new Layout to Sitecore. Go to /Sitecore/Layout/Layouts and create a new Layout item. The path in this item should be “/views/Shared/_Layout.cshtml”. We will use this Layout for all the pages of the site.

Next, we will need to create a component/rendering before we can add it to a page.
Go to /Sitecore/Layout/Renderings and create a base folder structure for all the renderings we are going to add here. Then create a Controller rendering. I created the rendering under /sitecore/layout/Renderings/MVC MusicStore/Home/Index.
In the rendering you should fill the Data fields “Controller” and “Controller Action”. Controller should be “Home” and Controller Action should be “Index”.

Because we also want to support the Experience Editor(in Sitecore 7 and before it was called the Page Editor) we need to add Placeholder Settings. These Placeholder Settings tell the Experience Editor which components can be placed in which placeholders. For each placeholder you can create default Placeholder Settings which you can override on page level by creating another Placeholder Settings item and adding it to the page presentation details.

Go to /Sitecore/Placeholder Settings and create a new Placeholder item with the Placeholder Key “main” and add the newly created rendering to the Allowed Controls.

Go to the homepage of your site and add set the layout in the Presentation Details to the Layout we just created.
Now we can open the page in the Experience Editor. You’ll see an empty page but with a placeholder we can use to add our components.

When you press the “Add here” button, you’ll be able to select a rendering you want to add to the page. Select the Index rendering we just created.

After adding the component to the page you’ll see the result of the component we created.

Congratulations, you just created your first component! Next thing we’ll do is add some styling.

Making it pretty

We are going to add the styling from the original MusicStore tutorial.
In the assets of this part there is a Content folder. In this folder you can find all the CSS files and images. Place these in the Content folder of your Visual Studio project.

Now open the default layout (/Views/Shared/_Layout.cshtml). We will add the CSS and JavaScript inserts, and a menu:

_Layout.cshtml
		@using Sitecore.Mvc
		<!DOCTYPE html>
		<html>
		<head>
			<meta charset="utf-8" />
			<title>@ViewBag.Title</title>
			<link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
			<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
			<script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script>
		</head>
		<body>
			<div id="header">
				<h1><a href="/">SITECORE MVC MUSIC STORE</a></h1>
				<ul id="navlist">
					<li class="first"><a href="@Url.Content("~")" id="current">Home</a></li>
					<li><a href="@Url.Content("~/Store/")">Store</a></li>
				</ul>
			</div>
				 
			<div id="main">
				@Html.Sitecore().Placeholder("main")
			</div>
		</body>
		</html>
	

This should result in the following look of the homepage:

As you can see we already added a “Store” menu link. In the next tutorial we will be creating the actual store functionalities!

Go to Part 4

Sitecore MVC MusicStore – Part 2

Standard

As mentioned in the part 1 of this series, we will be creating a site using the Glass.Mapper(a.k.a. Glass).
With the use of Glass you can create templates with Code-First models.

So before we start developing our site, we need to install a few NuGet packages.

Install the following packages in the web project (included the versions which worked for me):

  • BoC.InversionOfControl.Unity (2.4.1.0)
  • BoC.Sitecore.Mvc (4.0.0.0)
  • BoC.Persistence.SitecoreGlass (4.0.3.0)

Maybe you already noticed, but these packages have dependencies to other packages. When you install these packages it automatically installed the following packages:

  • BoC (4.0.1.0)
  • BoC.Web.Mvc (4.0.0.0)
  • BoC.Logging.Sitecore (4.0.0.0)
  • BoC.Glass.Mapper.Sc (4.0.0)
  • BoC.Glass.Mapper.Sc.ContentSearch.LuceneProvider (4.0.0)

Most of these BoC packages are essentially a shell around the Glass.Mapper module. They add some extra functionalities and bug fixes. But others also bring full functionalities you can use while developing an MVC/Sitecore website. For more info about each of these packages, see the package descriptions.
The Glass.Mapper itself is explained on http://glass.lu/Mapper/Sc.aspx. You can also find some nice tutorials there. We will also handle and explain some of the functionalities while creating the MusicStore website.

Enabling Glass.Mapper for Sitecore

Sitecore consists of pipelines to which you can hook onto. You can path these pipelines, through configuration, to execute a bit of code before, after or instead of a pipeline.

To enable usage of Glass and BoC for Sitecore, we need to add some custom configuration to let Glass and BoC hook into those pipelines.
As you might have already noticed, installing the packages also added some configuration files.

These config files add pipelines from the packages to Sitecore. Take the Sitecore.Glass.config for example, all it does is add two pipelines from the eFocus.Sitecore.Glass package.

<configuration xmlns:patch=">
  <sitecore>
    <pipelines>
      <initialize>
        <processor type="eFocus.Sitecore.Glass.Pipelines.Initialize.RegisterAppDomainGlassConfigLoader, eFocus.Sitecore.Glass" />
        <processor type="eFocus.Sitecore.Glass.Pipelines.Initialize.InitializeGlass, eFocus.Sitecore.Glass" />
      </initialize>
    </pipelines>
  </sitecore>
</configuration>

But to enable edit the extension of the Sitecore.GlassCodeFirst.config file in the eFocus folder. Just remove the .exclude from the filename and Sitecore should include it in the configuration.

Next to this config file there is a Glass.Mapper.Sc.CodeFirst.config.example file in the BoC folder. This is almost identical to the one we just included, so we can delete this file.

Last but not least we have to enable the Glass CodeFirst option. Add a new config file to the Include folder with the following content:

MusicStore.config
		
		  
			
			  
			  
			
		  
		
	

After these steps, check if Sitecore is still functioning. It is highly possible that you’ll get some assembly reference errors. You can fix those by redirecting older versions of the assembly to the version of the dll in the bin folder of the website root. You can do this in the web.config. Example:

Web.config
		
			
			
		
		
			
			
		
	

TIP: You can use Conditional Configs from Chris van de Steeg to transform your Web.config rather then changing it yourself: http://www.chrisvandesteeg.nl/2014/10/06/sitecore-conditional-configs-deploy-just-1-package/

Now we have a fully functioning Sitecore site running with Glass.Mapper support.

Go to Part 3

Sitecore MVC MusicStore – Part 1

Standard

There are many ways of creating an Sitecore website. You can choose to do this with MVC and even then there are multiple ways of creating a Sitecore MVC website.

In this tutorial I will create the ASP.NET MVC MusicStore site with the usage of Glass.Mapper and BoC. Both are Nuget packages which add very helpfull functionalities to Sitecore.

In this series we will learn the basics of developing a Sitecore MVC site. We will be creating a Sitecore 8 site with help of the Glass.Mapper to create code-first models and templates in Sitecore(more on this in part 2).
If you don’t want to use Sitecore 8, you can also use any other version of Sitecore above 7 but keep in mind that some of the Nuget packages we will be using might not support the older Sitecore version anymore!

Take note that I will not go in detail about the working of Sitecore, but only on how to implement an MVC project in a Sitecore website.

Feedback and questions about this series are always welcome. Just add a comment to any of the posts, and ill reply as soon as possible.

Overview

The website we’ll be building is the same site which is created in the ASP.NET tutorials.
The application is a simple store for music albums.

Visitors can browse albums by genre and add single albums to their cart.

The users can view their cart, remove items and checkout. But before they can checkout, the will have to login or register.

After checkout the visitor will be shown a simple checkout completed page.

All the album, genre, artist, account and order details are saved and managed in Sitecore.

First things first

Before we can start creating our store, we need to have a Visual Studio solution where we can develop in. 
There are a few ways of creating a solution for development of a Sitecore site. Everybody has its own favorite way of working. In another post I described one way of working. This isn’t even my favorite way of developing, but it’s easy to setup and create a new website.
So if you need any help setting up, look at that tutorial: http://guidovtricht.nl/2015/04/creating-sitecore-solution/

 

Go to Part 2

Creating Sitecore Solution

Standard

In this tutorial we will be creating a Visual Studio solution for creating your own Sitecore website.
There are a couple of ways to facilitate the creation of a Sitecore site. In this tutorial we’ll be using Web Deploy to deploy the binaries to an existing Sitecore site.

Prerequisites:

  • Microsoft Visual Studio
    I will be using 2013 Premium, but any other version above 2012 should work fine.
  • Sitecore
    Version should not matter, but I’ll be using Sitecore 8.0.

  • Internet Information Service (IIS) with Manager

  • SQL (Express) server

Before we can create the solution, we need a running Sitecore site.
You can do this by extracting the Sitecore zip and pointing a new IIS website to it, or by running the Sitecore installer.

Creating the solution

Now that we have a site running, we want to create a new web project in Visual Studio. Open your Visual Studio and create a new ASP.NET Web Application.
Give your Project and Solution a name. In this example I’ll be creating the MusicStore project solution, so I named the project “MusicStore.Web” and solution “MusicStore”.

Next you’ll be asked to select a project template. To keep this solution easy We’ll choose the “Empty” template. I did check the MVC option below the template select, so that the needed references and folders were already added for the site I was creating.

Now we have created a new solution. Though there are still some default settings added which we will have to remove.

First we should remove the created web.config transform files. If you expend the web.config file in the solution explorer, you’ll see a web.release.config and a web.debug.config file. Delete both of the files.

Next, go to the properties panel of the web.config file. Set the Build Action from Content to “None” and the Copy to Output Directory to “Copy if newer”.

Because Sitecore comes with a web.config, you’ll have to copy its contents to the web.config of your new project. Normally you should not do this, and only create new configs which would be included, but that’s too much work for this tutorial.
So we will just edit the location of the dataFolder in the web.config. Change it to the location of the Data folder which is placed where you installed/created the Sitecore site.

Now to get the files of the website to the Sitecore website root, we will use One-Click Publishing.
Right-click the project item in the solution explorer, and select “Publish…”

The publishing wizard will now open.

We want to use a Custom publishing target, so select that and enter a name for this publishing profile.

Next we need to specify a connection. We want to publish the site artifacts to the website root of our Sitecore site. Select “File System” as publish method and change the target location to you website root.

Next step is to select the publishing settings, not very exciting. Just select a configuration you want to build in and go to the next step. You should select a Debug configuration if you want to debug your site.

The last window of the wizard will show a preview of our selected options. In this case it doesn’t tell us all that much, just that we publish it to the website root folder.

If we now save the profile and start a publish, we’ll see that Visual Studio will build our solution and then publish the project.

Once you have created parts of the site, the publishing will copy over the binaries from the build and copy any content to the website root(like views, javascript and css files).

That’s all there is to it! We are now ready to start developing our site.

eFocus Lucene Web Search module

Standard

While updating the eFocus Lucene web search module, we came across a few difficult changes in the new Sitecore 7 content search since the old Sitecore 6 Search.

So what changed in this new version of the module? We gave it version number 2.0, which indicates that there were some major changes and even some breaking changes. For instance we removed the old site crawler and created a new one, based on content search. Also we added a new feature, the autocomplete service! This service creates an autocomplete index after each rebuild of the search index, but more about that later on in this article. And last but certainly not least, you can now use the Switch On Rebuild Lucene Index type for your site indexes. This index type has two index directories, between which it switches after every rebuild. So when you start a rebuild, the rebuild is triggered in directory two while the user can still search in directory one. After the rebuild these directories are switched so that the next rebuild is triggered in directory one, while you are now searching in directory two.

To get this module working, you will need to add an index to the Sitecore configs. For example:

<index id="corporate_crawledcontent" type="Sitecore.ContentSearch.LuceneProvider.SwitchOnRebuildLuceneIndex, Sitecore.ContentSearch.LuceneProvider">
	<param desc="name">$(id)</param>
	<param desc="folder">__corporate_crawledcontent</param>
	<!-- This initializes index property store. Id has to be set to the index id -->
	<param desc="propertyStore" ref="contentSearch/databasePropertyStore" param1="$(id)" />
	<configuration ref="contentSearch/indexConfigurations/defaultLuceneIndexConfiguration" />
	<strategies hint="list:AddStrategy">
		<!-- NOTE: order of these is controls the execution order -->
		<strategy ref="contentSearch/indexUpdateStrategies/manual" />
	</strategies>
	<commitPolicyExecutor type="Sitecore.ContentSearch.CommitPolicyExecutor, Sitecore.ContentSearch">
		<policies hint="list:AddCommitPolicy">
			<policy type="Sitecore.ContentSearch.TimeIntervalCommitPolicy, Sitecore.ContentSearch" />
		</policies>
	</commitPolicyExecutor>
	<locations hint="list:AddCrawler">
	<crawler type="Efocus.Sitecore.LuceneWebSearch.NCrawlerProviderCrawler, Efocus.Sitecore.LuceneWebSearch">
		<Urls hint="list">
			<url>thieme.corporate.localhost.efocus.local</url>
		</Urls>
		<Triggers hint="list">
			<Trigger>efocus:updateindex:corporate_crawledcontent</Trigger>
		</Triggers>
		<Tags>crawled</Tags>
		<Boost>1</Boost>
		<AdhereToRobotRules>true</AdhereToRobotRules>
		<MaximumThreadCount>2</MaximumThreadCount>
		<RegexExcludeFilter>(&amp;sc_mode=preview|\.jpg|\.pdf|\.css|\.js|\.gif|\.jpeg|\.png|\.ico|^((?!corporate).)*$)</RegexExcludeFilter>
		<IndexFilters hint="raw:AddIndexFilter">
			<filter start="&lt;!--BEGIN-NOINDEX--&gt;" end="&lt;!--END-NOINDEX--&gt;" />
		</IndexFilters>
		<FollowFilters hint="raw:AddFollowFilter">
			<filter start="&lt;!--BEGIN-NOFOLLOW--&gt;" end="&lt;!--END-NOFOLLOW--&gt;" />
			<!-- remove <a rel="nofollow"> tags -->
			<!-- <a[^><]+?rel="[^"><"]*nofollow[^"><"]*"(.*?)> -->
			<filter startregex="&lt;a[^&gt;&lt;]+?rel="[^"&gt;&lt;"]*nofollow[^"&gt;&lt;"]*?&quot;" endregex="&gt;" />
			<!-- remove entire documents that have <meta name="robots" content="noindex" /> (regex = <meta name="robots" content="[^"><"]*nofollow[^"><"]*?") -->
			<filter startregex="&lt;meta name=&quot;robots&quot; content=&quot;[^"&gt;&lt;"]*nofollow[^"&gt;&lt;"]*?&quot;" endregex="&lt;/html&gt;" />
		</FollowFilters>
	</crawler>
	</locations>
</index>

If you compare this config to the old implementation you can will see some changes. As said we now use the SwitchOnRebuildLuceneIndex which is mentioned as type in the first row. Secondly you can now ad strategies to the indexes. In this example we only added the manual index strategy, so you will need to trigger a rebuild in the Sitecore control panel. And finally we added a commit policy, so that the index is committed every once in a while rebuilding.

Auto complete

As mentioned earlier we added an auto complete service. After rebuilding a site’s index, another index is created based on the just rebuild one. In this auto complete index are all the different words stored, which are used in the site.

With the auto complete service you can search for the complete word in the auto complete index. Below is a quick example implementation:

string indexesFolder = Settings.GetSetting("IndexFolder", FileUtil.MakePath(Settings.DataFolder, "/indexes"));
AutoCompleteService autoCompleteService = new AutoCompleteService(indexesFolder + "/__corporate_crawledcontent_autocomplete");
autoCompleteService.SearchAutoComplete(indexesFolder + "/__corporate_crawledcontent_autocomplete");
IEnumerable<string> suggestTermsFor = autoCompleteService.SuggestTermsFor(term).ToList();

You could, for example, use this in combination with jQuery autocomplete to show the completed words below a search box.

That’s all there is to know about the changes made in version 2.0. Got any questions? Just leave a comment below!