I took a slightly more involved view on this one.
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 })
}
I first check if the user is logged in and thus "on a managed page" which only happens if the model is of a specific type "Core.Objects.Entities.CMS.Page".
I then call @Html.Action which calls the following child action on my controller, and I construct 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 should probably replace that case statement with some smarter looping code that builds the query.
In short though all this does is pull a hierarchy of Page objects off my OData API to the depth determined by my options passed in as menus tend to vary, some are single level options others go multiple levels but I wanted a reusable menuing system.
Having done that I can take the OData result and build out a model for a 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>
}
Menu views setup a list for the root and render a child for each child page, I plan to expand on this view at some point for things like spitting out the parent page which could be in the OData result I pulled in the controller.
And the final piece of the puzzle is the menu item view that deals with rendering 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 essentially a nested set of lists I then use CSS to handle things like popups or dynamic expands ect.
This functionality is not quite complete, I need to make sure I handle "marking the current item" with a class or something so I can style it differently and there are a few other menuing scenarios I would like to add, but also I have 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 like a nice logical improvement if its configurable :)
One last thing I forgot to put in before is the menu options model used on the Menu action in the controller ...
public class MenuOptions
{
public bool IsRootMenu { get; set; }
public int ForPageId { get; set; }
public int Depth { get; set; }
}
@Html.Raw(...)
but why are you building it as astring
and returning aContentResult
instead of aPartialView
? And why do you haveHtml.RenderAction
and@Html.Partial()
? – ElectrokineticHtmlGenericControl
? (this is not part of MVC). And the approach I would take is to use a extension method that recursively builds you html (refer this answer for an example - in you case you will also be usinghelper.ActionLink()
to generate the links and you model will contain properties for the text, controller and action method names. – ElectrokineticHtmlHelper
extension includingTagBuilder
which automatically generates all required tags for every route values provided as argument (controller-action name pair). – Caber