I took a slightly more active view of this.
In the layout I put ...
@if (Model != null && Model is Core.Objects.Entities.CMS.Page) { @Html.Action("Menu", new MenuOptions { IsRootMenu = true, ForPageId = Model.Id, Depth = 3 }) }
First, I check whether the user is registered and, therefore, โon a managed pageโ, which happens only if the model has a certain type โCore.Objects.Entities.CMS.Pageโ.
Then I call @ Html.Action, which calls the next child action on my controller, and I create my menu options ...
[HttpGet] [ChildActionOnly] [Route("~/Menu")] public ActionResult Menu(MenuOptions options) { //TODO: consider other complex menuing scenarios like a popup / tree based structures // New api calls may be needed to support more complex menuing scenarios // TODO: figure out how to build these, perhaps with a string builder var expand = string.Empty; if (options.Depth > 0) { switch (options.Depth) { case 1: expand = "?$expand=PageInfo,Pages($expand=PageInfo)"; break; case 2: expand = "?$expand=PageInfo,Pages($expand=PageInfo,Pages($expand=PageInfo))"; break; case 3: expand = "?$expand=PageInfo,Pages($expand=PageInfo,Pages($expand=PageInfo,Pages($expand=PageInfo)))"; break; case 4: expand = "?$expand=PageInfo,Pages($expand=PageInfo,Pages($expand=PageInfo,Pages($expand=Pages($expand=PageInfo))))"; break; case 5: expand = "?$expand=PageInfo,Pages($expand=PageInfo,Pages($expand=PageInfo,Pages($expand=PageInfo,Pages($expand=PageInfo,Pages($expand=PageInfo)))))"; break; } } var query = options.IsRootMenu ? "Core/Page/MainMenuFor(id=" + options.ForPageId + ")" + expand : "Core/Page/ChildrenOf(id=" + options.ForPageId + ")" + expand; var items = Task.Run(() => Api.GetODataCollection<Page>(query)).Result; return PartialView(new MenuViewModel { Origin = options.ForPageId, Pages = items }); }
In my case, this area needs a little work, and I probably should replace this case statement with a more reasonable loop loop that builds the query.
In short, although all of this does, itโs pulling the hierarchy of page objects from my OData API to the depth, determined by my parameters passed in the form of a menu, usually change, some of them are parameters of the same level, others go to several levels, but I wanted to use a reusable menu system.
Having done this, I can take the OData result and build a model for the menu view, which looks like this:
@model MenuViewModel @if (Model.Pages != null) { <ul class="menu"> @foreach (var p in Model.Pages) { Html.RenderPartial("MenuItem", new MenuItemViewModel { Page = p, Origin = Model.Origin }); } </ul> }
The menu views set the list for the root and display the children for each child page, I plan at some point to expand this view for things like spitting out the parent page, which may be the result of the OData that I pulled into the controller.
And the last piece of the puzzle is a menu item view that deals with the rendering of each individual menu item ...
@model MenuItemViewModel <li> <a href="@Html.Raw("/" + Model.Page.Path)" @CurrentItem(Model.Page)>@Html.PageInfo(Model.Page).Title</a> @if (Model.Page.Pages != null && Model.Page.Pages.Any()) { <ul class="submenu"> @foreach (var p in Model.Page.Pages) { Html.RenderPartial("MenuItem", new MenuItemViewModel { Page = p, Origin = Model.Origin }); } </ul> } </li> @functions{ MvcHtmlString CurrentItem(Core.Objects.Entities.CMS.Page p) { if (Model.Origin == p.Id) return new MvcHtmlString("class='selected'"); return null; } }
Having done all this, I end up with a nested set of lists, then I use CSS to handle things like pop-ups or dynamic ect extensions.
This functionality is not quite complete, I need to make sure that I handle the "marking of the current element" with a class or something else, so I can style it differently, and there are several other menu scripts that I would like to add, but also I considered reusing this html structure in some way for tree views, but trees may be a bit more involved in the longer term, but doing things like putting an image next to menu items seems to be good a big improvement in logic, if configured :)
The last thing I forgot to put before is the menu options model used in the menu action in the controller ...
public class MenuOptions { public bool IsRootMenu { get; set; } public int ForPageId { get; set; } public int Depth { get; set; } }