views:

38

answers:

2

I have a bit of a problem. I have an area called Framed. This area has a home controller. The default for the site also has a home controller.

What I'm trying to do with this is have a version of each controller/action that is suitable for an IFrame, and a version that is the normal site. I do this through Master pages, and the site masterpage has many different content place holders than the framed version. For this reason I can't just swap the master page in and out. For example, http://example.com/Framed/Account/Index will show a very basic version with just your account info for use in an external site. http://example.com/Account/Index will show the same data, but inside the default site.

My IoC container is structuremap. So, I found http://odetocode.com/Blogs/scott/archive/2009/10/19/mvc-2-areas-and-containers.aspx and http://odetocode.com/Blogs/scott/archive/2009/10/13/asp-net-mvc2-preview-2-areas-and-routes.aspx. Here's my current setup.

Structuremap Init

ObjectFactory.Initialize(x =>
            {
                x.AddRegistry(new ApplicationRegistry());
                x.Scan(s =>
                {
                    s.AssembliesFromPath(HttpRuntime.BinDirectory);
                    s.AddAllTypesOf<IController>()
                        .NameBy(type => type.Namespace + "." + type.Name.Replace("Controller", ""));
                });
            });

The problem here that I found through debugging is that because the controllers have the same name (HomeController), it only registers the first one, which is the default home controller. I got creative and appended the namespace so that it would register all of my controllers.

Default Route

routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { area = "", controller = "Home", action = "Index", id = UrlParameter.Optional }, // Parameter defaults
                new[] { "MySite.Controllers" }
                );

Area route

context.MapRoute(
                "Framed_default",
                "Framed/{controller}/{action}/{id}",
                new { area = "Framed", controller = "Home", action = "Index", id = UrlParameter.Optional },
                new string[] { "MySite.Areas.Framed.Controllers" }
            );

As recommended by Phil Haack, I am using the namespaces as the 4th parameter

app start, just to prove the order of initialization

protected void Application_Start()
        {
            InitializeControllerFactory();

            AreaRegistration.RegisterAllAreas();

            RouteConfiguration.RegisterRoutes();
        }

Controller Factory

protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {
            IController result = null;
            if (controllerType != null)
            {
                result = ObjectFactory.GetInstance(controllerType)
                    as IController;
            }
            return result;
        }

So, when I hit /Home/Index, it passes in the correct controller type. When I hit /Framed/Home/Index, controllerType is null, which errors because no controller is returned.

It's as if MVC is ignoring my area altogether. What's going on here? What am I doing wrong?

A: 

In case anyone tries to do something similar, I used the idea from this post: http://stackoverflow.com/questions/43201/categories-of-controllers-in-mvc-routing-duplicate-controller-names-in-separat I had to dump using areas altogether and implement something myself.

I have Controllers/HomeController.cs and Controllers/Framed/HomeController.cs

I have a class ControllerBase which all controllers in /Controllers inherit from. I have AreaController which inherits from ControllerBase which all controllers in /Controllers/Framed extend from.

Here's my Area Controller class

public class AreaController : ControllerBase
    {
        private string Area
        {
            get
            {
                return this.GetType().Namespace.Replace("MySite.Controllers.", "");
            }
        }
        protected override ViewResult View(string viewName, string masterName, object model)
        {
            string controller = this.ControllerContext.RequestContext.RouteData.Values["controller"].ToString();

            if (String.IsNullOrEmpty(viewName))
                viewName = this.ControllerContext.RequestContext.RouteData.Values["action"].ToString();

            return base.View(String.Format("~/Views/{0}/{1}/{2}.aspx", Area, controller, viewName), masterName, model);
        }

        protected override PartialViewResult PartialView(string viewName, object model)
        {
            string controller = this.ControllerContext.RequestContext.RouteData.Values["controller"].ToString();

            if (String.IsNullOrEmpty(viewName))
                viewName = this.ControllerContext.RequestContext.RouteData.Values["action"].ToString();

            PartialViewResult result = null;

            result = base.PartialView(String.Format("~/Views/{0}/{1}/{2}.aspx", Area, controller, viewName), model);

            if (result != null)
                return result;

            result = base.PartialView(String.Format("~/Views/{0}/{1}/{2}.ascx", Area, controller, viewName), model);

            if (result != null)
                return result;

            result = base.PartialView(viewName, model);

            return result;
        }
    }

I had to override the view and partialview methods. This way, the controllers in my "area" can use the default methods for views and partials and support the added folder structures.

As for the Views, I have Views/Home/Index.aspx and Views/Framed/Home/Index.aspx. I use the routing as shown in the post, but here's how mine looks for reference:

var testNamespace = new RouteValueDictionary();
            testNamespace.Add("namespaces", new HashSet<string>(new string[] 
            { 
                "MySite.Controllers.Framed"
            }));

            //for some reason we need to delare the empty version to support /framed when it does not have a controller or action
            routes.Add("FramedEmpty", new Route("Framed", new MvcRouteHandler())
            {
                Defaults = new RouteValueDictionary(new
                {
                    controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional
                }),
                DataTokens = testNamespace
            });

            routes.Add("FramedDefault", new Route("Framed/{controller}/{action}/{id}", new MvcRouteHandler())
            {
                Defaults = new RouteValueDictionary(new
                {
                    //controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional
                }),
                DataTokens = testNamespace
            });

var defaultNamespace = new RouteValueDictionary();
            defaultNamespace.Add("namespaces", new HashSet<string>(new string[] 
            { 
                "MySite.Controllers"
            }));

routes.Add("Default", new Route("{controller}/{action}/{id}", new MvcRouteHandler())
                {
                    Defaults = new RouteValueDictionary(new
                    {
                        controller = "Home",
                        action = "Index",
                        id = UrlParameter.Optional
                    }),
                    DataTokens = defaultNamespace
                });

Now I can go /Home/Index or /Framed/Home/Index on the same site and get two different views with a shared control. Ideally I'd like one controller to return one of 2 views, but I have no idea how to make that work without 2 controllers.

Josh
A: 

I had a similar issue using Structuremap with Areas. I had an Area named Admin and whenever you tried to go to /admin it would get to the StructureMap Controller Factory with a null controller type.

I fixed it by following this blog post: http://stephenwalther.com/blog/archive/2008/08/07/asp-net-mvc-tip-30-create-custom-route-constraints.aspx

Had to add a constraint on the default route to not match if the controller was admin.

Here's my default route definition:

routes.MapRoute(
    "Default",
    "{controller}/{action}/{id}",
    new { controller = "MyController", action = "AnAction", id = UrlParameter.Optional },
    new { controller = new NotEqualConstraint("Admin")},
    new string[] {"DailyDealsHQ.WebUI.Controllers"} 
);

and here's the implementation of the NotEqualConstraint:

public class NotEqualConstraint : IRouteConstraint
{
    private string match = String.Empty;

    public NotEqualConstraint(string match)
    {
        this.match = match;
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        return String.Compare(values[parameterName].ToString(), match, true) != 0;
    }
}

There's probably other ways to solve this problem, but this fixed it for me :)

Chris