1
votes

I am trying to use MVC sitemap to create a breadcrumb trail for my application. I have muliple routes into the User Controller and need to preserve the breadcrumbs for each. How does mvcSiteMap map each node to the route?

I have the following possible routes into a user:

/User/{action}/{userid}

/User/{groupid}/{action}/{userid}

I would like the following breadcrumb trails respectively:

App Root > Users > {User Name} > {Action}

App Root > Group Management > {Group Name} > {User Name} > {Action}

The first of the above routes is straight forward enough and I decorate the Details controller with [SiteMapTitle("Mail")] to display the objects Mail attribute instead of "Details" and the Edit is decorated with [SiteMapTitle("Mail", Target = AttributeTarget.ParentNode)] to preserve the email address when Editing.

But, I can't figure out how to do all of this when using the second route. The breadcrumb trail doesn't display anything at all. The Users section within the Group section appears to do nothing. What is the best way to achieve this?

Mvc.Sitemap:

    <mvcSiteMapNode title="User Management" controller="User" action="Index">
      <mvcSiteMapNode title="Details" action="Details" controller="User" preservedRouteParameters="id">
        <mvcSiteMapNode title="Edit" action="Edit" controller="User" preservedRouteParameters="id"/>
      </mvcSiteMapNode>
    </mvcSiteMapNode>

    <mvcSiteMapNode title="Group Management" controller="Group" action="Index">
      <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 title="New" action="Create" controller="Group" />
    </mvcSiteMapNode>
    ...
  </mvcSiteMapNode>

Routes:

routes.MapRoute("ByGroup", "User/{groupid}/{action}/{id}",
                        new { controller = "User", action = "Index", id = UrlParameter.Optional }, new { groupid = new GuidConstraint() });

routes.MapRoute("ByGroup2", "User/{groupid}/{action}/{id}",
                        new { controller = "User", action = "Index", id = UrlParameter.Optional, groupid = UrlParameter.Optional }, new { groupid = new GuidConstraint() });


routes.MapRoute("User", "User/{action}/{id}",
                        new { controller = "User", action = "Index", id = UrlParameter.Optional });

        
routes.MapRoute("Default", "{controller}/{action}/{id}",
                        new { controller = "Home", action = "Index", id = UrlParameter.Optional });
1
Could you display your entire route configuration including defaults and constraints? If you really have your user routes listed in that order with no constraints, then you have a problem with your routing.NightOwl888
Added routes - as requested.Jamie

1 Answers

2
votes

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)