Blazor : créer un composant avec une datagrid (1/3) : pagination

Blazor c’est la possibilité de réaliser des interfaces web riche et interactive sans JavaScript en utilisant du C# et toute la richesse de l’écosystème .Net. Le projet, expérimental à ses débuts, a été officialisé et a intégré ASPNET Core 3 récemment.

Comment réaliser un composant Blazor réutilisable ? Regardons cela ensemble, en se basant sur la preview 8. Pour illustrer cet article, je vais créer une datagrid, dont le code est présent sur Github.  

Création du projet Blazor

Pour suivre ce tutoriel, il faut installer la preview de .Net Core 3 et disposer de Visual Studio 2019. Un composant Blazor ne peut être utilisé que dans une application Blazor, je vous invite donc à lire cette page si vous n’êtes pas familiarisé avec la création d’une application Blazor.   Il est à présent temps de créer notre composant. 1.Dans l’outils d’interface de ligne de commande de .Net Core (.NET CLI) tapez la commande suivante :  Dotnet new razorclasslib -o BlazorComponent 2.Ajoutez le composant à votre solution en faisant un clic droit sur le nom de la solution dans visual studio puis Ajouter > Projet existant. 3.Choisissez le fichier .csprojcorrespondant à votre composant. Une fois le fichier ajouté, il faut faire un peu de ménage. Vous pouvez supprimer les fichiers :

  1. background.png
  2. exampleJsInterop.js
  3. Component1.cshtml
  4. ExampleJsInterop.cs

 

4.Dans le fichier .csproj, s’il n’est pas déjà présent, ajoutez la ligne <RazorLangVersion>3.0</RazorLangVersion> dans le bloc <PropertyGroup> :  

<PropertyGroup>

    <TargetFramework>netstandard2.0</TargetFramework>

    <OutputType>Library</OutputType>

    <IsPackable>true</IsPackable>

    <BlazorLinkOnBuild>false</BlazorLinkOnBuild>

    <LangVersion>7.3</LangVersion>

    <RazorLangVersion>3.0</RazorLangVersion>

  </PropertyGroup>

  Ajoutez également le paquet NuGet Microsoft.AspNetCore.Blazor.  

Création du composant

1.Créez un nouveau composant Razor en faisant un clic droit sur le projet et Ajouter > Nouvel élément, puis choisissez Composant Razoret appelez BlazorDataGrid.

 

  Le Fichier BlazorDataGrid.razor va servir de squelette à notre composant. C’est son contenu qui s’affichera lorsque l’on fera appel au composant. On va donc dans ce fichier créer les bases de notre datagrid qui sera une table HTML. 2.Dans le fichier razorajoutez le code suivant :

<table>

    <thead>

        <tr>

            <th>Princesse 1</th>

            <th>Princesse 2</th>

        </tr>

    </thead>

    <tbody>

        <tr>

            <td>Ariel</td>

            <td>Jasmine</td>

        </tr>

    </tbody>

</table>

Rien de bien exceptionnel, on se contente de créer un tableau avec 2 éléments. Cela me permet d’enchaîner sur une étape importante qui consiste à utiliser notre élément dans un projet, ce qui va également nous permettre de tester les différentes étapes de progression.  

Ajout du composant dans le projet de test

Dans votre solution, vous devriez avoir une application Blazor qui, lorsque vous l’exécutez, affiche une page avec les 3 menus Home, Counter et Fetch data. Comme nous souhaitons pouvoir utiliser notre composant dans cette solution, il faut ajouter une dépendance au projet du composant dans le projet de l’application.

  1. Faites un clic droit sur dépendances puis Ajouter une référence.
  2. Choisissez ensuite Projets puis le projet de notre composant Blazor.
  3. Ouvrez ensuite le fichier razorsitué à la racine et ajoutez une référence vers le composant.

@using  BlazorComponent Voilà tout est prêt pour tester le composant.

  1. Sur la page Razorajoutez

<BlazorDataGrid></BlazorDataGrid> Exécutez maintenant le projet. Vous voyez alors sur la page d’accueil notre tableau qui apparaît.

 

