Blazor WebAssembly - Paginação com CascadingValue
Hoje vamos criar uma aplicação Blazor WebAssembly e realizar a paginação usando uma abordagem bem simples. |
No
Blazor, a paginação é implementada através de componentes e técnicas que
permitem criar controles de navegação e exibição dos dados paginados. Esses
componentes geralmente incluem botões de página anterior e página seguinte,
números de página, opção para selecionar o número de itens exibidos por página e
outras funcionalidades relacionadas.
Para implementar a paginação no Blazor, geralmente são utilizados os seguintes passos:
Ao implementar a paginação no Blazor, é importante considerar a eficiência no carregamento e processamento dos dados, bem como garantir uma experiência de usuário intuitiva e responsiva.
Existem bibliotecas e componentes prontos disponíveis para auxiliar na implementação da paginação no Blazor, como o BlazorPager, Blazorise e Blazorise DataGrid, que oferecem funcionalidades avançadas e personalizáveis para a paginação de dados.
Neste artigo vou mostrar uma abordagem onde vamos criar um componente CascadingValue para paginação que compartilha o próprio componente como valor para todos os seus componentes filhos.
Para isso vamos criar uma aplicação Blazor WebAssembly que vai obter os dados a partir do site do JSONPlaceholder.
O JSONPlaceholder é um serviço online gratuito que fornece uma API RESTful para testar e simular interações com uma API real. Ele é comumente usado para fins de desenvolvimento e teste de aplicativos que consomem APIs.
O JSONPlaceholder oferece endpoints para recursos de exemplo, como posts, comentários, álbuns de fotos, tarefas, usuários e outros. Esses endpoints permitem realizar operações típicas de uma API REST, como criar, ler, atualizar e excluir (CRUD) dados.
Vamos obter os dados a partir da uri : https://jsonplaceholder.typicode.com/posts que vai retornar dados de posts contendo as informações de: userid, id, title e body:
Criando o projeto
Vamos abrir o Visual Studio 2022 clicar em Create a new Project e selecionar o template Blazor WebAssembly app :
Informe o nome do projeto BlazorPagination e a seguir defina as seguintes configurações :
Ao clicar no botão Create teremos o projeto criando onde vamos remover os arquivos não usados.
Vamos incluir a imagem posts1.jpg na pasta wwwroot do projeto e ajustar o código do componente Index.razor para exibir esta imagem:
@page
"/" < PageTitle>Index</PageTitle>< h1>Posts</h1>< img src="/posts1.jpg" width="500" height="270"/>
|
A seguir vamos ajustar o código do componente NavMenu.razor para exibir o menu para Posts:
... <div class="@NavMenuCssClass nav-scrollable" @onclick="ToggleNavMenu"> <nav class="flex-column"> <div class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="oi oi-home" aria-hidden="true"></span> Home </NavLink> </div> <div class="nav-item px-3"> <NavLink class="nav-link" href="posts"> <span class="oi oi-plus" aria-hidden="true"></span> Posts </NavLink> </div> </nav> </div> ... |
Implementando a paginação
Para iniciar a implementação da paginação vamos criar na pasta Models a classe PageItem que vai ser usada para representar e manipular itens de uma página em um contexto específico, como em um sistema de paginação de um aplicativo, onde cada item de página pode ter um texto, um índice, além de indicadores de habilitação e atividade.
public
class
PageItem { public string Text { get; set; } public int PageIndex { get; set; } public bool Enabled { get; set; } public bool Active { get; set; } public PageItem(int pageIndex, bool enabled, string text) { this.PageIndex = pageIndex; this.Enabled = enabled; this.Text = text; } } |
Essa classe representa um item de página e possui as seguintes propriedades:
Além disso, a classe possui um construtor que aceita três parâmetros: pageIndex (índice da página), enabled (habilitado) e text (texto) , e inicializa as propriedades PageIndex, Enabled e Text com os valores passados como argumentos.
Ainda na pasta Models vamos criar a classe Post contendo as propriedades que representam os dados que vamos obter da API:
public
class
Post { public int UserId { get; set; } public int Id { get; set; } public string? Title { get; set; } public string? Body { get; set; } } |
A seguir vamos criar na pasta Shared do projeto o componente Pagination.razor :
<div class="p-xl-2">
<nav aria-label="Paginação">
<ul class="pagination">
@foreach (var pageItem in pageItems)
{
<li @onclick="@(() => SelectCurrentPage(pageItem))"
class="page-item @(pageItem.Active ? "active" : null)
@(pageItem.Enabled ? null : "disabled")">
<span class="page-link" href="#">@pageItem.Text</span>
</li>
}
</ul>
</nav>
</div>
@code {
private List<PageItem>? pageItems;
[Parameter]
public int PageIndex { get; set; }
[Parameter]
public int TotalPages { get; set; }
[Parameter]
public int Radius { get; set; }
[Parameter]
public EventCallback<int> OnSelectedPage { get; set; }
protected override void OnParametersSet()
{
CreatePages();
}
private void CreatePages()
{
pageItems = new List<PageItem>();
// 1. Cria a página anterior
var hasPreviosPage = PageIndex > 1;
var previousPageIndex = PageIndex - 1;
pageItems.Add(new PageItem(previousPageIndex, hasPreviosPage, "Prev"));
// 2. Cria as paginas e as adiciona a lista de pageItems
if (Radius >= TotalPages)
Radius = TotalPages - 1;
for (int i = 1; i <= TotalPages; i++)
{
if (i >= PageIndex - Radius && i < PageIndex + Radius)
{
pageItems.Add(new PageItem(i, true, i.ToString())
{
Active = PageIndex == i
});
}
}
// 3. Cria a próxima pagina
var hasNextPage = PageIndex < TotalPages;
var nextPageIndex = PageIndex + 1;
pageItems.Add(new PageItem(nextPageIndex, hasNextPage, "Next"));
}
private async Task SelectCurrentPage(PageItem pageItem)
{
if (PageIndex == pageItem.PageIndex)
{
return;
}
if (!pageItem.Enabled)
{
return;
}
PageIndex = pageItem.PageIndex;
await OnSelectedPage.InvokeAsync(pageItem.PageIndex);
}
}
|
Vamos entender o código:
A estrutura HTML do componente inclui uma <div> com a classe "p-xl-2" que envolve uma navegação (<nav>) e uma lista não ordenada (<ul>) com a classe "pagination". Dentro da lista, há um loop foreach que itera sobre uma lista de objetos PageItem chamada pageItems. Aqui estamos usando os recursos do bootstrap para criar o paginador.
Dentro do loop foreach, cada pageItem é renderizado como um item de lista (<li>). O atributo @onclick é usado para associar o método SelectCurrentPage(pageItem) ao evento de clique do item de lista. A classe do item de lista é condicionalmente definida como "active" se o pageItem estiver ativo ou "disabled" se o pageItem não estiver habilitado.
Dentro do item de lista, há um elemento <span> com a classe "page-link" que exibe o texto pageItem.Text.
No bloco @code, são definidos os parâmetros e métodos do componente. Os parâmetros incluem PageIndex, TotalPages, Radius e OnSelectedPage.
O método OnParametersSet é invocado quando os parâmetros do componente são definidos. Dentro desse método, o método CreatePages é chamado para criar os pageItems.
O método CreatePages cria a lista de pageItems com base nos parâmetros do componente. Primeiro, ele cria um item de página anterior e adiciona à lista. Em seguida, cria os itens de página para o intervalo PageIndex - Radius a PageIndex + Radius e os adiciona à lista. Por fim, cria um item de página seguinte e o adiciona à lista.
O método SelectCurrentPage é chamado quando um item de página é selecionado. Ele verifica se a página selecionada é diferente da página atual e se o item de página está habilitado. Em seguida, atualiza o PageIndex com o valor selecionado e invoca o evento OnSelectedPage para notificar outros componentes sobre a seleção da página.
Precisamos definir no arquivo _Imports.razor o código a seguir :
@using
System.Net.Http @using System.Net.Http.Json @using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Web @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.Components.WebAssembly.Http @using Microsoft.JSInterop @using BlazorPagination @using BlazorPagination.Shared @using BlazorPagination.Models |
Agora na pasta Shared vamos criar o componente PageIndexStateProvider.razor :
<CascadingValue
Value="this"> @ChildContent </CascadingValue>
@code { public RenderFragment? ChildContent { get; set; } public int PageIndex { get; set; } = 1; } |
Esse código cria um componente CascadingValue que compartilha o próprio componente como valor para todos os seus componentes filhos. O conteúdo filho é renderizado através do parâmetro ChildContent. O componente também possui uma propriedade PageIndex.
A tag <CascadingValue> define um valor que pode ser compartilhado com todos os componentes filhos dentro de uma árvore de componentes. O valor compartilhado é especificado pelo atributo "Value" e, no caso, está definido como "this", o que significa que o valor compartilhado é o próprio componente em que o código está sendo utilizado.
Para concluir vamos alterar o código do componente MainLayout.razor e envolver o @Body com este componente:
@inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<main>
<div class="top-row px-4">
<a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
</div>
@*Application level state provider*@
<PageIndexStateProvider>
<article class="content px-4">
@Body
</article>
</PageIndexStateProvider>
</main>
</div>
|
Aqui vemos o componente PageIndexStateProvider sendo usado como um provedor de estado de nível de aplicativo. Ele envolve um <article> com a classe "content px-4", que representa a seção de conteúdo principal da página. O atributo @Body é usado para renderizar o conteúdo do componente filho dentro desse elemento <article>. O componente PageIndexStateProvider é responsável por fornecer e gerenciar o estado da página, que é compartilhado entre os componentes dentro dele.
Fazendo a Paginação dos dados
Agora vamos criar na pasta Pages o componente Posts.razor para exibir os posts paginados acessando os dados na uri : https://jsonplaceholder.typicode.com/posts :
@page "/posts"
@inject HttpClient Http
<h2 class="bg-primary text-white">Posts</h2>
<table class="table table-sm table-bordered table-striped ">
<thead>
<tr>
<th>UserId</th>
<th>Title</th>
<th>Body</th>
</tr>
</thead>
<tbody>
@foreach (Post post in posts)
{
<tr>
<td>@post.Id</td>
<td>@post.Title</td>
<td>@post.Body</td>
</tr>
}
</tbody>
<tfoot>
<Pagination TotalPages="@(totalPages != 0 ? totalPages : 1)"
PageIndex="@State.PageIndex"
Radius="3"
OnSelectedPage="@SelectedPage">
</Pagination>
</tfoot>
</table>
@code {
[CascadingParameter]
PageIndexStateProvider? State { get; set; }
private IEnumerable<Post> allposts = null;
private IEnumerable<Post> posts = Enumerable.Empty<Post>();
private int itemsPerPage = 5;
private int totalPages = 1;
protected override async Task OnInitializedAsync()
{
allposts = await Http.GetFromJsonAsync<IEnumerable<Post>>("https://jsonplaceholder.typicode.com/posts");
if (allposts != null)
{
// Inicializa o numero de "totalPages"
totalPages = (int)(allposts.Count() / itemsPerPage);
// inicializa os "posts" os quais serão exibidos na pagina na primeira vez
var skipCount = itemsPerPage * (State.PageIndex - 1);
posts = allposts.Skip(skipCount).Take(itemsPerPage);
}
}
private void SelectedPage(int selectedPageIndex)
{
if (allposts != null)
{
State.PageIndex = selectedPageIndex;
var skipCount = itemsPerPage * (State.PageIndex - 1);
posts = allposts.Skip(skipCount).Take(itemsPerPage);
}
}
}
|
Executando o projeto iremos obter o seguinte resultado:
Pegue o código do projeto aqui : BlazorPagination.zip ...
E estamos conversados
"Não retribuam mal com mal, nem insulto com insulto; ao
contrário, bendigam; pois para isso vocês foram chamados, para receberem bênção
por herança"
1 Pedro 3:9
Referências:
NET - Unit of Work - Padrão Unidade de ...