Problem with sitemap and areas

Aug 19, 2010 at 7:58 PM

Hi, i have a configured mvcsitemap 2.1  that works fine on homepage (without any area specified).

But when i go through any area, the sitemap gets filtered and shows only items thar refer to that area, it´s a configuration problem ?

Is it possible to have always the same menu doesn´t matter in which area i am ?

 

Thank´s

 

PD I´m on VS2010 with MVC2 webapplication

Aug 19, 2010 at 9:11 PM
I have the same problem. I have a root area (Views outside any area) and then i have areas. i want to have a menu that links to views across all areas and i want them visible in all areas, but when i go to a certain area i will only see the links that belong to the current area.
Coordinator
Aug 23, 2010 at 8:11 PM

Is there a sample somewhere that Ican use to reproduce?

Aug 23, 2010 at 8:38 PM

Here is a simple mvc application

 <?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-2.0" enableLocalization="true">
    <mvcSiteMapNode title="Home" controller="Home" action="Index" changeFrequency="Always" updatePriority="Normal" lastModifiedDate="2002-05-30T09:00:00">
        <mvcSiteMapNode title="About" controller="Home" action="About"></mvcSiteMapNode>
        <mvcSiteMapNode title="Admin" controller="StoreManager" action="Index" area="Admin"></mvcSiteMapNode>
    </mvcSiteMapNode>
</mvcSiteMap>

Visit /home and SiteMap.RootNode.ChildNodes.Count.ToString();  will return "2" (Which is correct)

Visit /Admin/StoreManager And SiteMap.RootNode.ChildNodes.Count.ToString() will return 1 (For 'Admin' and it will not show 'About')

I have pin pointed the reason to this peice of code at the  IsAccessibleToUser function at the AuthorizeAttributeAclModule

            try
            {
                controller = ControllerBuilder.Current.GetControllerFactory().CreateController(requestContext, mvcNode.Controller) as ControllerBase;
            }
            catch (HttpException ex)
            {
                return false;
            }
That's becuase 
if (!routes.Route.Equals(originalRoutes.Route)) 
{ 
	routes.DataTokens.Remove("area"); routes.DataTokens.Remove("Namespaces"); 
} 
Always returns true, even if your are at the About node where you rewrite the url to /Home/About it will still be true, and the DataTokens will still have 'area' as 'Admin' and it cannot create a controller this way

If i changed it it like this:

            if (!routes.Route.Equals(originalRoutes.Route) || originalPath != node.Url)
            {
                routes.DataTokens.Remove("area");
                routes.DataTokens.Remove("Namespaces");
            }

 It started working

 

But for some reason you are comparing the routs not the url, maybe i'm missing something

Coordinator
Aug 24, 2010 at 6:52 AM

Seems to work, thanks for the fix. I'll include it in the next version.

Coordinator
Aug 24, 2010 at 6:53 AM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.
Aug 24, 2010 at 1:54 PM

I appear to be getting the error even on 50732 :

Note: the routes work if I disabled the sitemap code.

This is my setup: 

web.sitemap

<?xml version="1.0" encoding="utf-8" ?>
<mvcSiteMap xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-2.0" enableLocalization="true">
  <mvcSiteMapNode title="Home" controller="Home" action="Index" changeFrequency="Always" updatePriority="Normal" area="" >
    <mvcSiteMapNode title="About" controller="Home" action="About" />
    <mvcSiteMapNode title="Admin" controller="Home" action="Index" area="Admin">
    </mvcSiteMapNode>    
  </mvcSiteMapNode>
</mvcSiteMap>

web.config

   <siteMap defaultProvider="MvcSiteMapProvider" enabled="true">
      <providers>
        <clear />
        <add name="MvcSiteMapProvider"
             type="MvcSiteMapProvider.DefaultSiteMapProvider, MvcSiteMapProvider"
             siteMapFile="~/Web.Sitemap"
             securityTrimmingEnabled="true"
             cacheDuration="5"
             enableLocalization="true"
             scanAssembliesForSiteMapNodes="true"
             skipAssemblyScanOn=""
             attributesToIgnore="bling"
             nodeKeyGenerator="MvcSiteMapProvider.DefaultNodeKeyGenerator, MvcSiteMapProvider"
             controllerTypeResolver="MvcSiteMapProvider.DefaultControllerTypeResolver, MvcSiteMapProvider"
             actionMethodParameterResolver="MvcSiteMapProvider.DefaultActionMethodParameterResolver, MvcSiteMapProvider"
             aclModule="MvcSiteMapProvider.DefaultAclModule, MvcSiteMapProvider"
             siteMapNodeUrlResolver="MvcSiteMapProvider.DefaultSiteMapNodeUrlResolver, MvcSiteMapProvider"
             siteMapNodeVisibilityProvider="MvcSiteMapProvider.DefaultSiteMapNodeVisibilityProvider, MvcSiteMapProvider"
             siteMapProviderEventHandler="MvcSiteMapProvider.DefaultSiteMapProviderEventHandler, MvcSiteMapProvider"
         />
      </providers>
    </siteMap>