Le composant se contente d’afficher notre tableau. Ce n’est pas très original mais on vient malgré tout de créer notre premier composant. Il est temps d’aller un peu loin afin de rendre le composant plus utile.  

Mise en forme du composant

Retournons sur le fichier BlazorDatagrid.razor. Nous allons maintenant faire en sorte de pouvoir afficher autre chose qu’un simple tableau statique. Pour commencer, nous allons créer une section functions dans le fichier. Cette section accueillera les variables et fonctions qui nous seront utiles.  

  1. Ajoutez donc ce code en bas de page :
@functions {

}
  1. Ajoutez ensuite en haut de la page @typeparam TItem

La directive typeparam permet de rendre notre composant générique. Nous pouvons ainsi passer un type générique au composant et l’utiliser ensuite dans un IEnumerable.

  1. Ajoutez le code ci-dessous dans la zone functions:
[Parameter]

public IEnumerable<TItem> Items { get; set; }

Items peut recevoir la liste qui sera utilisée pour la datagrid tout en restant générique et donc facilement réutilisable (ce qui est le but de notre composant). Cela étant fait, nous allons pouvoir nous attaquer à l’affichage des lignes dans notre tableau.

  1. Pour cela dans la zone functionsnous allons pouvoir ajouter un nouveau paramètre :
[Parameter]

public RenderFragment<TItem> GridRow { get; set; }

RenderFragment permet d’afficher des éléments en utilisant un modèle fourni par le composant. Ici on affichera une liste de td avec les valeurs associées. Afin d’afficher toutes les lignes, il va falloir itérer sur le IEnumerable envoyé en paramètre.

  1. Modifiez donc la partie <tbody> en remplaçant le contenu ainsi :
<tbody>

    @foreach (var item in Items)

    {

        <tr>@GridRow(item)</tr>

    }

</tbody>

Pour chaque information présente dans Items, on ajoute une ligne. Par ailleurs, afin que notre header soit en adéquation avec le body (et beaucoup plus générique qu’à présent), il faut également faire appel à un RenderFragment.

  1. Ajoutez dans la partie functions:
[Parameter]

public RenderFragment BlazorDataGridColumn { get; set; }
  1. Et remplacez le thead par :
<thead>

    <tr>

        @BlazorDataGridColumn

    </tr>

</thead>

  A ce stade votre fichier devrait ressembler à ça :

@typeparam TItem

<table>

    <thead>

        <tr>

            @BlazorDataGridColumn

        </tr>

    </thead>

    <tbody>

        @foreach (var item in Items)

        {

            <tr>@GridRow(item)</tr>

        }

    </tbody>

</table>

@functions {

    [Parameter]

    public IEnumerable<TItem> Items { get; set; }

    [Parameter]

    public RenderFragment<TItem> GridRow { get; set; }

    [Parameter]

    public RenderFragment BlazorDataGridColumn { get; set; }

}

Testons à présent ce nouveau code. Afin d’avoir quelques données à exploiter, nous allons cette fois modifier la page FetchData.razor.  

  1. Remplacez toute la partie <table>…</table> par :
<BlazorDataGrid Items="@forecasts">

        <BlazorDataGridColumn>

            <th>Date</th>

            <th>Temp. (C)</th>

            <th>Temp. (F)</th>

            <th>Summary</th>

        </BlazorDataGridColumn>

        <GridRow>

            <td>@context.Date.ToShortDateString()</td>

            <td>@context.TemperatureC</td>

            <td>@context.TemperatureF</td>

            <td>@context.Summary</td>

        </GridRow>

    </BlazorDataGrid>

Ici, on utilise notre composant pour faire la table. Le composant BlazorDataGrid prend en paramètre Items à qui on passe le tableau forecast qui contient les éléments à afficher. Dans la partie GridRowcontext fait référence à WeatherForecast et on précise quelle propriété on souhaite afficher. Si on lance l’application, on a actuellement le rendu suivant :  

 

