Per Magne Skuseth
Aug 19, 2013
(7 votes)

Developing a drag and droppable menu (MVC)

The main menu is usually a quite important part of your website. A traditional top/main menu typically displays section pages, and can easily be done with EPiServer. But what if you would like the menu items to be drag and droppable, and possible non-hierarchical? I thought I’d share some code on how to use a content area as a main menu, which will enable editors to drag and drop pages into the menu and customize it with ease.

drag it in

In order to accomplish this, I’ve created a controller which convert pages into view models. The view models are then rendered inside a content area which is located in the header view of my site.

First of, the menu item model. This is pretty basic, with string properties for url and heading, and a bool property to set whether the link is currently selected or not.

   1: public class MenuItemViewModel
   2: {
   3:     public string Url { get; set; }
   4:     public string Heading { get; set; }
   5:     public bool Active { get; set; }
   6: }

Secondly, the controller. To be able to make this work with all page types, I have set the PageController typed to SitePageData, which in this case is a base class for all page types. As for the TemplateDescriptor, I’ve set the TemplateTypeCategory to MvcPartialController. This way, it will render as a partial view. I’ve also set Tags to “mainmenu”, so that it will only match content areas with that certain tag.  Inherited is set to “true” so that it will match all types that inherit from SitePageData. I also needed to make sure that menu items are active when a child page is selected, but since this is a (possible) non-hierarchical menu, I also needed to make sure that only the closest menu item is set as active, as a page might be a descendent of several menu items. I’ve tried my best to describe this in the inline comments.


   1: [TemplateDescriptor(
   2:     TemplateTypeCategory = TemplateTypeCategories.MvcPartialController,
   3:     Tags = new[] { "mainmenu" }, Inherited = true)]
   4: public class MenuItemController : PageController<SitePageData>
   5: {
   6:     public ActionResult Index(SitePageData currentPage)
   7:     {
   8:         // get the currently loaded page
   9:         var currentContextPage = ControllerContext.RequestContext.GetContentLink();
  10:         bool active;
  12:         // check if the current menu item page is the same as the page in the context
  13:         active = currentContextPage.ID == currentPage.PageLink.ID;
  14:         if (!active)
  15:         {
  16:             // not the same page, we'll need to check if the page is on of the descendents
  17:             // (to see if one of the child pages is currently selected)
  18:             var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
  19:             List<ContentReference> pagePath = contentLoader.GetAncestors(currentContextPage)
  20:                 .Reverse()
  21:                 .Select(x => x.ContentLink)
  22:                 .SkipWhile(x => !x.CompareToIgnoreWorkID(currentPage.PageLink))
  23:                 .ToList();
  24:             pagePath.Add(currentContextPage);
  26:             // we also need to check if the page in context is a child to 
  27:             // another page in the menu, as we only want one active menu point at a time. 
  28:             // As an example, if we skipped this part, 
  29:             // the start page menu point would always be set as active
  30:             var startPage = contentLoader.Get<StartPage>(ContentReference.StartPage);
  31:             List<IContent> menuPages = startPage.MenuArea.Contents.ToList();
  33:             ContentReference firstMatch = pagePath.Join(menuPages, 
  34:                 p => p, m => m.ContentLink, (p, m) => p).LastOrDefault();
  36:             // if a match is found, active will be set to true
  37:             active = firstMatch == currentPage.PageLink;
  38:         }
  39:         var menuItem = new MenuItemViewModel()
  40:         {
  41:             // mark as active if the current menu item page equals the page in context,
  42:             // or if the current page is one of the descending pages
  43:             Active = currentPage.PageLink.ID == currentContextPage.ID || active,
  44:             Url = currentPage.LinkURL,
  45:             Heading = currentPage.PageName
  46:         };
  47:         return PartialView(menuItem);
  48:     }
  49: }


In the corresponding view, a link is rendered based on the properties in the model, and the Active property is used to add a css class. If active, a sky blue background should be displayed.

   1: @model MenuItemViewModel
   2: <a href="@Url.PageUrl(Model.Url)" class="@(Model.Active ? "active" : null)">
   3:     @Model.Heading
   4: </a>


