views:

601

answers:

3

So here's the issue: I'm building e-mails to be sent by my application by rendering full view pages to strings and sending them. This works without any problem so long as I'm not redirecting to another URL on the site afterwards. Whenever I try, I get "System.Web.HttpException: Cannot redirect after HTTP headers have been sent."

I believe the problem comes from the fact I'm reusing the context from the controller action where the call for creating the e-mail comes from. More specifically, the HttpResponse from the context. Unfortunately, I can't create a new HttpResponse that makes use of HttpWriter because the constructor of that class is unreachable, and using any other class derived from TextWriter causes response.Flush() to throw an exception, itself.

Does anyone have a solution for this?

    public static string RenderViewToString(
        ControllerContext controllerContext,
        string viewPath,
        string masterPath,
        ViewDataDictionary viewData,
        TempDataDictionary tempData)
    {
        Stream filter = null;
        ViewPage viewPage = new ViewPage();

        //Right, create our view
        viewPage.ViewContext = new ViewContext(controllerContext,
            new WebFormView(viewPath, masterPath), viewData, tempData);

        //Get the response context, flush it and get the response filter.
        var response = viewPage.ViewContext.HttpContext.Response;
        //var response = new HttpResponseWrapper(new HttpResponse
        //    (**TextWriter Goes Here**));
        response.Flush();
        var oldFilter = response.Filter;

        try
        {
            //Put a new filter into the response
            filter = new MemoryStream();
            response.Filter = filter;

            //Now render the view into the memorystream and flush the response
            viewPage.ViewContext.View.Render(viewPage.ViewContext,
                viewPage.ViewContext.HttpContext.Response.Output);
            response.Flush();

            //Now read the rendered view.
            filter.Position = 0;
            var reader = new StreamReader(filter, response.ContentEncoding);
            return reader.ReadToEnd();
        }
        finally
        {
            //Clean up.
            if (filter != null)
                filter.Dispose();

            //Now replace the response filter
            response.Filter = oldFilter;
        }
    }
+2  A: 

You'd have to initiate a new request. Bit, do you really want to send emails synchronously this way? If the mail server is down, the user could be waiting a good while.

I always put emails in an offline queue and have a service mail them. You might consider using the Spark template engine for this.

One other approach is to not redirect but write out a page with a meta redirect tag

Haacked
I want to use the WebForms engine for building the email bodies themselves. Spark looks kind of neat, but I'm still much more comfortable with the idea of using WebForms all through.By the way, by new request, I'm assuming HttpServerUtility.Execute is good?As for synchronous sending, it doesn't have to be that way, it all depends on the settings for SmtpClient doesn't it? Since I can have it dump mails in a folder for later sending, that's not a problem.
Chris Charabaruk
A: 

Have a look at the MVC Contrib EmailTemplateService which does exactly what you are after.

http://mvccontrib.googlecode.com/svn/trunk/src/MVCContrib/Services/EmailTemplateService.cs

Sorry Chris, not quite sure what I was thinking but I obviously didn't read the question. While I cannot give you a way around this, I can tell you why you are getting the error - HttpResponse.Flush() sends the headers before flushing the content to your filter. This sets a flag inside the response so that when you try to redirect you get the exception.

Using reflector to look at the code inside Flush, I can't see any clean way for you to get around this without a lot of reflection and other nastiness.

Neal
No, that'll have the same problem. EmailTemplateService.RenderMessage() is practically the same code as I'm already using, except making a MailMessage instead of a string.
Chris Charabaruk
I know it's the call to Flush causing it. Phil's right, about the only way around this is to create a whole new request.
Chris Charabaruk
DEAD LINK: Try this instead! http://github.com/mvccontrib/MvcContrib/blob/master/src/MVCContrib/Services/EmailTemplateService.cs
Dai Bok
+2  A: 

Here is an alternative method for rendering a view to a string that never results in data being output to the response (therefore it should avoid your problem): http://craftycodeblog.com/2010/05/15/asp-net-mvc-render-partial-view-to-string/

To render a regular view instead of a partial view, you'll need to change "ViewEngines.Engines.FindPartialView" to "ViewEngines.Engines.FindView".

Kevin Craft