Blazor WebAssembly - Paginação com CascadingValue


  Hoje vamos criar uma aplicação Blazor WebAssembly e realizar a paginação usando uma abordagem bem simples.

A paginação no Blazor é um recurso que permite dividir um conjunto grande de dados em várias páginas, facilitando a navegação e melhorando o desempenho da aplicação. Com a paginação, é possível exibir apenas uma parte dos dados por vez, permitindo ao usuário percorrer as diferentes páginas para visualizar o restante dos dados.



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:

  1. Obter os dados a serem paginados: Isso pode ser feito através de chamadas a uma API, acesso a um banco de dados ou qualquer outra fonte de dados.
  2. Calcular o número total de páginas: Com base na quantidade total de dados e no número de itens a serem exibidos por página, é possível determinar o número total de páginas.
  3. Exibir os dados de uma página específica: O componente responsável pela exibição dos dados deve receber informações sobre a página atual e exibir apenas os itens correspondentes a essa página.
  4. Implementar a navegação entre as páginas: O componente de paginação deve permitir ao usuário navegar entre as diferentes páginas, seja através de botões de página anterior/próxima, números de página ou outras opções de navegação.

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 {
   [
Parameter]
  
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:


José Carlos Macoratti