Generally in MVC you want to render views using a view engine with a standard action result response that lets MVC deal with the view engine.
public ActionResult Index(int id) { PersonViewModel model = new PersonViewModel(); model.Load(id); return View(model); }
But in a couple circumstances you may need to render that view to a string. One case I’ve seen in other blogs is that you may want to return an html string in a JSON response so that script on the calling page can update a partial page section.
The reason I have found to render a view as html is so that it can be passed into a 3rd party component to generate a PDF and then return that PDF response from the action method. To do this I wanted a single line return from my action method to do all the work like the following.
[AcceptVerbs(HttpVerbs.Post)] public ActionResult Print(int id) { PersonViewModel model= new PersonViewModel(); model.Load(id); return this.PDF("IndexPartial", model); }
The next step was to create a PDF method on the controller class. I didn’t want to create a custom controller class, so I opted to use an extension method instead.
public static class ControllerExtension { public static PDFActionResult PDF(this Controller controller, string viewName, object model) { return new PDFActionResult(viewName, model); } }
I decided to make it easy and derive PDFActionResult from ViewResult. ViewResult already has the implementation for finding the correct IView required to instantiate an HtmlHelper. Phil Haack already has a good writeup on RenderAction here.
using ExpertPdf.HtmlToPdf; namespace YourCompany.ProjectName { public class PDFActionResult : ViewResult { private object model_ = null; public PDFActionResult():base(){ } public PDFActionResult(string view) { base.ViewName = view; } public PDFActionResult(string view, object model) { base.ViewName = view; model_ = model; } public override void ExecuteResult(ControllerContext context) { IView view = FindView(context).View; StringBuilder sb = new StringBuilder(); StringWriter sw = new StringWriter(sb); ViewContext vc = new ViewContext(context, view, context.Controller.ViewData, context.Controller.TempData, sw); IViewDataContainer vdc = new PDFViewDataContainer() { ViewData = context.Controller.ViewData }; HtmlHelper helper = new HtmlHelper(vc, vdc); helper.RenderAction(base.ViewName, model_); string html = sb.ToString(); PdfConverter conv = CreatePdfConverter(); byte[] htmlBytes = conv.GetPdfBytesFromHtmlString(html); HttpResponseBase response = context.HttpContext.Response; response.Clear(); response.AddHeader("Content-Type", "binary/octet-stream"); response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}; size={1}", "generated.PDF", htmlBytes.Length)); response.BinaryWrite(htmlBytes); response.End(); } private PdfConverter CreatePdfConverter() { var converter = new PdfConverter(); //if (!string.IsNullOrEmpty(_licenseKey)) // converter.LicenseKey = _licenseKey; #warning need license key to remove watermark from generated PDF converter.PdfDocumentInfo.AuthorName = "Your company name here"; PdfDocumentOptions docOptions = converter.PdfDocumentOptions; docOptions.EmbedFonts = true; docOptions.LeftMargin = 60; docOptions.TopMargin = 40; docOptions.RightMargin = 60; docOptions.BottomMargin = 60; return converter; } private class PDFViewDataContainer : IViewDataContainer { public ViewDataDictionary ViewData { get; set; } } } }
The HtmlHelper RenderAction method is already capable of rendering a view to the ViewContext TextWriter that was passed into the constructor of the ViewContext. In a typical scenario where MVC creates the ViewContext the TextWriter is set to the output stream of the request. This code passes in a StringBuilder instead.
I found it very odd that the ControllerContext does not implement IViewDataContainer since it does have the ViewData property. If it did implement the interface then I would not have had to create the PDFViewDataContainer class.
The creation of the PDF will depend on what PDf library you have available. The PDF tool I used was ExpertPDF. This tool is free if you don’t mind a large watermark in the middle of each page, otherwise it will have a cost.
For all this to work you also need an action method of the partial view name. RenderAction will not work without this. You can mark this action method with ChildActionOnly so that routing does not let a normal request get to this partial view.
[ChildActionOnly] public ActionResult IndexPartial(PersonViewModel model) { return View("IndexPartial", model); }
[Imported June 28,2012]
[Originally published August 18,2011]