MvcSiteMapProvider - Multiple Pages Need To Link to One Menu Node
Asked Answered
M

3

7

In my MVC3 project, I have installed Maartenba's MvcSiteMapProvider v.3.2.1 and I have a very simple, static, two-level menu that I have created. Below is the conceptual map structure.

- Home
- Member Center
    - Member Listing [SELECTED]
    - Event Calendar
    - Documents
- Administration

Now, there are many sub-pages under Member Listing (e.g. Detail, Edit, etc.), but I don't want these displayed as 3rd level menu items (mainly because they are tied to a specific member ID). However, I do want all these third level pages to be "tied" to the Member Listing menu node so that it shows as selected when on these pages.

I have the following code in my Mvc.SiteMap file:

<mvcSiteMapNode title="Home" controller="Home" action="Index">
  <mvcSiteMapNode title="Member Center" area="Members" controller="Home" action="Index" roles="Approved Member" >
    <mvcSiteMapNode title="Member Listing" area="Members" controller="Member" action="List" />
    <mvcSiteMapNode title="Event Calendar" area="Members" controller="Event" action="List" />
    <mvcSiteMapNode title="Documents" area="Members" controller="Document" action="List" />
  </mvcSiteMapNode>
  <mvcSiteMapNode title="Administration" area="Admin" controller="Home" action="Index" roles="Site Administrator" >
  </mvcSiteMapNode>
</mvcSiteMapNode> 

To render the menu, I am using the following code in my _Layout.cshtml file:

@Html.MvcSiteMap().Menu(1, true, true, 1, true, true)

Finally, I modified the SiteMapNodeModel.cshtml file so that it adds a "selectedMenuItem" class to the node that correlates to the page the user is viewing. Here's the snippit that renders the menu node.

@model SiteMapNodeModel
  <a href="@Model.Url" class="@(Model.IsCurrentNode ? "selectedMenuItem" : "")">@Model.Title</a>

The display and navigation of the map works just fine, until I navigate further into the members area. For example, if I go past Members/Member/List (which displays the menu correctly) and to a page like Members/Member/Detail/1, the child nodes under Member Center ("Member Listing", "Event Calendar", etc.) disappear. Therefore, here are my two issues that I have with my current code:

  1. I want to specify that any given page is part of the "Member Center" parent menu node, so that the child menu nodes of "Member Center" will be displayed, regardless of whether the given page is defined as a specific node in the menu structure.

  2. I want to specify (possibly in the view or controller action) that a specific page should be tied to a specific menu node. For example, when the user is at Members/Member/Detail/1, I simply want the "Member Listing" child node to be specified as IsCurrentNode so that the SiteMapNodeModel.cshtml file properly decorates it with the "selectedMenuItem" class.

Any suggestions?

Matrices answered 30/3, 2012 at 15:9 Comment(0)
D
6

You can add 3rd level nodes to the sitemap XML and specify visibility to hide them from the menu. Here is the node declaration to display it only in breadcrumbs:

<mvcSiteMapNode area="Members"
                controller="Member"
                action="Detail"
                visibility="SiteMapPathHelper,!*"
                title="Member details" />

Edit:

As far as I know, you can not set IsCurrentNode. But you can check if menu node is currently selected with the following code (I use it in SiteMapNodeModel display template):

IList<string> classes = new List<string> ();
if (Model.IsCurrentNode || Model.IsInCurrentPath && !Model.Children.Any ())
{
    classes.Add ("menu-current");
}
Dagny answered 11/4, 2012 at 19:20 Comment(1)
Max, thank you very much! That solves the basic issue of the 2nd level nodes disappearing. However, is there anyway that you know of to have the second level node set to IsCurrentNode when this happens? For example, I would like the Member Listing node shown as selected when on the Member Details page. If not, it's not a huge deal... just an aesthetic request.Matrices
G
1

Adding to Max's answer I would also create an extension method for SiteMapNodeModel. Which you could use to implement all the custom logic needed to do this:

public static class SiteMapNodeModelExtender
{
  public static bool IsRealCurrentNode(this SiteMapNodeModel node)
  {
    // Logic to determine the "real" current node...
    // A naive implementation could be:
    var currentPath = HttpContext.Current.Request.Url.AbsolutePath;
    return currentPath.StartsWith("Members/Member/") && node.Title.Equals("Member Center")
  }
}

and change the display template accordingly:

/* Also check IsRealCurrentNode, depending on the use case maybe only
IsRealCurrentNode */
@if ((Model.IsCurrentNode || Model.IsRealCurrentNode()) && Model.SourceMetadata["HtmlHelper"].ToString() != "MvcSiteMapProvider.Web.Html.MenuHelper")  { 
  <text>@Model.Title</text>
} else if (Model.IsClickable) { 
  <a href="@Model.Url ">@Model.Title</a>
} else { 
  <text>@Model.Title</text>
}
Graiggrail answered 12/4, 2012 at 9:15 Comment(0)
P
1

Further to Max Kiselev's answer, if you want to use that technique but be able to use attributes on your controller actions, I did the following:

Define a custom visibility provider:

public class AlwaysInvisibleVisibilityProvider : ISiteMapNodeVisibilityProvider
{
    public bool IsVisible(SiteMapNode node, HttpContext context, IDictionary<string, object> sourceMetadata)
    {
        return false;
    }
}

Then subclass MvcSiteMapNodeAttribute:

public class InvisibleMvcSiteMapNodeAttribute : MvcSiteMapNodeAttribute
{
    public InvisibleMvcSiteMapNodeAttribute(string key, string parentKey)
    {
        Key = key;
        ParentKey = parentKey;
        VisibilityProvider = typeof (AlwaysInvisibleVisibilityProvider).AssemblyQualifiedName;
    }
}

And you can then use it on your controller actions:

[HttpGet]
[InvisibleMvcSiteMapNodeAttribute("ThisNodeKey", "ParentNodeKey")] 
public ViewResult OrderTimeout()
{
    return View("Timeout");
}
Plug answered 18/5, 2012 at 8:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.