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

4

6

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.

Mahican answered 25/9, 2012 at 12:13 Comment(0)
B
3

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.

Balling answered 8/10, 2012 at 9:30 Comment(5)
Can you edit your reply to show a mvcSiteMapNode that handles routes by route name rather than controller, etc?Mahican
I have not used the mvcSiteMapNode before but I would imagine it can handle a basic route name. I have just opened what seems to be the source code on github: github.com/maartenba/MvcSiteMapProvider/blob/master/src/… and there is a property called Route. Can you try with: <mvcSiteMapNode title="Process" area="Admin" route="Process_New_Members"/>Balling
I have found an error in my attempt to apply what you suggested. It now works. I am trying to find out how to reactivate the bounty on this question, at which point I will award it to you.Mahican
glad it helped. Too bad you couldn't re-enable the bounty thing.Balling
I have found my problem (see addendum answer below): giving the nodes the route attribute is not enough, you HAVE to give them a unique key attribute too! Once you do that, all is plain sailing.Mahican
M
2

Addendum - I've got it to work!!:

The big stumbling block:

When using different routes to point at the same controller action, so that one has different nodes, the big problem I had was the following:

YOU MUST GIVE THE DIFFERENT NODES A KEY!!! OTHERWISE THE SITEMAP WON'T RENDER!!

EG, just the route attribute on its own is NOT enough and you MUST give each node that point to the same action a UNIQUE key to distinguish them from each other:

<mvcSiteMapNode title="Edit Staff" area="Admin" controller="EditStaff" route="Admin_AdministratorDetails" action="Start" key="administrators_details" />
<mvcSiteMapNode title="Edit Staff" area="Admin" controller="EditStaff" route="Admin_StaffDetails" action="Start" key="staff_details" />

Once I realised that, it is now plain sailing. Otherwise, all is obscure and obtuse.

NOTE:

In the question, the action that was getting called by different routes was the Process action. I changed that to calls to different actions. But when editing my objects (Solicitors), I couldn't do that, as the editing is done by an MVC Wizard that, like my custom menu builder, I have written and works very well. It just simply wasn't possible (or rather, DRY) to recreate the wizard three times. So I HAD to get my menu higlighting working correctly with different routes pointing at the same action.

No Un-DRY fudges will do. My client doesn't deserve it.

The Solution (see NOTE as to why the actions and routes are different to the question):

The suggestion by Carlos Martinez works, but you need to use Html.RouteLink as opposed to Html.ActionLink, in conjunction with the edited sitemap, that details the routes.

Essentially, in your nodes, you need to use the route attribute:

<mvcSiteMapNode title="Details Active Solicitor" area="Solicitors" controller="EditSolicitor" action="Start" route="Active_Details" key="company_activeSolicitors_details"/>

Then, in your views, instead of action links, you use the RouteLink helper:

@Html.RouteLink("Details", "Active_Details", new { action="Start", controller="EditSolicitor", idSolicitor = item.SolicitorId, returnUrl = Request.RawUrl })

In your route registration file, you can now write routes that call the same action:

context.MapRoute(
                "Unprocessed_Details",
                "Unprocessed/Edit/{idSolicitor}/{action}",
                new { action = "Start", controller = "EditSolicitor", idSolicitor = UrlParameter.Optional }
            );

            context.MapRoute(
                "Inactive_Details",
                "Inactive/Edit/{idSolicitor}/{action}",
                new { controller = "EditSolicitor", action = "Start", idSolicitor = UrlParameter.Optional }
            );

            context.MapRoute(
                "Active_Details",
                "Solicitors/Edit/{idSolicitor}/{action}",
                new { controller = "EditSolicitor", action = "Start", idSolicitor = UrlParameter.Optional }
            );

As you can see, it is the exact same action that is getting called by the three routes. So long as I specify the route name in the mvcSiteMapNode, the routes are correctly distinguished when my menu is built and the highlighting works as required.

Note on getting befuddled with XML:

I hate XML, deeply and truly. It boggles and befuddles me, especially when I have a cold.

The problem being that adding the route attribute to the mvcSiteMapNodes adds to the potential for befuddlement.

And befuddled I got

I had initially tried Carlos' suggestion, but it didn't work. Not sure what the error was, but went through it with a tooth comb and it is now working. The annoying thing is that I am not sure what I was doing wrong.

Living in Hope:

I hope this documents an albeit fringe aspect of mvcSiteMapNode.

Mahican answered 9/10, 2012 at 12:22 Comment(0)
B
0

Just in case, can you try the following:

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

This way you will be distinguishing between each route by requiring this additional parameter.

Balling answered 8/10, 2012 at 9:35 Comment(0)
S
0

You can use url in your mvcSiteMapNode. like below:

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

Of course, you should use the appropriate RoutConfig for the url to work. there is a sample here.

Skullcap answered 15/5, 2019 at 8:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.