On voit que notre tableau s’affiche bien avec les informations souhaitées. Notre composant peut à présent être utilisé pour remplacer l’utilisation d’une <table>. Il est possible d’aller plus loin (ici on ne fait pas grand-chose de plus qu’une simple table et le gain n’est pas forcément évident). La prochaine étape sera d’ajouter de la pagination à la table. Il sera ainsi possible de limiter l’affichage du nombre d’éléments par page ce qui peut être pratique si on a de nombreuses lignes à afficher.  

Pagination

Pour faire de la pagination, il va falloir ajouter quelques éléments dans notre page. Pour commencer, il faut ajouter un nouveau IEnumerable. En effet, la pagination va nécessiter de modifier la liste des éléments à afficher pour ne garder que ceux de la page en cours. Pour autant, il ne faut pas perdre la liste initiale, sans quoi on ne pourrait paginer qu’une fois.  

  1. Ajoutons donc ceci dans la partie functions:
IEnumerable<TItem> ItemList { get; set; }

On ne place pas d’annotation [Parameter] parce qu’il s’agit juste d’une variable qui ne sera pas passée en paramètre du composant.  

  1. Modifions également le foreach du gridRow car c’est dorénavant sur cet élément qu’il faudra itérer.
<tbody>        @foreach (var item in ItemList)        {            <tr>@GridRow(item)</tr>        }    </tbody>

Il va falloir également quelques variables afin de savoir sur quelle page on se trouve, combien il y a de page au totale etc.

  1. Comme d’habitude ajoutons dans la partie functions ces quelques variables :
int totalPages;

int curPage;

int pagerSize;

int startPage;

int endPage;
  1. Et on va passer en paramètre de notre composant le nombre d’élément souhaité par page. Il faut donc ajouter également :
[Parameter]

public int PageSize { get; set; }

 

  1. Il est également nécessaire de définir notre point de départ et l’affichage initial. On va ajouter une fonction d’initialisation.
protected override async Task OnInitializedAsync()    

    {        

pagerSize = 5;        

curPage = 1;        

 ItemList = Items.Skip((curPage - 1) * PageSize).Take(PageSize);       

 totalPages = (int)Math.Ceiling(Items.Count() / (decimal)PageSize);SetPagerSize(); 

    }

6. Pagersize correspond au nombre de pages qui seront visibles entre les boutons suivant et précédent. Justement, voici la fonction qui calcule les pages à afficher entre les boutons suivant et précédent :

 public void SetPagerSize()

    {        

if (endPage < totalPages)        

{            

startPage = endPage + 1;            

if (endPage + pagerSize < totalPages)            

{                

endPage = startPage + pagerSize - 1;            

}            

else            

{                

endPage = totalPages;            

}            

this.StateHasChanged();        

}        

else if (startPage > 1)        

{            

endPage = startPage - 1;            

startPage = startPage - pagerSize;        

}   

 }

  7. Il faut aussi prévoir une fonction permettant de mettre à jour la liste lorsque l’on change de page. public void updateList(int currentPage)   

{        

ItemList = Items.Skip((currentPage - 1) * PageSize).Take(PageSize);       

 curPage = currentPage;        

this.StateHasChanged();    

}

  StateHasChanged permet de notifier d’un changement afin de rafraîchir l’interface.   8.Comme on parlait juste avant des boutons suivant et précédent, c’est le moment d’introduire une nouvelle fonction permet de naviguer dans les pages :

public void NavigateToPage(string direction)    

{        

if (direction == "next")        

{            

if (curPage < totalPages)            

{                

if (curPage == endPage)                

{                    

SetPagerSize();                

}               

 curPage += 1;            

}        

}        

else if (direction == "previous")        

{            

if (curPage > 1)            

{                

if (curPage == startPage)                

{                    

SetPagerSize();                

}               

 curPage -= 1;            

}        

}        

 updateList(curPage);    

}

  9. Après la navigation, n’oubliez pas de mettre à jour la liste avec la fonction updateList.   10. Les fonctions pour naviguer dans les pages sont prêtes, il faut maintenant afficher les boutons de navigation. Sous la balise fermante </table> nous allons ajouter une zone de pagination où seront affiché les boutons adéquats.

