asp.net-mvc-4mvcsitemapprovider

MVC SiteMap - when different nodes point to same action SiteMap.CurrentNode does not map to the correct route


Setup:

I am using ASP.NET MVC 4, with mvcSiteMapProvider to manage my menus.

I have a custom menu builder that evaluates whether a node is on the current branch (ie, if the SiteMap.CurrentNode is either the CurrentNode or the CurrentNode is nested under it). The code is included below, but essentially checks the url of each node and compares it with the url of the currentnode, up through the currentnodes "family tree".

The CurrentBranch is used by my custom menu builder to add a class that highlights menu items on the CurrentBranch.

The Problem:

My custom menu works fine, but I have found that the mvcSiteMapProvider does not seem to evaluate the url of the CurrentNode in a consistent manner:

When two nodes point to the same action and are distinguished only by a parameter of the action, SiteMap.CurrentNode does not seem to use the correct route (it ignores the distinguishing parameter and defaults to the first route that that maps to the action defined in the node).

Example of the Problem:

In an app I have Members.

A Member has a MemberStatus field that can be "Unprocessed", "Active" or "Inactive". To change the MemberStatus, I have a ProcessMemberController in an Area called Admin. The processing is done using the Process action on the ProcessMemberController.

My mvcSiteMap has two nodes that BOTH map to the Process action. The only difference between them is the alternate parameter (such are my client's domain semantics), that in one case has a value of "Processed" and in the other "Unprocessed":

Nodes:

    <mvcSiteMapNode title="Process" area="Admin" controller="ProcessMembers" action="Process" alternate="Unprocessed" />
    <mvcSiteMapNode title="Change Status" area="Admin" controller="ProcessMembers" action="Process" alternate="Processed" />

Routes:

The corresponding routes to these two nodes are (again, the only thing that distinguishes them is the value of the alternate parameter):

context.MapRoute(
                    "Process_New_Members",
                    "Admin/Unprocessed/Process/{MemberId}",
                    new { controller = "ProcessMembers", 
                        action = "Process", 
                        alternate="Unprocessed", 
                        MemberId = UrlParameter.Optional }
                );


context.MapRoute(
                    "Change_Status_Old_Members",
                    "Admin/Members/Status/Change/{MemberId}",
                    new { controller = "ProcessMembers", 
                        action = "Process", 
                        alternate="Processed", 
                        MemberId = UrlParameter.Optional }
                );

What works:

The Html.ActionLink helper uses the routes and produces the urls I expect:

@Html.ActionLink("Process", MVC.Admin.ProcessMembers.Process(item.MemberId, "Unprocessed")

// Output (alternate="Unprocessed" and item.MemberId = 12):   Admin/Unprocessed/Process/12


@Html.ActionLink("Status", MVC.Admin.ProcessMembers.Process(item.MemberId, "Processed")

// Output (alternate="Processed" and item.MemberId = 23):   Admin/Members/Status/Change/23

In both cases the output is correct and as I expect.

What doesn't work:

Let's say my request involves the second option, ie, /Admin/Members/Status/Change/47, corresponding to alternate = "Processed" and a MemberId of 47.

Debugging my static CurrentBranch property (see below), I find that SiteMap.CurrentNode shows:

PreviousSibling: null
Provider: {MvcSiteMapProvider.DefaultSiteMapProvider}
ReadOnly: false
ResourceKey: ""
Roles: Count = 0
RootNode: {Home}
Title: "Process"
Url: "/Admin/Unprocessed/Process/47"

Ie, for a request url of /Admin/Members/Status/Change/47, SiteMap.CurrentNode.Url evaluates to /Admin/Unprocessed/Process/47. Ie, it is ignorning the alternate parameter and using the wrong route.

CurrentBranch Static Property:

/// <summary>
        /// ReadOnly. Gets the Branch of the Site Map that holds the SiteMap.CurrentNode
        /// </summary>
        public static List<SiteMapNode> CurrentBranch
        {
            get
            {
                List<SiteMapNode> currentBranch = null;
                if (currentBranch == null)
                {
                    SiteMapNode cn = SiteMap.CurrentNode;
                    SiteMapNode n = cn;
                    List<SiteMapNode> ln = new List<SiteMapNode>();
                    if (cn != null)
                    {
                        while (n != null && n.Url != SiteMap.RootNode.Url)
                        {
                            // I don't need to check for n.ParentNode == null
                            // because cn != null && n != SiteMap.RootNode
                            ln.Add(n);
                            n = n.ParentNode;
                        }
                        // the while loop excludes the root node, so add it here
                        // I could add n, that should now be equal to SiteMap.RootNode, but this is clearer
                        ln.Add(SiteMap.RootNode);

                        // The nodes were added in reverse order, from the CurrentNode up, so reverse them.
                        ln.Reverse();
                    }
                    currentBranch = ln;
                }
                return currentBranch;
            }
        }

The Question:

What am I doing wrong?

The routes are interpreted by Html.ActionLlink as I expect, but are not evaluated by SiteMap.CurrentNode as I expect. In other words, in evaluating my routes, SiteMap.CurrentNode ignores the distinguishing alternate parameter.


Solution

  • I think it is because you are trying to obtain the route FROM the parameters. Basically, MVC is just trying to GUESS what route you could be referring to.

    The correct way would be to handle routes by name. So the sitemap should reference to a Route name rather than the Controller, action, etc.