I should have known something was up when I caught Jeffrey Palermo (my boss at Headspring) browsing the ASP.NET MVC Preview 5 source.
We're working on a enterprise web application using ASP.NET MVC. That week one of our tasks was to create a rather complex page full of robust components that did intricate little things like field-level security. We wanted the ability to push behavior decisions to smaller and smaller pieces, and reuse those pieces on other pages. We needed testability, dependency injection and minimal friction. We needed composition.
Since I have been (hardly) working on components of CodeCampServer and hanging out with others interested in MVC I had heard about these SubController things. "We need SubControllers!" and "When they finally put in SubControllers we can do X!" "RenderAction is troublesome because of Y and partials are limited for reasons of Z!"
Admittedly I had no idea what they were talking about, but I sort of fell in line and waited for something cool to come out of Redmond.
Still, since he's literally writing the book on ASP.NET MVC, I shouldn't have been surprised when after a few hours of keyboard clicking Jeffrey looked up and said, "Hey, I just committed SubControllers."
Then a few days later he wrote a sample application and sent some info to the MvcContrib mailing list. He's been heads-down on our project and I've seen a few questions online about how to create reusable components in ASP.NET MVC, so I asked Jeffrey if I could do a write-up here. To be clear, I didn't invent, write, or even imagine SubControllers.. it's all Jeffrey… but I have been using them with success for a few weeks.
You may want to check out the sample before I dive into the details.
Here's a few screenshots of the sample in action. The rendered page (each component there is rendered by an isolated SubController's action) :
A nested SubController:
So let's take each of these in turn.
First, a look at the ISubController interface:
public interface ISubController : IController { Action GetResult(ControllerBase parentController); }
Pretty straight forward. It's a method named GetResult that takes a ControllerBase parameter and returns an Action.
Here is the SubController class' implementation of that method:
public virtual Action GetResult(ControllerBase parentController) { RequestContext requestContext = GetNewRequestContextFromController(parentController); return () => Execute(requestContext); }
Ok. Now we are getting somewhere. This System.Action is a delegate with no arguments and a return type of void. What it's actually doing is encapsulating a regular Controller's Execute method. Which pops off the controller action method: stuffs ViewData and renders the View and all that. The parameterless delegate Action is new in .NET 3.5 (though Action<T> was introduced in .NET 2.0).
Anyway, that's the real meaty bit of SubControllers: providing a delegate that can be put into ViewData.
So the next thing that has to happen is the delegate has to be created to represent the SubController's action method (that returns an ActionResult). This is where we start using the convention of naming the SubController's action method in an obvious way. We'll come back to this convention.
Let's take a look at the rest of the SubController class:
public RequestContext GetNewRequestContextFromController(ControllerBase parentController) { RouteData parentRouteData = parentController.ControllerContext.RouteData; var routeData = new RouteData(parentRouteData.Route, parentRouteData.RouteHandler); string controllerName = GetControllerName(); routeData.Values["controller"] = controllerName; routeData.Values["action"] = controllerName; return new RequestContext(parentController.ControllerContext.HttpContext, routeData); } public string GetControllerName() { return GetType().Name.ToLowerInvariant().Replace("subcontroller", "").Replace("controller", ""); }
The important thing to note is that the GetControllerName() method returns the part of the controller name minus the word "controller" and "subcontroller". So if your SubController is named ProductDetailSubController that method would return "ProductDetail".
The other important thing to note is that in the GetNewRequestContextFromController(…) method the action key of the routeData is set to this same value. In our example, "ProductDetail" is the action.
When the SubController's delegate is eventually invoked it will execute a method on the SubController with the name "ProductDetail".
Therefore the convention is to use an action method with the same name as the SubController (minus the text "subcontroller" or "controller").
So far we see that SubControllers give us a delegate (System.Action) that represents an invocation of its method with the conventional name.
Let's take a quick detour to see what a SubController might look like:
public class FirstLevelSubController : SubController { public ViewResult FirstLevel() { ViewData["text"] = "I am a first level controller"; return View(); } }
This is straight out of Jeffrey's example. Looks just like a regular controller except it derives from SubController and has one action method with the conventional name. FirstLevelSubController's GetResult() will return a delegate that represents the FirstLevel() method.
By the way, you test SubControllers exactly the same way as you would regular controllers: execute the action method and assert that the ActionResult is in the proper state with the correct ViewData and ViewName.
Now that we have the System.Action delegate we need to get it into ViewData. This is how I started out doing it:
public class HomeController : Controller { public ActionResult Index(FirstLevelSubController firstLevel) { ViewData["firstLevel"] = firstLevel.GetResult(this); return View(); } }
… and so on, and so on, for each SubController in the parameter list. This works fine. The System.Action gets put in ViewData and we can invoke it from the view just like we expect. Testing it is straightforward as well, just mock the SubController and expect that its action gets put in ViewData:
[Test] public void Index_puts_subcontroller_action_into_viewdata() { var controller = new HomeController(); var firstLevelSubController = MockRepository.GenerateStub<FirstLevelSubController>(); Action stubAction = () => { }; firstLevelSubController.Stub(sub => sub.GetResult(controller)).Return(stubAction); var result = controller.Index(firstLevelSubController) as ViewResult; Assert.That(result.ViewData.Get<Action>("firstLevel"), Is.EqualTo(stubAction)); }
Yeah, straight forward as in straight forward but not as in easy or concise (especially when it comes to actions that depend on many SubControllers - imagine 5 or 7 or 10 stubs in there..)..
Enter the SubControllerActionToViewDataAttribute! This ActionFilterAttribute cycles through the host action's parameters (filterContext.ActionParameters - a neat trick) and puts each SubController parameter's System.Action into ViewData:
public class SubControllerActionToViewDataAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { foreach (var pair in filterContext.ActionParameters) { object value = pair.Value; if(value == null) { continue; } if (typeof(ISubController).IsAssignableFrom(value.GetType())) { var controller = (ISubController) value; filterContext.Controller.ViewData.Add(pair.Key, controller.GetResult(filterContext.Controller)); } } base.OnActionExecuting(filterContext); } }
We have added a second convention: the SubController's parameter variable name (pair.Key above) is the key of the respective System.Action in ViewData.
The concern now becomes testing that the attribute works and that the your controllers have the attribute, not testing that individual SubController's System.Actions are put in ViewData. These tests are in MvcContrib. This frees developers from the weight of having to write cumbersome tests.
In fact, the SubController base class in MvcContrib has this attribute allowing for infinite nesting.
Once we pop that System.Action delegate into ViewData we can invoke it straight from the host action's view:
<% ViewData.Get<Action>("firstLevel").Invoke(); %>
The strong-typed Get<T>() method is another feature built into MvcContrib.
Finally, we definitely need rich dependency injection support in these SubControllers. MVC Preview 5 introduced the IModelBinder interface that is responsible for constructing action parameters. We can add them in Global.asax or wherever the controllers are constructed:
protected void Application_Start() { RegisterRoutes(RouteTable.Routes); ModelBinders.DefaultBinder = new SubControllerBinder(); }
Here is the basic implementation of SubControllerBinder in MvcContrib:
public class SubControllerBinder : DefaultModelBinder { protected override object ConvertType(CultureInfo culture, object value, Type destinationType) { if (typeof (ISubController).IsAssignableFrom(destinationType)) { object instance = CreateSubController(destinationType); if (instance == null) { throw new InvalidOperationException(destinationType + " not created properly."); } return instance; } return base.ConvertType(culture, value, destinationType); } ///<summary> /// Creates the subcontroller given its type. Override this method to wire into an IoC container ///</summary> ///<param name="destinationType">The type of subcontroller</param> ///<returns>an object instance</returns> public virtual object CreateSubController(Type destinationType) { return Activator.CreateInstance(destinationType, true); } }
And you can wire this up to a container with ease:
public class StructureMapSubControllerBinder : SubControllerBinder { public override object CreateSubController(Type destinationType) { object instance = ObjectFactory.GetInstance(destinationType); if (instance == null) { throw new InvalidOperationException(destinationType + " not registered with StructureMap"); } return instance; } }
So there you have it. You can download and explore a complete working sample from MvcContrib. If you have a suggestion for improvement MvcContrib is accepting patches. I have a few more posts in the queue about some intermediate usages and testing.
10 Responses
Dew Drop - September 19, 2008 | Alvin Ashcraft's Morning Dew
19|Sep|2008 1[...] SubControllers in ASP.NET MVC (Matt Hinze) [...]
MvcContrib - now with SubController support for ASP.NET MVC : Jeffrey Palermo (.com)
19|Sep|2008 2[...] Hinze has recently done a very nice write-up on his experience using MvcContrib SubControllers. I also recommend subscribing to his [...]
Fredrik Kalseth
19|Sep|2008 3Awesome stuff guys
Passing objects to SubControllers
22|Sep|2008 4[...] SubControllers are MVC Controllers that are also parameters to your action methods. Incorporating their use in large systems allows for composition, dependency inversion, and separation of concerns. [...]
Ben Scheirman
23|Sep|2008 5Thanks for the detailed write-up! I "get" it now.
Sergio Pereira
24|Sep|2008 6I think this was a very clever solution. It also shows that the model binder is being hacked like crazy (or "trick" as you called it
) to do things it was not meant to do (judging by its name.)
Would it work if the subcontrollers were constructor-injected instead of actionParameter-injected ? Similar conventions could be set to populate the viewdata from controller properties.
MvcContrib latest release now with SubController support : Jeffrey Palermo (.com)
05|Oct|2008 7[...] You can follow MvcContrib on twitter. See what Matt Hinze has said about his experience with SubControllers. [...]
Steve Sanderson’s blog » Blog Archive » Partial Requests in ASP.NET MVC
14|Oct|2008 8[...] Option 2 is conceptually much simpler and enables simpler code, though at runtime there are more moving parts. It's more like having a collection of genuinely independent widgets. This is what you get with <%= Html.RenderAction(…) %> (which is sadly relegated to the MVC Futures assembly and has been left with some technical problems), and also with MvcContrib's new idea of subcontrollers. [...]
RenderAction and SubControllers in ASP.NET MVC
01|Nov|2008 9[...] like RenderAction or Components. Matt Hinze presented this feature from the MvcContrib project in a blog post. SubController actions can be included in the view data as delegates. The views can then invoke [...]
Asp.net MVC : SubControllers | The .Net frog
18|Nov|2008 10[...] Grâce à MVC Contrib, un librairie open source couvrant certains aspects absents d'Asp.net MVC, il est possible d'utiliser des "SubControllers" afin de réutiliser des unités logiques au sein de plusieurs actions en minimisant la duplication de code. Matt Hinze, à l'origine du projet en explique le fonctionnement ici. [...]
Leave a reply
Search
Categories
Archives
Links
Recent Posts
Tags
.htaccess 443 Agile asp.net ASP.NET MVC aspnetmvc auditing business career certificate codecampserver continuous improvement culture ddd delicious dependency injection dry fluent interface headspring iis iis 6 linq log4net logging mvc nhibernate ninject refactoring resharper sed self selfssl seo sql ssl stats subcontrollers tabs tdd tempdata tortoisesvn treesurgeon visual studio whitespace wordpressA design creation of Design Disease
Copyright © 2008 - mhinze.com