Search code examples
asp.net-corepdf-generationwkhtmltopdfrazor-pages

Razor Pages PDF generation with Rotativa - @Model null


I have a web app build with .net core 3.1 Razor Pages. I need to add functionality to generate a PDF from a view. Generally the library is working because i can generate a static PDF, but the problem occurs when I want to use model to seed the template.

The PageModel looks like this:

    public class DetailsPdfModel : PageModel
    {
        private readonly ICablesData cablesData;
        private readonly IConfiguration configuration;

        public DetailsPdfModel(ICablesData cablesData, IConfiguration configuration)
        {
            this.cablesData = cablesData;
            this.configuration = configuration;
        }

        public Cable Cable { get; set; }

        public IActionResult OnGet(int cableId)
        {
            Cable = cablesData.GetById(cableId);

            if (Cable == null)
            {
                return RedirectToPage("NotFound");
            }
            else
            {
                return new ViewAsPdf("DetailsPdf", this);
            }
        }
    }

The view looks like this:

@page
@model DetailsPdfModel
@{
    Layout = null;
}

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>DetailsPdf</title>
</head>
<body>
    <p>@Model.Cable.Name</p>
</body>
</html>

When I am trying to get the pdf the exception occurs. I noticed that the @Model is always null. If I change the return new ViewAsPdf("DetailsPdf", this); to return Page(); the @Model is not null but after that is only regular view not the pdf file.

Any ideas how to solve this problem?


Solution

  • If I change the return new ViewAsPdf("DetailsPdf", this); to return Page(); the @Model is not null but after that is only regular view not the pdf file.

    That's because ViewAsPdf is not designed for Razor Page. And Rotativa doesn't expose a built-in API for RazorPage. For more details, see source code of Rotativa.AspNetCore.

    As a walkaround, you could create a custom RazorPageAsPdf class to achieve the same goal as below:

    public class DetailsPdfModel : PageModel
    {
        ...
    
        public IActionResult OnGet(int cableId)
        public RazorPageAsPdf OnGet(int cableId)
        {
            Cable = cablesData.GetById(cableId);
    
            if (Cable == null)
            {
                return RedirectToPage("NotFound");
            }
            else
            {
                return new ViewAsPdf("DetailsPdf", this);
                return new RazorPageAsPdf(this);       // we don't need page path because it can be determined by current route
            }
        }
    }
    

    Here's my implementation of RazorPageAsPdf for your reference:

    public class RazorPageAsPdf : AsPdfResultBase
    {
        private readonly IRazorViewEngine _razorViewEngine;
        private readonly ITempDataProvider _tempDataProvider;
        private readonly IRazorPageActivator _activator;    
        private string _razorPageName {get;set;}
        public PageModel PageModel {get;set;}
        public RazorPageAsPdf(PageModel pageModel)
        {
            PageModel = pageModel;
            var httpContext = pageModel.HttpContext;
            this._razorPageName = httpContext.Request.RouteValues["page"].ToString().Trim('/');
            if(string.IsNullOrEmpty(_razorPageName)){
                throw new ArgumentException("there's no such a 'page' in this context");
            }
            this._razorViewEngine =  httpContext.RequestServices.GetRequiredService<IRazorViewEngine>();
            this._tempDataProvider=  httpContext.RequestServices.GetRequiredService<ITempDataProvider>();
            this._activator = httpContext.RequestServices.GetRequiredService<IRazorPageActivator>();
        }
    
        private ViewContext GetViewContext( ActionContext actionContext, IRazorPage page, StringWriter sw)
        {
            var view = new RazorView( _razorViewEngine, _activator, new List<IRazorPage>(), page, HtmlEncoder.Default, new DiagnosticListener(nameof(RazorPageAsPdf)));
            return new ViewContext( actionContext, view, this.PageModel.ViewData, this.PageModel.TempData, sw, new HtmlHelperOptions());
        } 
    
        private async Task<string> RenderPageAsString(ActionContext actionContext){
            using (var sw = new StringWriter())
            {
                var pageResult = this._razorViewEngine.FindPage(actionContext, this._razorPageName);;
                if (pageResult.Page == null)
                {
                    throw new ArgumentNullException($"The page {this._razorPageName} cannot be found.");
                }
                var viewContext = this.GetViewContext(actionContext, pageResult.Page, sw);
                var page = (Page)pageResult.Page;
                page.PageContext = this.PageModel.PageContext;
                page.ViewContext = viewContext;
                _activator.Activate(page, viewContext);
                await page.ExecuteAsync();
                return sw.ToString();
            }
        }
    
        protected override async Task<byte[]> CallTheDriver(ActionContext actionContext)
        {
            var html = await this.RenderPageAsString(actionContext);
            // copied from https://github.com/webgio/Rotativa.AspNetCore/blob/c907afa8c7dd6a565d307901741c336c429fc698/Rotativa.AspNetCore/ViewAsPdf.cs#L147-L151
            string baseUrl = string.Format("{0}://{1}",  actionContext.HttpContext.Request.Scheme, actionContext.HttpContext.Request.Host);
            var htmlForWkhtml = Regex.Replace(html.ToString(), "<head>", string.Format("<head><base href=\"{0}\" />", baseUrl), RegexOptions.IgnoreCase);
            byte[] fileContent = WkhtmltopdfDriver.ConvertHtml(this.WkhtmlPath, this.GetConvertOptions(), htmlForWkhtml);
            return fileContent;
        }
        protected override string GetUrl(ActionContext context) => string.Empty;
    }