Mvc Dynamic Menu Populating plain text
Asked Answered
R

2

1

I have created the N'th level Dynamic Menu recursive method which creates a menu.

enter image description here

As seen in above picture the content is returned to ParialView named "_Menu.cshtml" and this Partial View file is empty.

enter image description here

and then there is a _LayoutPage.Cshtml

    <!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>@ViewBag.Title</title>
</head>
<body>
    <div>
        <div>
            <input type="text" id="txtSearch" />
            <button id="btnSearch">Search</button>
         </div>
        <div id="Menu">
            @{Html.RenderAction("_Menu", "Home"); }
            @Html.Partial("_Menu")
        </div>
        <div>
            @RenderBody()
        </div>
    </div>
</body>
</html>

it successfully puts the result into the browser but as a plain text as i mentioned above and can be seen below.

enter image description here

How can i make these act like links, not like a plaintext? Help will be appreciated. Thanks :)

Ravine answered 29/3, 2017 at 7:33 Comment(5)
@Html.Raw(...) but why are you building it as a string and returning a ContentResult instead of a PartialView? And why do you have Html.RenderAction and @Html.Partial()?Electrokinetic
@StephenMuecke because i'm new to mvc. I am just learning and don't know what i am doing. Just going through some tutorials. I'll appreciate it if you'd be kind enough explain me what i am doing wrong and which good practice should i follow. :)Ravine
@StephenMuecke i'm building it as a string, because when using HtmlGenericControl, it returns an exception 'variable is not literal' something.Ravine
What is HtmlGenericControl? (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 using helper.ActionLink() to generate the links and you model will contain properties for the text, controller and action method names.Electrokinetic
Seems that your problem is similar to this: #28187231. As a workaround, you can set up a custom HtmlHelper extension including TagBuilder which automatically generates all required tags for every route values provided as argument (controller-action name pair).Caber
S
2

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; }
}
Sheepdog answered 29/3, 2017 at 8:31 Comment(0)
R
4

Though War resolved my query above. But i still want to answer for people for whom the above answer seem complex. in most easiest possible way in MVC to populate Dynamic Navigation Menu from Database.

in Controller:

    public ActionResult Index()
{
    Menu menu_ = new Menu();
    ViewBag.Menu = menu_.getMenuList();
    return View();
}

in _layoutpage.Cshtml

        <div id="Menu">
       @{ List<WebApplicationMVC.Core.Models.menu> All = ViewBag.Menu;}
        <ul>
            @foreach (var One in All.Where(m => m.ParentId == 0))
            {
                List<WebApplicationMVC.Core.Models.menu> Child = All.Where(m => One.id == m.ParentId).ToList();
                if (Child.Count > 0)
                {
                    if(One.ActionLink == "Yes")
                    {
                        <li>
                            @Html.ActionLink(One.Name, One.Action, One.Controller)
                            @SubMenu(One, All)
                        </li>
                    }
                    else
                    {
                        <li>
                            @One.Name;
                            @SubMenu(One, All)
                        </li>
                    }
                }
                else
                {
                    <li>@Html.ActionLink(One.Name, One.Action, One.Controller)</li>
                }
            }
        </ul>

        @helper SubMenu(WebApplicationMVC.Core.Models.menu Object, List<WebApplicationMVC.Core.Models.menu> List)
        {
        List<WebApplicationMVC.Core.Models.menu> subChilds = (from a in List where Object.id == a.ParentId select a).ToList();
        <ul>
            @foreach (var subOne in subChilds)
            {
                List<WebApplicationMVC.Core.Models.menu> InnerChilds = (from a in List where subOne.id == a.ParentId select a).ToList();
                if (InnerChilds.Count > 0)
                {
                    if (subOne.ActionLink == "Yes")
                    {
                        <li>
                            @Html.ActionLink(subOne.Name, subOne.Action, subOne.Controller)
                            @SubMenu(subOne, List)
                        </li>
                    }
                    else
                    {
                        <li>
                            @subOne.Name
                            @SubMenu(subOne, List)
                        </li>
                    }
                }
                else
                {
                    <li>@Html.ActionLink(subOne.Name, subOne.Action, subOne.Controller)</li>
                }
            }
        </ul>
        }
    </div>
Ravine answered 31/3, 2017 at 13:10 Comment(0)
S
2

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; }
}
Sheepdog answered 29/3, 2017 at 8:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.