Simon Green's Developer Blog
Developing .NET in the cold white north ...

RenderSubAction alternative to RenderAction for Sub-Controllers in MVC

Saturday, 21 February 2009 10:08 by simon

The ASP.NET MVC Futures assembly contains several RenderAction extension methods for HtmlHelper to allow another action to be rendered at some point within a view. Typically, this allows each controller to handle different responsibilities rather than things being combined into the parent.

So, for example, a PersonController is responsible for retrieving and assembling the model to represent a Person and pass it to the View for rendering but it should not handle Contacts – the display and CRUD operations on contacts should be handled by a ContactController and RenderAction is a convenient way to insert a list of contacts for a person into the persion display view.

So, we have a PersonController which will retrieve a Person model and pass it to the Display view. Inside this Display view, we have a call to render a list of contacts for that person:

<% Html.RenderSubAction("List", "Contact", new { personId = Model.Id }); %>

I’ve come across two problems when using this though:

1. If the parent controller action requested uses the HTTP POST method then the controller action picked up for all child actions will also be the POST version (if there is one). This is rarely the desired behavior though – I’d only expect to be sending a POST to the ContactController when I want to change something related to a contact and not when updating a person.

2. If the [ValidateInput(false)] attribute is used to allow HTML code to be posted (imagine a ‘Biography’ field on Person with a nice WYSIWYG TinyMCE Editor control …) then the request will fail unless all the child actions are automatically marked with the same attribute. I would prefer to only have to mark the methods I specifically want a POST request containing HTML input to be called.

So, I created a set of alternative RenderSubAction extension methods which address both these issues:

1. Whatever the HTTP method used for the parent action, the routing will match the GET version for child actions called.

2. The state of the [ValidateInput()] attribute will be set on all child actions called.

The code is below … just reference the namespace that you put it in within your web.config file and then change the RenderAction method to RenderSubAction – the method signatures are identical so it is a drop-in replacement.

I’d be interested in any feedback on this approach.

public static class HtmlHelperExtensions {
    public static void RenderSubAction<TController>(this HtmlHelper helper, 
Expression<Action<TController>> action) where TController : Controller { RouteValueDictionary routeValuesFromExpression = ExpressionHelper
            .GetRouteValuesFromExpression(action);
        helper.RenderRoute(routeValuesFromExpression);
    }

    public static void RenderSubAction(this HtmlHelper helper, string actionName) {
        helper.RenderSubAction(actionName, null);
    }

    public static void RenderSubAction(this HtmlHelper helper, string actionName, string controllerName) {
        helper.RenderSubAction(actionName, controllerName, null);
    }

    public static void RenderSubAction(this HtmlHelper helper, string actionName, string controllerName, 
object routeValues) { helper.RenderSubAction(actionName, controllerName, new RouteValueDictionary(routeValues)); } public static void RenderSubAction(this HtmlHelper helper, string actionName, string controllerName, RouteValueDictionary routeValues) { RouteValueDictionary dictionary = routeValues != null ? new RouteValueDictionary(routeValues)
: new RouteValueDictionary(); foreach (var pair in helper.ViewContext.RouteData.Values) { if (!dictionary.ContainsKey(pair.Key)) { dictionary.Add(pair.Key, pair.Value); } } if (!string.IsNullOrEmpty(actionName)) { dictionary["action"] = actionName; } if (!string.IsNullOrEmpty(controllerName)) { dictionary["controller"] = controllerName; } helper.RenderRoute(dictionary); } public static void RenderRoute(this HtmlHelper helper, RouteValueDictionary routeValues) { var routeData = new RouteData(); foreach (var pair in routeValues) { routeData.Values.Add(pair.Key, pair.Value); } HttpContextBase httpContext = new OverrideRequestHttpContextWrapper(HttpContext.Current); var context = new RequestContext(httpContext, routeData); bool validateRequest = helper.ViewContext.Controller.ValidateRequest; new RenderSubActionMvcHandler(context, validateRequest).ProcessRequestInternal(httpContext); } #region Nested type: RenderSubActionMvcHandler private class RenderSubActionMvcHandler : MvcHandler { private bool _validateRequest; public RenderSubActionMvcHandler(RequestContext context, bool validateRequest) : base(context) { _validateRequest = validateRequest; } protected override void AddVersionHeader(HttpContextBase httpContext) {} public void ProcessRequestInternal(HttpContextBase httpContext) { AddVersionHeader(httpContext); string requiredString = RequestContext.RouteData.GetRequiredString("controller"); IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory(); IController controller = controllerFactory.CreateController(RequestContext, requiredString); if (controller == null) { throw new InvalidOperationException(string.Format(CultureInfo.CurrentUICulture,
"The IControllerFactory '{0}' did not return a controller for a controller named '{1}'.",
new object[] { controllerFactory.GetType(), requiredString })); } try { ((ControllerBase) controller).ValidateRequest = _validateRequest; controller.Execute(RequestContext); } finally { controllerFactory.ReleaseController(controller); } } } private class OverrideHttpMethodHttpRequestWrapper : HttpRequestWrapper { public OverrideHttpMethodHttpRequestWrapper(HttpRequest httpRequest) : base(httpRequest) { } public override string HttpMethod { get { return "GET"; } } } private class OverrideRequestHttpContextWrapper : HttpContextWrapper { private readonly HttpContext _httpContext; public OverrideRequestHttpContextWrapper(HttpContext httpContext) : base(httpContext) { _httpContext = httpContext; } public override HttpRequestBase Request { get { return new OverrideHttpMethodHttpRequestWrapper(_httpContext.Request); } } } #endregion }

