Access y DataBinding en C#: obtener PDF desde HTML

Aplicación que carga datos desde una base de datos de Microsoft Access, los muestra en unos controles TextBox que se enlazan a los datos por medio de DataBinding y genera un informe PDF usando NReco.PDFGenerator a partir de un archivo HTML intermedio obtenido al aplicar transformación XSL a un archivo XML que reproduce el estado actual de selección de registros, en C#

DataBinding: enlace de datos a controles

En este ejercicio se enlazan directamente controles TextBox del formulario con campos del origen de datos. Para enlazar controles de un formulario con un origen de datos se usa la propiedad DataBinding que sirve para obtener acceso a la ControlBindingsCollection que representa a la colección de enlaces de datos de un control. Al objeto Binding le pasamos la propiedad del control que se debe enlazar, el origen de datos (DataSet) y la tabla o columna. El objeto Binding se añade a la colección de enlaces de datos del control elegido utilizando el método Add().

//conexión de tipo OleDb
private OleDbConnection conn = new OleDbConnection();
//almacena la ruta a la base de datos
private string ruta;
//DataAdapter: adaptador de datos
private OleDbDataAdapter adaptador; private DataSet datos; //DataSet: conjunto de datos desconectados
//constructor de comandos para el DataAdapter
private OleDbCommandBuilder comando; private bool abierto = false; //para saber si ya está abierta la base de datos
//administrador de los objetos de enlace a datos
private BindingManagerBase enlaceBase;
//objeto que enlaza control y datos
private Binding enlace;

//ruta a la base de datos en la carpeta del ensamblado
ruta = Application.StartupPath + "\\500empresas.mdb";
// para usar BD de Access con extensión accdb (también sirve para .mdb) hay que recurrir a Provider=Microsoft.ACE.OLEDB.12.0
conn.ConnectionString = "Provider=Microsoft.ACE.OLEDB.16.0;Data Source=" + ruta + ";User Id=admin;Password=;";
// para usar BD de Access con extensión mdb más antigua hay que recurrir a Provider=Microsoft.Jet.OLEDB.4.0
//con.ConnectionString = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" & ruta & ";User Id=admin;Password=;"

//DataAdapter: objeto que hace de puente entre la base de datos y el DataSet o conjunto de datos
adaptador = new OleDbDataAdapter("SELECT * FROM AgendaTb ORDER BY Nombre", conn);
//DataAdapter puede contener sentencias SQL y objetos Command
comando = new OleDbCommandBuilder(adaptador);
//DataSet: contiene una copia de la base de datos, en esquema XML, independiente del proveedor, con sus elementos tablas y relaciones. Es el verdadero almacén de datos desconectados: actuamos sobre el DataSet desconectado y el DataAdapter se conecta para volcar los datos entre base de datos y DataSet, en ambos sentidos
datos = new DataSet();

//Abrir la conexión, el DataAdapter llena el DataSet, cerrar la conexión. DataAdapter se encarga de abrir y cerrar la conexión
//conn.Open();
adaptador.Fill(datos, "AgendaTb");
//conn.Close();

//Para enlazar controles del formulario con el DataSet usamos DataBinding; al objeto Binding le pasamos la propiedad del control que se debe enlazar, el DataSet y la tabla o columna. El objeto Binding se añade a la colección de enlaces de datos (ControlBindingsCollection) del control elegido, con el método Add()
enlace = new Binding("Text", datos, "AgendaTb.Nombre");
txNombre.DataBindings.Add(enlace);
enlace = null;
enlace = new Binding("Text", datos, "AgendaTb.Teléfono1");
txTel1.DataBindings.Add(enlace);
enlace = null;
enlace = new Binding("Text", datos, "AgendaTb.Teléfono2");
txTel2.DataBindings.Add(enlace);
enlace = null;

//Al objeto BindingManagerBase le pasamos el DataSet y la tabla desde el objeto BindingContext del formulario
this.enlaceBase = this.BindingContext[datos, "AgendaTb"];

En una etiqueta se informa del nº de orden del registro mostrado y del total del nº de registros de la vista actual.

lbRegistro.Text = "Registro " + (enlaceBase.Position + 1).ToString() + " de " + (this.enlaceBase.Count).ToString();

