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());
}
January 26, 2009 at 11:34
[...] 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 [...]