views:

811

answers:

2

Hi --

I can't render a view to a string and then redirect, despite this answer from Feb (after version 1.0, I think) that claims it's possible. I thought I was doing something wrong, and then I read this answer from Haack in July that claims it's not possible.

If somebody has it working and can help me get it working, that's great (and I'll post code, errors). However, I'm now at the point of needing workarounds. There are a few, but nothing ideal. Has anybody solved this, or have any comments on my ideas?

  1. This is to render email. While I can surely send the email outside of the web request (store info in a db and get it later), there are many types of emails and I don't want to store the template data (user object, a few other LINQ objects) in a db to let it get rendered later. I could create a simpler, serializable POCO and save that in the db, but why? ... I just want rendered text!
  2. I can create a new RedirectToAction object that checks if the headers have been sent (can't figure out how to do this -- try/catch?) and, if so, builds out a simple page with a meta redirect, a javascript redirect, and also a "click here" link.
  3. Within my controller, I can remember if I've rendered an email and, if so, manually do #2 by displaying a view.
  4. I can manually send the redirect headers before any potential email rendering. Then, rather than using the MVC infrastructure to redirecttoaction, I just call result.end. This seems easiest, but really messy.
  5. Anything else?

EDIT: I've tried Dan's code (very similar to the code from Jan/Feb that I've already tried) and I'm still getting the same error. The only substantial difference I can see is that his example uses a view while I use a partial view. I'll try testing this later with a view.

Here's what I've got:

Controller

public ActionResult Certifications(string email_intro)
        {
            //a lot of stuff

            ViewData["users"] = users;

            if (isPost())
            {
                //create the viewmodel
                var view_model = new ViewModels.Emails.Certifications.Open(userContext)
                {
                    emailIntro = email_intro
                };

                //i've tried stopping this after just one iteration, in case the problem is due to calling it multiple times
                foreach (var user in users)
                {
                    if (user.Email_Address.IsValidEmailAddress())
                    {
                        //add more stuff to the view model specific to this user
                        view_model.user = user;
                        view_model.certification302Summary.subProcessesOwner = new SubProcess_Certifications(RecordUpdating.Role.Owner, null, null, user.User_ID, repository);
                        //more here....

                        //if i comment out the next line, everything works ok
                        SendEmail(view_model, this.ControllerContext);
                    }
                }

                return RedirectToAction("Certifications");
            }

            return View();
        }

SendEmail()

   public static void SendEmail(ViewModels.Emails.Certifications.Open model, ControllerContext context)
        {
            var vd = context.Controller.ViewData;
            vd["model"] = model;
            var renderer = new CustomRenderers();
            //i fixed an error in your code here
            var text = renderer.RenderViewToString3(context, "~/Views/Emails/Certifications/Open.ascx", "", vd, null);
            var a = text;
        }

CustomRenderers

public class CustomRenderers
    {
        public virtual string RenderViewToString3(ControllerContext controllerContext, string viewPath, string masterPath, ViewDataDictionary viewData, TempDataDictionary tempData)
        {
            //copy/paste of dan's code
        }
    }

Error

[HttpException (0x80004005): Cannot redirect after HTTP headers have been sent.]
   System.Web.HttpResponse.Redirect(String url, Boolean endResponse) +8707691

Thanks, James

A: 
public Action SendEmail(int id)
{
  //Let's say that id is the db id of an order that a customer has just placed.

  //Go get that model from the db.
  MyModel model = new Model(id);

  //Now send that email. Don't forget the model and controller context.
  SendEmail(model, this.ControllerContext);

  //Render (or redirect!)
  return RedirectToAction("Wherever");
}

private static void SendEmail(MyModel model, ControllerContext controllerContext)
{
  //Recreate the viewdata
  ViewDataDictionary viewData = controllerContext.Controller.ViewData;
  viewData["Order"] = model;
  string renderedView = "";
  CustomRenderers customRenderers = new CustomRenderers();

  //Now render the view to string
  //ControllerContext, ViewPath, MasterPath, ViewDataDictionary, TempDataDictionary
  //As you can see, we're not passing a master page, and the tempdata is in this instance.
  renderedView = RenderViewToString(controllerContext, "~/Views/Orders/Email.aspx", "", viewData, null);

  //Now send your email with the string as the body.
  //Not writing that, as the purpose is just to show the rendering. :)
}


//Elsewhere...
public class CustomRenderers
{
  public virtual string RenderViewToString(ControllerContext controllerContext, string viewPath, string masterPath, ViewDataDictionary viewData, TempDataDictionary tempData)
  {
    if (tempData == null)
    {
    tempData = new TempDataDictionary();
    }

    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;
    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;
    }
  }
}

In your Orders/Email.aspx view, make sure you refer to everything from the ViewData, rather than the model. you can do this:

<% MyModel model = (MyModel)ViewData["Order"] %>
Dan Atkinson
Dan -- Thanks for trying, but I'm getting the same 'cannot redirect error' that I was getting after using the code from your Jan posting. I've pasted everything in my question. What am I doing wrong? Thanks.
James S
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