Partie 3, Razor Pages avec EF Core dans ASP.NET Core - Tri, Filtrage, Pagination

Par Tom Dykstra, Jeremy Likness et Jon P Smith

L’application web Contoso University montre comment créer des applications web Pages Razor avec EF Core et Visual Studio. Pour obtenir des informations sur la série de didacticiels, consultez le premier didacticiel.

Si vous rencontrez des problèmes que vous ne pouvez pas résoudre, téléchargez l’application finale et comparez ce code à celui que vous avez créé en suivant le tutoriel.

Ce tutoriel ajoute des fonctionnalités de tri, de filtrage et de pagination aux pages des étudiants.

L’illustration suivante présente une page complète. Les en-têtes de colonne sont des liens hypertexte permettant de trier la colonne. Cliquez de façon répétée sur un en-tête de colonne pour changer l’ordre de tri (croissant ou décroissant).

Students index page

Ajouter la fonctionnalité de tri

Remplacez le code de Pages/Students/Index.cshtml.cs par le code suivant pour ajouter le tri.

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;
    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

    public IList<Student> Students { get; set; }

    public async Task OnGetAsync(string sortOrder)
    {
        // using System;
        NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        DateSort = sortOrder == "Date" ? "date_desc" : "Date";

        IQueryable<Student> studentsIQ = from s in _context.Students
                                        select s;

        switch (sortOrder)
        {
            case "name_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                break;
            case "Date":
                studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                break;
            case "date_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                break;
            default:
                studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                break;
        }

        Students = await studentsIQ.AsNoTracking().ToListAsync();
    }
}

Le code précédent :

  • Nécessite l’ajout de using System;.
  • Ajoute des propriétés devant contenir les paramètres de tri.
  • Remplace le nom de la propriété Student par Students.
  • Remplace le code de la méthode OnGetAsync.

La méthode OnGetAsync reçoit un paramètre sortOrder à partir de la chaîne de requête dans l’URL. L’URL et la chaîne de requête sont générées par le Tag Helper d’ancre.

Le paramètre sortOrder est Name ou Date. Le paramètre sortOrder peut être suivi de _desc pour spécifier l’ordre décroissant. L’ordre de tri par défaut est croissant.

Quand la page Index est demandée à partir du lien Students, il n’existe aucune chaîne de requête. Les étudiants sont affichés par nom de famille dans l’ordre croissant. L’ordre croissant par nom est le default dans l’instruction switch. Quand l’utilisateur clique sur un lien d’en-tête de colonne, la valeur sortOrder appropriée est fournie dans la valeur de chaîne de requête.

NameSort et DateSort sont utilisés par la page Razor pour configurer les liens hypertexte d’en-tête de colonne avec les valeurs de chaîne de requête appropriées :

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

Le code utilise l’opérateur conditionnel C# ?:. L’opérateur ?: est un opérateur ternaire ; il prend trois opérandes. La première ligne indique que quand sortOrder est null ou vide, NameSort prend la valeur name_desc. Si sortOrder n’est pas null ou vide, NameSort prend pour valeur une chaîne vide.

Ces deux instructions permettent à la page de définir les liens hypertexte d’en-tête de colonne comme suit :

Ordre de tri actuel Lien hypertexte Nom de famille Lien hypertexte Date
Nom de famille croissant descending ascending
Nom de famille décroissant ascending ascending
Date croissante ascending descending
Date décroissante ascending ascending

La méthode utilise LINQ to Entities pour spécifier la colonne d’après laquelle effectuer le tri. Le code initialise un IQueryable<Student> avant l’instruction switch, et le modifie dans l’instruction switch :

IQueryable<Student> studentsIQ = from s in _context.Students
                                select s;

switch (sortOrder)
{
    case "name_desc":
        studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
        break;
    case "Date":
        studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
        break;
    case "date_desc":
        studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
        break;
    default:
        studentsIQ = studentsIQ.OrderBy(s => s.LastName);
        break;
}

Students = await studentsIQ.AsNoTracking().ToListAsync();

Quand un IQueryable est créé ou modifié, aucune requête n’est envoyée à la base de données. La requête n’est pas exécutée tant que l’objet IQueryable n’a pas été converti en collection. Les IQueryable sont convertis en collection en appelant une méthode telle que ToListAsync. Ainsi, le code IQueryable génère une requête unique qui n’est pas exécutée avant l’instruction suivante :

Students = await studentsIQ.AsNoTracking().ToListAsync();

OnGetAsync peut contenir un grand nombre de colonnes triables. Pour connaître les autres méthodes permettant de coder cette fonctionnalité, consultez Utiliser du code dynamique LINQ pour simplifier le code dans la version MVC de cette série de tutoriels.

