Partager via


Implémenter une pagination des données efficace

par Microsoft

Télécharger le PDF

Il s’agit de l’étape 8 d’un didacticiel gratuit sur l’application « NerdDinner » qui explique comment créer une application web petite mais complète à l’aide de ASP.NET MVC 1.

L’étape 8 montre comment ajouter la prise en charge de la pagination à notre URL /Dinners afin qu’au lieu d’afficher 1 000 dîners à la fois, nous n’affichons que 10 dîners à venir à la fois et nous permettons aux utilisateurs finaux de parcourir la liste entière d’une manière conviviale pour le référencement.

Si vous utilisez ASP.NET MVC 3, nous vous recommandons de suivre les didacticiels Prise en main Avec MVC 3 ou MVC Music Store.

Étape 8 de NerdDinner : Prise en charge de la pagination

Si notre site réussit, il y aura des milliers de dîners à venir. Nous devons nous assurer que notre interface utilisateur est mise à l’échelle pour gérer tous ces dîners et permettre aux utilisateurs de les parcourir. Pour ce faire, nous allons ajouter la prise en charge de la pagination à notre URL /Dinners afin qu’au lieu d’afficher 1 000 dîners à la fois, nous n’affichons que 10 dîners à venir à la fois et nous permettons aux utilisateurs finaux de parcourir la liste entière de manière conviviale pour le référencement.

Récap des méthodes d’action Index()

La méthode d’action Index() de notre classe DinnersController se présente actuellement comme suit :

//
// GET: /Dinners/

public ActionResult Index() {

    var dinners = dinnerRepository.FindUpcomingDinners().ToList();
    return View(dinners);
}

Lorsqu’une demande est envoyée à l’URL /Dinners , elle récupère une liste de tous les dîners à venir, puis affiche une liste de tous les dîners :

Capture d’écran de la page Liste des dîners à venir.

Présentation de IQueryable<T>

Iqueryable<T> est une interface qui a été introduite avec LINQ dans le cadre de .NET 3.5. Il permet de puissants scénarios d’exécution différée dont nous pouvons tirer parti pour implémenter la prise en charge de la pagination.

Dans notre DinnerRepository, nous renvoyons une séquence IQueryable<Dinner> à partir de notre méthode FindUpcomingDinners() :

public class DinnerRepository {

    private NerdDinnerDataContext db = new NerdDinnerDataContext();

    //
    // Query Methods

    public IQueryable<Dinner> FindUpcomingDinners() {
    
        return from dinner in db.Dinners
               where dinner.EventDate > DateTime.Now
               orderby dinner.EventDate
               select dinner;
    }

L’objet IQueryable<Dinner> retourné par notre méthode FindUpcomingDinners() encapsule une requête pour récupérer des objets Dinner de notre base de données à l’aide de LINQ to SQL. Il est important de noter qu’il n’exécutera pas la requête sur la base de données tant que nous n’aurons pas tenté d’accéder aux données de la requête ou d’itérer les données de la requête, ou jusqu’à ce que nous appelons la méthode ToList() sur celle-ci. Le code appelant notre méthode FindUpcomingDinners() peut éventuellement choisir d’ajouter des opérations/filtres « chaînés » supplémentaires à l’objet IQueryable<Dinner> avant d’exécuter la requête. LINQ to SQL est alors suffisamment intelligent pour exécuter la requête combinée sur la base de données lorsque les données sont demandées.

Pour implémenter la logique de pagination, nous pouvons mettre à jour la méthode d’action Index() de DinnersController afin qu’elle applique des opérateurs « Skip » et « Take » supplémentaires à la séquence IQueryable<Dinner> retournée avant d’appeler ToList() sur celle-ci :

//
// GET: /Dinners/

public ActionResult Index() {

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();

    return View(paginatedDinners);
}

Le code ci-dessus ignore les 10 premiers dîners à venir dans la base de données, puis retourne 20 dîners. LINQ to SQL est suffisamment intelligent pour construire une requête SQL optimisée qui effectue cette logique d’évitement dans la base de données SQL, et non dans le serveur web. Cela signifie que même si nous avons des millions de dîners à venir dans la base de données, seuls les 10 que nous voulons seront récupérés dans le cadre de cette demande (ce qui la rend efficace et évolutive).

Ajout d’une valeur « page » à l’URL

Au lieu de coder en dur une plage de pages spécifique, nous voulons que nos URL incluent un paramètre « page » qui indique la plage Dinner demandée par un utilisateur.

Utilisation d’une valeur Querystring

Le code ci-dessous montre comment nous pouvons mettre à jour notre méthode d’action Index() pour prendre en charge un paramètre de chaîne de requête et activer les URL telles que /Dinners?page=2 :

//
// GET: /Dinners/
//      /Dinners?page=2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();

    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
                                          .Take(pageSize)
                                          .ToList();