<div class="pagination">     

<button class="btn pagebutton btn-secondary" @onclick=@(async () 

=> NavigateToPage("previous"))>Prec.</button>     

@for (int i = startPage; i <= endPage; i++)    

{        

var currentPage = i;       

 <button class="btn pagebutton @(currentPage==curPage?"currentpage":"")" @onclick=@(async () => 

updateList(currentPage))>            

@currentPage        

</button>    

}    

 <button class="btn pagebutton btn-secondary" @onclick=@(async () 

=> NavigateToPage("next"))>Suiv.</button>     

<span class="pagebutton btn btn-link disabled">Page @curPage de @totalPages</span> 

</div>

Le premier bouton permet de revenir sur la page précédente. Pour réaliser cette action, il faut passer la fonction NavigateToPage avec en paramètre la direction souhaitée à l’action onClick. Au clic sur le bouton, on appelle la fonction et on change de page. Il s’agit du même mécanisme utilisé pour le bouton suivant. La dernière ligne indique simplement la page actuelle et le nombre de page au total. Au centre, on affiche toutes les numéros de pages qui doivent être affiché en fonction de la variable pagersize et on affecte simplement la fonction de mise à jour de la liste avec la page souhaitée. Le contenu de votre page devrait ressembler à ça :

@typeparam TItem 

<table class="table table-striped table-bordered mdl-data-table">    <thead class="thead-inverse">       

 <tr>            

@BlazorDataGridColumn       

 </tr>    

</thead>    

<tbody>        

@foreach (var item in ItemList)        

{            

<tr>@GridRow(item)</tr>        

}   

 </tbody>

</table>

<div class="pagination">     

<button class="btn pagebutton btn-secondary" @onclick=@(async () => NavigateToPage("previous"))>Prec.</button>     

@for (int i = startPage; i <= endPage; i++)    

{       

 var currentPage = i;        

<button class="btn pagebutton @(currentPage==curPage?"currentpage":"")" @onclick=@(async () => 

updateList(currentPage))>            

@currentPage        

</button>    

}    

 <button class="btn pagebutton btn-secondary" @onclick=@(async () => NavigateToPage("next"))>Suiv.</button>     

<span class="pagebutton btn btn-link disabled">Page @curPage de @totalPages</span> 

</div>   

@functions {    

int totalPages;    

int curPage;    

int pagerSize;     

int startPage;    

int endPage;     

[Parameter]    

public IEnumerable<TItem> Items { get; set; }     

[Parameter]    

RenderFragment<TItem> GridRow { get; set; }     

[Parameter]    

private RenderFragment BlazorDataGridColumn { get; set; }     

[Parameter]    

int PageSize { get; set; }     

IEnumerable<TItem> ItemList { get; set; }     

protected override async Task OnInitializedAsync()    

{        

pagerSize = 5;        

curPage = 1;         

ItemList = Items.Skip((curPage - 1) * PageSize).Take(PageSize);        totalPages = (int)Math.Ceiling(Items.Count() / (decimal)PageSize);        SetPagerSize();     

}    

 public void updateList(int currentPage)    

{       

 ItemList = Items.Skip((currentPage - 1) * PageSize).Take(PageSize);        curPage = currentPage;        this.StateHasChanged();    

}     

public void NavigateToPage(string direction)    {        if (direction == "next")        {            if (curPage < totalPages)            

{                

if (curPage == endPage)                

{                   

 SetPagerSize();               

 }               

 curPage += 1;           

 }       

 }        

else if (direction == "previous")        

{           

 if (curPage > 1)            

{               

 if (curPage == startPage)                

{                   

 SetPagerSize();               

 }                

curPage -= 1;            

}       

 }        

 updateList(curPage);   

 }    

 public void SetPagerSize()   

 {        

if (endPage < totalPages)        

{            

startPage = endPage + 1;           

 if (endPage + pagerSize < totalPages)           

 {               

 endPage = startPage + pagerSize - 1;           

 }           

 else            

{               

 endPage = totalPages;           

 }            

this.StateHasChanged();       

 }        

else if (startPage > 1)        

{            

endPage = startPage - 1;            

startPage = startPage - pagerSize;        

}    

} 

}