Remplacez le code dans Students/Index.cshtml par le code suivant. Les modifications sont mises en surbrillance.

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>
<p>
    <a asp-page="Create">Create New</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Le code précédent :

  • Ajoute des liens hypertexte aux en-têtes de colonne LastName et EnrollmentDate.
  • Utilise les informations contenues dans NameSort et DateSort pour définir des liens hypertexte avec les valeurs d’ordre de tri actuelles.
  • Remplace l’en-tête Index de la page par l’en-tête Students.
  • Remplace Model.Student par Model.Students.

Pour vérifier que le tri fonctionne

  • Exécutez l’application et sélectionnez l’onglet Students.
  • Cliquez sur les en-têtes de colonne.

Ajouter la fonctionnalité de filtrage

Pour ajouter le filtrage à la page d’index des étudiants :

  • Une zone de texte et un bouton d’envoi sont ajoutés à la page Razor. La zone de texte fournit une chaîne de recherche sur le prénom ou le nom de famille.
  • Le modèle de page est mis à jour pour utiliser la valeur de zone de texte.

Mettre à jour la méthode OnGetAsync

Remplacez le code de Students/Index.cshtml.cs par le code suivant pour ajouter le filtrage :

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;

    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

    public IList<Student> Students { get; set; }

    public async Task OnGetAsync(string sortOrder, string searchString)
    {
        NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        DateSort = sortOrder == "Date" ? "date_desc" : "Date";

        CurrentFilter = searchString;
        
        IQueryable<Student> studentsIQ = from s in _context.Students
                                        select s;
        if (!String.IsNullOrEmpty(searchString))
        {
            studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
                                   || s.FirstMidName.Contains(searchString));
        }

        switch (sortOrder)
        {
            case "name_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                break;
            case "Date":
                studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                break;
            case "date_desc":
                studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                break;
            default:
                studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                break;
        }

        Students = await studentsIQ.AsNoTracking().ToListAsync();
    }
}

Le code précédent :

  • Ajoute le paramètre searchString à la méthode OnGetAsync, et enregistre la valeur de paramètre dans la propriété CurrentFilter. La valeur de chaîne de recherche est reçue à partir d’une zone de texte qui est ajoutée dans la section suivante.
  • Ajoute une clause Where à l’instruction LINQ. La clause Where sélectionne uniquement les étudiants dont le prénom ou le nom de famille contient la chaîne de recherche. L’instruction LINQ est exécutée uniquement s’il y a une valeur à rechercher.

IQueryable et IEnumerable

Le code appelle la méthode Where de l’objet IQueryable, et le filtre est traité sur le serveur. Dans certains scénarios, l’application peut appeler la méthode Where en tant que méthode d’extension sur une collection en mémoire. Par exemple, supposez que _context.Students passe de EF CoreDbSet à une méthode de référentiel qui retourne une collection IEnumerable. Le résultat serait normalement le même, mais dans certains cas il peut être différent.

