Cómo crear un PDF a partir de una Vista en ASP.NET MVC
En el anterior artículo de este Bolg Cómo crear un PDF a partir de una Vista en ASP.NET Core MVC, vimos cómo crear dinámicamente un PDF a partir de una Vista definida en una aplicación ASP.NET Core MVC.
En esta ocasión y basándonos en el ejemplo ya desarrollado en el artículo anterior, veremos cómo realizar la misma funcionalidad, pero esta vez para aplicaciones ASP.NET MVC sobre .NET Framework 4.6.1 o superior.
Para realizar este ejemplo, utilizaremos la herramienta Rotativa. Esta librería de uso gratuito, podemos integrarla en nuestros proyectos ASP.NET MVC vía NuGet, y es compatible con versiones Microsoft.AspNet.Mvc (>= 5.2.3).
Básicamente, Rotativa para ASP.NET MVC, crea archivos PDF de forma dinámica a partir de cualquier Vista existente en nuestra aplicación MVC. Esta funcionalidad la consigue con el uso de la herramienta wkhtmltopdf, que no es más que una librería de código abierto (LGPLv3) en línea de comandos, que transforma HTML e imágenes en PDF utilizando utilizando el motor de renderizado Qt Web Kit.
Integrando Rotativa en nuestra aplicación ASP.NET MVC
En primer lugar, crearemos un nuevo proyecto Web ASP.NET MVC desde Visual Studio 2019 (o cualquier otra versión), con plataforma de destino .NET Framework 4.7.2 (.NET Framework 4.6.1 o superior).
La instalación de Rotativa la haremos a través de NuGet. Para esto abriremos la consola de administración de paquetes NuGet de Visual Studio (Herramientas > Administrador de paquetes NuGet > Consola del Administrador de paquetes), y ejecutamos el siguiente comando:
PM> Install-Package Rotativa -Version 1.7.3
Una vez finalizada la instalación, observaremos que se ha creado una nueva carpeta en nuestro proyecto de nombre Rotativa
.
Dentro de esta carpeta deberán estar los archivos ejecutables wkhtmltopdf.exe
y wkhtmltoimage.exe
. Estos ejecutables conforman la herramienta wkhtmltopdf, que es necesaria para que Rotativa pueda generar los PDFs.
Además encontraremos los documentos de ayuda help-wkhtmltoimage.txt
y help-wkhtmltopdf.txt
, que son básicamente guías de referencia para configurar la opciones de generación del PDF en la herramienta wkhtmltopdf.
Importante: Para que estos dos ejecutables, sean accesibles cuando publiquemos nuestra aplicación en un entorno de producción, es necesario establecer su propiedad [Copiar en el directorio de salida] = Copiar siempre.
Construyendo nuestra aplicación ASP.NET MVC de ejemplo
El Modelo de datos
Para realizar este ejemplo, crearemos el nuevo Modelo de datos Customer.cs
(tabla Customers
de la base de datos de pruebas Northwind de Microsoft). Como ORM utilizaremos Entity Framework .
Nota: Los Scripts de la tabla Customers y los datos de prueba, están disponibles para descargar al final del artículo.
public class Customer
{
[Key]
[StringLength(5)]
public string CustomerID { get; set; }
[Required]
[StringLength(40)]
public string CompanyName { get; set; }
[Required]
[StringLength(30)]
public string ContactName { get; set; }
[StringLength(30)]
public string ContactTitle { get; set; }
[StringLength(100)]
public string Address { get; set; }
[StringLength(15)]
public string City { get; set; }
[StringLength(15)]
public string Region { get; set; }
[StringLength(10)]
public string PostalCode { get; set; }
[Required]
[StringLength(15)]
public string Country { get; set; }
[StringLength(24)]
public string Phone { get; set; }
[StringLength(24)]
public string Fax { get; set; }
[Required]
[StringLength(100)]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}
Nota: Como no es el objetivo de este artículo explicar como "enlazar" Entity Framework a una base de datos, supondremos que ya hemos definido el
DbContext
de la aplicación con el correspondienteDbSet
del modelo de datosCustomer
en la claseAppDbContext.cs
.
public class AppDbContext : DbContext
{
// You can add custom code to this file. Changes will not be overwritten.
//
// If you want Entity Framework to drop and regenerate your database
// automatically whenever you change your model schema, please use data migrations.
// For more information refer to the documentation:
// http://msdn.microsoft.com/en-us/data/jj591621.aspx
public AppDbContext() : base("name=DefaultConnection")
{
}
public DbSet<Customer> Customers { get; set; }
}
La Vista
A continuación, en la Vista Index.cshtml
de la aplicación, eliminaremos todo el contenido que viene por defecto, y crearemos una tabla <table />
que nos muestre los datos de contacto del Modelo Customer
.
Pero antes de ver como quedaría el código resultante, veremos una serie de puntos a tener en cuenta, en lo que se refiere a la maquetación y estilos CSS de la Vista.
Puntos a tener en cuenta en el formato de la Vista
Como en todo proceso de maquetación, debemos tener en cuenta que el contenido HTML generado, se debe ajustar a las dimensiones de impresión de un folio DIN A4 a la hora de generar nuestro PDF.
Para ello, es aconsejable realizar ciertas modificaciones en los estilos CSS de la Vista, para solucionar una serie problemas de formato reconocidos a la hora de generar el PDF.
El solapamiento en las cabeceras de tablas <table />
Este es un problema identificado y recurrente en Rotativa, que seguramente os encontrareis a la hora de generar PDFs a partir de tablas HTML. El problema consiste en que la cabecera de la tabla se solapa con la primera línea de la siguiente página del listado.
Para solucionarlo deberemos aplicar el siguiente estilo CSS a la tabla:
<style>
/* SOLUCIONA PROBLEMA DE SOLAPAMIENTO EN TABLAS <table /> */
thead { display: table-header-group }
tfoot { display: table-row-group }
tr { page-break-inside: avoid }
</style>
Márgenes de del documento
Para asegurarnos de que la página HTML se ajuste a los márgenes del documento PDF generado, es conveniente eliminar cualquier margin
o padding
que tenga la Vista en los estilos heredados de la misma.
Esto lo haremos en los estilos en linea de la etiqueta <body />
:
<body style="padding: 0px; margin: 0px;">
</body>
Sin página maestra
Siempre que podamos es conveniente crear Vistas que no dependan de una página maestra. Para nuestro ejemplo, la página Index.cshtml
será una página completa e independiente con sus propios estilos CSS.
Recordar siempre en ASP.NET MVC, indicar que no queremos que se renderice la página maestra _Layout.cshtml
:
@{
Layout = null;
}
Una vez realizadas las recomendaciones de formato, ya podemos construir la Vista Index.cshtml
:
@model IEnumerable<Customer>
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ContactPDF</title>
<link rel="stylesheet" href="~/Content/bootstrap.css" />
<link rel="stylesheet" href="~/Content/Site.css" />
<style>
/* SOLUCIONA PROBLEMA DE SOLAPAMIENTO EN TABLAS <table /> */
thead { display: table-header-group }
tfoot { display: table-row-group }
tr { page-break-inside: avoid }
</style>
</head>
<body style="background: #b6ff00; padding: 0px; margin: 0px;">
<div class="container">
<table class="table">
<thead>
<tr>
<th>Nº </th>
<th>
@Html.DisplayNameFor(model => model.CompanyName)
</th>
<th>
@Html.DisplayNameFor(model => model.ContactName)
</th>
<th>
@Html.DisplayNameFor(model => model.Phone)
</th>
<th>
@Html.DisplayNameFor(model => model.Email)
</th>
</tr>
</thead>
<tbody>
@{
var _count = 0;
}
@foreach (var item in Model)
{
_count++;
<tr>
<td>
@_count
</td>
<td>
@Html.DisplayFor(modelItem => item.CompanyName)
</td>
<td>
@Html.DisplayFor(modelItem => item.ContactName)
</td>
<td>
@Html.DisplayFor(modelItem => item.Phone)
</td>
<td>
@Html.DisplayFor(modelItem => item.Email)
</td>
</tr>
}
</tbody>
</table>
</div>
</body>
</html>
La cabecera y el pié de página
En ciertas ocasiones, podemos necesitar que nuestro PDF auto-generado, disponga de una cabecera y un pie personalizados en cada página del documento (por ejemplo una factura, un albarán etc.)
Para esto, y en primer lugar, debemos definir las plantillas Html que actuarán como cabecera y pie de página. Estas plantillas serán Vistas cshtml
como las habituales en nuestro proyecto (no valen Vistas parciales), pudiendo recibir si fuera necesario un Modelo de datos desde el Controlador.
Para este ejemplo crearemos una vista HeaderPDF.cshtml
para la cabecera, con un logotipo corporativo y un título descriptivo.
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>HeaderPDF</title>
<link rel="stylesheet" href="~/Content/bootstrap.css" />
<link rel="stylesheet" href="~/Content/Site.css" />
</head>
<body style="background: #00ffff; padding: 0px; margin: 0px;">
<div class="row">
<div class="col-xs-4">
<img src="~/images/generic-logo.png" />
</div>
<div class="col-xs-8 text-right">
<h2>Customers Lista</h2>
</div>
</div>
</body>
</html>
También crearemos otra para el pie de página FooterPDF.cshtml
, que nos indicará el número de página del documento.
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>ContactFooterPDF</title>
<link rel="stylesheet" href="~/Content/bootstrap.css" />
<link rel="stylesheet" href="~/Content/Site.css" />
</head>
<body style="background: #ffd800; padding: 0px; margin: 0px;">
<div class="text-center">
<strong>
Página <span id='page'></span> de
<span id='topage'></span>
</strong>
<script>
var vars = {};
var x = window.location.search.substring(1).split('&');
for (var i in x) {
var z = x[i].split('=', 2);
vars[z[0]] = unescape(z[1]);
}
document.getElementById('page').innerHTML = vars.page;
document.getElementById('topage').innerHTML = vars.topage;
</script>
</div>
</body>
</html>
Nota: Como vemos en la plantilla para el pie de página, hemos insertado un pequeño script que se encargará de analizar el QueryString de la petición de la página (
window.location.search
), donde Rotativa inserta como parámetros el número de páginapage
y el total de páginastopage
, entre otras variables informativas.
El Controlador
Por último, implementaremos el código que generará el archivo PDF a partir de la Vista anteriormente creada.
Esto lo realizaremos de una manera extremadamente simple y a la vez elegante. Simplemente devolveremos desde la Acción Index()
del Controlador HomeController
, un objeto de la clase Rotativa.ViewAsPdf
el cual recibe como parámetros el nombre de la Vista y el Modelo de datos.
// GET: Customers
public ActionResult Index()
{
// Define la URL de la Cabecera
string _headerUrl = Url.Action("HeaderPDF", "Home", null, "https");
// Define la URL del Pie de página
string _footerUrl = Url.Action("FooterPDF", "Home", null, "https");
return new ViewAsPdf("Index", appDbContext.Customers.ToList())
{
// Establece la Cabecera y el Pie de página
CustomSwitches = "--header-html " + _headerUrl + " --header-spacing 0 " +
"--footer-html " + _footerUrl + " --footer-spacing 0"
,PageSize = Rotativa.Options.Size.A4
//,FileName = "CustomersLista.pdf" // SI QUEREMOS QUE EL ARCHIVO SE DESCARGUE DIRECTAMENTE
,PageMargins = new Rotativa.Options.Margins(40, 10, 10, 10)
};
}
Como podemos ver en el código, hemos hecho referencia a las Acciones HeaderPDF()
y FooterPDF()
. Estas Acciones serán las encargadas de devolver las Vistas que conformarán la cabecera y el pie de página de nuestro PDF auto-generado.
Por lo tanto, incluiremos estas dos Acciones en nuestro HomeController
:
public ActionResult HeaderPDF()
{
return View("HeaderPDF");
}
public ActionResult FooterPDF()
{
return View("FooterPDF");
}
En este punto, y si todo a ido bien, ya podemos ejecutar nuestra aplicación ASP.NET MVC y comprobar como se genera dinámicamente el archivo PDF a partir de la Vista Index.cshtml
.
Nota: Como vemos en el PDF, hemos resaltado en colores las secciones que conforman la cabecera, el cuerpo y el pie de página. Esta división en colores es altamente recomendable durante el proceso de maquetación, ya que nos permite tener una visión exacta del resultado final en lo que respecta a márgenes, espaciado, etc.
Configurando Rotativa.ViewAsPdf()
La clase ViewAsPdf
de la librería Rotativa
, nos permite a través de sus propiedades, configurar múltiples características del archivo PDF generado dinámicamente. Aspectos como el nombre del archivo, la orientación, los márgenes, tamaño, etc. son fácilmente modificables desde las propiedades del objeto ViewAsPdf
.
Una de las propiedades más interesantes de la clase ViewAsPdf
es CustomSwitches
. A través de ella, podemos acceder directamente a la opciones de configuración en línea de comandos de la herramienta wkhtmltopdf.
Como vimos anteriormente en el código del Controlador, es en la propiedad CustomSwitches
donde le indicamos a Rotativa las Vistas que serán utilizadas como cabecera y pie de página del archivo PDF.
// Define la URL de la Cabecera
string _headerUrl = Url.Action("HeaderPDF", "Home", null, "https");
// Define la URL del Pie de página
string _footerUrl = Url.Action("FooterPDF", "Home", null, "https");
return new ViewAsPdf("Index", appDbContext.Customers.ToList())
{
// Establece la Cabecera y el Pie de página
CustomSwitches = "--header-html " + _headerUrl + " --header-spacing 0 " +
"--footer-html " + _footerUrl + " --footer-spacing 0"
,PageMargins = new Rotativa.Options.Margins(40, 10, 10, 10)
};
Nota: Como en todo proceso de maquetación, no siempre quedarán ajustadas a la primera la nueva cabecera y pie de página al documento PDF generado. Para ello, debemos realizar nosotros este ajuste manualmente a través de los parámetros
--header-spacing
y--footer-spacing
de wkhtmltopdf, y la propiedadPageMargins
de la claseViewAsPdf
.
Nuevo comentario
Comentarios
Hola me sale error 401.2 cuando quiero poner el paginado, que puede ser? Gracias
public ActionResult FooterPDF()
{
return View("FooterPDF");
} return new ViewAsPdf("CentroParaImprimir", model)
{ // Establece la Cabecera y el Pie de página
CustomSwitches = "--footer-html " + _footerUrl + " --footer-spacing 0"
,
PageSize = Rotativa.Options.Size.A4
,FileName = "CustomersLista.pdf"
,PageOrientation=Rotativa.Options.Orientation.Landscape
,
PageMargins = new Rotativa.Options.Margins(10, 10, 10, 10)
};
Muchisimas gracias
Por ningún lado encontraba la solución al solapamiento de la cabecera, este post me sirvió en demasía.
Gracias de verdad.
Tengo problemas con rotativa y no se a que se debe, estoy usando rotativa con Net framework 4.7.2 publicado en un servidor windows server 2012 y unas veces funciona y otras no, ahora es más frecuente, sabe alguien a que se debe??? alguna configuración? en mi local funciona perfecto.
muy buena explicación
el único problema que tuve fue que no me creo la carpeta de Rotativa con los ejecutales.
Hola, que tal. Muchas gracias por el contenido. Tengo una duda para cuando quiero generar un pdf con un gráfico utilizando la libre chart.js. Ya que al verlo en la vista muy bien diseñado, pero luego al imprimirlo no aparece tal gráfico. Favor tu ayuda, para la impresión de gráficos con Rotativa.
buen post, pero tengo un problema, cuando envío un modelo al header en algunas páginas sale la data y otras no. ¿cómo podría solucionar esto?