Wednesday, June 04, 2008

MVC Framework: Capturing the output of a view

Update: I found that this technique has problems because it changes the state of the HttpResponse by setting its internal _headersWritten flag which means you can't subsequently execute a redirect. I've developed a different and better technique which involves replacing the current HttpContext. See this post:

MVC Framework: Capturing the output of a view (part 2)

Sometimes it's useful to be able to capture the output of a view rather than simply have it streamed to the Response.OutputStream. A couple of things have made this quite easy to do. Firstly, from CTP 3 of the MVC Framework, controllers now return an ActionResult rather than just setting properties and leaving the infrastructure to act on them. This means we can take a ViewResult and call ExecuteResult on it before it's returned, allowing us to take control of the rendering.

The second innovation is a nice piece of work from the MVCContrib project. This is an open source add-on to the MVC Framework that provides lots of useful stuff like standard error handling, data binding, IoC container integration and lots more. If you're building MVC Framework applications you really should look at what this project can do for you. Deep in its bowels is the BlockRenderer class, this can take any action that renders to Response.OutputStream and divert it to a string. It does it by adding a custom filter to the Response filter chain.

Here's a silly example of it in action. I've created a test controller and in its Index action I'm calling the Item action on another controller called orderController. I'm then using the BlockRenderer to capture the rendering of the Item view in a string, which is then simply displayed.

using System.Web.Mvc;
using MvcContrib.UI;

namespace Suteki.Shop.Controllers
{
public class TestController : Controller
{
private OrderController orderController;

public TestController(OrderController orderController)
{
this.orderController = orderController;
}

public ActionResult Index()
{
// pass the current controller context to orderController
orderController.ControllerContext = ControllerContext;

// create a new BlockRenderer
var blockRenderer = new BlockRenderer(HttpContext);

// execute the Item action
var viewResult = (ViewResult) orderController.Item(1);

// change the master page name
viewResult.MasterName = "Print";

// we have to set the controller route value to the name of the controller we want to execute
// because the ViewLocator class uses this to find the view
this.RouteData.Values["controller"] = "Order";

string result = blockRenderer.Capture(() => viewResult.ExecuteResult(ControllerContext));

return Content(result);
}
}
}


Note that I change the name of the master page. You can imagine that the "Print" master page is a simple body tag container without any of the site furniture: menus and wotnot, that my standard master page might have. I also have to change the controller route value because the ViewLocator class of the MVC Framework uses it to find the view name.



The main reason I have for doing this, is so that I can send a view by email, but you can imagine how it might be useful to do other things like build up a page from multiple controllers and or views.

No comments: