PDF generator using Asp.Net MVC views as templates | ASSIST Software Romania
get in touch

LIKE

SHARE

FOLLOW

Ion Balan ASSIST Software

Software Development Engineer at ASSIST

PDF generator using Asp.Net MVC views as templates. Ion Balan - ASSIST Software
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.

I. The existing solution 

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.

II. The new solution

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();
Once you have a reference for the view engine then you can access the views with FindView method. And once you have the view you can manipulate it as needed.

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:
 
1. PdfGenerator class: ​
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();
            }
        }
    }
3. A cshtml file, representing the view/the template that will be rendered. This is a regular MVC view as any other.
 
Using this will be like this: 
pdfGenerator.GeneratePdf("ControllerName", "TemplateViewName", pdfTemplateModel)
HiQPdf was the only free library that supports the entire spectrum of Html styling. The only problem is that its free for PDF of up to 3 pages.
 

III. An additional problem that needed to be solved
 

As a side problem to be solved, was the fact that the PDF that it needed to be generated, included a series of questions, which could have child question like following, and needed to be displayed without a question number under parent:

1. Question 1: Answer
  • Question 1 child question 1: child question answer 1 
  • Question 1 child question 2: child question answer 2 
Applying "brute force" on this will result in a convoluted code with a lot of ifs. The solution was to apply the Chain of Responsibilities design pattern, and ended with the following class:
 
    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")
}

If you enjoyed reading this, you can also check the HealthPoint solution.

Ready to make it happen?

Drop us a line. We’d love to hear from you and see how we can help in solving your digital challenges. As one of the best software outsourcing companies in Romania, Eastern Europe, Europe and the world really, we are sure we can ASSIST.

GET IN TOUCH