PDF generator using Asp.Net MVC views as templates
Not too long ago, I had to work on a task which was asking to create a PDF for a given page/form. Therefore, in this post I would like to take you in a journey that will end with a generic solution, that can be applied anywhere else in the solution in a simple manner, making use of Asp.Net MVC features.
If you don’t have experience with C#, Asp.Net or MVC please keep reading, as it has as well some good lessons about programming in general.
Not too long ago, I had to work on a task which was asking to create a PDF for a given page/form. Therefore, in this post I would like to take you in a journey that will end with a generic solution, that can be applied anywhere else in the solution in a simple manner, making use of Asp.Net MVC features.
If you don’t have experience with C#, Asp.Net or MVC please keep reading, as it has as well some good lessons about programming in general.
Represents a class, PdfManager, which is responsible for PDF generation, in a "brute" manner: passing the object for which the PDF is generated, as a parameter. This means that for every form/page/object for which the PDF is generated has its own method.
Then, depending on what PDF is generated, each method constructs the PDF line by line, cell by cell, applying styles for each of these as required. The result: a class that has more than 1600 lines of code, which is hard to read and understand.
As said above, the styling is mixed in the code, making refinements hard. To generate the PDF, PdfManager is making use of some helpers of different kinds, breaking the Single Responsibility Principle. In contrast, the new class that I’ve created, responsible for PDF generation, and name it PdfGenerator, to avoid name clash with the existing class, it has only 37 lines.
Obviously, the class itself is not the only thing that is required to generate the PDF, but I wanted to show that if you distribute the responsibilities correctly your code will become a lot simple and easy to read and understand.
There are a couple of articles on the web describing how to generate PDFs using Asp.Net MVC. Our biggest challenge was the fact that our application was on Asp.Net MVC 3 – yes, I know, very old, out of date, exposed to so many issues and problems that have been fixed since then, but the biggest problem of all was that most of the articles that I found were targeting much newer versions (although in the end, it did not make a difference).
As such, one of the steps for our solution was to migrate to MVC 4, at least. Initially, my efforts were around making use of RazorGenerator (custom tool for Visual Studio that allows you to precompile the view, to get a smaller published bundle and faster startup time). However, this turned into a problem with the deployment, as it would have required changing that.
In order to make use of its PrecompiledMvcEngine, RazorGenerator registers this on App_Start, like so:
public static class RazorGeneratorMvcStart { public static void Start() { var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly) { UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal }; ViewEngines.Engines.Insert(0, engine); // StartPage lookups are done by WebPages. VirtualPathFactoryManager.RegisterVirtualPathFactory(engine); } }
Then in the code, you can make use of the engine like following:
var viewEngine = ViewEngines.Engines.OfType<PrecompiledMvcEngine>().FirstOrDefault();
As such, when we run into problems with the deployment, after a couple of attempts, I realized that, if it is possible to use the PrecompiledMvcEngine in such a manner, then it shouldn’t be a difference if using RazorViewEngine, the Asp.Net MVC engine. And so it was. As such the entire solution consists of the following 3 things:
using HiQPdf; /// <summary> /// A generic PDF generator /// Note: HiQPdf can be used free for PDF with up to 3 pages. /// </summary> public class PdfGenerator : IPdfGenerator { /// <summary> /// Generates a PDF, for a specific view/template /// </summary> /// <param name="controllerName">Controller owning the view/template</param> /// <param name="viewName">The view/template name</param> /// <param name="viewModel">The view model for view/template</param> /// <returns></returns> public byte[] GeneratePdf(string controllerName, string viewName, IPdfTemplateViewModel form) { var templateService = new TemplateService(); string documentContent = templateService.RenderTemplate(controllerName, viewName, form); // instantiate the HiQPdf HTML to PDF converter var htmlToPdfConverter = new HtmlToPdf(); htmlToPdfConverter.Document.Margins = new PdfMargins(20); htmlToPdfConverter.Document.PageSize = PdfPageSize.A4; htmlToPdfConverter.Document.PageOrientation = PdfPageOrientation.Portrait; // render the HTML code as PDF in memory byte[] bytes = htmlToPdfConverter.ConvertHtmlToMemory(documentContent, null); return bytes; } }
2. TemplateService class:
public class TemplateService : ITemplateService { /// <summary> /// Renders a PDF template, returning the resulting page as a string /// </summary> /// <param name="controllerName">The controller coresponding to view </param> /// <param name="viewName">The template name</param> /// <param name="viewModel">The view model for template</param> /// <returns></returns> public string RenderTemplate(string controllerName, string viewName, IPdfTemplateViewModel viewModel) { var routeData = new RouteData(); routeData.Values.Add("controller", controllerName); var controllerContext = new ControllerContext(new HttpContextWrapper(new HttpContext(new HttpRequest(null, "http://google.com", null), new HttpResponse(null))), routeData, new FakeController()); var viewEngine = ViewEngines.Engines.OfType<RazorViewEngine>().FirstOrDefault(); if (viewEngine == null) { throw new InvalidOperationException("Could not get and instance for RazorViewEngine."); } using (var outputWriter = new StringWriter()) { var viewResult = viewEngine.FindView(controllerContext, viewName, "", false); if (viewResult.View == null) { throw new TemplateServiceException(string.Format("Failed to render template {0} because it was not found.", viewName)); } var viewDictionary = new ViewDataDictionary<IPdfTemplateViewModel>() { Model = viewModel }; try { var viewContext = new ViewContext(controllerContext, viewResult.View, viewDictionary, new TempDataDictionary(), outputWriter); viewResult.View.Render(viewContext, outputWriter); } catch (Exception ex) { throw new TemplateServiceException("Failed to render template due to a engine failure", ex); } return outputWriter.ToString(); } } }
pdfGenerator.GeneratePdf("ControllerName", "TemplateViewName", pdfTemplateModel)
III. An additional problem that needed to be solved
1. Question 1: Answer
- Question 1 child question 1: child question answer 1
- Question 1 child question 2: child question answer 2
public class QuestionAnswerViewModel { public QuestionAnswerViewModel() { } public QuestionAnswerViewModel(Answer answer) { if (answer.Value.HasValue) { Answer = answer.Value.Value ? Shared.Label_Yes : Shared.Label_No; } else if (!string.IsNullOrWhiteSpace(answer.Text)) { Answer = answer.Text; } Question = answer.Question.Text; } public string Answer { get; set; } public int Order { get; set; } public string Question { get; set; } public QuestionAnswerViewModel Successor { get; protected set; } public void SetSuccessor(QuestionAnswerViewModel successor) { Successor = successor; } }
This then allows you to call the DisplayTemplate for this class recursively if any of the questions have a Successor:
@if (Model.Successor != null) { @Html.DisplayFor(m => m.Successor,"QuestionAnswer") }