Nodes:
<mvcSiteMapNode title="User Management" controller="User" action="Index" route="Default">
<mvcSiteMapNode title="Details" action="Details" controller="User" route="Default" preservedRouteParameters="id">
<mvcSiteMapNode title="Edit" action="Edit" controller="User" route="Default" preservedRouteParameters="id"/>
</mvcSiteMapNode>
</mvcSiteMapNode>
<mvcSiteMapNode title="Group Management" controller="Group" action="Index">
<mvcSiteMapNode title="Details" action="Details" controller="Group" preservedRouteParameters="groupid">
<mvcSiteMapNode title="Edit" action="Edit" controller="Group" preservedRouteParameters="groupid"/>
<mvcSiteMapNode title="Users" controller="User" action="Index" route="ByGroup" preservedRouteParameters="groupid">
<mvcSiteMapNode title="Details" action="Details" controller="User" route="ByGroup" preservedRouteParameters="id, groupid">
<mvcSiteMapNode title="Edit" action="Edit" controller="User" route="ByGroup" preservedRouteParameters="id, groupid"/>
<mvcSiteMapNode title="Manage" action="Manage" controller="User" route="ByGroup" preservedRouteParameters="id, groupid"/>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
<mvcSiteMapNode title="New" action="Create" controller="Group" />
</mvcSiteMapNode>
Routes:
routes.MapRoute(
name: "Group",
url: "Group/{action}/{groupid}",
defaults: new { controller = "Group", action = "Index", groupid = UrlParameter.Optional });
routes.MapRoute(
name: "ByGroup",
url: "User/{groupid}/{action}/{id}",
defaults: new { controller = "User", action = "Index", id = UrlParameter.Optional },
constraints: new { groupid = new GuidConstraint() });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
Explanation
You have defined 2 optional segments on the ByGroup2
route, which isn't allowed. An optional segment must be at the right-most position of the URL and cannot be followed by a required segment. This is probably causing issues.
Besides, I am not seeing any real purpose for the ByGroup2
route.
/User/131f89da-0dca-40f0-bc99-41559d13fc7f/Edit/123 - matches ByGroup
/User/131f89da-0dca-40f0-bc99-41559d13fc7f/Index - matches ByGroup
/User/Edit/123 - matches User
/User/Index - matches User
I can't think of a case that will match ByGroup2
. But if it does match, your parameters will be put into different route keys than if it matches the User
route, which could be confusing things.
Also, your User
route doesn't seem to add anything over the Default
route. So, your route configuration could look like this and do exactly the same thing (minus the confusing of route value positions, which might be causing problems for you).
routes.MapRoute("ByGroup", "User/{groupid}/{action}/{id}",
new { controller = "User", action = "Index", id = UrlParameter.Optional }, new { groupid = new GuidConstraint() });
routes.MapRoute("Default", "{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = UrlParameter.Optional });
And then the matching would look like this.
/User/131f89da-0dca-40f0-bc99-41559d13fc7f/Edit/123 - matches ByGroup
/User/131f89da-0dca-40f0-bc99-41559d13fc7f/Index - matches ByGroup
/User/Edit/123 - matches Default
/User/Index - matches Default
However, you also have an issue with your preservedRouteParameters.
<mvcSiteMapNode title="Details" action="Details" controller="Group" preservedRouteParameters="id">
<mvcSiteMapNode title="Edit" action="Edit" controller="Group" preservedRouteParameters="id"/>
<mvcSiteMapNode title="Users" controller="User" action="Index" preservedRouteParameters="groupid">
<mvcSiteMapNode title="Details" action="Details" controller="User" preservedRouteParameters="id, groupid">
<mvcSiteMapNode title="Edit" action="Edit" controller="User" preservedRouteParameters="id, groupid"/>
<mvcSiteMapNode title="Manage" action="Manage" controller="User" preservedRouteParameters="id, groupid"/>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
</mvcSiteMapNode>
For preservedRouteParameters to match multiple levels, the all of the custom route values (in this case id
and groupid
) of the ancestor nodes must be supplied. Furthermore, they must have the same meaning. For this to work, id
must always refer to the same entity all the way down through the nodes, and must be included in every link no matter how deep. You must choose a different route key for the Group entity than for the User entity.
To clean this up, you could alter the routes one more time to put all of the information into the route that is required. You are most of the way there already - you just need to fix the id of the Group nodes.
routes.MapRoute(
name: "Group",
url: "Group/{action}/{groupid}",
defaults: new { controller = "Group", action = "Index", groupid = UrlParameter.Optional });
routes.MapRoute(
name: "ByGroup",
url: "User/{groupid}/{action}/{id}",
defaults: new { controller = "User", action = "Index", id = UrlParameter.Optional },
constraints: new { groupid = new GuidConstraint() });
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);
And nodes:
<mvcSiteMapNode title="Details" action="Details" controller="Group" preservedRouteParameters="groupid">
<mvcSiteMapNode title="Edit" action="Edit" controller="Group" preservedRouteParameters="groupid"/>
<mvcSiteMapNode title="Users" controller="User" action="Index" route="ByGroup" preservedRouteParameters="groupid">
<mvcSiteMapNode title="Details" action="Details" controller="User" route="ByGroup" preservedRouteParameters="id, groupid">
<mvcSiteMapNode title="Edit" action="Edit" controller="User" route="ByGroup" preservedRouteParameters="id, groupid"/>
<mvcSiteMapNode title="Manage" action="Manage" controller="User" route="ByGroup" preservedRouteParameters="id, groupid"/>
</mvcSiteMapNode>
Notice that every route that is below the /Group/Index
node now has a groupid
, and the groupid
key always refers to the same entity?
Also, to ensure that we only match the pertinent route, we specify it explicitly. If we didn't, the user nodes would be ambiguous so you will get the wrong breadcrumb trail (the first node that matches wins).
route="ByGroup"
With the above configuration, you would need to build the links to the Users
, Users/Details
, Users/Edit
, and Users/Manage
that include the current groupid
(and of course, the current user id).
@Html.ActionLink("Edit User", "Edit", "User", new { id = <userid>, groupid = <groupid> }, null)
Then when you navigate to the "Edit User" link, the groupid will be in the current request, which will feed it to the Users/Details
node, Group/Edit
node, and Group/Details
node when resolving the URLs so you can navigate back to those locations through the breadcrumb trail.
See the Forcing-A-Match-2-Levels example in the code download of this article for another example.
The user section that doesn't have a group will match the Default
route, and will show the appropriate breadcrumb trail when building the URL without the groupid
.
So we explicitly specify the default route:
route="Default"
And build the URL like:
@Html.ActionLink("Edit User", "Edit", "User", new { id = <userid> }, null)