Réaliser un composant Blazor réutilisable : Exemple avec une datagrid (partie 1/3 : pagination)

Avant de rentrer dans le vif du sujet, commençons par découvrir un peu Blazor.

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. Vous pouvez avoir un aperçu plus détaillé dans la documentation de Microsoft.

Le but de cet article est de vous montrer comment réaliser un composant Blazor réutilisable. Je vais créer une Datagrid pour illustrer mon article. Tous le code est présent sur Github.

Blazor est encore en bêta. Je mettrai à jour cet article lorsque la version finale sera disponible.

MISE à JOUR : L’article se base sur la preview 8

Création du projet

Pour suivre ce tutoriel, il faut installer la preview de .net Core 3. Cette dernière est disponible ici .

Il faut également disposer de Visual Studio 2019 qui est disponible ici.

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.

Dans l’outils d’interface de ligne de commande de .Net Core (.NET CLI) tapez la commande suivante

Dotnet new razorclasslib -o BlazorComponent

Ajoutez le composant à votre solution en faisant un clic droit sur le nom de la solution dans visual studio puis Ajouter > Projet existant

Et choisissez le fichier .csproj correspondant à votre composant. Une fois ajouter, il faut faire un peu de ménage. Vous pouvez supprimer les fichiers :

  • background.png
  • exampleJsInterop.js
  • Component1.cshtml
  • ExampleJsInterop.cs

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>

Ajouter également le paquet NuGet Microsoft.AspNetCore.Blazor

   Création du composant

Créez un nouveau composant Razor. Faîtes un clic droit sur le projet et Ajouter > Nouvel élément.

Choisissez Composant Razor et 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.

Dans le fichier BlazorDataGrid.razor ajoutez 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’enchainer sur une étape importante qui consiste à utiliser notre élément dans un projet. Cela 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, vous affiche une page avec 3 menus Home, Counter et Fetch data.

Nous souhaitons pouvoir utiliser notre composant dans cette solution. Il faut donc ajouter une dépendance au projet du composant dans le projet de l’application.

Faîtes un clic droit sur dépendances puis Ajouter une référence.

Choisissez ensuite Projets puis le projet de notre composant Blazor.

Ouvrez ensuite le fichier _Imports.razor situé à la racine et ajouter une référence vers le composant.

@using  BlazorComponent

Voilà tout est prêt pour tester le composant. Sur la page Index.Razor ajouter

<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 un peu plus utile.

 Mise en forme du composant

Retourner 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.

Ajoutez donc ce code en bas de page :

@functions {

}

Ajoutez ensuite en haut de la page

@typeparam TItem

La directive typeparam nous permet de rendre notre composant générique. Ceci permet de passer un type générique au composant et de l’utiliser ensuite dans un IEnumerable. Et d’ailleurs 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. Pour cela dans la zone functions nous 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.

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.

Ajoutez dans la partie functions :

[Parameter]
public RenderFragment BlazorDataGridColumn { get; set; }

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.

Remplacer 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 GridRow, context 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>. Mais 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. Mais il ne faut pas pour autant perdre la liste initiale sans quoi on ne pourrait paginer qu’une fois.

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.

Et 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.

Comme d’habitude ajoutons dans la partie functions ces quelques variables :

int totalPages;
int curPage;
int pagerSize;

int startPage;
int endPage;

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; }

Et il faut également définir notre point de départ et l’affichage initial. On va donc 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();

    } 

Pagersize correspond au nombre de pages qui seront visible 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;
        }
    }

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.

Et 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);
    }

Après la navigation, il ne faut pas oublier de mettre à jour la list avec la fonction updateList.

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" @[email protected](async () => NavigateToPage("previous"))>Prec.</button>

    @for (int i = startPage; i <= endPage; i++)
    {
        var currentPage = i;
        <button class="btn pagebutton @(currentPage==curPage?"currentpage":"")" @[email protected](async () => updateList(currentPage))>
            @currentPage
        </button>
    }

    <button class="btn pagebutton btn-secondary" @[email protected](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 contenue 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" @[email protected](async () => NavigateToPage("previous"))>Prec.</button>

    @for (int i = startPage; i <= endPage; i++)
    {
        var currentPage = i;
        <button class="btn pagebutton @(currentPage==curPage?"currentpage":"")" @[email protected](async () => updateList(currentPage))>
            @currentPage
        </button>
    }

    <button class="btn pagebutton btn-secondary" @[email protected](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.

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;
        }
    }

Et à 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);
    }

Et 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" @[email protected](async () => SetPagerSize("back"))>«</button>
    <button class="btn pagebutton btn-secondary" @[email protected](async () => NavigateToPage("previous"))>Prec.</button>

    @for (int i = startPage; i <= endPage; i++)
    {
        var currentPage = i;
        <button class="btn pagebutton @(currentPage==curPage?"currentpage":"")" @[email protected](async () => updateList(currentPage))>
            @currentPage
        </button>
    }

    <button class="btn pagebutton btn-secondary" @[email protected](async () => NavigateToPage("next"))>Suiv.</button>
    <button class="btn pagebutton btn-info" @[email protected](async () => SetPagerSize("forward"))>»</button>

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

</div>

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 bleue.

Voilà qui conclue pour la pagination.

Conclusion 

Dans cet article vous avez vu comment créer un composant Blazor et vous avez appliquez 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.

1 réflexion au sujet de « Réaliser un composant Blazor réutilisable : Exemple avec une datagrid (partie 1/3 : pagination) »

Laisser un commentaire