Asbestos Supply

2009-09-16 Rendering ViewPage to random stream (or not)

Update - I actually seem to have gotten a working solution to this (one I’m comfortable with).  I blogged about it here.

I had this crazy idea.

My company has some Response Filters that replace tokens in the outputted HTML into some other data.  Now that we're moving over to MVC I figured hey -- maybe I can do a better job with these.  You see, these Filters have inline HTML -- that is, they use a StringBuilder to create the necessary HTML to replace the token.  That's no fun -- first of all it's not too pretty, but also, what happens when the designer wants to make a change?

My crazy idea was to call an Action from within the Filter.  I figured this would have a couple benefits:

  1. Decouple UI from the data (that's what MVC's all about right?)
  2. Provide 1 stop for this information (so now the same method can be called from within the web app, without the need for a token, and the same method will be called from the filter to replace the token)

I figured the process would go something like this:

  1. Create a StreamWriter that points to a MemoryStream
  2. Create a faux Response object that uses this StreamWriter
  3. Have the ControllerFactory.Current create the controller
  4. Invoke the Action on the Controller using the in-memory Response
  5. Since the output will be sent to MemoryStream, just read it from there and replace the token

Sound simple, right?  It's not!

First of all, HttpContext is a magical, super-inter-dependent beast.

IControllerFactory factory = ControllerBuilder.Current.GetControllerFactory();
// I need an HttpRequest    
HttpRequest httpRequest = new HttpRequest(HttpContext.Current.Request.FilePath, 
    HttpContext.Current.Request.Url.AbsoluteUri, string.Empty);
// Here's the stream I'm going to put the response into
MemoryStream memStream = new MemoryStream();
// I need a Response to hold the stream
HttpResponse response = new HttpResponse(new StreamWriter(memStream));
// I need an HttpContextBase, which wraps the HttpContext
// which requires the Request & Response
HttpContextBase httpContext = new HttpContextWrapper(
    new HttpContext(httpRequest, response));
// I need a RequestContext to feed into the Execute method on my Controller
RequestContext requestContext = new RequestContext(httpContext, new RouteData()
    {
        Values = { { "controller", "Home" }, 
        { "action", "SomeAction" } }
    });
IController controller = factory.CreateController(requestContext, "Home");
controller.Execute(requestContext);

Man, that was a lot of work!  And you know what?  It doesn’t even work!

That’s right, get used to it because this post is all about things not working.  If you’re looking for a quick fix, you might want to visit google because you won’t find it in this post.  The above code generates the following Exception on line 16:

The SessionStateTempDataProvider requires SessionState to be enabled.

And a quick peek at Reflector tells me this is because the HttpContext (that I’m passing in) has a Session object that’s null.

Ok, so I came up with another, simpler and more elegant way of accomplishing this, if not a little more dependent:

// Queue up all my HttpContext dependencies
HttpRequest httpRequest = new HttpRequest(HttpContext.Current.Request.FilePath, 
    HttpContext.Current.Request.Url.AbsoluteUri, string.Empty);
MemoryStream memStream = new MemoryStream();
HttpResponse response = new HttpResponse(new StreamWriter(memStream));
HttpContextBase httpContext = new HttpContextWrapper(
    new HttpContext(httpRequest, response));

// In reality I'd use reflection for this, 
// but as you'll see the code won't work anyway so why bother?
HomeController homeController = new HomeController();
ControllerContext controllerContext = new ControllerContext(httpContext, new RouteData(){
    Values = {{ "controller", "Home"}}, 
    { "action", "SomeAction"}}
}, homeController);

// Again, I would use reflection but it's a waste of time
ActionResult result = homeController.SomeAction();
result.ExecuteResult(controllerContext);

Looks pretty good, right?  What I’m doing is creating the Controller (in a non proof-of-concept I’d use reflection to queue that up) and then calling the necessary Action which returns an ActionResult.  I’m then running the ExecuteResult method on said ActionResult passing in my ControllerContext which has a reference to my HttpContext which in turn writes data to my MemoryStream.

However, this also doesn’t work as (I) expected.  What ends up happening is that the MemoryStream length remains 0 but the output from the ActionResult ends up on the calling page – at the top of the page.

I debugged against the MVC source code and you want to see the culprit?  ViewPage.cs, line 110:

public virtual void RenderView(ViewContext viewContext) {
    ViewContext = viewContext;
    InitHelpers();
    // Tracing requires Page IDs to be unique.
    ID = Guid.NewGuid().ToString();
    ProcessRequest(HttpContext.Current);
}

Notice the last line there:

ProcessRequest(HttpContext.Current);

So, no matter what you do, you’ll still be passing the HttpContext.Current (with its StreamWriter) back to the Page to get rendered.  Damn!

So what are the options here?

I could inherit from ViewPage and override the RenderView.  That might work, but could cause other bugs because HttpContext is still super duper dependent and to debug I’ll have to head on over to the .NET Framework code (which, luckily, is available).  I’ll also probably want to create a new class for my Controllers (which we already have, actually) to inherit from Controller so that I can create some sort of View method that will return my special inherited ViewPage that calls this overridden RenderView.

This no longer strikes me as quite the elegant solution I first assumed it would be.

I’m not quite sure the direction I’m going to take this now.