Captain Codeman Captain Codeman

RenderSubAction alternative to RenderAction for Sub-Controllers in MVC

Contents

Introduction

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.

<span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">class</span> HtmlHelperExtensions {
    <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> RenderSubAction<TController>(<span class="kwrd">this</span> HtmlHelper helper, <br></br>            Expression<Action<TController>> action)
        <span class="kwrd">where</span> TController : Controller {
        RouteValueDictionary routeValuesFromExpression = ExpressionHelper





            .GetRouteValuesFromExpression(action);
        helper.RenderRoute(routeValuesFromExpression);
    }

    <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> RenderSubAction(<span class="kwrd">this</span> HtmlHelper helper, <span class="kwrd">string</span> actionName) {
        helper.RenderSubAction(actionName, <span class="kwrd">null</span>);
    }

    <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> RenderSubAction(<span class="kwrd">this</span> HtmlHelper helper, <span class="kwrd">string</span> actionName, <span class="kwrd">string</span> controllerName) {
        helper.RenderSubAction(actionName, controllerName, <span class="kwrd">null</span>);
    }

    <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> RenderSubAction(<span class="kwrd">this</span> HtmlHelper helper, <span class="kwrd">string</span> actionName, <span class="kwrd">string</span> controllerName, <br></br>            <span class="kwrd">object</span> routeValues) {
        helper.RenderSubAction(actionName, controllerName, <span class="kwrd">new</span> RouteValueDictionary(routeValues));
    }

    <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> RenderSubAction(<span class="kwrd">this</span> HtmlHelper helper, <span class="kwrd">string</span> actionName, <span class="kwrd">string</span> controllerName,
                                    RouteValueDictionary routeValues) {
        RouteValueDictionary dictionary = routeValues != <span class="kwrd">null</span> ? <span class="kwrd">new</span> RouteValueDictionary(routeValues) <br></br>            : <span class="kwrd">new</span> RouteValueDictionary();
        <span class="kwrd">foreach</span> (var pair <span class="kwrd">in</span> helper.ViewContext.RouteData.Values) {
            <span class="kwrd">if</span> (!dictionary.ContainsKey(pair.Key)) {
                dictionary.Add(pair.Key, pair.Value);
            }
        }
        <span class="kwrd">if</span> (!<span class="kwrd">string</span>.IsNullOrEmpty(actionName)) {
            dictionary[<span class="str">"action"</span>] = actionName;
        }
        <span class="kwrd">if</span> (!<span class="kwrd">string</span>.IsNullOrEmpty(controllerName)) {
            dictionary[<span class="str">"controller"</span>] = controllerName;
        }
        helper.RenderRoute(dictionary);
    }

    <span class="kwrd">public</span> <span class="kwrd">static</span> <span class="kwrd">void</span> RenderRoute(<span class="kwrd">this</span> HtmlHelper helper, RouteValueDictionary routeValues) {
        var routeData = <span class="kwrd">new</span> RouteData();
        <span class="kwrd">foreach</span> (var pair <span class="kwrd">in</span> routeValues) {
            routeData.Values.Add(pair.Key, pair.Value);
        }
        HttpContextBase httpContext = <span class="kwrd">new</span> OverrideRequestHttpContextWrapper(HttpContext.Current);
        var context = <span class="kwrd">new</span> RequestContext(httpContext, routeData);
        <span class="kwrd">bool</span> validateRequest = helper.ViewContext.Controller.ValidateRequest;
        <span class="kwrd">new</span> RenderSubActionMvcHandler(context, validateRequest).ProcessRequestInternal(httpContext);
    }

    <span class="preproc">#region</span> Nested type: RenderSubActionMvcHandler

    <span class="kwrd">private</span> <span class="kwrd">class</span> RenderSubActionMvcHandler : MvcHandler {
        <span class="kwrd">private</span> <span class="kwrd">bool</span> _validateRequest;
        <span class="kwrd">public</span> RenderSubActionMvcHandler(RequestContext context, <span class="kwrd">bool</span> validateRequest) : <span class="kwrd">base</span>(context) {
            _validateRequest = validateRequest;
        }

        <span class="kwrd">protected</span> <span class="kwrd">override</span> <span class="kwrd">void</span> AddVersionHeader(HttpContextBase httpContext) {}

        <span class="kwrd">public</span> <span class="kwrd">void</span> ProcessRequestInternal(HttpContextBase httpContext) {
            AddVersionHeader(httpContext);
            <span class="kwrd">string</span> requiredString = RequestContext.RouteData.GetRequiredString(<span class="str">"controller"</span>);
            IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
            IController controller = controllerFactory.CreateController(RequestContext, requiredString);
            <span class="kwrd">if</span> (controller == <span class="kwrd">null</span>)
            {
                <span class="kwrd">throw</span> <span class="kwrd">new</span> InvalidOperationException(<span class="kwrd">string</span>.Format(CultureInfo.CurrentUICulture, <br></br>                    <span class="str">"The IControllerFactory '{0}' did not return a controller for a controller named '{1}'."</span>, <br></br>                    <span class="kwrd">new</span> <span class="kwrd">object</span>[] { controllerFactory.GetType(), requiredString }));
            }
            <span class="kwrd">try</span>
            {
                ((ControllerBase) controller).ValidateRequest = _validateRequest;
                controller.Execute(RequestContext);
            }
            <span class="kwrd">finally</span>
            {
                controllerFactory.ReleaseController(controller);
            }
        }
    }

    <span class="kwrd">private</span> <span class="kwrd">class</span> OverrideHttpMethodHttpRequestWrapper : HttpRequestWrapper {
        <span class="kwrd">public</span> OverrideHttpMethodHttpRequestWrapper(HttpRequest httpRequest) : <span class="kwrd">base</span>(httpRequest) { }

        <span class="kwrd">public</span> <span class="kwrd">override</span> <span class="kwrd">string</span> HttpMethod {
            get { <span class="kwrd">return</span> <span class="str">"GET"</span>; }
        }
    }

    <span class="kwrd">private</span> <span class="kwrd">class</span> OverrideRequestHttpContextWrapper : HttpContextWrapper {
        <span class="kwrd">private</span> <span class="kwrd">readonly</span> HttpContext _httpContext;
        <span class="kwrd">public</span> OverrideRequestHttpContextWrapper(HttpContext httpContext) : <span class="kwrd">base</span>(httpContext) {
            _httpContext = httpContext;
        }

        <span class="kwrd">public</span> <span class="kwrd">override</span> HttpRequestBase Request {
            get { <span class="kwrd">return</span> <span class="kwrd">new</span> OverrideHttpMethodHttpRequestWrapper(_httpContext.Request); }
        }
    }

    <span class="preproc">#endregion</span>
}

.csharpcode, .csharpcode pre { font-size: small; color: black; font-family: consolas, “Courier New”, courier, monospace; background-color: #ffffff; /white-space: pre;/ } .csharpcode pre { margin: 0em; } .csharpcode .rem { color: #008000; } .csharpcode .kwrd { color: #0000ff; } .csharpcode .str { color: #006080; } .csharpcode .op { color: #0000c0; } .csharpcode .preproc { color: #cc6633; } .csharpcode .asp { background-color: #ffff00; } .csharpcode .html { color: #800000; } .csharpcode .attr { color: #ff0000; } .csharpcode .alt { background-color: #f4f4f4; width: 100%; margin: 0em; } .csharpcode .lnum { color: #606060; }