Renderizar tabla con Entity Framework

Hay una serie de tareas cuando, por alguna razón, es más conveniente exportar datos de bases de datos a un documento PDF sin utilizar el esquema de conversión de HTML a PDF que ha ganado popularidad recientemente.

Este artículo te mostrará cómo generar un documento PDF utilizando el Aspose.PDF for .NET.

Conceptos básicos de generación de PDF con Aspose.PDF

Una de las clases más importantes en Aspose.PDF es la clase Document. Esta clase es un motor de renderización de PDF. Para presentar una estructura PDF, la biblioteca Aspose.PDF utiliza el modelo Documento-Página, donde:

  • Documento - contiene las propiedades del documento PDF, incluida la colección de páginas.
  • Página - contiene las propiedades de una página específica y varias colecciones de elementos asociados con esta página.

Por lo tanto, para crear un documento PDF con Aspose.PDF, debes seguir estos pasos:

  1. Crear el objeto Document.
  2. Agregar la página (el objeto Page) al objeto Document.
  3. Crear objetos que se colocarán en la página (por ejemplo, fragmento de texto, tabla, etc.).
  4. Agregar los elementos creados a la colección correspondiente en la página (en nuestro caso, será una colección de párrafos).
  5. Guardar el documento como archivo PDF.

El problema más común es la salida de datos en formato de tabla. La clase Table se utiliza para procesar tablas. Esta clase nos da la capacidad de crear tablas y colocarlas en el documento, utilizando Rows y Cells. Así que, para crear la tabla, necesitas agregar el número requerido de filas y llenarlas con el número apropiado de celdas.

El siguiente ejemplo crea la tabla 4x10.

Al inicializar el objeto Table, se utilizaron las configuraciones mínimas de apariencia:

  • ColumnWidths - ancho de las columnas (por defecto).
  • DefaultCellPadding - los campos predeterminados para la celda de la tabla.
  • Border - atributos del marco de la tabla (estilo, grosor, color).
  • DefaultCellBorder - atributos del marco de la celda (estilo, grosor, color).

Como resultado, obtenemos la tabla 4x10 con columnas de igual ancho.

Tabla 4x10

Exportando datos desde objetos ADO.NET

La clase Table proporciona métodos para interactuar con fuentes de datos ADO.NET - ImportDataTable y ImportDataView. El primer método importa datos desde el DataTable, el segundo desde el DataView. Premitiendo que estos objetos no son muy convenientes para trabajar en la plantilla MVC, nos limitaremos a un breve ejemplo. En este ejemplo (línea 50), se llama al método ImportDataTable y recibe como parámetros una instancia de DataTable y configuraciones adicionales como la bandera de encabezado y la posición inicial (filas/columnas) para la salida de datos.

Exportando datos desde Entity Framework

Más relevante para .NET moderno es la importación de datos desde marcos ORM. En este caso, es una buena idea extender la clase Table con métodos de extensión para importar datos desde una lista simple o desde los datos agrupados. Demos un ejemplo para uno de los ORM más populares - Entity Framework.

public static class PdfHelper
{
    private static void ImportEntityList<TSource>(this Aspose.Pdf.Table table, IList<TSource> data)
    {
        var headRow = table.Rows.Add();

        var props = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance);
        foreach (var prop in props)
        {
            headRow.Cells.Add(prop.GetCustomAttribute(typeof(DisplayAttribute)) is DisplayAttribute dd ? dd.Name : prop.Name);
        }