    return View(paginatedDinners);
}

La méthode d’action Index() ci-dessus a un paramètre nommé « page ». Le paramètre est déclaré sous la forme d’un entier nullable (c’est ce qu’indique int ? ). Cela signifie que l’URL /Dinners?page=2 entraîne le passage de la valeur « 2 » en tant que valeur de paramètre. L’URL /Dinners (sans valeur de chaîne de requête) entraîne le passage d’une valeur null.

Nous multiplions la valeur de la page par la taille de la page (dans ce cas, 10 lignes) pour déterminer le nombre de dîners à ignorer. Nous utilisons l’opérateur « coalescing » C# null (??) qui est utile pour traiter des types nullables. Le code ci-dessus affecte à la page la valeur 0 si le paramètre de page a la valeur Null.

Utilisation de valeurs d’URL incorporées

Une alternative à l’utilisation d’une valeur de chaîne de requête consiste à incorporer le paramètre de page dans l’URL elle-même. Par exemple : /Dinners/Page/2 ou /Dinners/2. ASP.NET MVC comprend un puissant moteur de routage d’URL qui facilite la prise en charge de scénarios comme celui-ci.

Nous pouvons inscrire des règles de routage personnalisées qui mappent n’importe quelle URL entrante ou format d’URL à n’importe quelle classe de contrôleur ou méthode d’action souhaitée. Tout ce que nous avons à faire est d’ouvrir le fichier Global.asax dans notre projet :

Capture d’écran de l’arborescence de navigation Nerd Dinner. Point global a s a x est sélectionné et mis en surbrillance.

Puis inscrivez une nouvelle règle de mappage à l’aide de la méthode d’assistance MapRoute(), comme le premier appel aux itinéraires. MapRoute() ci-dessous :

public void RegisterRoutes(RouteCollection routes) {

   routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute(                                        
        "UpcomingDinners",                               // Route name
        "Dinners/Page/{page}",                           // URL with params
        new { controller = "Dinners", action = "Index" } // Param defaults
    );

    routes.MapRoute(
        "Default",                                       // Route name
        "{controller}/{action}/{id}",                    // URL with params
        new { controller="Home", action="Index",id="" }  // Param defaults
    );
}

void Application_Start() {
    RegisterRoutes(RouteTable.Routes);
}

Ci-dessus, nous enregistrons une nouvelle règle de routage nommée « UpcomingDinners ». Nous indiquons qu’elle a le format d’URL « Dîners/Page/{page} », où {page} est une valeur de paramètre incorporée dans l’URL. Le troisième paramètre de la méthode MapRoute() indique que nous devons mapper les URL qui correspondent à ce format à la méthode d’action Index() sur la classe DinnersController.

Nous pouvons utiliser exactement le même code Index() que celui que nous avions auparavant avec notre scénario Querystring, sauf que maintenant, notre paramètre « page » provient de l’URL et non de la chaîne de requête :

//
// GET: /Dinners/
//      /Dinners/Page/2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    
    var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
                                          .Take(pageSize)
                                          .ToList();

    return View(paginatedDinners);
}

Et maintenant, lorsque nous exécutons l’application et tapez /Dinners , nous voyons les 10 premiers dîners à venir :

Capture d’écran de la liste des dîners à venir de Nerd.

Et lorsque nous tapez /Dinners/Page/1 , nous voyons la page suivante des dîners :

Capture d’écran de la page suivante de la liste des dîners à venir.

Ajout de l’interface utilisateur de navigation de page

La dernière étape pour terminer notre scénario de pagination consistera à implémenter l’interface utilisateur de navigation « suivante » et « précédente » dans notre modèle d’affichage pour permettre aux utilisateurs d’ignorer facilement les données dinner.

Pour l’implémenter correctement, nous devons connaître le nombre total de dîners dans la base de données, ainsi que le nombre de pages de données vers lesquelles cela se traduit. Nous devons ensuite calculer si la valeur « page » actuellement demandée se trouve au début ou à la fin des données, et afficher ou masquer l’interface utilisateur « précédente » et « suivante » en conséquence. Nous pourrions implémenter cette logique dans notre méthode d’action Index(). Nous pouvons également ajouter une classe d’assistance à notre projet qui encapsule cette logique de manière plus réutilisable.

Vous trouverez ci-dessous une classe d’assistance simple « PaginatedList » qui dérive de la classe de collection List<T> intégrée au .NET Framework. Il implémente une classe de collection réutilisable qui peut être utilisée pour paginer n’importe quelle séquence de données IQueryable. Dans notre application NerdDinner, il fonctionnera sur les résultats IQueryable<Dinner> , mais il pourrait tout aussi facilement être utilisé par rapport aux résultats IQueryable<Product> ou IQueryable<Customer> dans d’autres scénarios d’application :

public class PaginatedList<T> : List<T> {

