Hace cosa de un año escribí un post hablando de paginación en ASP NET CORE pero hoy vamos a amplicar un poco más ese mismo blog. El código no lo voy a volver a escribir, ya que es exáctametne el mismo. Pero comentar que, a diferencia de lo que comentaba en ese blog, he ampliado el concepto y probado en todas las plataformas que nos obrece .NET satisfactoramente.
He subudo a mi GitHub un repositorio con todos los ejemplos que se ma han ocurrido. Tomando un poco de especial atención en un componente Blazor para ser utilizado en Web Asembly o Server. Éste es el código que os voy a compartir en este blog en el día de hoy.
Creando un projecto de librería
Lo ideal para trabajar con este tipo de utilidades es crear un projecto de tipo ClassLibrary y crear en éste projecto el código necesario para hacer la paginación. Pero para gustos código. En éste proyecto es dónde pondremos el código del blog paginación en ASP NET CORE y haremos una referencia de proyecto en visual estudio desde nuestro projecto de Blazor.
Creando un componente genérico que utilice la librería.
Ahora, para seguir utilizando las bondades de los componentes, vamos a crear un componente un poco genérico, bastante básico, que como siempre se podría refactorizar y mejorar, pero como introducción, puede que os ayude en más de una ocasión.
Agregamos un componente en nuestra carpeta SHARED con el nombre que más nos guste. Yo lo voy a llamar PaginationComponent.razor, y como amí me gusta más trabajar con un archivo separado de código, voy a crear un fichero de clase al que le pondremos el mismo nombre PaginationComponent.razor.cs, y no olvidar, hacer la case partial ya que para el framework es sólo una clase, el componente y el archivo de código. Comparto los archivos por separado y los comentaré para explicar que estoy haciendo en cada uno.
El código
Voy a comenzar explicando el código. Luego veremos cómo se ha creado el componente para visualizar los datos.
public partial class PaginationComponent< T >
{
#region Items
[Parameter] public IEnumerable< T > Items { get; set; }
#endregion
#region list
[Parameter] public string TableContainerCss { get; set; } = "table-container";
[Parameter] public RenderFragment Head { get; set; }
///
/// Only one with this property
///
[Parameter(CaptureUnmatchedValues = true)]
public Dictionary AdditionalAttributes { get; set; }
[Parameter] public RenderFragment Body { get; set; }
[Parameter] public RenderFragment Loading { get; set; }
[Parameter] public RenderFragment Empty { get; set; }
[Parameter] public int PageSize { get; set; } = 10;
#endregion
#region Paging footer
[Parameter] public string ContainerCss { get; set; }
[Parameter] public string ElementCss { get; set; }
[Parameter] public string ActiceCss { get; set; }
[Parameter] public int Columns { get; set; } = 1;
#endregion
PagedList PagedItems;
MarkupString DefaultHead;
MarkupString DefaultBody;
private readonly string DefaultCSSClass = "table table-responsive is-bordered is-striped is-hoverable is-fullwidth";
protected override void OnParametersSet()
{
DrawList(1);
}
void ToPage(int page)
{
PagedItems = PagedList< T >.ToPagedList(Items, page, 10);
DrawList(page);
}
void DrawList(int page)
{
if (Items != null)
{
PagedItems = PagedList< T >.ToPagedList(Items, page, PageSize);
if (AdditionalAttributes == null)
{
AdditionalAttributes = new Dictionary();
}
if (!AdditionalAttributes.ContainsKey("class"))
{
//check if have a table css class on the model send
DisplayTableAttribute cssClass = typeof(T).GetCustomAttribute();
if (cssClass != null && cssClass.TableClass != null) AdditionalAttributes.Add("class", cssClass.TableClass);
else AdditionalAttributes.Add("class", DefaultCSSClass);
}
PropertyInfo[] properties = typeof(T).GetProperties(BindingFlags.Public | //get public names
BindingFlags.Instance); //get instance names
//get all my attributes
DisplayTableAttribute[] attributes = new DisplayTableAttribute[properties.Length];
StringBuilder html;
if (Body == null)
{
if (Head == null)
{
html = new StringBuilder();
for (int i = 0; i < Columns; i++)
{
attributes[i] = properties[i].GetCustomAttribute< DisplayTableAttribute >(); //get if my custom attributes
//custom header class
string OpenTHTag = attributes[i] != null && attributes[i].HeaderClass != null ? $"< th class="{attributes[i].HeaderClass}" >" : "< th >";
//custom header name
Attribute alias = Attribute.GetCustomAttribute(properties[i], typeof(DisplayAttribute)); //get if have attribute display to change the name of the property
string header = attributes[i] != null && attributes[i].Header != null ? attributes[i].Header : //custom header name
alias == null ? properties[i].Name : ((DisplayAttribute)alias).GetName(); //if not get the display attribute or name
html.Append($"{OpenTHTag}{header}< /th >");
}
DefaultHead = new MarkupString(html.ToString());
}
Columns = properties.Length;
html = new StringBuilder();
//get all the item to show the values
foreach (T item in PagedItems)
{
html.Append("< tr >");
//show the values
for (int i = 0; i < properties.Length; i++)
{
attributes[i] = properties[i].GetCustomAttribute(); //get if my custom attributes
string OpenTDTag = attributes[i] != null && attributes[i].ColClass != null ? $"< td class="{attributes[i].ColClass}" >" : "< td >";
var value = attributes[i] != null && attributes[i].ValueFormat != null ? string.Format(attributes[i].ValueFormat, properties[i].GetValue(item)) : properties[i].GetValue(item);
html.Append($"{OpenTDTag}{value}< /td >");
}
html.Append("");
}
DefaultBody = new MarkupString(html.ToString());
}
}
}
}
Primero imprescindible, hacer que nuestra clase sea genérica y que reciba el tipo de dato que vamos a iterar en la lista. Luego, como podrás imaginar, necesitamos recibir como parámetro la lista y vamos a almacenarlo como un IEnumerable< T >. Luego para confugrar nuestro componente y hacer que se pueda utilizar prácticamente en cualquier lado.
- TableContainerCss: recibiremos la case para formatear al contenedor de la lista.
- Head: es un render fragment para recibir como queremos formatear la cabecera de nuestra tabla.
- AdditionalAttributes: podremos pasar una lista de atributos para formatear la tabla
- Body: es un render fragment para recibir como queremos mostrar los datos dentro de la tabla. Es decir, como mostrar cada columna de datos.
- Loading: es un render fragment para mostrar el tipico cargando o gif para que el usuario sepa que los datos aún no están disponibles.
- Empty: el último render fragment para mostrar al usuario cuando no hay datos.
- ContainerCss: clase CSS para formatear el contenedor de los botones de selección de página.
- ElementCss: clase CSS para formatear el botón de selección de página.
- ActiveCss: clase CSS para formatear el botón de la página activa en cada momento.
- Columns: numero de columnas que hay en la tabla.
Hasta aquí todos los parámetros para poder configurar el componente. Ahora veamos las variables y funciones de apoyo y manejo del componente.
- PagedList< T > PagedItems: esta es la variable que va a contener la lista ya paginada.
- MarkupString DefaultHead: si no se envía un Head con esta variable crearemos la cabecera dependiendo del nombre de las propiedades.
- MarkupString DefaultBody: si no se envía un Body con esta varialbe createmos la lista que va a mostrar todas las filas y columnas dependiendo de las propiedades de la case enviada.
- DrawList(int page): este método será el encargado de dibujar la tabla si no se ha enviado Body y Head. Recibe el número de página en la que nos encontramos en este momento. Dentro de éste método lo primero que se comprueba es que hayamos recibido datos con el parámetro Items y entonces hacemos la paginación y la almacenamos en la variable de apoyo PagedItems.
- OnParameterSet(): cuando el componente reciba los parámetros invocará al método DrawList(1) para hacer qeu se dibuje la primera página.
- ToPage(int page): éste método es el que será invocado desde los botones de paginación invoicando al método DrawList(page) para hacer que dibuje, si es necesario, la página correspondiente recibida como parámetro.
Poco más que explicar, creo que el código es bastante legible y comprensible, pasemos ahora a ver como está definido el componente que muestra los datos.
El componente
@typeparam T
@if (Items is null)
{
if (Loading is not null)
{
@Loading
}
else
{
< text >...< /text >
}
}
else if (PagedItems.TotalCount > 0)
{
< div class="@TableContainerCss" >
< table @attributes="AdditionalAttributes" >
< thead >
< tr >
@if (Head is not null)
{
@Head
}
else
{
@DefaultHead
}
< /tr >
< /thead >
< tbody >
@if (Body is not null)
{
foreach (T item in PagedItems)
{
< tr >
@this.Body(item)
< /tr >
}
}
else
{
@DefaultBody
}
< /tbody >
< tfooter >
< tr >
< td colspan="@Columns" >
< div class="@ContainerCss" >
< ul >
@{
string active = 1 == PagedItems.CurrentPage ? ActiceCss : "";
int i;
if (PagedItems.CurrentPage > 5)
{
i = PagedItems.CurrentPage - 5;
}
else
{
i = 1;
}
if (PagedItems.CurrentPage > 5)
{
< li class="@ElementCss" @onclick="() => ToPage(1)" ><<< /li >
< li class="@ElementCss" @onclick="() => ToPage(PagedItems.CurrentPage - 1)" ><< /li >
}
int c = 0;
do
{
int p = i;
if (i <= PagedItems.TotalPages)
{
active = i == PagedItems.CurrentPage ? ActiceCss : "";
< li class="@ElementCss @active" @onclick="() => ToPage(p)" >@i< /li >
}
else c = 10;
i++;
c++;
} while (c < 10 && i <= PagedItems.TotalPages);
if (PagedItems.CurrentPage < PagedItems.TotalPages - 5)
{
< li class="@ElementCss" @onclick="() => ToPage(PagedItems.CurrentPage+1)" >>< /li >
< li class="@ElementCss" @onclick="() => ToPage(PagedItems.TotalPages)" >>>< /li >
}
}
< /ul >
< /div >
< /td >
< /tr >
< /tfooter >
< /table >
< /div >
}
else
{
@Empty
}
Primero definimos que nuestro componente recibe un parámetro de tipo con la instrucción @typeparam T. Con ésto es como preparamos el componente para decibir un tipo de dato genérico, por lo general en Blazor siempre recibirá un List< MiTipoDato >.
Luego dividiremos el componente en 3 estados:
1. Cuando aun no ha recibido los datos, o éstos son nulos.
2. Cuando ya tiene los datos y hay algo que mostrar.
3. Cuando ya tiene los datos, no son nulos pero no tiene nada que mostrar.
Luego el componente se apolla de una variable de tipo PagedList< T > donde se hace la paginación de la lista que vamos a recibir como parámetro. En este caso lo he llamado a este parámetro Items.
Consulta todo el código con diferentes ejemplos en el GitHub que he creado