Menu fails when using Areas with duplicate controllers

Apr 1, 2010 at 7:00 PM

I recently upgraded from MVC 1.0 to MVC 2.0 and I have downloaded the latest build of MvcSiteMap.  The site I am working on is kind of big so I decided to break it up into areas knowing that MvcSiteMap supported them.  I was having no trouble at all until I tried to give two areas a controller with the same name.  This being a nice feature of Areas that I really need to have work I am kind of stuck at the moment.  Just to make sure I created a very simple MVC 2.0 project and duplicated the error.  This is what I did.

I created a standard MVC 2.0 application and copied the site map definition from sample web.config file provided in the latest source code.

<siteMap defaultProvider="MvcSiteMap">
    <providers>
        <add
             name="MvcSiteMap"
             type="MvcSiteMap.Core.MvcSiteMapProvider"
             siteMapFile="~/Web.Sitemap"
             securityTrimmingEnabled="true"
             enableLocalization="true"
             cacheDuration="10"
             scanAssembliesForSiteMapNodes="true"
             treatAttributesAsRouteValues="true"
             defaultControllerName="Home"
             defaultActionName="Index"
             aclModule="MvcSiteMap.Core.DefaultMvcSiteMapAclModule, MvcSiteMap.Core" />
    </providers>
</siteMap>

I then created a simple Web.sitemap file.

<?xml version="1.0" encoding="utf-8" ?>
<siteMap xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-1.0" enableLocalization="false">
    <mvcSiteMapNode title="Home" controller="home" action="index" isDynamic="true" dynamicParameters="*" visibility="Full">
        <mvcSiteMapNode title="Sales Reports" area="sales" controller="reports" action="index" />
        <mvcSiteMapNode title="Marketing Reports" area="marketing" controller="reports" action="index" />
    </mvcSiteMapNode>
</siteMap>

Next I created two areas.  One called Marketing and the other Sales.  I added a ReportsController.cs to both and created the basic index.aspx view for both controllers.

On the Index.aspx view of the HomeController I added this:

<%= Html.Menu("MvcSiteMap")%>

Fire up the website and this is what I see:

Server Error in '/' Application.

Multiple types were found that match the controller named 'reports'. 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 'reports' has found the following matching controllers:

MvcApplication1.Areas.Sales.Controllers.ReportsController

MvcApplication1.Areas.Marketing.Controllers.ReportsController

I have name-spaced out the routes and the problem still persists.  Was wondering if anyone else had this problem?  Thanks in advance for the help.

Apr 2, 2010 at 4:10 PM

Okay from what I can figure the AclModule is where the issue lies.  As the SiteMapProvider determines whether or not a node is visible to the user it goes through a series of checks to do so.  One of the checks attempts to create a controller from the controller name entered in the MvcSiteMapNode then cycle through every action with an AuthorizeAttribute looking for the particular Action that was also named in the MvcSiteMapNode.  Therein lies the problem.

ASP.NET MVC 2 has changed the CreateController() signature to require a RequestContext object allong with the controller name.  The RequestContext is created with the current httpContext and the RouteData for the current request.

The code in the AclModule is:

HttpContextBase httpContext = new HttpContextWrapper(context);
var routes = RouteTable.Routes.GetRouteData(httpContext);
RequestContext requestContext = null;

if (routes != null)
{
    requestContext = new RequestContext(httpContext, routes);
}

For the example I built in the post above on the initial application start the current request is for "~/" and the request context will reflect that.  But as the Menu is being built and the nodes are cycled through you come to a mvcSiteMap node that is defined as:

<mvcSiteMapNode title="Sales Reports" area="sales" controller="reports" action="index" />
So an attempt to create a controller is made with a requestContext object that contains RouteData of a route that is namespaced to "MvcApplication1.Controllers" and a controller name of Reports.  The controller factory can't find the Reports controller in the current namespace so it scans everywhere else and finds two controllers named Reports in two different namespaces and can't figure out which one to use and then throws a InvalidOperationException.

So it would seem to me that the requestContext needs to contain RouteData of a route that matches the area, controller, and action values defined in the mvcSiteMapNode.  And I am sorry to say that I can't figure out how to do that.  Any ideas?

Apr 2, 2010 at 9:35 PM

So I think I have a fix for this although I am not sure that it is the best way to do it... but here it goes.

The problem has always been that the Controller object in the AclModule needed a RequestContext that had the correct RouteData for the mvcNode being examined.  This would help the Controller Factory look in the correct namespace for the desired controller avoiding the "Ambiguous Naming" issue.

But if you used the current httpContext and made the call to:

 

var routes = RouteTable.Routes.GetRouteData(httpContext);

 

and the current request was for "~/" then you will always get the RouteData reflecting the route that maps to "~/".

So to get around this I created a fake HttpContext that reflects the current mvcNode being examined.

 

var page = string.IsNullOrEmpty(mvcNode.Area)
                           ? string.Format("{0}/{1}/", mvcNode.Controller, mvcNode.Action)
                           : string.Format("{0}/{1}/{2}/", mvcNode.Area.ToLower(), mvcNode.Controller, mvcNode.Action);
var request = new SimpleWorkerRequest(page, null, new StringWriter());
var myContext = new HttpContext(request);

HttpContextBase myhttpContext = new HttpContextWrapper(myContext);
var routes = RouteTable.Routes.GetRouteData(myhttpContext);

 

Now you can create a RequestContext object that can be passed to the Controller Factory to help create a controller from a particular namespace.

 

HttpContextBase httpContext = new HttpContextWrapper(context);
RequestContext requestContext = null;

if (routes != null)
{
    requestContext = new RequestContext(httpContext, routes);
}

As I said before... this works.  But there might be a better way.  Hope this helps someone.

 

Apr 24, 2010 at 1:02 PM

When I try, then come this message

Line 49:   </runtime>
Line 50:
Line 51: <siteMap defaultProvider="MvcSiteMap">
Line 52: <providers>
Line 53: <add

What can the error be? thanks.

Apr 26, 2010 at 2:56 PM

FYI: This is now fixed in the latest source code download.