Par exemple, l’implémentation .NET Framework de Contains effectue par défaut une comparaison respectant la casse. Dans SQL Server, le respect de la casse de Contains est déterminé par le paramètre de classement de l’instance de SQL Server. Par défaut, SQL Server ne respecte pas la casse. Par défaut, SQLite est sensible à la casse. ToUpper peut être appelée pour que le test ne respecte pas la casse de manière explicite :

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())`

Le code précédent garantit que le filtre n’est pas sensible à la casse, même si la méthode Where est appelée sur un IEnumerable ou s’exécute sur SQLite.

Quand Contains est appelée sur une collection IEnumerable, l’implémentation .NET Core est utilisée. Quand Contains est appelée sur un objet IQueryable, l’implémentation de base de données est utilisée.

Pour des raisons de performances, il est généralement préférable d’appeler Contains sur un IQueryable. Avec IQueryable, le filtrage est effectué par le serveur de base de données. Si un IEnumerable est créé en premier, toutes les lignes doivent être retournées à partir du serveur de base de données.

Il existe un coût en matière de performances en cas d’appel à ToUpper. Le code ToUpper ajoute une fonction dans la clause WHERE de l’instruction TSQL SELECT. La fonction ajoutée empêche l’optimiseur d’utiliser un index. Étant donné que SQL est installé sans respect de la casse, il est préférable d’éviter l’appel à ToUpper quand il n’est pas nécessaire.

Pour plus d’informations, consultez How to use case-insensitive query with Sqlite provider.

Mettre à jour la page Razor

Remplacez le code dans Pages/Students/Index.cshtml pour ajouter un bouton Rechercher.

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name:
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-primary" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Le code précédent utilise le Tag Helper<form> pour ajouter le bouton et la zone de texte de recherche. Par défaut, le Tag Helper <form> envoie les données de formulaire avec un POST. Avec POST, les paramètres sont passés dans le corps du message HTTP et non dans l’URL. Quand HTTP GET est utilisé, les données du formulaire sont transmises dans l’URL sous forme de chaînes de requête. La transmission des données avec des chaînes de requête permet aux utilisateurs d’ajouter l’URL aux favoris. Les recommandations du W3C stipulent que GET doit être utilisé quand l’action ne produit pas de mise à jour.

Testez l’application :

  • Sélectionnez l’onglet Students et entrez une chaîne de recherche. Si vous utilisez SQLite, le filtre n’est pas sensible à la casse seulement si vous avez implémenté le code ToUpper facultatif indiqué plus haut.

  • Sélectionnez Recherche.

Notez que l’URL contient la chaîne de recherche. Par exemple :

https://localhost:5001/Students?SearchString=an

Si la page est dans les favoris, le favori contient l’URL de la page et la chaîne de requête SearchString. method="get" dans la balise form est ce qui a provoqué la génération de la chaîne de requête.

Actuellement, quand un lien de tri d’en-tête de colonne est sélectionné, la valeur du filtre de la zone Search est perdue. La valeur de filtre perdue est corrigée dans la section suivante.

Ajouter la fonctionnalité de pagination

Dans cette section, nous allons créer une classe PaginatedList pour prendre en charge la pagination. La classe PaginatedList utilise des instructions Skip et Take pour filtrer les données sur le serveur au lieu de récupérer toutes les lignes de la table. L’illustration suivante montre les boutons de pagination.

Students index page with paging links

Créer la classe PaginatedList

Dans le dossier du projet, créez PaginatedList.cs avec le code suivant :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(
            IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip(
                (pageIndex - 1) * pageSize)
                .Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

La méthode CreateAsync dans le code précédent prend la taille de page et le numéro de page, et applique les instructions Skip et Take appropriées au IQueryable. Quand ToListAsync est appelée sur le IQueryable, elle retourne une liste contenant uniquement la page demandée. Les propriétés HasPreviousPage et HasNextPage sont utilisées pour activer ou désactiver les boutons de pagination Previous et Next.

La méthode CreateAsync est utilisée pour créer le PaginatedList<T>. Un constructeur ne peut pas créer l’objet PaginatedList<T>, car les constructeurs ne peuvent pas exécuter du code asynchrone.

Ajouter la taille de page à la configuration

Ajoutez PageSize au fichier de appsettings.jsonConfiguration :

{
  "PageSize": 3,
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

Ajouter la pagination à IndexModel

Remplacez le code dans Students/Index.cshtml.cs pour ajouter la pagination.

using ContosoUniversity.Data;
using ContosoUniversity.Models;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using System;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Pages.Students
{
    public class IndexModel : PageModel
    {
        private readonly SchoolContext _context;
        private readonly IConfiguration Configuration;

        public IndexModel(SchoolContext context, IConfiguration configuration)
        {
            _context = context;
            Configuration = configuration;
        }

        public string NameSort { get; set; }
        public string DateSort { get; set; }
        public string CurrentFilter { get; set; }
        public string CurrentSort { get; set; }

        public PaginatedList<Student> Students { get; set; }

        public async Task OnGetAsync(string sortOrder,
            string currentFilter, string searchString, int? pageIndex)
        {
            CurrentSort = sortOrder;
            NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
            DateSort = sortOrder == "Date" ? "date_desc" : "Date";
            if (searchString != null)
            {
                pageIndex = 1;
            }
            else
            {
                searchString = currentFilter;
            }

            CurrentFilter = searchString;

            IQueryable<Student> studentsIQ = from s in _context.Students
                                             select s;
            if (!String.IsNullOrEmpty(searchString))
            {
                studentsIQ = studentsIQ.Where(s => s.LastName.Contains(searchString)
                                       || s.FirstMidName.Contains(searchString));
            }
            switch (sortOrder)
            {
                case "name_desc":
                    studentsIQ = studentsIQ.OrderByDescending(s => s.LastName);
                    break;
                case "Date":
                    studentsIQ = studentsIQ.OrderBy(s => s.EnrollmentDate);
                    break;
                case "date_desc":
                    studentsIQ = studentsIQ.OrderByDescending(s => s.EnrollmentDate);
                    break;
                default:
                    studentsIQ = studentsIQ.OrderBy(s => s.LastName);
                    break;
            }

            var pageSize = Configuration.GetValue("PageSize", 4);
            Students = await PaginatedList<Student>.CreateAsync(
                studentsIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
        }
    }
}

Le code précédent :

  • Remplace le type IList<Student> de la propriété Students par le type PaginatedList<Student>.
  • Ajoute l’index de page, le sortOrder actuel et le currentFilter à la signature de méthode OnGetAsync.
  • Enregistre l’ordre de tri dans la propriété CurrentSort.
  • Rétablit la valeur 1 pour l’index de la page lorsqu’il existe une nouvelle chaîne de recherche.
  • Utilise la classe PaginatedList pour accéder aux entités d’étudiants.
  • Définit pageSize sur 3 à partir de Configuration, 4 si la configuration échoue.

Tous les paramètres reçus par OnGetAsync sont Null si :

  • La page est appelée à partir du lien Students.
  • L’utilisateur n’a pas cliqué sur un lien de pagination ou de tri.

Quand l’utilisateur clique sur un lien de pagination, la variable d’index de page contient le numéro de page à afficher.

La propriété CurrentSort fournit l’ordre de tri actuel à la page Razor. L’ordre de tri actuel doit être inclus dans les liens de pagination afin de conserver l’ordre de tri lors de la pagination.

La propriété CurrentFilter fournit la chaîne de filtrage actuelle à la page Razor. La valeur CurrentFilter :

  • Doit être incluse dans les liens de pagination afin de conserver les paramètres de filtre lors de la pagination.
  • Doit être restaurée à la zone de texte quand la page est réaffichée.

Si la chaîne de recherche est modifiée pendant la pagination, la page est réinitialisée à 1. La page doit être réinitialisée à 1, car le nouveau filtre peut entraîner l’affichage de données différentes. Quand une valeur de recherche est entrée et que le bouton Submit est sélectionné :

  • La chaîne de recherche est changée.
  • Le paramètre searchString n’est pas null.

La méthode PaginatedList.CreateAsync convertit la requête d’étudiant en une seule page d’étudiants dans un type de collection qui prend en charge la pagination. Cette page unique d’étudiants est passée à la page Razor.

Les deux points d’interrogation situés après pageIndex dans l’appel PaginatedList.CreateAsync représentent l’opérateur de fusion avec valeur Null. L’opérateur de fusion de Null définit une valeur par défaut pour un type nullable. L’expression pageIndex ?? 1 retourne la valeur pageIndex si elle a une valeur ; sinon, elle retourne 1.

Remplacez le code dans Students/Index.cshtml par le code suivant. Les modifications apparaissent en surbrillance :

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Students";
}

<h2>Students</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: 
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-primary" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Students[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Students[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Students[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Students)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.Students.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.Students.HasNextPage ? "disabled" : "";
}

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @nextDisabled">
    Next
</a>

Les liens d’en-tête de colonne utilisent la chaîne de requête pour passer la chaîne de recherche actuelle à la méthode OnGetAsync :

<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Students[0].LastName)
</a>

Les boutons de changement de page sont affichés par des Tag Helpers :


<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Students.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-primary @nextDisabled">
    Next
</a>

Exécutez l’application et accédez à la page des étudiants.

  • Pour vérifier que la pagination fonctionne, cliquez sur les liens de pagination dans différents ordres de tri.
  • Pour vérifier que la pagination fonctionne correctement avec le tri et le filtrage, entrez une chaîne de recherche et essayez de changer de page.

students index page with paging links

Regroupement

Cette section crée la page About (À propos) qui indique le nombre d’étudiants inscrits pour chaque date d’inscription. La mise à jour utilise le regroupement et comprend les étapes suivantes :

  • Créer un modèle de vue pour les données utilisées par la page About.
  • Mettre à jour la page About pour utiliser le modèle de vue.

Créer le modèle d’affichage

Créez un dossier Models/SchoolViewModels.

Créez SchoolViewModels/EnrollmentDateGroup.cs avec le code suivant :

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}

Créer la page Razor

Créez un fichier Pages/About.cshtml avec le code suivant :

@page
@model ContosoUniversity.Pages.AboutModel

@{
    ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
    <tr>
        <th>
            Enrollment Date
        </th>
        <th>
            Students
        </th>
    </tr>

    @foreach (var item in Model.Students)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

Créer le modèle de page

Mettez à jour le fichier Pages/About.cshtml.cs avec le code suivant :

using ContosoUniversity.Models.SchoolViewModels;
using ContosoUniversity.Data;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;

namespace ContosoUniversity.Pages
{
    public class AboutModel : PageModel
    {
        private readonly SchoolContext _context;

        public AboutModel(SchoolContext context)
        {
            _context = context;
        }

        public IList<EnrollmentDateGroup> Students { get; set; }

        public async Task OnGetAsync()
        {
            IQueryable<EnrollmentDateGroup> data =
                from student in _context.Students
                group student by student.EnrollmentDate into dateGroup
                select new EnrollmentDateGroup()
                {
                    EnrollmentDate = dateGroup.Key,
                    StudentCount = dateGroup.Count()
                };

            Students = await data.AsNoTracking().ToListAsync();
        }
    }
}

L’instruction LINQ regroupe les entités Student par date d’inscription, calcule le nombre d’entités dans chaque groupe et stocke les résultats dans une collection d’objets de modèle de vue EnrollmentDateGroup.

Exécutez l’application et accédez à la page About. Le nombre d’étudiants pour chaque date d’inscription s’affiche dans une table.

About page

Étapes suivantes

Dans le didacticiel suivant, l’application utilise des migrations pour mettre à jour le modèle de données.

Dans ce didacticiel, nous allons ajouter des fonctionnalités de tri, de filtrage, de regroupement et de pagination.

L’illustration suivante présente une page complète. Les en-têtes de colonne sont des liens hypertexte permettant de trier la colonne. Un clic sur un en-tête de colonne permet de changer l’ordre de tri (croissant ou décroissant).

Students index page

Si vous rencontrez des problèmes que vous ne pouvez pas résoudre, téléchargez l’application terminée.

Ajouter le tri à la page Index

Ajoutez des chaînes au Students/Index.cshtml.csPageModel pour contenir les paramètres de tri :

public class IndexModel : PageModel
{
    private readonly SchoolContext _context;

    public IndexModel(SchoolContext context)
    {
        _context = context;
    }

    public string NameSort { get; set; }
    public string DateSort { get; set; }
    public string CurrentFilter { get; set; }
    public string CurrentSort { get; set; }

Mettez à jour la méthode Students/Index.cshtml.csOnGetAsync avec le code suivant :

public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

Le code précédent reçoit un paramètre sortOrder à partir de la chaîne de requête dans l’URL. L’URL (y compris la chaîne de requête) est générée par le Tag Helper d’ancre.

Le paramètre sortOrder est « Name » ou « Date ». Le paramètre sortOrder est éventuellement suivi de « _desc » pour spécifier l’ordre décroissant. L’ordre de tri par défaut est croissant.

Quand la page Index est demandée à partir du lien Students, il n’existe aucune chaîne de requête. Les étudiants sont affichés par nom de famille dans l’ordre croissant. Le tri croissant par nom de famille est la valeur par défaut dans l’instruction switch. Quand l’utilisateur clique sur un lien d’en-tête de colonne, la valeur sortOrder appropriée est fournie dans la valeur de chaîne de requête.

NameSort et DateSort sont utilisés par la page Razor pour configurer les liens hypertexte d’en-tête de colonne avec les valeurs de chaîne de requête appropriées :

public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

Le code suivant contient l’opérateur ?: conditionnel C# :

NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
DateSort = sortOrder == "Date" ? "date_desc" : "Date";

La première ligne spécifie que lorsque sortOrder est null ou vide, NameSort a la valeur « name_desc ». Si sortOrder n’est pas null ou vide, NameSort est défini sur une chaîne vide.

?: operator est également appelé opérateur ternaire.

Ces deux instructions permettent à la page de définir les liens hypertexte d’en-tête de colonne comme suit :

Ordre de tri actuel Lien hypertexte Nom de famille Lien hypertexte Date
Nom de famille croissant descending ascending
Nom de famille décroissant ascending ascending
Date croissante ascending descending
Date décroissante ascending ascending

La méthode utilise LINQ to Entities pour spécifier la colonne d’après laquelle effectuer le tri. Le code initialise un IQueryable<Student> avant l’instruction switch, et le modifie dans l’instruction switch :

public async Task OnGetAsync(string sortOrder)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

Quand un IQueryable est créé ou modifié, aucune requête n’est envoyée à la base de données. La requête n’est pas exécutée tant que l’objet IQueryable n’a pas été converti en collection. Les IQueryable sont convertis en collection en appelant une méthode telle que ToListAsync. Ainsi, le code IQueryable génère une requête unique qui n’est pas exécutée avant l’instruction suivante :

Student = await studentIQ.AsNoTracking().ToListAsync();

OnGetAsync peut contenir un grand nombre de colonnes triables.

Remplacez le code de Students/Index.cshtml par le code mis en évidence suivant :

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>
<p>
    <a asp-page="Create">Create New</a>
</p>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort">
                    @Html.DisplayNameFor(model => model.Student[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Student[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort">
                    @Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Student)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Le code précédent :

  • Ajoute des liens hypertexte aux en-têtes de colonne LastName et EnrollmentDate.
  • Utilise les informations contenues dans NameSort et DateSort pour définir des liens hypertexte avec les valeurs d’ordre de tri actuelles.

Pour vérifier que le tri fonctionne

  • Exécutez l’application et sélectionnez l’onglet Students.
  • Cliquez sur Last Name.
  • Cliquez sur Enrollment Date.

Pour mieux comprendre le fonctionnement du code

  • Dans Students/Index.cshtml.cs, définissez un point d’arrêt sur switch (sortOrder).
  • Ajoutez un espion pour NameSort et DateSort.
  • Dans Students/Index.cshtml, définissez un point d’arrêt sur @Html.DisplayNameFor(model => model.Student[0].LastName).

Effectuez un pas à pas détaillé dans le débogueur.

Ajouter une zone de recherche à la page d’index des étudiants

Pour ajouter le filtrage à la page d’index des étudiants :

  • Une zone de texte et un bouton d’envoi sont ajoutés à la page Razor. La zone de texte fournit une chaîne de recherche sur le prénom ou le nom de famille.
  • Le modèle de page est mis à jour pour utiliser la valeur de zone de texte.

Ajouter la fonctionnalité de filtrage à la méthode Index

Mettez à jour la méthode Students/Index.cshtml.csOnGetAsync avec le code suivant :

public async Task OnGetAsync(string sortOrder, string searchString)
{
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";
    CurrentFilter = searchString;

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }

    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    Student = await studentIQ.AsNoTracking().ToListAsync();
}

Le code précédent :

  • Ajoute le paramètre searchString à la méthode OnGetAsync. La valeur de chaîne de recherche est reçue à partir d’une zone de texte qui est ajoutée dans la section suivante.
  • A ajouté une clause Where à l’instruction LINQ. La clause Where sélectionne uniquement les étudiants dont le prénom ou le nom de famille contient la chaîne de recherche. L’instruction LINQ est exécutée uniquement s’il y a une valeur à rechercher.

Remarque : Le code précédent appelle la méthode Where sur un objet IQueryable, et le filtre est traité sur le serveur. Dans certains scénarios, l’application peut appeler la méthode Where en tant que méthode d’extension sur une collection en mémoire. Par exemple, supposez que _context.Students passe de EF CoreDbSet à une méthode de référentiel qui retourne une collection IEnumerable. Le résultat serait normalement le même, mais dans certains cas il peut être différent.

Par exemple, l’implémentation .NET Framework de Contains effectue par défaut une comparaison respectant la casse. Dans SQL Server, le respect de la casse de Contains est déterminé par le paramètre de classement de l’instance de SQL Server. Par défaut, SQL Server ne respecte pas la casse. ToUpper peut être appelée pour que le test ne respecte pas la casse de manière explicite :

Where(s => s.LastName.ToUpper().Contains(searchString.ToUpper())

Le code précédent garantit que les résultats ne respectent pas la casse si le code change et utilise IEnumerable. Quand Contains est appelée sur une collection IEnumerable, l’implémentation .NET Core est utilisée. Quand Contains est appelée sur un objet IQueryable, l’implémentation de base de données est utilisée. Retourner un IEnumerable à partir d’un référentiel peut entraîner une dégradation significative des performances :

  1. Toutes les lignes sont retournées à partir du serveur de base de données.
  2. Le filtre est appliqué à toutes les lignes retournées dans l’application.

Il existe un coût en matière de performances en cas d’appel à ToUpper. Le code ToUpper ajoute une fonction dans la clause WHERE de l’instruction TSQL SELECT. La fonction ajoutée empêche l’optimiseur d’utiliser un index. Étant donné que SQL est installé sans respect de la casse, il est préférable d’éviter l’appel à ToUpper quand il n’est pas nécessaire.

Ajouter une zone de recherche à la page d’index des étudiants

Dans Pages/Students/Index.cshtml, ajoutez le code en évidence suivant pour créer un bouton Search et le contrôle Chrome assorti.

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name:
            <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">

Le code précédent utilise le Tag Helper<form> pour ajouter le bouton et la zone de texte de recherche. Par défaut, le Tag Helper <form> envoie les données de formulaire avec un POST. Avec POST, les paramètres sont passés dans le corps du message HTTP et non dans l’URL. Quand HTTP GET est utilisé, les données du formulaire sont transmises dans l’URL sous forme de chaînes de requête. La transmission des données avec des chaînes de requête permet aux utilisateurs d’ajouter l’URL aux favoris. Les recommandations du W3C stipulent que GET doit être utilisé quand l’action ne produit pas de mise à jour.

Testez l’application :

  • Sélectionnez l’onglet Students et entrez une chaîne de recherche.
  • Sélectionnez Recherche.

Notez que l’URL contient la chaîne de recherche.

http://localhost:5000/Students?SearchString=an

Si la page est dans les favoris, le favori contient l’URL de la page et la chaîne de requête SearchString. method="get" dans la balise form est ce qui a provoqué la génération de la chaîne de requête.

Actuellement, quand un lien de tri d’en-tête de colonne est sélectionné, la valeur du filtre de la zone Search est perdue. La valeur de filtre perdue est corrigée dans la section suivante.

Ajouter la fonctionnalité de changement de page à la page d’index des étudiants

Dans cette section, nous allons créer une classe PaginatedList pour prendre en charge la pagination. La classe PaginatedList utilise des instructions Skip et Take pour filtrer les données sur le serveur au lieu de récupérer toutes les lignes de la table. L’illustration suivante montre les boutons de pagination.

Students index page with paging links

Dans le dossier du projet, créez PaginatedList.cs avec le code suivant :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity
{
    public class PaginatedList<T> : List<T>
    {
        public int PageIndex { get; private set; }
        public int TotalPages { get; private set; }

        public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)
        {
            PageIndex = pageIndex;
            TotalPages = (int)Math.Ceiling(count / (double)pageSize);

            this.AddRange(items);
        }

        public bool HasPreviousPage => PageIndex > 1;

        public bool HasNextPage => PageIndex < TotalPages;

        public static async Task<PaginatedList<T>> CreateAsync(
            IQueryable<T> source, int pageIndex, int pageSize)
        {
            var count = await source.CountAsync();
            var items = await source.Skip(
                (pageIndex - 1) * pageSize)
                .Take(pageSize).ToListAsync();
            return new PaginatedList<T>(items, count, pageIndex, pageSize);
        }
    }
}

La méthode CreateAsync dans le code précédent prend la taille de page et le numéro de page, et applique les instructions Skip et Take appropriées au IQueryable. Quand ToListAsync est appelée sur le IQueryable, elle retourne une liste contenant uniquement la page demandée. Les propriétés HasPreviousPage et HasNextPage sont utilisées pour activer ou désactiver les boutons de pagination Previous et Next.

La méthode CreateAsync est utilisée pour créer le PaginatedList<T>. Un constructeur ne peut pas créer l’objet PaginatedList<T> ; les constructeurs ne peuvent pas exécuter du code asynchrone.

Ajouter la fonctionnalité de pagination à la méthode Index

Dans Students/Index.cshtml.cs, mettez à jour le type de Student de IList<Student> à PaginatedList<Student> :

public PaginatedList<Student> Student { get; set; }

Mettez à jour la méthode Students/Index.cshtml.csOnGetAsync avec le code suivant :

public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)
{
    CurrentSort = sortOrder;
    NameSort = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
    DateSort = sortOrder == "Date" ? "date_desc" : "Date";
    if (searchString != null)
    {
        pageIndex = 1;
    }
    else
    {
        searchString = currentFilter;
    }

    CurrentFilter = searchString;

    IQueryable<Student> studentIQ = from s in _context.Student
                                    select s;
    if (!String.IsNullOrEmpty(searchString))
    {
        studentIQ = studentIQ.Where(s => s.LastName.Contains(searchString)
                               || s.FirstMidName.Contains(searchString));
    }
    switch (sortOrder)
    {
        case "name_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.LastName);
            break;
        case "Date":
            studentIQ = studentIQ.OrderBy(s => s.EnrollmentDate);
            break;
        case "date_desc":
            studentIQ = studentIQ.OrderByDescending(s => s.EnrollmentDate);
            break;
        default:
            studentIQ = studentIQ.OrderBy(s => s.LastName);
            break;
    }

    int pageSize = 3;
    Student = await PaginatedList<Student>.CreateAsync(
        studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);
}

Le code précédent ajoute l’index de page, le sortOrder actuel et le currentFilter à la signature de méthode.

public async Task OnGetAsync(string sortOrder,
    string currentFilter, string searchString, int? pageIndex)

Tous les paramètres sont null quand :

  • La page est appelée à partir du lien Students.
  • L’utilisateur n’a pas cliqué sur un lien de pagination ou de tri.

Quand l’utilisateur clique sur un lien de pagination, la variable d’index de page contient le numéro de page à afficher.

CurrentSort fournit l’ordre de tri actuel à la page Razor. L’ordre de tri actuel doit être inclus dans les liens de pagination afin de conserver l’ordre de tri lors de la pagination.

CurrentFilter fournit la chaîne de filtre actuelle à la page Razor. La valeur CurrentFilter :

  • Doit être incluse dans les liens de pagination afin de conserver les paramètres de filtre lors de la pagination.
  • Doit être restaurée à la zone de texte quand la page est réaffichée.

Si la chaîne de recherche est modifiée pendant la pagination, la page est réinitialisée à 1. La page doit être réinitialisée à 1, car le nouveau filtre peut entraîner l’affichage de données différentes. Quand une valeur de recherche est entrée et que le bouton Submit est sélectionné :

  • La chaîne de recherche est changée.
  • Le paramètre searchString n’est pas null.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

La méthode PaginatedList.CreateAsync convertit la requête d’étudiant en une seule page d’étudiants dans un type de collection qui prend en charge la pagination. Cette page unique d’étudiants est passée à la page Razor.

Student = await PaginatedList<Student>.CreateAsync(
    studentIQ.AsNoTracking(), pageIndex ?? 1, pageSize);

Les deux points d’interrogation dans PaginatedList.CreateAsync représentent l’opérateur de fusion de Null. L’opérateur de fusion de Null définit une valeur par défaut pour un type nullable. L’expression (pageIndex ?? 1) signifie qu’il faut retourner la valeur de pageIndex s’il a une valeur. Si pageIndex n’a pas de valeur, il faut retourner 1.

Mettez à jour le balisage dans Students/Index.cshtml. Les modifications apparaissent en surbrillance :

@page
@model ContosoUniversity.Pages.Students.IndexModel

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-page="Create">Create New</a>
</p>

<form asp-page="./Index" method="get">
    <div class="form-actions no-color">
        <p>
            Find by name: <input type="text" name="SearchString" value="@Model.CurrentFilter" />
            <input type="submit" value="Search" class="btn btn-default" /> |
            <a asp-page="./Index">Back to full List</a>
        </p>
    </div>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Student[0].LastName)
                </a>
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Student[0].FirstMidName)
            </th>
            <th>
                <a asp-page="./Index" asp-route-sortOrder="@Model.DateSort"
                   asp-route-currentFilter="@Model.CurrentFilter">
                    @Html.DisplayNameFor(model => model.Student[0].EnrollmentDate)
                </a>
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Student)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.EnrollmentDate)
                </td>
                <td>
                    <a asp-page="./Edit" asp-route-id="@item.ID">Edit</a> |
                    <a asp-page="./Details" asp-route-id="@item.ID">Details</a> |
                    <a asp-page="./Delete" asp-route-id="@item.ID">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

@{
    var prevDisabled = !Model.Student.HasPreviousPage ? "disabled" : "";
    var nextDisabled = !Model.Student.HasNextPage ? "disabled" : "";
}

<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @nextDisabled">
    Next
</a>

Les liens d’en-tête de colonne utilisent la chaîne de requête pour passer la chaîne de recherche actuelle à la méthode OnGetAsync afin que l’utilisateur puisse trier dans les résultats du filtrage :

<a asp-page="./Index" asp-route-sortOrder="@Model.NameSort"
   asp-route-currentFilter="@Model.CurrentFilter">
    @Html.DisplayNameFor(model => model.Student[0].LastName)
</a>

Les boutons de changement de page sont affichés par des Tag Helpers :


<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex - 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @prevDisabled">
    Previous
</a>
<a asp-page="./Index"
   asp-route-sortOrder="@Model.CurrentSort"
   asp-route-pageIndex="@(Model.Student.PageIndex + 1)"
   asp-route-currentFilter="@Model.CurrentFilter"
   class="btn btn-default @nextDisabled">
    Next
</a>

Exécutez l’application et accédez à la page des étudiants.

  • Pour vérifier que la pagination fonctionne, cliquez sur les liens de pagination dans différents ordres de tri.
  • Pour vérifier que la pagination fonctionne correctement avec le tri et le filtrage, entrez une chaîne de recherche et essayez de changer de page.

students index page with paging links

Pour mieux comprendre le fonctionnement du code

  • Dans Students/Index.cshtml.cs, définissez un point d’arrêt sur switch (sortOrder).
  • Ajoutez un espion pour NameSort, DateSort, CurrentSort et Model.Student.PageIndex.
  • Dans Students/Index.cshtml, définissez un point d’arrêt sur @Html.DisplayNameFor(model => model.Student[0].LastName).

Effectuez un pas à pas détaillé dans le débogueur.

Mettez à jour la page About pour afficher les statistiques sur les étudiants

Lors de cette étape, nous allons mettre à jour Pages/About.cshtml afin d’afficher le nombre d’étudiants qui se sont inscrits pour chaque date d’inscription. La mise à jour utilise le regroupement et comprend les étapes suivantes :

  • Créer un modèle de vue pour les données utilisées par la page About.
  • Mettre à jour la page About pour utiliser le modèle de vue.

Créer le modèle d’affichage

Créez un dossier SchoolViewModels dans le dossier Models.

Dans le dossier SchoolViewModels, ajoutez un EnrollmentDateGroup.cs avec le code suivant :

using System;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}

Mettre à jour le modèle de page About

Les modèles web dans ASP.NET Core 2.2 n’incluent pas la page About. Si vous utilisez ASP.NET Core 2.2, créez la page About Razor Page.

Mettez à jour le fichier Pages/About.cshtml.cs avec le code suivant :

using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ContosoUniversity.Models;

namespace ContosoUniversity.Pages
{
    public class AboutModel : PageModel
    {
        private readonly SchoolContext _context;

        public AboutModel(SchoolContext context)
        {
            _context = context;
        }

        public IList<EnrollmentDateGroup> Student { get; set; }

        public async Task OnGetAsync()
        {
            IQueryable<EnrollmentDateGroup> data =
                from student in _context.Student
                group student by student.EnrollmentDate into dateGroup
                select new EnrollmentDateGroup()
                {
                    EnrollmentDate = dateGroup.Key,
                    StudentCount = dateGroup.Count()
                };

            Student = await data.AsNoTracking().ToListAsync();
        }
    }
}

L’instruction LINQ regroupe les entités Student par date d’inscription, calcule le nombre d’entités dans chaque groupe et stocke les résultats dans une collection d’objets de modèle de vue EnrollmentDateGroup.

Modifier la page About Razor

Remplacez le code du fichier Pages/About.cshtml par le code suivant :

@page
@model ContosoUniversity.Pages.AboutModel

@{
    ViewData["Title"] = "Student Body Statistics";
}

<h2>Student Body Statistics</h2>

<table>
    <tr>
        <th>
            Enrollment Date
        </th>
        <th>
            Students
        </th>
    </tr>

    @foreach (var item in Model.Student)
    {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                @item.StudentCount
            </td>
        </tr>
    }
</table>

Exécutez l’application et accédez à la page About. Le nombre d’étudiants pour chaque date d’inscription s’affiche dans une table.

Si vous rencontrez des problèmes que vous ne pouvez pas résoudre, téléchargez l’application terminée pour cette phase.

About page

Ressources supplémentaires

Dans le didacticiel suivant, l’application utilise des migrations pour mettre à jour le modèle de données.