My Resume

  • My Resume (MS Word) My Resume (PDF)


Affiliations

  • Microsoft Most Valuable Professional
  • INETA Community Champion
  • Leader, NJDOTNET: Central New Jersey .NET User Group

Tuesday, July 1, 2008

Use ASP.NET MVC ActionFilters to Render AJAX Responses!

In my previous blog post on ASP.NET MVC, I walked through an example based on the MVC Preview 2 bits which showed how to make your own custom view engines and ActionFilters.  I also proved (or at least hope I proved) just how powerful these parts of the framework can be, especially when used in conjunction with each other.  The truth is that - while I thought they made great examples on how to extend the base parts of the framework - the whole thing seemed kind of kludgie to me.  What I mean is that it felt kind of awkward having to use the ActionFilter to change which ViewEngine is being used on the fly.  I don't know, maybe I just feel like view engines should be more closely tied to the controller and shouldn't just be chosen willy-nilly like that!

This was all before the introduction of the ActionResult in MVC Preview 3.  Oh man, was this a great addition!  Expanding the notion of being "loosely coupled", the controller action's job is no longer to execute logic and chose which view to display-- ok, well, it is...  only now its main responsibility is returning an ActionResult, which is basically a command object that tells the framework what to do instead the controller taking that action itself.  This not only helps us to decouple the controller (allowing for easier testing, etc.), it also lets us know what the controller wants the framework to do before it happens.  When you think of it, this is pretty powerful stuff, and gives us access to a whole new part of the process in which can inject additional logic.  And inject it we shall...

The SerializableViewDataAttribute Revisited

In my previous post, the attribute that I adorned my Actions with was pretty stupid.  It just said, "Is this an AJAX request? If so, set our custom view engine."  Pretty much all of the meaningful (and interesting) logic happened in our custom view engine.  Now, with the introduction of the ActionResult we no longer have to wait until the view engine phase to get access to (and override) the way our data is returned to the client - we can hijack it in the attribute, and manipulate the Action's result!  Before I confuse you any more, here's the code:

SerializableViewDataAttribute: Preview 3 style
public class SerializableViewDataAttribute : ActionFilterAttribute
{
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
// Get the (lowercase) render mode from the querystring
var method = filterContext.HttpContext.Request.QueryString["renderMode"] ?? string.Empty;
method = method.ToLowerInvariant();

// Get the view data from the context (using a custom extension method defined elsewhere)
var viewData = filterContext.GetViewData();

// If this isn't a serializable request or it has no data, just return
if (string.IsNullOrEmpty(method) || viewData == null) return;

// Decide which method to use
switch (method)
{
case ("xml"):
filterContext.Result = getXmlResult(viewData);
break;
case ("json"):
filterContext.Result = getJsonResult(viewData);
break;
case ("partial"):
// If this is supposed to be a partial request, just change the view name.
// Obviously, this implies that this view exists (in addition to your
// original view that the controller was pointing to to begin with.
var viewResult = ((ViewResult)filterContext.Result);
viewResult.ViewName = "Partial" + viewResult.ViewName;
break;
}
}

private static ActionResult getJsonResult(object viewData)
{
// Since there's a JsonResult Action,
// all we have to do is pass in the data
return new JsonResult { Data = viewData };
}

private static ActionResult getXmlResult(object viewData)
{
// There's no "XmlResult" so we have to serialize it ourselves
var sb = new StringBuilder();
using (var writer = new StringWriter(sb))
{
new XmlSerializer(viewData.GetType()).Serialize(writer, viewData);
}

// ...then pass it into a ContentResult
return new ContentResult
{
ContentType = "text/xml", // Be sure to set the ContentType!
Content = sb.ToString()
};
}

}


As you can see, it is a lot like the logic that was used to create my custom view engine in the last post.  In fact, I essentially copied and pasted it (and added support for partial rendering)!  Also, now that all of this logic lives in the ActionFilter we have no more need for that pesky custom view engine.  So long, cruft!


For the benefit of those who may not have read the explanation of this logic in my previous blog post, I'll go ahead and repeat (as well as expand on) it here.  This version should actually be a lot easier to understand since the logic is now contained in one place instead of spread out between the ActionFilter and the ViewEngine.  Here's a basic run-down of what's going on:


  1. The first thing to note is that we're overriding the OnActionExecuted method of the ActionFilter.  What this implies is that the controller action to which this attribute is applied has already completed, so we now have access to everything it's done: ViewData, TempData, ViewName, etc.  We could also have overridden the OnResultExecuting method as well; this would be somewhat similar to the difference between the PreRender and Render events when using Web Forms.
  2. Once the "event" has been triggered, the first thing we want to do is figure out what, exactly, is expected of us - AKA, which serialization mode (if any) we should be using.  In this implementation, I have decided to glean this information from the querystring, but you can just as easily use any other environment variable you'd like (such as the Route Data, cookies... whatever!).
  3. Next, I get the ViewData from the context, if any, using the filterContext.GetViewData() extension method.  Though this method is defined elsewhere and you can't see it in this snippet, it's not really all that interesting - it's just a convenience method that drills down into filterContext.Controller.ViewData (or ViewData.Model, if available) and returns it.
  4. Check the render method that we got in Step #2 and if that or the ViewData is invalid, there's nothing for us to do, so we're done and we just return.
  5. Assuming we didn't bail out in Step #4, we then go on to overwrite the existing ActionResult (or its properties), depending on what the user has asked for.  This particular version supports:
    1. JSON:  Along with the introduction of the ActionResult came the JsonResult, which is a super-cool implementation of ActionResult that serializes whatever data you give to it into JSON to be rendered down to the browser.  As you can see in this example, this makes handling requests for JSON data really simple and easy.
    2. XML:  Though there is no "XmlResult" as there is for JSON, the MVC framework still provides a ContentResult which is a lot like the JsonResult in spirit.  Both of them allow us to send data in some form back to the browser without requiring a View to render it with.  Going back to our example - you can see that it takes a bit of work, but the end result is a raw batch of just XML sent back to the client for processing.
    3. Partial Rendering:  I included this render mode as a bit of a teaser - I should have a follow-up post going in-depth about using partial rendering in ASP.NET MVC.  For now, I'll just say that you can think of it a lot like the ASP.NET AJAX UpdatePanel server control...

  6. That's it!  If we've gotten to this step and not hit any of the three types above, this particular filter will just let the request continue on and have no effects what-so-ever.  That probably means it'll just end up rendering out a fully-rendered page like any other normal (non-AJAX) request.  The important part to note here is that this filter only injects behavior when it is appropriate.  Otherwise, things proceed as normal and noone even needs to know that these actions are AJAX-ified unless you want them to.


I don't know about you, but the power that this kind of stuff gives us just makes my head spin!  Hopefully you find the above useful; if you do, please feel free to contact me and let me know, especially if you end up using it in one of your apps!



As always, you can check out all of the source code in my repository: http://code.jesschadwick.com/


This particular piece of code can most easily be found at this direct link.