Creating a commerce site in MVC – Part 2: Menus
In my last blog post (Creating a commerce site in MVC - Part 1), we created a site with core functionality that can be used on all your commerce MVC sites. We will continue with core functionality in this blog, but with focus on menus.
I have got several questions the last year about menus, and how to create them in EPiServer projects. There is a good example for menus in the MVC sample project for CMS. We will create menu that looks similar as the MVC sample menu, but created for a commerce site. We will also create breadcrumb support, with inspiration from the CMS MVC sample site.
The code for this tutorial can be downloaded here
Menu
The menu should be done very generic, with the possibility to define how many levels down in the tree the menu should render.
Models
We will create view models to make it possible to define how the menu will be rendered.
Menu model
We will start by creating a view model called “MenuModel”. The model will contain a starting point for the menu, and properties for the level in the menu. The model will also contain a property specifying if only content marked as “visible in menu” should be rendered.
1. Create a new class called “MenuModel”, and place it it the ViewModels folder.
2. Add a property of type ContentReference, and name if “ContentLink”.
3. Add a property of type nullable byte, and name it “Levels”.
4. Add a property of type byte, and name it “CurrentLevel”.
5. Add a property of type boolean, and name it “RequireVisibleInMenu”.
public class MenuModel
{
public ContentReference ContentLink { get; set; }
public byte? Levels { get; set; }
public byte CurrentLevel { get; set; }
public bool RequireVisibleInMenu { get; set; }
}
Menu model item
We will create another model called “MenuItem”. This model will represent each item that will be rendered. The model will contain a property for the menu model created earlier, and a property for the content that will be rendered. The view model will also contain a property for the routed content link, and a lazy property specifying if the content contains children.
1. Create a new class called “MenuItem”, and place it it the ViewModels folder.
2. Add a property of type MenuModel, and name if “MenuModel”. Make the setter private.
3. Add a property of type IContent, and name it “Content”. Make the setter private.
4. Add a property of type ContentReference, and name it “RoutedContentLink”. Make the setter private.
5. Add a property of type lazy boolean, and name it “HasChildren”. Make the setter private.
6. Add a constructor taking parameters of the same types as the properties. Set the properties in the constructor.
public class MenuItem
{
public MenuItem(MenuModel menuModel, IContent content, ContentReference routedContentLink,Lazy<bool> hasChildren)
{
MenuModel = menuModel;
Content = content;
RoutedContentLink = routedContentLink;
HasChildren = hasChildren;
}
public MenuModel MenuModel { get; private set; }
public IContent Content { get; private set; }
public ContentReference RoutedContentLink { get; private set; }
public Lazy<bool> HasChildren { get; private set; }
}
Menu renderer
We will create a class that will render the menu. The class will use the view models we just created.
1. Create a new class called “MenuRenderer”, and place it it the Business folder.
2. Create a read only field of type IContentLoader. Name it _contentLoader.
3. Create a read only field of type ILanguageSelector. Name it _languageSelector.
4. Create a read only field of type ILinksRepository. Name it _linksRepository.
5. Create a read only field of type PartialRouteHandler. Name it _partialRouteHandler.
6. Create a constructor that takes IContent, ILanguageSelectoras , ILinksRepositoryas, and PartialRouteHandler as it’s parameters. Set the fields using the parameters.
public class MenuRenderer
{
private readonly IContentLoader _contentLoader;
private readonly ILanguageSelector _languageSelector;
private readonly ILinksRepository _linksRepository;
private readonly PartialRouteHandler _partialRouteHandler;
public MenuRenderer(IContentLoader contentLoader, ILanguageSelector languageSelector,
ILinksRepository linksRepository, PartialRouteHandler partialRouteHandler)
{
_contentLoader = contentLoader;
_languageSelector = languageSelector;
_linksRepository = linksRepository;
_partialRouteHandler = partialRouteHandler;
}
}
Visible in menu
We will create a private method that tells us if a specified content should be visible in the menu. The method will take one parameter of type content, and one of type MenuModel.
1. Create a new method called “VisibleInMenu”, which will return a boolean.
2. Add a parameter of type IContent, and call it “content”.
3. Add a parameter of type MenuModel, and call it “menu”.
4. Check if the property RequireVisibleInMenu is false in the menu parameter. If that’s the case, return true.
5. Try to cast the content to page data, if the content isn’t a page data object, return false.
6. If “VisibleInMenu” is set on the page data object, then return true, otherwise false.
private bool VisibleInMenu(IContent content, MenuModel menu)
{
if (!menu.RequireVisibleInMenu)
{
return false;
}
var pageData = content as PageData;
return pageData != null && pageData.VisibleInMenu;
}
Get related entries
In CMS, it’s easy to get children for content using "GetChildren” in the content loader. In commerce, it’s possible to get the children in the same way for nodes, but not for products. If a product contains child products, or variants, the links repository has to be used to get the “children”. We will create a method, which gets all child entries for content that can contain variants.
1. Create a new method called “GetRelatedEntries”, which will return a IEnumerable<EntryContentBase>.
2. Add a parameter of type IVariantContainer, and call it “content”.
3. Get the variant relations using the extension method “GetVariantRelations” on the content parameter. Make a projection on the target property of the result, and store the result in a variable. Name the variable “relatedItems”.
4. Use the content loader to load the related items. Filter the result to only receive relations that inherit from EntryContentBase. Return the result.
private IEnumerable<EntryContentBase> GetRelatedEntries(IVariantContainer content)
{
var relatedItems = content.GetVariantRelations(_linksRepository).Select(x => x.Target);
return _contentLoader.GetItems(relatedItems, _languageSelector).OfType<EntryContentBase>();
}
Create menu item
We will create another private method. This time it will be a method that creates an instance of the view model “MenuItem”. The method will take parameters for the menu model, the content for the menu item, and the routed content link.
1. Create a new method called “CreateMenuItem”, which will return a MenuItem.
2. Add a parameter of type IContent, and call it “content”.
3. Add a parameter of type MenuModel, and call it “menuModel”.
4. Add a parameter of type ContentReference, and call it “routedContentLink”.
5. Create a new lazy<bool> variable in the method, and use the content loader to determine if the content contains any children. Name the variable “hasChildren”.
6. Create a new instance of MenuModel, and set the properties RequireVisibleInMenu, and Levels to the same values as the menuModel parameter.
7. Set the content link in the new menu model to the content link of the content parameter.
8. Set the CurrentLevel property in the new menu model to the same value as the parameter menu model + 1.
9. Create and return an instance of MenuItem. Use the new model menu, the content, the routed content link, and the has children variables.
private MenuItem CreateMenuItem(IContent content, MenuModel menuModel, ContentReference routedContentLink)
{
var hasChildren = new Lazy<bool>(() => _contentLoader.GetChildren<IContent>(content.ContentLink).Any());
var menuModelForItem = new MenuModel
{
RequireVisibleInMenu = menuModel.RequireVisibleInMenu,
Levels = menuModel.Levels,
ContentLink = content.ContentLink,
CurrentLevel = ++menuModel.CurrentLevel
};
return new MenuItem(menuModelForItem, content, routedContentLink, hasChildren);
}
Create menu items
After we have created the method for creating a menu item, we will create a method that creates several menu items. The only parameters to this method will be the menu model, and the routed content link.
1. Create a new method called “CreateMenuItems”, which will return a IEnumerable<MenuItem>.
2. Add a parameter of type MenuModel, and call it “menuModel”.
3. Add a parameter of type ContentReference, and call it “routedContentLink”.
4. Get the children using “GetChildren” in the content loader. Save the result in a variable. Name the variable “children”.
5. Try to get the content for the menu content link as EntryContentBase. If the content is of type EntryContentBase, try to cast it to IVariantContainer. If the content is IVariantContainer, add related entries to the children variable using the “GetRelatedEntries” method.
6. Filter the children using the method “VisibleInMenu”.
7. Create menu items for the filtered children using “CreateMenuItem”. Return the menu items.
private IEnumerable<MenuItem> CreateMenuItems(MenuModel menuModel, ContentReference routedContentLink)
{
var children = _contentLoader.GetChildren<IContent>(menuModel.ContentLink).ToList();
EntryContentBase entry;
if (_contentLoader.TryGet(menuModel.ContentLink, _languageSelector, out entry))
{
var variantContainer = entry as IVariantContainer;
if (variantContainer != null)
{
var relatedItems = GetRelatedEntries(variantContainer);
children.AddRange(relatedItems);
}
}
return children
.Where(x => VisibleInMenu(x, menuModel))
.Select(x => CreateMenuItem(x, menuModel, routedContentLink));
}
Render menu list
We will create a method called “RenderMenuList”. This method will create a html string containing the whole menu, supporting recursion. The method will use a delegate for rendering the menu items. By using a delegate, the method itself don’t have to contain any html.
1. Create a new method called “RenderMenuList”, which will return an “IHtmlString” instance.
2. Add a parameter of type MenuModel, and call it “menuModel”.
3. Add a parameter of type RequestContext, and call it “requestContext”.
4. Add a parameter of type Func<MenuItem, HelperResult>, and call it “itemTemplate”.
5. Check if the current menu level is greater than the specified menu levels. If that’s the case, return an empty MvcHtmlString.
6. Check if the ContentLinkin the menu is an empty reference. Return an empty MvcHtmlString if thats the case
7. Get the routed data from the requestContext parameter using the extension method “GetRoutedData<IContent>” in the EPiServer.Web.Routing namespace. Create a variable called “currentContent” that will hold the result.
8. Get the menu items using the method “CreateMenuItems”.
9. Create a string writer, and call the delegate for each menu item. Call the method “WriteTo” for each result from the deleage, and pass the string writer.
10. Create a MVC html string using the result from the string writer. Return the html string.
public IHtmlString RenderMenuList(MenuModel menuModel, RequestContext requestContext,
Func<MenuItem, HelperResult> itemTemplate)
{
if (menuModel.Levels.HasValue && menuModel.CurrentLevel > menuModel.Levels.Value)
{
return MvcHtmlString.Empty;
}
if (menuModel.ContentLink == ContentReference.EmptyReference)
{
return MvcHtmlString.Empty;
}
var routedData = requestContext.GetRoutedData<IContent>();
var menuItems = CreateMenuItems(menuModel, routedData.ContentLink);
var buffer = new StringBuilder();
using (var writer = new StringWriter(buffer))
{
foreach (var menuItem in menuItems)
{
itemTemplate(menuItem).WriteTo(writer);
}
}
return new MvcHtmlString(buffer.ToString());
}
Helper methods
We now have a class that can render a menu. One thing that is missing is an easy way for a view to call the class. We will create an extension method on “HtmlHelper” to solve that problem.
1. Create a new folder called “Helpers” in the root of the site.
2. Create a new class in the folder called “HtmlHelpers”. Make the class static.
3. Create an extension method for HtmlHelper, that takes MenuModel, and Func<MenuItem, HelperResult> as paraneters. Name the method “MenuList”, and return an instance of IHtmlString.
4. Get an instance of the MenuRenderer class using the service locator. Set the result to a variable named “menuRenderer”.
5. Call “RenderMenuList” on the instance. Get the requestContext using helper.ViewContext.RequestContext.
6. Return the result.
public static class HtmlHelpers
{
public static IHtmlString MenuList(this HtmlHelper helper, MenuModel menuModel,
Func<MenuItem, HelperResult> itemTemplate)
{
var menuRenderer = ServiceLocator.Current.GetInstance<MenuRenderer>();
return menuRenderer.RenderMenuList(menuModel, helper.ViewContext.RequestContext, itemTemplate);
}
}
Partial view
We will now make use of the class we have created. The view we will create is just an example of a menu that uses the helper method. Many views can be created with different HTML, that uses the method “MenuList”.
Menu
We will add a partial view, where we will place a function that will be used as the delegate in the “RenderMenuList” method. The method will work recursive, by calling the “RenderMenuList” method inside the method with it self as delegate.
1. Create a new partial view in the “Shared” folder under “Views”. Call the view “Menu”, and set the model to MenuModel.
2. Register a method using the @helper keyword. Name the method “ItemTemplate”, and let it take a MenuItem as parameter.
3. In the method, create a link to the menu item content using the HTML helper “ContentLink”. The helper method is located in the EPiServer.Web.Mvc.Html namespace.
4. Call the HTML helper “MenuList” with the menu model, and the method (ItemTemplate).
5. Outside the method, call the HTML helper “MenuList” with the model, and the method (ItemTemplate).
@using EPiServer.Commerce.SampleMvc.Models.ViewModels
@using EPiServer.Web.Mvc.Html
@using EPiServer.Commerce.SampleMvc.Helpers
@model MenuModel
@helper ItemTemplate(MenuItem menuItem)
{
@Html.ContentLink(menuItem.Content)
<span>/</span>
@Html.MenuList(menuItem.MenuModel, ItemTemplate)
}
@Html.MenuList(Model, ItemTemplate)
IPageViewModel
We will create a top menu property for the interface IPageViewModel. The IPageViewModel will be implemented by all view models in our project, which makes it possible for us to use the property in the layout file.
1. Add a new property to the interface of type MenuModel. Name it “TopMenu”.
public interface IPageViewModel<out T>
where T : PageData
{
T CurrentPage { get; }
MenuModel TopMenu { get; set; }
}
PageViewModel
We have to implement the new property that we just added to the interface.
1. Add a new property to the interface of type MenuModel. Name it “TopMenu”.
public class PageViewModel<T> : IPageViewModel<T>
where T : PageData
{
public PageViewModel(T currentPage)
{
CurrentPage = currentPage;
}
public T CurrentPage
{
get;
set;
}
public MenuModel TopMenu { get; set; }
}
ContentViewModelFactory
We now have a property on the IPageViewModel, which should be set in the ContentViewModelFactory.
When looking at the factory class, we can see that we made a little miss in the first tutorial. The methods “InitializeCatalogContentViewModel” and “InitializeVariationContentViewModel” should have called the method “InitializePageViewModel”. We will set the TopMenu property in the “InitializePageViewModel”, and therefore it’s important to fix our miss at this point. The menu should only render children at the top level.
1. Call InitializePageViewModel with the model inside the InitializeVariationContentViewModel method.
2. Call InitializePageViewModel with the model inside the InitializeCatalogContentViewModel method.
3. Inside the InitializePageViewModel method, create a new instance of the menu model.
4. Set the content link property in the menu model instance to the start page of the site, and levels to 1.
5. Set the levels property in the menu model instance to 1.
6. Set the menu model to require visible in menu.
public void InitializeVariationContentViewModel<TViewModel>(TViewModel model, string warehouseCode)
where TViewModel : IVariationContentViewModel<VariationContent, PageData>
{
InitializePageViewModel(model);
model.Inventory = GetInventory(model.CatalogContent, warehouseCode);
model.Price = GetPrices(model.CatalogContent);
model.ParentProduct = GetParentProduct(model.CatalogContent, model.CurrentPage);
}
public void InitializeCatalogContentViewModel<TViewModel>(TViewModel model)
where TViewModel : ICatalogContentViewModel<CatalogContentBase, PageData>
{
InitializePageViewModel(model);
model.ChildCategories = GetChildNodes(model.CatalogContent.ContentLink);
model.Products = CreateLazyProductContentViewModels(model.CatalogContent, model.CurrentPage);
model.Variants = CreateLazyVariantContentViewModels(model.CatalogContent, model.CurrentPage);
}
public void InitializePageViewModel<TViewModel>(TViewModel model)
where TViewModel : IPageViewModel<PageData>
{
model.TopMenu = new MenuModel
{
ContentLink = ContentReference.StartPage,
Levels = 1
RequireVisibleInMenu = true
};
}
Layout
We will make use of everything we just have created in the layout file.
1. In the _layout.cshtml (Views->Shared), add a partial request to the “Menu” view inside the body element. Use the top menu property on the model as the model for the menu.
<body>
<div>Menu: @Html.Partial("Menu", Model.TopMenu)</div>
<div class="container">
@RenderBody()
</div>
</body>
Breadcrumb
A common type of menu is the breadcrumb, which pretty much is a must on a commerce site.
Menu renderer
We will extend the menu renderer to get support for breadcrumbs.
GetCommerceRouters
To be able to get a whole breadcrumb for catalog content, we need to be able to get the commerce route that are used for the current catalog content. We will create a method that gets all commerce routers for a specific partial route handler. We will also cache the result to gain some performance.
1. In the “MenuRenderer” class create a static field of type IEnumerable<ICommerceRouter>. Name it _commerceRouters.
2. Create a new method called “GetCommerceRouters”, which will return IEnumerable<ICommerceRouter>.
3. If the _commerceRouters field is set, return the field.
4. If the field is null, get the routers using “GetIncomingRouters” with the page data type on the partial route handler.
5. Filter the result to only get routes of type “PartialRouter<PageData, CatalogContentBase>.
6. Make a projection of the filter items to only receive the router, and cast the result to ICommerceRouter.
7. Set the field _commerceRouters to the result.
8. Return the field.
private IEnumerable<ICommerceRouter> _commerceRouters;
private IEnumerable<ICommerceRouter> GetCommerceRouters()
{
if (_commerceRouters == null)
{
_commerceRouters = _partialRouteHandler.GetIncomingRouters(typeof(PageData))
.OfType<PartialRouter<PageData, CatalogContentBase>>()
.Select(x => x.Router)
.OfType<ICommerceRouter>();
}
return _commerceRouters;
}
ExecuteForContent
We will now create a method, which will execute a delegate for a specified content link. The method will use the method “GetCommerceRouters” to change from catalog content to page data when the content link is the route starting point.
1. Create a new method called “ExecuteForContent”, which will return an “IHtmlString” instance.
2. Add a parameter of type ContentReference, and call it “contentLink”.
3. Add a parameter of type Func<IContent, HelperResult>, and call it “itemTemplate”.
4. In the method, check if the content link is empty or the root page. If that’s the case, return an empty html string.
5. Get the commerce route using the method “GetCommerceRouters”, and check if any of those has the content link set as it’s commerce root.
6. If a commerce route was found with the root set to the content link, set the content link to the starting point of the commerce route.
7. Try to get the content for the content link using the content loader. If the content can’t be found, return an empty html string.
8. Create a string writer, and call the delegate for the content. Call the method “WriteTo” for the result from the deleage, and pass the string writer.
9. Create a MVC html string using the result from the string writer. Return the html string.
public IHtmlString ExecuteForContent(ContentReference contentLink, Func<IContent, HelperResult> itemTemplate)
{
if (contentLink == ContentReference.EmptyReference || contentLink == ContentReference.RootPage)
{
return MvcHtmlString.Empty;
}
var commerceRoute = GetCommerceRouters().FirstOrDefault(x => x.CommerceRoot.ContentLink.CompareToIgnoreWorkID(contentLink));
if (commerceRoute != null)
{
contentLink = commerceRoute.RouteStartingPoint;
}
IContent content;
if (!_contentLoader.TryGet(contentLink, _languageSelector, out content))
{
return MvcHtmlString.Empty;
}
var buffer = new StringBuilder();
using (var writer = new StringWriter(buffer))
{
itemTemplate(content).WriteTo(writer);
}
return new MvcHtmlString(buffer.ToString());
}
ExecuteForParent
We have a method for executing a delegate for a content link. We will now create another method for executing a delegate for the parent. In this method, we will use the current model to get the parent item.
1. Create a new method called “ExecuteForParent”, which will return an “IHtmlString” instance.
2. Add a parameter of type IPageViewModel<PageData>, and call it “model”.
3. Add a parameter of type Func<IContent, HelperResult>, and call it “itemTemplate”.
4. In the method, try to cast the model to ICatalogContentLeafViewModel<CatalogContentBase, PageData>.
5. Create a new variable, and set it to the catalog content if the model was successfully casted, otherwise set it to current page using the model parameter. Name the variable “content”.
6. Call “ExecuteForContent” with the the parent link for the content, and the delegate. Return the result.
public IHtmlString ExecuteForParent(IPageViewModel<PageData> model, Func<IContent, HelperResult> itemTemplate)
{
var catalogContentViewModel = model as ICatalogContentLeafViewModel<CatalogContentBase, PageData>;
var currentContent = catalogContentViewModel != null ? catalogContentViewModel.CatalogContent : model.CurrentPage as IContent;
return ExecuteForContent(currentContent.ParentLink, itemTemplate);
}
Helper methods
We will create helper methods for the methods “ExecuteForContent”, and “ExecuteForParent”. We will also create a helper method that returns the name of the routed content.
1. Create an extension method for HtmlHelper in the HtmlHelpers class, that takes ContentReference, and Func<IContent, HelperResult> as parameters. Name the method “ExecuteForContent”, and set the return type to IHtmlString.
2. Get an instance of the MenuRenderer class using the service locator. Set the result to a variable named “menuRenderer”.
3. Call “ExecuteForContent” on the instance, and return the result.
4. Create another extension method for HtmlHelper, that takes IPageViewModel<PageData>, and Func<IContent, HelperResult> as parameters. Name the method “ExecuteForParent”, and set the return type to IHtmlString.
5. Get an instance of the MenuRenderer class using the service locator. Set the result to a variable named “menuRenderer”.
6. Call “ExecuteForParent” on the instance, and return the result.
7. Create an extension method for HtmlHelper in the HtmlHelpers class, and name it RoutedContentName. Return IHtmlString for the method.
8. Create a variable called “routedData” and set it to the routed data using the extension method “GetRoutedData” in the EPiServer.Web.Routing namespace.
9. If the routed data isn’t null, return the html string containing the name of the routed data. Otherwise return an empty html string.
public static IHtmlString ExecuteForContent(this HtmlHelper helper, ContentReference contentLink,
Func<IContent, HelperResult> itemTemplate)
{
var menuRenderer = ServiceLocator.Current.GetInstance<MenuRenderer>();
return menuRenderer.ExecuteForContent(contentLink, itemTemplate);
}
public static IHtmlString ExecuteForParent(this HtmlHelper helper, IPageViewModel<PageData> model,
Func<IContent, HelperResult> itemTemplate)
{
var menuRenderer = ServiceLocator.Current.GetInstance<MenuRenderer>();
return menuRenderer.ExecuteForParent(model, itemTemplate);
}
public static IHtmlString RoutedContentName(this HtmlHelper helper)
{
var routedData = helper.ViewContext.RequestContext.GetRoutedData<IContent>();
return routedData != null ? new HtmlString(routedData.Name) : MvcHtmlString.Empty;
}
Partial view
We will now make use of the methods we have created.
Breadcrumb
We will add a partial view, where we will place a function that will be used as the delegate in the “ExecuteForContent” and “ExecuteForParent” methods. The method will work recursive, by calling the “ExecuteForContent” method inside the method with it self as delegate.
1. Create a new partial view in the “Shared” folder under “Views”. Call the view “Breadcrumb”, and set the model to IPageViewModel<PageData>.
2. Register a method using the @helper keyword. Name the method “ItemTemplate”, and let it take a IContent as parameter.
3. Call the HTML helper “ExecuteForContent” with the parent link from the parameter, and the method (ItemTemplate).
4. In the method, create a link to the menu item content using the HTML helper “ContentLink”. The extension method is located in the EPiServer.Web.Mvc.Html namespace.
5. Outside the method, call the HTML helper “ExecuteForParent” with the model, and the method (ItemTemplate).
6. Outside the method, call the HTML helper “RoutedContentName”.
@using EPiServer.Commerce.SampleMvc.Models.ViewModels
@using EPiServer.Core
@using EPiServer.Commerce.SampleMvc.Helpers
@using EPiServer.Web.Mvc.Html
@model IPageViewModel<PageData>
@helper ItemTemplate(IContent content)
{
@Html.ExecuteForContent(content.ParentLink, ItemTemplate)
<span>/</span>
@Html.ContentLink(content)
}
@Html.ExecuteForParent(Model, ItemTemplate)
<span>/</span>
@Html.RoutedContentName()
Layout
We will make use of the breadcrumb we just have created in the layout file.
1. In the _layout.cshtml (Views->Shared), add a partial request to the “Breadcrumb” view inside the body element. Use the current model as model for the breadcrumb.
<body>
<div>Menu: @Html.Partial("Menu", Model.TopMenu)</div>
<div>Breadcrumb: @Html.Partial("Breadcrumb", Model)</div>
<div class="container">
@RenderBody()
</div>
</body>
Next part
Next part will probably contain interaction with EPiServer Find/Owl. Happy coding!
Cool!
Nice article, Jonas. Thanks for such detailed explanation. How does the current implementation deal with simple address?
code monkey: In the commerce catalog, we have "SEO URI", which is a for of simple adress. When initializing the route for the catalog, you can specify if you like to render the links with SEO URI, or as hierarichal links. The SEO URI:s will always work if you browse to them, but they will only be rendered if you specify it doing initialization (Google shouldn't find those if you dont want it to).
Simple address for pages will work the normal way.