ASP.NET MVC ‘Extendable-hooks’ – ControllerActionInvoker class
There’s a class ControllerActionInvoker in ASP.NET MVC. This can be used as one of an hook-points to allow customization of your application. Watching Brad Wilsons’ Advanced MP3 from MVC Conf inspired me to write about this class.
What MSDN says:
“Represents a class that is responsible for invoking the action methods of a controller.”
Well if MSDN says it, I think I can instill a fair amount of confidence into what the class does. But just to get to the details, I also looked into the source code for MVC.
Seems like the base class Controller is where an IActionInvoker is initialized:
1: protected virtual IActionInvoker CreateActionInvoker() {
2: return new ControllerActionInvoker();
3: }
In the ControllerActionInvoker (the O-O-B behavior), there are different ‘versions’ of InvokeActionMethod() method that actually call the action method in question and return an instance of type ActionResult.
1: protected virtual ActionResult InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters) {
2: object returnValue = actionDescriptor.Execute(controllerContext, parameters);
3: ActionResult result = CreateActionResult(controllerContext, actionDescriptor, returnValue);
4: return result;
5: }
I guess that’s enough on the ‘behind-the-screens’ of this class. Let’s see how we can use this class to hook-up extensions.
Say I have a requirement that the user should be able to get different renderings of the same output, like html, xml, json, csv and so on. The user will type-in the output format in the url and should the get result accordingly. For example:
http://site.com/RenderAs/ – renders the default way (the razor view)
http://site.com/RenderAs/xml
http://site.com/RenderAs/csv
… and so on where RenderAs is my controller.
There are many ways of doing this and I’m using a custom ControllerActionInvoker class (even though this might not be the best way to accomplish this).
For this, my one and only route in the Global.asax.cs is:
1: routes.MapRoute("RenderAsRoute", "RenderAs/{outputType}",
2: new {controller = "RenderAs", action = "Index", outputType = ""});
Here the controller name is ‘RenderAsController’ and the action that’ll get called (always) is the Index action. The outputType parameter will map to the type of output requested by the user (xml, csv…).
I intend to display a list of food items for this example.
1: public class Item
2: {
3: public int Id { get; set; }
4: public string Name { get; set; }
5: public Cuisine Cuisine { get; set; }
6: }
7:
8: public class Cuisine
9: {
10: public int CuisineId { get; set; }
11: public string Name { get; set; }
12: }
Coming to my ‘RenderAsController’ class. I generate an IList<Item> to represent my model.
1: private static IList<Item> GetItems()
2: {
3: Cuisine cuisine = new Cuisine { CuisineId = 1, Name = "Italian" };
4: Item item = new Item { Id = 1, Name = "Lasagna", Cuisine = cuisine };
5: IList<Item> items = new List<Item> { item };
6: item = new Item {Id = 2, Name = "Pasta", Cuisine = cuisine};
7: items.Add(item);
8: //...
9: return items;
10: }
My action method looks like
1: public IList<Item> Index(string outputType)
2: {
3: return GetItems();
4: }
There are two things that stand out in this action method. The first and the most obvious one being that the return type is not of type ActionResult (or one of its derivatives). Instead I’m passing the type of the model itself (IList<Item> in this case). We’ll convert this to some type of an ActionResult in our custom controller action invoker class later.
The second thing (a little subtle) is that I’m not doing anything with the outputType value that is passed on to this action method. This value will be in the RouteData dictionary and we’ll use this in our custom invoker class as well.
It’s time to hook up our invoker class. First, I’ll override the Initialize() method of my RenderAsController class.
1: protected override void Initialize(RequestContext requestContext)
2: {
3: base.Initialize(requestContext);
4: string outputType = string.Empty;
5:
6: // read the outputType from the RouteData dictionary
7: if (requestContext.RouteData.Values["outputType"] != null)
8: {
9: outputType = requestContext.RouteData.Values["outputType"].ToString();
10: }
11:
12: // my custom invoker class
13: ActionInvoker = new ContentRendererActionInvoker(outputType);
14: }
Coming to the main part of the discussion – the ContentRendererActionInvoker class:
1: public class ContentRendererActionInvoker : ControllerActionInvoker
2: {
3: private readonly string _outputType;
4:
5: public ContentRendererActionInvoker(string outputType)
6: {
7: _outputType = outputType.ToLower();
8: }
9: //...
10: }
So the outputType value that was read from the RouteData, which was passed in from the url, is being set here in a private field. Moving to the crux of this article, I now override the CreateActionResult method.
1: protected override ActionResult CreateActionResult(ControllerContext controllerContext, ActionDescriptor actionDescriptor, object actionReturnValue)
2: {
3: if (actionReturnValue == null)
4: return new EmptyResult();
5:
6: ActionResult result = actionReturnValue as ActionResult;
7: if (result != null)
8: return result;
9:
10: // This is where the magic happens
11: // Depending on the value in the _outputType field,
12: // return an appropriate ActionResult
13: switch (_outputType)
14: {
15: case "json":
16: {
17: JavaScriptSerializer serializer = new JavaScriptSerializer();
18: string json = serializer.Serialize(actionReturnValue);
19: return new ContentResult { Content = json, ContentType = "application/json" };
20: }
21: case "xml":
22: {
23: XmlSerializer serializer = new XmlSerializer(actionReturnValue.GetType());
24: using (StringWriter writer = new StringWriter())
25: {
26: serializer.Serialize(writer, actionReturnValue);
27: return new ContentResult { Content = writer.ToString(), ContentType = "text/xml" };
28: }
29: }
30: case "csv":
31: controllerContext.HttpContext.Response.AddHeader("Content-Disposition", "attachment; filename=items.csv");
32: return new ContentResult
33: {
34: Content = ToCsv(actionReturnValue as IList<Item>),
35: ContentType = "application/ms-excel"
36: };
37: case "pdf":
38: string filePath = controllerContext.HttpContext.Server.MapPath("~/items.pdf");
39: controllerContext.HttpContext.Response.AddHeader("content-disposition",
40: "attachment; filename=items.pdf");
41: ToPdf(actionReturnValue as IList<Item>, filePath);
42: return new FileContentResult(StreamFile(filePath), "application/pdf");
43:
44: default:
45: controllerContext.Controller.ViewData.Model = actionReturnValue;
46: return new ViewResult
47: {
48: TempData = controllerContext.Controller.TempData,
49: ViewData = controllerContext.Controller.ViewData
50: };
51: }
52: }
A big method there! The hook I was talking about kinda above actually is here. This is where different kinds / formats of output get returned based on the output type requested in the url.
When the _outputType is not set (string.Empty as set in the Global.asax.cs file), the razor view gets rendered (lines 45-50). This is the default behavior in most MVC applications where-in a view (webform/razor) gets rendered on the browser. As you see here, this gets returned as a ViewResult. But then, for an outputType of json/xml/csv, a ContentResult gets returned, while for pdf, a FileContentResult is returned.
Here are how the different kinds of output look like:
This is how we can leverage this feature of ASP.NET MVC to developer a better application.
I’ve used the iTextSharp library to convert to a pdf format. Mike gives quite a bit of detail regarding this library here. You can download the sample code here. (You’ll get an option to download once you open the link).
Verdict: Hot chocolate: $3; Reebok shoes: $50; Your first car: $3000; Being able to extend a web application: Priceless.