Notez au passage que j’ai ajouté un peu de style à la table afin que l’affichage soit plus plaisant. A l’exécution vous obtenez ce résultat :

 

 

Et vous constatez que cliquer sur les boutons permet de changer de page. Ici il n’y a que 2 pages, mais si ce nombre grossit, il peut être intéressant d’avoir un bouton permettant de changer de zone de page. C’est ce que nous allons mettre en place maintenant. 11. Il faut modifier la fonction SetPagerSize en lui passant une direction en paramètre.

public void SetPagerSize(string direction)

    {

        if (direction == "forward" && endPage < totalPages)

        {

            startPage = endPage + 1;

            if (endPage + pagerSize < totalPages)

            {

                endPage = startPage + pagerSize - 1;

            }

            else

            {

                endPage = totalPages;

            }

            this.StateHasChanged();

        }

        else if (direction == "back" && startPage > 1)

        {

            endPage = startPage - 1;

            startPage = startPage - pagerSize;

        }

    }


12. A chaque endroit où l’on utilise la fonction, il faut ajouter ce paramètre, c’est-à-dire dans les fonctions NavigateToPage et OnInitializedAsync.

protected override async Task OnInitializedAsync()    

{        

pagerSize = 5;        

curPage = 1;        

 ItemList = Items.Skip((curPage - 1) * PageSize).Take(PageSize);        

totalPages = (int)Math.Ceiling(Items.Count() / 

(decimal)PageSize);        

SetPagerSize("forward");     

}

public void NavigateToPage(string direction)    

{        

if (direction == "next")        

{            

if (curPage < totalPages)            

{               

 if (curPage == endPage)                

{                    

SetPagerSize("forward");                

}                

curPage += 1;            

}        

}        

else if (direction == "previous")        

{            

if (curPage > 1)            

{                

if (curPage == startPage)                

{                    

SetPagerSize("back");                

}                

curPage -= 1;            

}        

}       

updateList(curPage);    

}

13.Il faut également ajouter les boutons dans la zone de pagination. Nous affecterons à l’action onclick de ces boutons, la fonction SetPagerSize avec la direction souhaitée.

<div class="pagination">

     <button class="btn pagebutton btn-info" @onclick=@(async () => SetPagerSize("back"))>«</button>

    <button class="btn pagebutton btn-secondary" @onclick=@(async () => NavigateToPage("previous"))>Prec.</button>

     @for (int i = startPage; i <= endPage; i++)

    {        

var currentPage = i;

        <button class="btn pagebutton @(currentPage==curPage?"currentpage":"")" @onclick=@(async () => updateList(currentPage))>            @currentPage

        </button>

    }

     <button class="btn pagebutton btn-secondary" @onclick=@(async () => NavigateToPage("next"))>Suiv.</button>

    <button class="btn pagebutton btn-info" @onclick=@(async () => SetPagerSize("forward"))>»</button>

     <span class="pagebutton btn btn-link disabled">Page @curPage de @totalPages</span>

 </div><

 

 

  14.Enfin, afin de marquer le bouton de la page actuelle, ajoutons un peu de style. Dans le haut de la page, ajoutez ceci :

<style>

         .pagebutton {

            margin-right: 5px;

            margin-top: 5px;

        }




        .currentpage {

            background-color: dodgerblue;

            color: white;

        }

    </style>

La page en cours est maintenant identifiée en bleu :  

 

Conclusion

Dans cet article vous avez vu comment créer un composant Blazor, et nous avons appliqué ceci à la réalisation d’une datagrid avec pagination. Vous pouvez réutiliser ce composant chaque fois qu’il vous faut mettre en place de la pagination. Dans le prochain article, nous verrons comment ajouter la possibilité de trier nos colonnes puis comment ajouter un filtre sur les colonnes.