Hierarchical Controllers in ASP.NET MVC

When I read that Jeffrey Palermo had added sub-controllers to ASP.NET MVC, I hoped that it would be the exact solution I was looking for in my app. Unfortunately, the sub-controllers in this context meant something different to what I understood when I first read the term.

What I wanted is perhaps better named “Hierarchical Controllers”. The common pattern with ASP.NET MVC is to have a controller for each of your entities. However, my domain has some entities that “own” other entities in such a manner that accessing those owned entities at the top level didn’t make sense.

Here’s the concrete example. I have an entity that represents a project. Anyone on that project may post a project news item. Instead of having urls like this:

http://mywebapp/Projects/ProjectX
http://mywebapp/News/ProjectX_NewsItemY

I wanted to route them like this:

http://mywebapp/Projects/ProjectX
http://mywebapp/Projects/ProjectX/News/NewsItemY

Not a problem, you might think, just define a new route for the news item display

Projects/{projectId}/{controller}/{newsId}

I initially started out with that, but it became apparent that it fit better with ASP.NET MVC’s convention-over-configuration paradigm to be able to just create a new controller with the name like {toplevelentity}_{subentity}Controller and have things wired up automatically for me. Here’s what I came up with:

First off, define my route:

routes.MapRoute(
	"RouteWithSubController",
	"{controller}/{topId}/{subController}/{action}/{subId}",
	new { topId = "", subController = "Home", action = "Index", subId = "" }
);

If subController is defined, we want to return a different type of controller than ASP.NET MVC will get by default, so we need a custom ControllerFactory:

public class MyControllerFactory : DefaultControllerFactory
{
	protected override Type GetControllerType(string controllerName)
	{
		object subControllerName;
		if (RequestContext != null
			&& RequestContext.RouteData.Values.TryGetValue("subController", out subControllerName))
		{
			return base.GetControllerType(String.Format("{0}_{1}", controllerName, (string)subControllerName));
		}

		return base.GetControllerType(controllerName);
	}
}

Don’t forget to register this in Global.asax:

protected void Application_Start()
{
	RegisterRoutes(RouteTable.Routes);
	ControllerBuilder.Current.SetControllerFactory(new MyControllerFactory());
}

This will call the correct controller if you follow the new convention. However, it messes up when identifying the view.

I opted for the convention here of placing the views in the same folder as those for the top-level controller and naming them {subController}_{action} rather than just {action}. To get this working, we need to subclass the default WebFormViewEngine:

public class MyWebFormViewEngine : WebFormViewEngine
{
	public override ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName)
	{
		object subControllerName;
		if (controllerContext != null
			&& controllerContext.RouteData.Values.TryGetValue("subController", out subControllerName))
		{
			return base.FindView(
				controllerContext,
				String.Format("{0}_{1}", (string)subControllerName, viewName),
				masterName);
		}
		return base.FindView(controllerContext, viewName, masterName);
	}
}

And, again, register it in Global.asax:

protected void Application_Start()
{
	RegisterRoutes(RouteTable.Routes);
	ControllerBuilder.Current.SetControllerFactory(new MyControllerFactory());
	ViewEngines.Engines.Clear();
	ViewEngines.Engines.Add(new MyWebFormViewEngine());
}

One Response to “Hierarchical Controllers in ASP.NET MVC”

  1. Sub-Controllers, PartialRequests and Separating Views From Controllers « Giraffe: Developer Says:

    [...] PartialRequests and Separating Views From Controllers Earlier, I blogged about an alternative to sub-controllers. That post wasn’t about sub-controllers being rubbish, rather that the problem I was trying [...]

Leave a Reply