At this point, drag and drop of pages will work if the current content area is placed within a page view. However, since this should be used as a top menu, I want to put it in my header view. In order to accomplish this, I’ve  created a layout model, which will be accessible for partial views such as the header or footer. This will contain a ContentArea property, which will be populated by a ContentArea property on the StartPage. I’ve chosen to do it this way to keep it simple, but you could take a look at the Alloy MVC templates(highly recommended!) to see a different, more dynamic approach on how to handle this. Please note that in a real-world scenario, the model would most likely contain more properties.

The (really, really simple) layout model:

   1: public class LayoutModel
   2: {
   3:     public ContentArea Area { get; set; }
   4: }


Then I’ve created a base class for all my PageControllers. This will make sure the layout model is populated with the correct data, no matter which page you are currently on.

   1: public abstract class PageControllerBase<T> : PageController<T> where T : SitePageData
   2: {
   3:     protected override void OnActionExecuted(ActionExecutedContext filterContext)
   4:     {
   5:         // get the start page
   6:         var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
   7:         var startPage = contentLoader.Get<StartPage>(ContentReference.StartPage);
   8:         // populate the layout model with data from the start page
   9:         var layoutModel = new LayoutModel()
  10:             {
  11:                 Area = startPage.MenuArea
  12:             };
  13:         // populate viewdata with layout model
  14:         filterContext.Controller.ViewData["LayoutModel"] = layoutModel;
  15:         base.OnActionExecuted(filterContext);
  16:     }
  17: }


And in my header view, I’ll render the ContentArea property. I’ve added some custom tags and css classes in order to make it look nice in both edit mode and view mode (you can read more about those settings here). Also notice that I set the Tag to “mainmenu”.

   1: @{ var layoutModel = ViewData["LayoutModel"] as LayoutModel; }
   3: other header stuff here goes here
   5: @Html.PropertyFor(x => layoutModel.Area, new
   6: {
   7:     Tag = "mainmenu", CustomTag = "ul" , CssClass = "nav navbar-nav pull-right",
   8:     ChildrenCustomTagName = "li", EditContainerClass = "nav navbar-nav pull-right"
   9: })   


In order to get on-page-editing working, connections are added from the page model to my view model. I do this in the start page controller, since my property is located in my StartPage type.

   1: public class StartController : PageControllerBase<StartPage>
   2: {
   3:     public ActionResult Index(StartPage currentPage)
   4:     {
   5:         var editHints = ViewData.GetEditHints<LayoutModel, StartPage>();
   6:         editHints.AddConnection(m => m.Area, p => p.MenuArea);
   7:         return View(currentPage);
   8:     }
   9: }

Final result – view mode:



And in edit mode:


Keep in mind that since a content area is used, you have the possibility to extend the menu options by using blocks. How about a DropDownMenuItemBlock? Or maybe a MegaMenuItemBlock?

Aug 19, 2013


Aug 20, 2013 11:55 AM

Haven't thought of using a ContentArea like that. Nice one!

Aug 20, 2013 04:56 PM

Nice one! Thanks for sharing.

Please login to comment.
Latest blogs
Content Graph - Letting GraphQL do all the hard work for you

Background As we have seen before, setting up Content Graph on the CMS side is pretty easy. However, when it comes to the “head” part of the setup,...

| May 26, 2023 | Syndicated blog

Improved headless functionality in Customized Commerce

Did you know that with the release of Content Delivery Commerce API 3.7 we have massively improved the out of the box headless capabilities of...

Marcus Hoffmann | May 25, 2023

Boost Your Productivity with the AI Assistant Addon for Optimizely Content Cloud

In today's fast-paced digital world, efficiency and convenience are paramount. That's why we're excited to introduce the Optimizely AI-Assistant...

Luc Gosso (MVP) | May 25, 2023 | Syndicated blog

Swapcode.Optimizely.AuditLog updated to v1.4.1

If you are using my audit log add-on Swapcode.Optimizely.AuditLog then I suggest that you update it in your solution. I've been waiting now for few...

Antti Alasvuo | May 20, 2023

The Web Project Podcast: Episode 19: Implement the Design (w/ Ethan Marcotte)

Corey and Deane talk about how front-end development has evolved past the early days. Then, Ethan Marcotte, author of Responsive Web Design and...

Corey Vilhauer | May 16, 2023 | Syndicated blog

Output Cache Options in Optimizely CMS 12

Let's start with the basics what is Output Caching? Output Cache is a feature that allows you to store the rendered output of a webpage in memory o...

PuneetGarg | May 16, 2023