Error:

Server Error in '/' Application.

Multiple types were found that match the controller named 'Home'. This can happen if the route that services this request ('{controller}/{action}/{id}') does not specify namespaces to search for a controller that matches the request. If this is the case, register this route by calling an overload of the 'MapRoute' method that takes a 'namespaces' parameter.

The request for 'Home' has found the following matching controllers:
MvcApplication5.Controllers.HomeController
MvcApplication5.Areas.Admin.Controllers.HomeController

 

 

Aug 26, 2010 at 9:26 AM
Edited Aug 26, 2010 at 9:30 AM

I have exactly the same trouble. Does anyone have a solution?

I found thread that describes this trouble:

http://mvcsitemap.codeplex.com/Thread/View.aspx?ThreadId=222191

Hope this will be fixed as soon as possible.

Aug 26, 2010 at 10:19 AM

As I understand a trouble is here:

var originalRoutes = RouteTable.Routes.GetRouteData(httpContext);
httpContext.RewritePath(node.Url, true);
var routes = RouteTable.Routes.GetRouteData(httpContext);

When you do RewritePath you change FilePath, Path but AppRelativeCurrentExecutionFilePath remains unchanged. (in may case it is ~/  )
Then you try to get route data, but internally it searches by AppRelativeCurrentExecutionFilePath property and find a default route (in my case {controller}/{action} ) instead of correct route that includes correct area and namespaces.
So we find the same route as originalRoute that is incorrect route.

 After that, we try to create controller from our route (in my case it is {controller}/{action}), and it start to search all possible locations to find required controller. Because default route don't know about area and it's namespaces it found 2 controllers in different areas with same name.

So... i have no idea how to fix this...

Coordinator
Aug 26, 2010 at 2:00 PM
Consider narrowing the places to search by adding you controller namespaces to ControllerBuilder.Current.DefaultNamespaces. Or in the route.
Aug 26, 2010 at 3:07 PM
Edited Aug 26, 2010 at 5:25 PM

The same error. For example I have 2 areas, so I add 2 namespaces. Each namespace contains a controller with name, for ex., PoorController. And we have the same situation.

I fixed this error by adding 2 extension methods GetRouteDataEx: to RouteCollection and to BaseRoute.
This methods do the same things as default GetRouteData but them operates with FilePath not with AppRelativeCurrentExecutionFilePath.

And I changed the code to the next:

var originalRoutes = RouteTable.Routes.GetRouteData(httpContext);
httpContext.RewritePath(node.Url, true);
var routes = RouteTable.Routes.GetRouteDataEx(httpContext);
//if (!routes.Route.Equals(originalRoutes.Route) || originalPath != node.Url)
//{
// routes.DataTokens.Remove("area");
// routes.DataTokens.Remove("Namespaces");
//}

Now all works fine, but I was surprised when I found that 80% of my menu items disappears. The next is the magic.
I start debugging and found that in DefaultSiteMapProvider.BuildSiteMap after we do isBuildingSiteMap = false; contents of root variable changed! And now it contains only 3 nodes.
I spent 3 hours for debugging trying to understand what happens, I check code for threads, for unexpected cache dependency callbacks, even I changed isBuildingSiteMap from field to property to see who can access it's getter.

And now I have this trouble...
So, if anybody have more "right" solution to previous trouble or know what a magic happens on  isBuildingSiteMap = false;, please help me

Sep 2, 2010 at 11:58 AM

Hi, this looks like the same issue i'm having.  It appears the first node is the one that gets selected.  I tried modifying the DefaultControllerTypeResolver as this seems to be my problem (not i have disabled security trimming as this is not needed for me which avoids the other issue mentioned above).

So far my attemps have failed but i'm looking at the lines around lines 150.  I tried all sorts of variations by adding name spaces, eg:

if (possibleType.FullName == ns + "." + controller + "Controller")

But this does not seem to work.  I'd appreciate it if we could get to the bottom of this as many people seem to be having this issue.  Thanks