        foreach (var item in data)
        {
            // Add row to table
            var row = table.Rows.Add();
            // Add table cells
            foreach (var t in props)
            {
                var dataItem = t.GetValue(item, null);
                if (t.GetCustomAttribute(typeof(DataTypeAttribute)) is DataTypeAttribute dataType)
                    switch (dataType.DataType)
                    {

                        case DataType.Currency:
                            row.Cells.Add(string.Format("{0:C}", dataItem));
                            break;
                        case DataType.Date:
                            var dateTime = (DateTime)dataItem;
                            if (t.GetCustomAttribute(typeof(DisplayFormatAttribute)) is DisplayFormatAttribute df)
                            {
                                row.Cells.Add(string.IsNullOrEmpty(df.DataFormatString)
                                    ? dateTime.ToShortDateString()
                                    : string.Format(df.DataFormatString, dateTime));
                            }
                            break;
                        default:
                            row.Cells.Add(dataItem.ToString());
                            break;
                    }
                else
                {
                    row.Cells.Add(dataItem.ToString());
                }
            }
        }
    }

    private static void ImportGroupedData<TKey, TValue>(this Aspose.Pdf.Table table, IEnumerable<Models.GroupViewModel<TKey, TValue>> groupedData)
    {
        var headRow = table.Rows.Add();
        var props = typeof(TValue).GetProperties(BindingFlags.Public | BindingFlags.Instance);
        foreach (var prop in props)
        {
            headRow.Cells.Add(prop.GetCustomAttribute(typeof(DisplayAttribute)) is DisplayAttribute dd ? dd.Name : prop.Name);
        }

        foreach (var group in groupedData)
        {
            // Add group row to table
            var row = table.Rows.Add();
            var cell = row.Cells.Add(group.Key.ToString());
            cell.ColSpan = props.Length;
            cell.BackgroundColor = Aspose.Pdf.Color.DarkGray;
            cell.DefaultCellTextState.ForegroundColor = Aspose.Pdf.Color.White;

            foreach (var item in group.Values)
            {
                // Add data row to table
                var dataRow = table.Rows.Add();
                // Add cells
                foreach (var t in props)
                {
                    var dataItem = t.GetValue(item, null);

                    if (t.GetCustomAttribute(typeof(DataTypeAttribute)) is DataTypeAttribute dataType)
                        switch (dataType.DataType)
                        {
                            case DataType.Currency:
                                dataRow.Cells.Add(string.Format("{0:C}", dataItem));
                                break;
                            case DataType.Date:
                                var dateTime = (DateTime)dataItem;
                                if (t.GetCustomAttribute(typeof(DisplayFormatAttribute)) is DisplayFormatAttribute df)
                                {
                                    dataRow.Cells.Add(string.IsNullOrEmpty(df.DataFormatString)
                                        ? dateTime.ToShortDateString()
                                        : string.Format(df.DataFormatString, dateTime));
                                }
                                break;
                            default:
                                dataRow.Cells.Add(dataItem.ToString());
                                break;
                        }
                    else
                    {
                        dataRow.Cells.Add(dataItem.ToString());
                    }
                }
            }
        }
    }
}

Los atributos de Data Annotations se utilizan a menudo para describir modelos y nos ayudan a crear la tabla. Por lo tanto, se eligió el siguiente algoritmo de generación de tabla para ImportEntityList:

  • líneas 12-18: construir una fila de encabezado y agregar celdas de encabezado de acuerdo con la regla “Si el DisplayAttribute está presente, entonces toma su valor, de lo contrario toma el nombre de la propiedad”.
  • líneas 50-53: construir las filas de datos y agregar celdas de fila de acuerdo con la regla “Si el atributo DataTypeAttribute está definido, entonces verificamos si necesitamos hacer configuraciones de diseño adicionales para él, y de lo contrario simplemente convertimos los datos a cadena y los agregamos a la celda;”.

En este ejemplo, se realizaron personalizaciones adicionales para DataType.Currency (líneas 32-34) y DataType.Date (líneas 35-43), pero puedes agregar otras si es necesario. El algoritmo del método ImportGroupedData es casi el mismo que el anterior. Se utiliza una clase adicional GroupViewModel, para almacenar los datos agrupados.

using System.Collections.Generic;
public class GroupViewModel<K,T>
{
    public K Key;
    public IEnumerable<T> Values;
}

Dado que procesamos grupos, primero generamos una línea para el valor clave (líneas 66-71), y después las líneas de este grupo.