    public int PageIndex  { get; private set; }
    public int PageSize   { get; private set; }
    public int TotalCount { get; private set; }
    public int TotalPages { get; private set; }

    public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {
        PageIndex = pageIndex;
        PageSize = pageSize;
        TotalCount = source.Count();
        TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize);

        this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
    }

    public bool HasPreviousPage {
        get {
            return (PageIndex > 0);
        }
    }

    public bool HasNextPage {
        get {
            return (PageIndex+1 < TotalPages);
        }
    }
}

Notez ci-dessus comment il calcule, puis expose des propriétés telles que « PageIndex », « PageSize », « TotalCount » et « TotalPages ». Il expose également deux propriétés d’assistance « HasPreviousPage » et « HasNextPage » qui indiquent si la page de données de la collection se trouve au début ou à la fin de la séquence d’origine. Le code ci-dessus entraîne l’exécution de deux requêtes SQL : la première pour récupérer le nombre total d’objets Dinner (cela ne retourne pas les objets ; elle exécute plutôt une instruction « SELECT COUNT » qui retourne un entier) et la seconde pour récupérer uniquement les lignes de données dont nous avons besoin à partir de notre base de données pour la page de données active.

Nous pouvons ensuite mettre à jour notre méthode d’assistance DinnersController.Index() pour créer un dîner PaginatedList<à> partir de notre résultat DinnerRepository.FindUpcomingDinners() et le passer à notre modèle d’affichage :

//
// GET: /Dinners/
//      /Dinners/Page/2

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = new PaginatedList<Dinner>(upcomingDinners, page ?? 0, pageSize);

    return View(paginatedDinners);
}

Nous pouvons ensuite mettre à jour le modèle de vue \Views\Dinners\Index.aspx pour hériter de ViewPage<NerdDinner.Helpers.PaginatedList Dinner<>> au lieu de ViewPage<IEnumerable<Dinner>>, puis ajouter le code suivant au bas de notre modèle d’affichage pour afficher ou masquer l’interface utilisateur de navigation suivante et précédente :

<% if (Model.HasPreviousPage) { %>

    <%= Html.RouteLink("<<<", "UpcomingDinners", new { page = (Model.PageIndex-1) }) %>

<% } %>

<% if (Model.HasNextPage) {  %>

    <%= Html.RouteLink(">>>", "UpcomingDinners", new { page = (Model.PageIndex + 1) }) %>

<% } %>

Notez ci-dessus comment nous utilisons la méthode d’assistance Html.RouteLink() pour générer nos liens hypertexte. Cette méthode est similaire à la méthode d’assistance Html.ActionLink() que nous avons utilisée précédemment. La différence est que nous générons l’URL à l’aide de la règle de routage « UpcomingDinners » que nous avons configurée dans notre fichier Global.asax. Cela garantit que nous allons générer des URL pour notre méthode d’action Index() au format /Dinners/Page/{page} , où la valeur {page} est une variable que nous fournissons ci-dessus en fonction du PageIndex actuel.

Et maintenant, lorsque nous réexécutons notre application, nous voyons 10 dîners à la fois dans notre navigateur :

Capture d’écran de la liste des dîners à venir sur la page Nerd Dinners.

Nous avons <<< également et >>> l’interface utilisateur de navigation en bas de la page qui nous permet d’ignorer l’avant et l’arrière sur nos données à l’aide des URL accessibles par le moteur de recherche :

Capture d’écran de la page Dîners Nerd avec la liste des dîners à venir.

Rubrique secondaire : Compréhension des implications de IQueryable<T>
IQueryable<T> est une fonctionnalité très puissante qui permet divers scénarios d’exécution différées intéressants (comme la pagination et les requêtes basées sur la composition). Comme avec toutes les fonctionnalités puissantes, vous voulez être prudent dans la façon dont vous l’utilisez et vous assurer qu’il n’est pas abusé. Il est important de reconnaître que le renvoi d’un résultat IQueryable<T> à partir de votre dépôt permet d’ajouter du code appelant sur des méthodes d’opérateur chaînées, et donc de participer à l’exécution finale de la requête. Si vous ne souhaitez pas fournir de code appelant cette capacité, vous devez retourner les résultats IList<T> ou IEnumerable<T> , qui contiennent les résultats d’une requête qui a déjà été exécutée. Pour les scénarios de pagination, vous devez envoyer (push) la logique de pagination des données réelle dans la méthode de dépôt appelée. Dans ce scénario, nous pouvons mettre à jour notre méthode de recherche FindUpcomingDinners() pour avoir une signature qui a retourné un PaginatedList: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } Ou renvoyer un dîner> IList<, et utiliser un paramètre « totalCount » out pour renvoyer le nombre total de dîners : IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

étape suivante

Voyons maintenant comment ajouter la prise en charge de l’authentification et de l’autorisation à notre application.