maartenba / MvcSiteMapProvider

An ASP.NET MVC SiteMapProvider implementation for the ASP.NET MVC framework.
Microsoft Public License
537 stars 220 forks source link

Route attribute not working with area routes #446

Open git4sastra opened 8 years ago

git4sastra commented 8 years ago

When using the route attribute in XML configuration for a defined area route it seems to be ignored. Is that by design and would I need to use an external DI or is it a bug?

NightOwl888 commented 8 years ago

I am unable to reproduce the behavior you describe. Here is a project demonstrating that the route attribute works exactly the same way that RouteLink works in an Area.

The behavior of the route attribute is meant to be exactly the same as using Html.RouteLink or Url.RouteUrl in MVC. The route name acts as a filter. It makes the routing framework compare only a single route with the supplied route values rather than every route in the route table. However, all of the required route values must still be supplied to get a match (that is, any non-optional values that don't have defaults set), or you will get a null result. In a non-matching scenario in MvcSiteMapProvider, you will instead get a #, which effectively makes the current page link to itself (see #115 for an explanation).

It is possible that your routing is misconfigured. See this question as well as this question for some common routing pitfalls.

git4sastra commented 8 years ago

Ok, first wouldn't it be a better idea to use the route attribute to takeover the way the route is configured?!

Second, I think I know where the issue is ....

My configured routes looks like: context.MapRoute( "Crm_Contacts_List", "Crm/Contacts/List/{group}/{sort}", new { controller = "Contacts", action = "Index", group = "Alle", sort = UrlParameter.Optional }, new { sort = "^Company_Asc$|^Company_Desc$|^Lastname_Asc$|^Lastname_Desc$" } ); And I tried the following XML configuration (also tried with preserved Parameters group and/or sort): <mvcSiteMapNode title="$resources:Sitemap,Contacts_Title" route="Crm_Contacts_List" controller="Contacts" action="Index" visibility="CrmMenu,CrmBreadcrumb,!*" />

Is it possible that the fixed name "List" is an issue here?

NightOwl888 commented 8 years ago

Ok, first wouldn't it be a better idea to use the route attribute to takeover the way the route is configured?!

MvcSiteMapProvider doesn't configure routing for you. It consumes your existing route configuration. Routing happens near the beginning of the ASP.NET lifecycle, and displaying the view (where MvcSiteMapProvider comes into play) happens near the end of the lifecycle.

Perhaps in a future version, it might be possible to extend MVC routing to add the parent-child relationship and title that routing is missing in order to configure MvcSiteMapProvider there. But for now, this is the way it works. Most people would argue that something that deals with the UI such as MvcSiteMapProvider should be a completely separate concern than routing.

Is it possible that the fixed name "List" is an issue here?

On the contrary. Adding fixed values to the URL is one of the ways to make a route unique so routing works correctly. These fixed values affect the matching of the incoming URL and the building of the outgoing URL only. They do not affect the route values that are generated by the route (which are what MvcSiteMapProvider uses).

Routing

I believe one problem is that you are putting a constraint on an optional value. I tried doing that before, but was unable to make it function and I am pretty sure routing is not designed to work that way.

An alternative way to configure it would be to make 2 different routes to replace your optional sort value - one with the constraint and one without. In the first case, the sort parameter is required and in the second case, it is not part of the URL at all.

Do note that making sort required means that everything to its left is also required. Therefore, it makes no sense to have an optional group in the first route.

context.MapRoute( 
    "Crm_Contacts_List_Sort", 
    "Crm/Contacts/List/{group}/{sort}", 
    new { controller = "Contacts", action = "Index" }, 
    new { sort = "^Company_Asc$|^Company_Desc$|^Lastname_Asc$|^Lastname_Desc$" } 
);

context.MapRoute( 
    "Crm_Contacts_List", 
    "Crm/Contacts/List/{group}", 
    new { controller = "Contacts", action = "Index", group = "Alle" }
);

I am not sure if this is exactly right for your use case, though. If it doesn't make sense to have a "default" group at all, then you should make it required by not supplying a default.

context.MapRoute( 
    "Crm_Contacts_List_Sort", 
    "Crm/Contacts/List/{group}/{sort}", 
    new { controller = "Contacts", action = "Index"  }, 
    new { sort = "^Company_Asc$|^Company_Desc$|^Lastname_Asc$|^Lastname_Desc$" } 
);

context.MapRoute( 
    "Crm_Contacts_List", 
    "Crm/Contacts/List/{group}", 
    new { controller = "Contacts", action = "Index"  }
);

Usually, the right way to configure routing is to use required URL parameters, not optional parameters. The typical case is to make 1 URL map to an action. Using optional parameters makes multiple URLs map to an action (unless the alternates are explicitly suppressed using IgnoreRoute), which is neither SEO-freindly or intuitive.

In my first example, Crm_Contacts_List matches both of these URLs:

/Crm/Contacts/List
/Crm/Contacts/List/Alle

In the second example, it only matches /CrmContacts/List/Alle.

Nodes

As for your node configuration, it is not matching in this case because the node is missing matching route values for group and sort. One way fix it would be to use preservedRouteParameters:

<mvcSiteMapNode title="$resources:Sitemap,Contacts_Title"  area="Crm" controller="Contacts" action="Index" preservedRouteParameters="group,sort" visibility="CrmMenu,CrmBreadcrumb,!*" />

If group is an ambient value, that may work for you. Do note that since there is no longer a single route that this node can match, you must not specify it explicitly. When you do that, you need to specify the area to ensure the route will match.

If group is something that identifies the page rather than an ambient value, you should instead make a node for each group (typically using a dynamic node provider).

<mvcSiteMapNode title="$resources:Sitemap,Contacts_Title" area="Crm" controller="Contacts" action="Index" group="Group1" preservedRouteParameters="sort" visibility="CrmMenu,CrmBreadcrumb,!*" />
<mvcSiteMapNode title="$resources:Sitemap,Contacts_Title"  area="Crm" controller="Contacts" action="Index" group="Group2" preservedRouteParameters="sort" visibility="CrmMenu,CrmBreadcrumb,!*" />
<mvcSiteMapNode title="$resources:Sitemap,Contacts_Title"  area="Crm" controller="Contacts" action="Index" group="Group3" preservedRouteParameters="sort" visibility="CrmMenu,CrmBreadcrumb,!*" />

See How to Make MvcSiteMapProvider Remember a User's Position for a detailed explanation of these options and how they work.

git4sastra commented 8 years ago

First of all - thank you very much for your detailed explanation which gets me a little bit further. Maybe you didn't see my note, but I already tried preservedRouteParameters Nevertheless, I have now an approach that at least is generating a working link, but it adds the URL parameter area to the link, which still is not very nice. It doesn't even matter if I set the area attribute or not, I get the same result. Also I get an error if I set group as preservedRouteParameter stating that group is set in route parameters and preservedRouteParameters.

My configuration now looks like the following:

         context.MapRoute(
                "Crm_Contacts_List_Sorted",
                "Crm/Contacts/List/{group}/{sort}",
                new { controller = "Contacts", action = "Index", group = "", sort = UrlParameter.Optional },
                new { sort = "^Company_Asc$|^Company_Desc$|^Lastname_Asc$|^Lastname_Desc$" }
            );

            context.MapRoute(
                "Crm_Contacts_List",
                "Crm/Contacts/List/{group}",
                new { controller = "Contacts", action = "Index", group = "" }
            );

and the XML configuration like:

<mvcSiteMapNode title="$resources:Sitemap,Contacts_Title" route="Crm_Contacts_List" controller="Contacts" action="Index" visibility="CrmMenu,CrmBreadcrumb,!*" />

The resulting link looks like the following: /Crm/Contacts/List?area=Crm

If I now can get rid of the URL parameter area it would be perfect suited.