Coordinator
Sep 9, 2010 at 7:31 PM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.
Sep 22, 2010 at 9:53 AM
Edited Sep 22, 2010 at 10:04 AM

Hello colleagues!

I've tried revision 51110 but still have a problem. But, I think I found a solution.. at last in my situation.. So, what I've done - I decided not to use extension methods with lot's of reflection and calling private methods. Instead of this I created custom HttpContext2 and HttpRequest2 classes:

#region Using directives

using System;
using System.Reflection;
using System.Web;

#endregion

namespace MvcSiteMapProvider
{
    class HttpContext2 : HttpContextWrapper
    {
        private readonly HttpContext _context;
 
        public HttpContext2(HttpContext httpContext)
            : base(httpContext)
        {
            _context = httpContext;
        }

        public override HttpRequestBase Request
        {
            get { return new HttpRequest2(this._context.Request); }
        }
    }

    class HttpRequest2 : HttpRequestWrapper
    {
        private HttpRequest _httpRequest;

        public HttpRequest2(HttpRequest httpRequest)
            : base(httpRequest)
        {
            _httpRequest = httpRequest;
        }

        public override string AppRelativeCurrentExecutionFilePath
        {
            get
            {
                Type urlPath = Type.GetType("System.Web.Util.UrlPath, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
                return (string)urlPath.InvokeMember("MakeVirtualPathAppRelative", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.InvokeMethod, null, null, new[] { this.CurrentExecutionFilePath });
            }
        }

        public override string CurrentExecutionFilePath
        {
            get { return base.FilePath; }
        }
    }
}

The main feature - HttpRequest2.CurrentExecutionFilePath and HttpRequest2.AppRelativeCurrentExecutionFilePath - both return value of base.FilePath property that can be changed by using httpContext.RewritePath(node.Url, true);

Then in AuthorizeAttributeAclModule.IsAccessibleToUser I use custom context to retrieve correct routes based on rewritten paths:

// Find routes for the sitemap node's url
HttpContextBase httpContext = new HttpContextWrapper(context);
string originalPath = httpContext.Request.Path;
var originalRoutes = RouteTable.Routes.GetRouteData(httpContext);
httpContext.RewritePath(node.Url, true);

/* CHANGES START HERE */
HttpContextBase httpContext2 = new HttpContext2(context);
var routes = RouteTable.Routes.GetRouteData(httpContext2);
//if (!routes.Route.Equals(originalRoutes.Route))
//{
//    routes.DataTokens.Remove("area");
//    routes.DataTokens.Remove("Namespaces");
//}
/* CHANGES END HERE */

foreach (var routeValue in mvcNode.RouteValues)
{
    routes.Values[routeValue.Key] = routeValue.Value;
}
var requestContext = new RequestContext(httpContext, routes);

The next thing we should change is the logic of DefaultControllerTypeResolver;FindNamespacesForArea - this method used to find area's namespaces that can be used in controller search. In default implementation this method returns exactly area's namespaces and common namespaces. In my implementation - only area's namespaces, and if they not specified - common namespaces. I do this to avoid "multiple controllers" exception if we have controllers with same name in area and in common namespace:

protected IEnumerable<string> FindNamespacesForArea(string area, RouteCollection routes)
{
    List<string> namespacesForArea = new List<string>();
    List<string> namespacesCommon = new List<string>();

    foreach (var route in routes.OfType<Route>().Where(r => r.DataTokens != null && r.DataTokens["Namespaces"] != null))
    {
        // search for area-based namespaces
        if (route.DataTokens["area"] != null && route.DataTokens["area"].ToString().Equals(area, StringComparison.OrdinalIgnoreCase))
            namespacesForArea.AddRange((IEnumerable<string>)route.DataTokens["Namespaces"]);
        else if (route.DataTokens["area"] == null)
            namespacesCommon.AddRange((IEnumerable<string>)route.DataTokens["Namespaces"]);
    }

    if (namespacesForArea.Count > 0)
        return namespacesForArea;
    else if (namespacesCommon.Count > 0)
        return namespacesCommon;

    return null;
}

Thats it. Now it works in my case.

PS: if you still have a trouble, consider to disable securityTrimmingEnabled and do not use custom menu and\or path renderers if you have them, and try again.

Coordinator
Sep 23, 2010 at 4:30 PM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.
Coordinator
Sep 23, 2010 at 4:30 PM
This discussion has been copied to a work item. Click here to go to the work item and continue the discussion.