Ode to the MVC Framework

Sunday, 9 December 2007 13:36 by Simon

We've all been there ... thought we were going to get some code out of the door but then for one reason or another we couldn't manage it. I hope ScottGu et al who worked so hard to try and get it out this weekend don't take this the wrong way but here is a modified version of the lyrics to a song from the musical 'Les Miserables' for all of us who are sooo keen to get our hands on the new framework and waited all weekend for it ...

MVC framework (to 'Les Miserables - Empty Chairs At Empty Tables' music)

[A developer who got his hands on the MVC framework and blogged about it]

There's a grief that can't be spoken
There's a pain goes on and on
Empty pages, empty tables
Now my sites are dead and gone

Here they talked of view-controllers
Here it was they lit the flame
Here they sang about the framework
And the framework never came.

From a blog page in Seattle
They could see a world reborn
And they rose with voices ringing
I can hear them now!
The very code that they had posted
Became their last communion
On the lowly CTP...
At dawn.

Oh my friends, my friends forgive me.

[The ghosts of those who died waiting for the MVC framework appear.]

That I have code and you have none
There's a grief that can't be spoken
There's a pain goes on and on

Phantom frameworks run on windows
Mock assemblies on the floor
Databases with no tables
Where my view will run no more.

[The ghosts fade away.]

Oh my friends, my friends, don't ask me
What your sacrifice was for
Empty projects with no tables
Where my code will run no more...

Ah well, at least I have something to look forward to next week!!


Tags:   ,
Categories:   .NET | MVC
Actions:   E-mail | del.icio.us | Permalink | Comments (1) | Comment RSSRSS comment feed

MVC Framework for ASP.NET

Sunday, 9 December 2007 04:02 by Simon

The ASP.NET 3.5 Extensions Preview (which includes microsoft's new MVC framework) should be release today on ASP.NET. It was due to ship earlier in the week but due to a bug got delayed. However, the team behind it have been working over the weekend to get it out. I can't wait to get my hands on it.

I've used ASP.NET for development even though I know about the other approaches (Ruby on Rails, Castle etc...) simply because I make a living out of coding and most clients want the big corporate supplied platform. Going into a company and trying to sell Ruby on Rails can be quite difficult and there simply aren't the opportunities / jobs about to justify switching to it (although I can understand people using it on private projects and startups).

Some question whether Microsoft should be producing a copy or clone of what existing open source projects already do. Well, I'm glad they are doing it!

Love em or hate em, Microsoft make some great development tools and platforms / frameworks and using these is how I make a living. 

What I think the ASP.NET MVC framework will do is validate the approach (if it needed it) and also allow it to be used in corporate / enterprise environments - i.e. it will become mainstream and less time will have to be spent selling it to people who really don't know the difference between a MVC and an MCP but don't want to risk their position by allowing some developer or consultant to lead them down the road to a minority used platform.

ScottGu has made some posts about what the MVC framework is and getting started with it which are a great read:

ASP.NET MVC Framework
ASP.NET MVC Framework (Part 1)
ASP.NET MVC Framework (Part 2): URL Routing
ASP.NET MVC Framework (Part 3): Passing ViewData from Controllers to Views
ASP.NET MVC Framework (Part 4): Handling Form Edit and Post Scenarios

Also, some other blog posts provide more detailed information about various aspects:

TDD and Dependency Injection with ASP.NET MVC
ASP.NET MVC Preview: Using The MVC UI Helpers

I can't wait to use this and already have some apps planned to use it ...

Tags:   ,
Categories:   .NET | MVC
Actions:   E-mail | del.icio.us | Permalink | Comments (0) | Comment RSSRSS comment feed

NHibernate VistaDB Driver and Dialect

Friday, 7 December 2007 13:35 by Simon

Well, I finally got a bit of spare time and published the NHibernate driver and dialect for VistaDB as a CodePlex project.

As I mentioned in my previous blog entry, VistaDB is really useful even if you only use it during unit testing.

The really great thing about it though is that you can do normal database / NHibernate development and have a simple XCopy deployment which makes application demos very easy (complicated installs and configurations are often a barrier to evaluation and possible sales). Depending on your application and the usage that it gets VistaDB may or may not be up to the task of handling the persistence for the full deployment (it is a file-based database although a proper server version is on the way). With NHibernate though this isn't a worry as the customer is free to use any of the other supported database engines for the deployed version.

I intend to update a forum application I wrote a number of years ago to the new ASP.NET MVC about to be release and use the VistaDB database during development. The current SQL Server database is fairly large (about 4Gb) and has well over 2.5 million posts on it so it should give it a good test - I will let you know how I get on with it.

Tags:   , , ,
Categories:   .NET
Actions:   E-mail | del.icio.us | Permalink | Comments (1) | Comment RSSRSS comment feed