Gracias al enlace automático entre controles y datos (DataBinding) es muy sencillo navegar entre registros y se necesita poco código. El objeto enlaceBase (BindingManagerBase) se encarga de actualizar de manera automática el control con los datos de la fila actual.

//avanzar un registro
this.enlaceBase.Position++;

//retroceder un registro
this.enlaceBase.Position--;

 //ir al primer registro
this.enlaceBase.Position = 0;

//ir al último registro
this.enlaceBase.Position = this.enlaceBase.Count - 1;

Espacios de nombres

Hay que importar espacios de nombres para que la aplicación funcione.

//conectar con bases de datos de Microsoft Access
using System.Data.OleDb;
//transformación XSLT de archivos XML
using System.Xml.Xsl;
//lectura y escritura de archivos en el disco
using System.IO;
//procesos del sistema local
using System.Diagnostics;

Buscar texto coincidente

Hay un cuadro de texto en el que se puede escribir y 2 botones de búsqueda, uno busca el texto introducido al principio del campo Nombre y otro lo busca en cualquier lugar del campo Nombre. Después se vacía el DataSet de los datos que ya contiene y se vuelve a rellenar desde OleDbDataAdapter con el comando SQL.

//crear nuevos ojetos OleDbDataAdapter y OleDbCommandBuilder con sentencia SQL para buscar al principio del nombre
adaptador = new OleDbDataAdapter("SELECT * FROM AgendaTb WHERE Nombre LIKE '" + this.txBuscar.Text + "%'", conn);
//crear nuevos ojetos OleDbDataAdapter y OleDbCommandBuilder con sentencia SQL para buscar en cualquier lugar del nombre
adaptador = new OleDbDataAdapter("SELECT * FROM AgendaTb WHERE Nombre LIKE '%" + this.txBuscar.Text + "%'", conn);

comando = new OleDbCommandBuilder(adaptador);
//vaciar el DataSet y rellenarlo de nuevo
datos.Clear();
adaptador.Fill(datos, "AgendaTb");

XML desde el DataSet transformado en HTML como origen del informe PDF

Si el informe generado como PDF carga los datos directamente desde la base de datos de Access, se muestran para imprimir todos los registros de la tabla independientemente de los criterios de búsqueda o selección que estén aplicados. Un truco para solucionar esto y que el informe muestre los registros que en ese momento están seleccionados es, cada vez que se pulsa el botón que muestra el informe, volcar el DataSet actual en un archivo XML en la carpeta del ensamblado y que este archivo XML sea el origen de los datos del archivo HTML y del informe PDF

void XmlUpdate()
{
	//Actualizar DataAdapter, se puede pasar la tabla como parámetro de 2 maneras: datos as DataSet, "tabla" as string o datos.tables("tabla") as DataSet
	this.adaptador.Update(this.datos.Tables["AgendaTb"]);
	//Vaciar el DataSet: conserva los datos de tablas y registros y si no se limpia acumula las operaciones sucesivas de modificación de filas
	this.datos.Clear();
	//Volver a llenar el DataSet desde el DataAdapter ya modificado
	this.adaptador.Fill(datos, "AgendaTb");
	//actualizar el archivo XML con la selección actual de registros
	this.datos.WriteXml("500empresas.xml");
}

El archivo XML es transformado en HTML de acuerdo con una hoja de estilos XSL creada para ello y guardada en la carpeta del ejecutable. El archivo XSL configura el diseño HTML del archivo XML para mejorar su presentación. El archivo HTML actúa como fuente del informe PDF gracias a NReco.PDFGenerator, conversor de archivos HTML en PDF para Visual Studio .NET con licencia libre. La hoja de estilos XSL tiene este contenido:

<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:template match="/">
    <head>
      <title>500 empresas</title>
    </head>
    <html>
      <body style="font-family: Arial; font-size: 10pt;">
        <div align="center">
          <h3>
            <u>Agenda con 500 empresas</u>
          </h3>
          <br></br>
          <table width="500" border="0">
            <tr style="font-family: Arial; font-size: 10pt;">
              <td width="300" align="left" style="background-color:#f0f2f3;">
                <b>
                  <font color="Black">Nombre</font>
                </b>
              </td>
              <td width="100" align="left" style="background-color:#f0f2f3;">
                <b>
                  <font color="Black">Teléfono 1</font>
                </b>
              </td>
              <td width="100" align="left" style="background-color:#f0f2f3;">
                <b>
                  <font color="Black">Teléfono 2</font>
                </b>
              </td>
            </tr>
            <tr>
              <td align="center" colspan="4">
                <!--<HR></HR>-->
                <br></br>
              </td>
            </tr>
            <xsl:for-each select="NewDataSet/AgendaTb">
              <xsl:sort select="Nombre" order="ascending"></xsl:sort>
              <tr bgcolor="white" style="font-family: Arial; font-size: 10pt;">
                <td align="left">
                  <font color="black">
                    <xsl:value-of select="Nombre"></xsl:value-of>
                  </font>
                </td>
                <td align="left">
                  <font color="black">
                    <xsl:value-of select="Teléfono1"></xsl:value-of>
                  </font>
                </td>
                <td align="left">
                  <font color="black">
                    <xsl:value-of select="Teléfono2"></xsl:value-of>
                  </font>
                </td>
              </tr>
              <tr>
                <td align="left" colspan="4">
                  <HR width="100%"></HR>
                </td>
              </tr>
            </xsl:for-each>
          </table>
        </div>
      </body>
    </html>
  </xsl:template>
</xsl:stylesheet>

Lo que hace es generar una tabla cuya línea primera es la cabecera de las columnas con los nombres de los campos y el resto de filas se rellena con bucles xsl:for-each para mostrar los registros actuales del DataSet leídos desde el archivo XML. Esto produce un archivo HTML con el formato diseñado. NReco.PDFGenerator coge el archivo HTML y lo convierte en un archivo PDF apto para impresión. La conversión es buena pero el aspecto del PDF no es totalmente idéntico al HTML.

El código que efectúa la transformación XSLT del archivo XML recure a la clase XslCompiledTransform que transforma datos XML utilizando una hoja de estilos XSLT. Su método Load carga la hoja de estilos y su método Transform convierte el archivo XML en HTML aplicando la hoja de estilos.

string ruta1 = "500empresas.xsl";
string ruta2 = "500empresas.xml";
string ruta3 = "500empresas.html";
XslCompiledTransform xslt = new XslCompiledTransform();
xslt.Load(ruta1);
xslt.Transform(ruta2, ruta3);

Generar el informe PDF con NReco.PdfGenerator

Para utilizar NReco hay que instalar el paquete NuGet NReco.PdfGenerator, generar una instancia de la clase HtmlToPdfConverter que convierte un archivo HTML en un archivo PDF y llamar al método GeneratePdfFromFile al que se le pasan 3 parámetros: string archivo HTML de entrada obligatorio, página de portada opcional y string archivo PDF de salida obligatorio.

//generar una instancia de la clase HtmlToPdfConverter
var html2Pdf = new NReco.PdfGenerator.HtmlToPdfConverter();
//opciones de personalización del archivo PDF
html2Pdf.Zoom = 1.2F;
//html2Pdf.PageWidth = 210;
//html2Pdf.PageHeight = 297;
//método GeneratePdfFromFile que genera el PDF desde el HTML
html2Pdf.GeneratePdfFromFile(Application.StartupPath + "\\500empresas.html", null, Application.StartupPath + "\\500empresas.pdf");

Mostrar los archivos HTML y PDF con su programa asociado en Windows

Se utiliza la clase ProcessStartInfo de System.Diagnostics que especifica valores para iniciar procesos en Windows: la propiedad FileName para el nombre de la aplicación o el documento y la propiedad Verb para el modo de apertura (abrir, editar, imprimir, etc.). La clase Process de System.Diagnostics permite iniciar y detener procesos del sistema local y su método Start inicia el proceso pasando el nombre del archivo. Con este código se abren ambos archivos, HTML y PDF, y se pueden comparar viendo sus diferencias.

ProcessStartInfo abrirPdf = new ProcessStartInfo();
abrirPdf.FileName = Application.StartupPath + "\\500empresas.pdf";
//modo de apertura de archivo (también puede ser edit, print...)
abrirPdf.Verb = "open";
//abrir en ventana normal
abrirPdf.WindowStyle = ProcessWindowStyle.Normal;
//abrir el archivo mediante su programa asociado en Windows
Process.Start(abrirPdf);  

Puedes descargar un archivo ZIP con la aplicación C#.