问题
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?
- 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!
- 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.
- Within my controller, I can remember if I've rendered an email and, if so, manually do #2 by displaying a view.
- 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.
- 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
回答1:
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"] %>
回答2:
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".
回答3:
Updated.
Now that I understand that you want to use the view engine to generate the actual email in html, I propose the following:
Code to render action to text within a controller: http://mikehadlow.blogspot.com/2008/06/mvc-framework-capturing-output-of-view_05.html
Minor edits to your code:
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 };
foreach (var user in users)
{
if (user.Email_Address.IsValidEmailAddress())
{
view_model.user = user;
view_model.certification302Summary.subProcessesOwner = new SubProcess_Certifications(RecordUpdating.Role.Owner, null, null, user.User_ID, repository);
SendEmail(view_model);
}
}
return RedirectToAction("Certifications");
}
return View();
}
public void SendEmail(ViewModels.Emails.Certifications.Open model)
{
var vd = context.Controller.ViewData;
vd["model"] = model;
var renderer = new CustomRenderers();
// Implement the actual email rendering as a regular action method on this controller
var text = this.CaptureActionHtml(c => (ViewResult)c.RenderEmail(model));
var a = text;
}
来源:https://stackoverflow.com/questions/1281430/rendering-a-view-to-a-string-in-mvc-then-redirecting-workarounds