Página 3. Razor Pages con EF Core en ASP.NET Core: Ordenación, filtrado y paginación

De Tom Dykstra, Jeremy Likness y Jon P Smith

En la aplicación web Contoso University se muestra cómo crear aplicaciones web Razor Pages con EF Core y Visual Studio. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial.

Si surgen problemas que no puede resolver, descargue la aplicación completada y compare ese código con el que ha creado siguiendo el tutorial.

En este tutorial se agrega funcionalidad de ordenación, filtrado y paginación a las páginas Student.

En la siguiente ilustración se muestra una página completa. Los encabezados de columna son vínculos interactivos para ordenar la columna. Haga clic de forma consecutiva en el encabezado de una columna para alternar el criterio de ordenación entre ascendente y descendente.

Students index page

Adición de ordenación

Reemplace el código de Pages/Students/Index.cshtml.cs por el código siguiente para agregar ordenación.

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

El código anterior:

  • Requiere que se agregue using System;.
  • Agrega propiedades que contienen los parámetros de ordenación.
  • Cambia el nombre de la propiedad Student a Students.
  • Reemplaza el código del método OnGetAsync.

El método OnGetAsync recibe un parámetro sortOrder de la cadena de consulta en la dirección URL. El asistente de etiquetas delimitadoras genera la dirección URL y la cadena de consulta.

El valor del parámetro sortOrder es Name o Date. Opcionalmente, el parámetro sortOrder puede ir seguido de _desc para especificar el orden descendente. El criterio de ordenación predeterminado es el ascendente.

Cuando se solicita la página de índice del vínculo Students no hay ninguna cadena de consulta. Los alumnos se muestran en orden ascendente por apellido. El orden ascendente por apellido es default en la instrucción switch. Cuando el usuario hace clic en un vínculo de encabezado de columna, se proporciona el valor sortOrder correspondiente en el valor de la cadena de consulta.

La instancia de Razor Pages usa NameSort y DateSort para configurar los hipervínculos del encabezado de columna con los valores de cadena de consulta adecuados:

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

En el código se usa el operador condicional ?: de C#. El operador ?: es ternario, por lo que toma tres operandos. La primera línea especifica que, cuando sortOrder es NULL o está vacío, NameSort se establece en name_desc. Si nosortOrder es NULL ni está vacío, NameSort se establece en una cadena vacía.

Estas dos instrucciones habilitan la página para establecer los hipervínculos de encabezado de columna de la siguiente forma:

Criterio de ordenación actual Hipervínculo de apellido Hipervínculo de fecha
Apellido: ascendente descending ascending
Apellido: descendente ascending ascending
Fecha: ascendente ascending descending
Fecha: descendente ascending ascending

El método usa LINQ to Entities para especificar la columna por la que se va a ordenar. El código inicializa un IQueryable<Student> antes de la instrucción switch y lo modifica en la instrucción 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();

Cuando se crea o se modifica IQueryable, no se envía ninguna consulta a la base de datos. La consulta no se ejecuta hasta que el objeto IQueryable se convierte en una colección. IQueryable se convierte en una colección mediante una llamada a un método como ToListAsync. Por lo tanto, el código IQueryable produce una única consulta que no se ejecuta hasta la siguiente instrucción:

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

OnGetAsync se podría detallar con un gran número de columnas ordenables. Para obtener información sobre una forma alternativa de codificar esta funcionalidad, vea Usar LINQ dinámico para simplificar el código en la versión para MVC de esta serie de tutoriales.

Reemplace el código de Students/Index.cshtml por el código siguiente. Los cambios aparecen resaltados.

@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>

El código anterior:

  • Agrega hipervínculos a los encabezados de columna LastName y EnrollmentDate.
  • Usa la información de NameSort y DateSort para configurar hipervínculos con los valores de criterio de ordenación actuales.
  • Cambia el encabezado de la página de Index a Students.
  • Cambia Model.Student por Model.Students.

Para comprobar que la ordenación funciona:

  • Ejecute la aplicación y haga clic en la pestaña Students.
  • Haga clic en los encabezados de columna.

Adición de filtrado

Para agregar un filtro a la página de índice de Students:

  • Se agregan un cuadro de texto y un botón de envío a la página de Razor. El cuadro de texto proporciona una cadena de búsqueda de nombre o apellido.
  • El modelo de página se actualiza para usar el valor del cuadro de texto.

Actualización del método OnGetAsync

Reemplace el código de Students/Index.cshtml.cs por el código siguiente para agregar filtrado:

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

El código anterior:

  • Agrega el parámetro searchString al método OnGetAsync y guarda el valor del parámetro en la propiedad CurrentFilter. El valor de la cadena de búsqueda se recibe desde un cuadro de texto que se agrega en la siguiente sección.
  • Agrega una cláusula Where a la instrucción LINQ. La cláusula Where selecciona solo los alumnos cuyo nombre o apellido contienen la cadena de búsqueda. La instrucción LINQ se ejecuta solo si hay un valor para buscar.

Diferencias entre IQueryable e IEnumerable

El código llama al método Where en un objeto IQueryable y el filtro se procesa en el servidor. En algunos escenarios, la aplicación puede hacer una llamada al método Where como un método de extensión en una colección en memoria. Por ejemplo, suponga que _context.Students cambia de EF CoreDbSet a un método de repositorio que devuelve una colección IEnumerable. Lo más habitual es que el resultado fuera el mismo, pero en algunos casos puede ser diferente.

Por ejemplo, la implementación de .NET Framework de Contains realiza una comparación que distingue mayúsculas de minúsculas de forma predeterminada. En SQL Server, la distinción entre mayúsculas y minúsculas de Contains viene determinada por la configuración de intercalación de la instancia de SQL Server. SQL Server no diferencia entre mayúsculas y minúsculas de forma predeterminada. De forma predeterminada, SQLite distingue mayúsculas de minúsculas. Se podría llamar a ToUpper para hacer explícitamente que la prueba no distinga entre mayúsculas y minúsculas:

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

El código anterior se aseguraría de que el filtro no distinga entre mayúsculas y minúsculas incluso si se llama al método Where en una interfaz IEnumerable o se ejecuta en SQLite.

Cuando se llama a Contains en una colección IEnumerable, se usa la implementación de .NET Core. Cuando se llama a Contains en un objeto IQueryable, se usa la implementación de la base de datos.

La llamada a Contains en una instancia de IQueryablesuele ser preferible por motivos de rendimiento. Con IQueryable, el filtrado lo realiza el servidor de base de datos. Si primero se crea IEnumerable, todas las filas tendrán que devolverse desde el servidor de base de datos.

Hay una disminución del rendimiento por llamar a ToUpper. El código ToUpper agrega una función en la cláusula WHERE de la instrucción SELECT de TSQL. La función agregada impide que el optimizador use un índice. Dado que SQL está instalado para no distinguir entre mayúsculas y minúsculas, es mejor evitar llamar a ToUpper cuando no sea necesario.

Para obtener más información, vea este artículo, en el que se describen los procedimientos para usar consultas que no distinguen mayúsculas de minúsculas con el proveedor de SQLite.

Actualización de la página de Razor

Reemplace el código de Pages/Students/Index.cshtml para agregar un botón Search.

@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>

El código anterior usa el asistente de etiquetas<form> para agregar el cuadro de texto de búsqueda y el botón. De forma predeterminada, el asistente de etiquetas <form> envía datos de formulario con POST. Con POST, los parámetros se pasan en el cuerpo del mensaje HTTP y no en la dirección URL. Cuando se usa el método HTTP GET, los datos del formulario se pasan en la dirección URL como cadenas de consulta. Pasar los datos con cadenas de consulta permite a los usuarios marcar la dirección URL. Las directrices de W3C recomiendan el uso de GET cuando la acción no produzca ninguna actualización.

Pruebe la aplicación:

  • Seleccione la pestaña Students y escriba una cadena de búsqueda. Si usa SQLite, el filtro no distingue entre mayúsculas y minúsculas solo si ha implementado el código ToUpper opcional que se ha mostrado antes.

  • Seleccione Search.

Fíjese en que la dirección URL contiene la cadena de búsqueda. Por ejemplo:

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

Si se colocó un marcador en la página, el marcador contiene la dirección URL a la página y la cadena de consulta de SearchString. El method="get" en la etiqueta form es lo que ha provocado que se generara la cadena de consulta.

Actualmente, cuando se selecciona un vínculo de ordenación del encabezado de columna, el filtro de valor del cuadro Search se pierde. El valor de filtro perdido se fija en la sección siguiente.

Adición de paginación

En esta sección, se crea una clase PaginatedList para admitir la paginación. La clase PaginatedList usa las instrucciones Skip y Take para filtrar los datos en el servidor en lugar de recuperar todas las filas de la tabla. La ilustración siguiente muestra los botones de paginación.

Students index page with paging links

Creación de la clase PaginatedList

En la carpeta del proyecto, cree PaginatedList.cs con el código siguiente:

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

El método CreateAsync en el código anterior toma el tamaño y el número de la página, y aplica las instrucciones Skip y Take correspondientes a IQueryable. Cuando ToListAsync se llama en IQueryable, devuelve una lista que solo contiene la página solicitada. Las propiedades HasPreviousPage y HasNextPage se usan para habilitar o deshabilitar los botones de página Previous y Next.

El método CreateAsync se usa para crear la PaginatedList<T>. Un constructor no puede crear el objeto PaginatedList<T>; los constructores no pueden ejecutar código asincrónico.

Adición del tamaño de página a la configuración

Agregue PageSize al archivo de appsettings.jsonconfiguración:

{
  "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"
  }
}

Adición de paginación a IndexModel

Reemplace el código de Students/Index.cshtml.cs para agregar paginación.

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

El código anterior:

  • Cambia el tipo de la propiedad Students de IList<Student> a PaginatedList<Student>.
  • Agrega el índice de la página, el elemento sortOrder actual y el elemento currentFilter a la firma del método OnGetAsync.
  • Guarda el criterio de ordenación en la propiedad CurrentSort.
  • Restablece el índice de la página en 1 cuando hay una cadena de búsqueda nueva.
  • Usa la clase PaginatedList para obtener las entidades Student.
  • Establece pageSize en 3 desde Configuración, o en 4 si se produce un error de configuración.

Todos los parámetros que recibe OnGetAsync son NULL cuando:

  • Se llama a la página desde el vínculo Students.
  • El usuario no ha seleccionado un vínculo de ordenación o paginación.

Cuando se hace clic en un vínculo de paginación, la variable de índice de página contiene el número de página que se tiene que mostrar.

La propiedad CurrentSort proporciona a la instancia de Razor Pages el criterio de ordenación actual. Se debe incluir el criterio de ordenación actual en los vínculos de paginación para mantener el criterio de ordenación durante la paginación.

La propiedad CurrentFilter proporciona a la instancia de Razor Pages la cadena de filtro actual. El valor CurrentFilter:

  • Debe incluirse en los vínculos de paginación para mantener la configuración del filtro durante la paginación.
  • Debe restaurarse en el cuadro de texto cuando se vuelva a mostrar la página.

Si se cambia la cadena de búsqueda durante la paginación, la página se restablece a 1. La página debe restablecerse a 1 porque el nuevo filtro puede hacer que se muestren diferentes datos. Cuando se escribe un valor de búsqueda y se selecciona Submit:

  • La cadena de búsqueda cambia.
  • El parámetro searchString no es NULL.

El método PaginatedList.CreateAsync convierte la consulta del alumno en una sola página de alumnos de un tipo de colección que admita la paginación. Esa única página de alumnos se pasa a la página de Razor.

Los dos signos de interrogación después de pageIndex en la llamada a PaginatedList.CreateAsync representan el operador de uso combinado de NULL. El operador de uso combinado de NULL define un valor predeterminado para un tipo que acepta valores NULL. La expresión pageIndex ?? 1 devuelve el valor de pageIndex si tiene un valor; de lo contrario, devuelve 1.

Reemplace el código de Students/Index.cshtml por el código siguiente. Se resaltan los cambios:

@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>

En los vínculos de encabezado de columna se usa la cadena de consulta para pasar la cadena de búsqueda actual al método OnGetAsync:

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

Los botones de paginación se muestran mediante asistentes de etiquetas:


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

Ejecute la aplicación y vaya a la página Students.

  • Para comprobar que la paginación funciona correctamente, haga clic en los vínculos de paginación en distintos criterios de ordenación.
  • Para comprobar que la paginación también funciona correctamente con filtrado y ordenación, escriba una cadena de búsqueda e intente llevar a cabo la paginación de nuevo.

students index page with paging links

Agrupación

En esta sección se crea una página About en la que se muestra cuántos alumnos se han inscrito en cada fecha. La actualización usa la agrupación e incluye los siguientes pasos:

  • Cree un modelo de vista para los datos que se usan en la página About.
  • Actualice la página About para usar el modelo de vista.

Creación del modelo de vista

Cree una carpeta Models/SchoolViewModels.

Cree el archivo SchoolViewModels/EnrollmentDateGroup.cs con el siguiente código:

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

Creación de la instancia de Razor Pages

Cree un archivo Pages/About.cshtml con el código siguiente:

@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>

Creación del modelo de página

Actualice el archivo Pages/About.cshtml.cs con el código siguiente:

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

La instrucción LINQ agrupa las entidades de alumnos por fecha de inscripción, calcula la cantidad de entidades que se incluyen en cada grupo y almacena los resultados en una colección de objetos de modelo de la vista EnrollmentDateGroup.

Ejecute la aplicación y vaya a la página About. En una tabla se muestra el número de alumnos para cada fecha de inscripción.

About page

Pasos siguientes

En el tutorial siguiente, la aplicación usa las migraciones para actualizar el modelo de datos.

En este tutorial se agregan las funcionalidades de ordenación, filtrado, agrupación y paginación.

En la siguiente ilustración se muestra una página completa. Los encabezados de columna son vínculos interactivos para ordenar la columna. Si se hace clic de forma consecutiva en el encabezado de una columna, el criterio de ordenación cambia entre ascendente y descendente.

Students index page

Si experimenta problemas que no puede resolver, descargue la aplicación completada.

Agregar ordenación a la página de índice

Agregue cadenas al elemento PageModel de Students/Index.cshtml.cs para que contenga los parámetros de ordenación:

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

Actualice el elemento OnGetAsync de Students/Index.cshtml.cs con el código siguiente:

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

El código anterior recibe un parámetro sortOrder de la cadena de consulta en la dirección URL. El asistente de etiquetas delimitadoras genera la dirección URL (incluida la cadena de consulta).

El parámetro sortOrder es "Name" o "Date". Opcionalmente, el parámetro sortOrder puede ir seguido de "_desc" para especificar el orden descendente. El criterio de ordenación predeterminado es el ascendente.

Cuando se solicita la página de índice del vínculo Students no hay ninguna cadena de consulta. Los alumnos se muestran en orden ascendente por apellido. El orden ascendente por apellido es el valor predeterminado (caso de paso explícito) en la instrucción switch. Cuando el usuario hace clic en un vínculo de encabezado de columna, se proporciona el valor sortOrder correspondiente en el valor de la cadena de consulta.

La instancia de Razor Pages usa NameSort y DateSort para configurar los hipervínculos del encabezado de columna con los valores de cadena de consulta adecuados:

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

El código siguiente contiene el operador ?: condicional de C#:

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

La primera línea especifica que cuando sortOrder es NULL o está vacío, NameSort se establece en "name_desc". Si sortOrderno es NULL o está vacío, se establece NameSort en una cadena vacía.

El ?: operator también se conoce como el operador ternario.

Estas dos instrucciones habilitan la página para establecer los hipervínculos de encabezado de columna de la siguiente forma:

Criterio de ordenación actual Hipervínculo de apellido Hipervínculo de fecha
Apellido: ascendente descending ascending
Apellido: descendente ascending ascending
Fecha: ascendente ascending descending
Fecha: descendente ascending ascending

El método usa LINQ to Entities para especificar la columna por la que se va a ordenar. El código inicializa un IQueryable<Student> antes de la instrucción switch y lo modifica en la instrucción 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();
}

Cuando se crea o se modifica un IQueryable, no se envía ninguna consulta a la base de datos. La consulta no se ejecuta hasta que el objeto IQueryable se convierte en una colección. IQueryable se convierte en una colección mediante una llamada a un método como ToListAsync. Por lo tanto, el código IQueryable produce una única consulta que no se ejecuta hasta la siguiente instrucción:

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

OnGetAsync se podría detallar con un gran número de columnas ordenables.

Reemplace el código de Students/Index.cshtml con el siguiente código resaltado:

@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>

El código anterior:

  • Agrega hipervínculos a los encabezados de columna LastName y EnrollmentDate.
  • Usa la información de NameSort y DateSort para configurar hipervínculos con los valores de criterio de ordenación actuales.

Para comprobar que la ordenación funciona:

  • Ejecute la aplicación y haga clic en la pestaña Students.
  • Haga clic en Last Name.
  • Haga clic en Enrollment Date.

Para comprender mejor el código:

  • En Students/Index.cshtml.cs, establezca un punto de interrupción en switch (sortOrder).
  • Agregue una inspección para NameSort y DateSort.
  • En Students/Index.cshtml, establezca un punto de interrupción en @Html.DisplayNameFor(model => model.Student[0].LastName).

Ejecute paso a paso el depurador.

Agregar un cuadro de búsqueda a la página de índice de Students

Para agregar un filtro a la página de índice de Students:

  • Se agregan un cuadro de texto y un botón de envío a la página de Razor. El cuadro de texto proporciona una cadena de búsqueda de nombre o apellido.
  • El modelo de página se actualiza para usar el valor del cuadro de texto.

Agregar la funcionalidad de filtrado al método Index

Actualice el elemento OnGetAsync de Students/Index.cshtml.cs con el código siguiente:

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

El código anterior:

  • Agrega el parámetro searchString al método OnGetAsync. El valor de la cadena de búsqueda se recibe desde un cuadro de texto que se agrega en la siguiente sección.
  • Se agregó una cláusula Where a la instrucción LINQ. La cláusula Where selecciona solo los alumnos cuyo nombre o apellido contienen la cadena de búsqueda. La instrucción LINQ se ejecuta solo si hay un valor para buscar.

Nota: El código anterior llama al método Where en un objeto IQueryable y el filtro se procesa en el servidor. En algunos escenarios, la aplicación puede hacer una llamada al método Where como un método de extensión en una colección en memoria. Por ejemplo, suponga que _context.Students cambia de EF CoreDbSet a un método de repositorio que devuelve una colección IEnumerable. Lo más habitual es que el resultado fuera el mismo, pero en algunos casos puede ser diferente.

Por ejemplo, la implementación de .NET Framework de Contains realiza una comparación que distingue mayúsculas de minúsculas de forma predeterminada. En SQL Server, la distinción entre mayúsculas y minúsculas de Contains viene determinada por la configuración de intercalación de la instancia de SQL Server. SQL Server no diferencia entre mayúsculas y minúsculas de forma predeterminada. Se podría llamar a ToUpper para hacer explícitamente que la prueba no distinga entre mayúsculas y minúsculas:

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

El código anterior garantiza que los resultados no distingan entre mayúsculas y minúsculas si cambia el código para que use IEnumerable. Cuando se llama a Contains en una colección IEnumerable, se usa la implementación de .NET Core. Cuando se llama a Contains en un objeto IQueryable, se usa la implementación de la base de datos. Devolver un IEnumerable desde un repositorio puede acarrear una disminución significativa del rendimiento:

  1. Todas las filas se devuelven desde el servidor de base de datos.
  2. El filtro se aplica a todas las filas devueltas en la aplicación.

Hay una disminución del rendimiento por llamar a ToUpper. El código ToUpper agrega una función en la cláusula WHERE de la instrucción SELECT de TSQL. La función agregada impide que el optimizador use un índice. Dado que SQL está instalado para no distinguir entre mayúsculas y minúsculas, es mejor evitar llamar a ToUpper cuando no sea necesario.

Agregar un cuadro de búsqueda a la página de índice de Student

En Pages/Students/Index.cshtml, agregue el siguiente código resaltado para crear un botón Search y cromo ordenado.

@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">

El código anterior usa el asistente de etiquetas<form> para agregar el cuadro de texto de búsqueda y el botón. De forma predeterminada, el asistente de etiquetas <form> envía datos de formulario con POST. Con POST, los parámetros se pasan en el cuerpo del mensaje HTTP y no en la dirección URL. Cuando se usa el método HTTP GET, los datos del formulario se pasan en la dirección URL como cadenas de consulta. Pasar los datos con cadenas de consulta permite a los usuarios marcar la dirección URL. Las directrices de W3C recomiendan el uso de GET cuando la acción no produzca ninguna actualización.

Pruebe la aplicación:

  • Seleccione la pestaña Students y escriba una cadena de búsqueda.
  • Seleccione Search.

Fíjese en que la dirección URL contiene la cadena de búsqueda.

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

Si se colocó un marcador en la página, el marcador contiene la dirección URL a la página y la cadena de consulta de SearchString. El method="get" en la etiqueta form es lo que ha provocado que se generara la cadena de consulta.

Actualmente, cuando se selecciona un vínculo de ordenación del encabezado de columna, el filtro de valor del cuadro Search se pierde. El valor de filtro perdido se fija en la sección siguiente.

Agregar la funcionalidad de paginación a la página de índice de Students

En esta sección, se crea una clase PaginatedList para admitir la paginación. La clase PaginatedList usa las instrucciones Skip y Take para filtrar los datos en el servidor en lugar de recuperar todas las filas de la tabla. La ilustración siguiente muestra los botones de paginación.

Students index page with paging links

En la carpeta del proyecto, cree PaginatedList.cs con el código siguiente:

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

El método CreateAsync en el código anterior toma el tamaño y el número de la página, y aplica las instrucciones Skip y Take correspondientes a IQueryable. Cuando ToListAsync se llama en IQueryable, devuelve una lista que solo contiene la página solicitada. Las propiedades HasPreviousPage y HasNextPage se usan para habilitar o deshabilitar los botones de página Previous y Next.

El método CreateAsync se usa para crear la PaginatedList<T>. No se puede crear un constructor del objeto PaginatedList<T>, los constructores no pueden ejecutar código asincrónico.

Agregar la funcionalidad de paginación al método Index

En Students/Index.cshtml.cs, actualice el tipo de Student de IList<Student> a PaginatedList<Student>:

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

Actualice el elemento OnGetAsync de Students/Index.cshtml.cs con el código siguiente:

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

El código anterior agrega el índice de la página, el sortOrder actual y el currentFilter a la firma del método.

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

Todos los parámetros son NULL cuando:

  • Se llama a la página desde el vínculo Students.
  • El usuario no ha seleccionado un vínculo de ordenación o paginación.

Cuando se hace clic en un vínculo de paginación, la variable de índice de página contiene el número de página que se tiene que mostrar.

CurrentSort proporciona a la página de Razor el criterio de ordenación actual. Se debe incluir el criterio de ordenación actual en los vínculos de paginación para mantener el criterio de ordenación durante la paginación.

CurrentFilter proporciona a la página de Razor la cadena de filtrado actual. El valor CurrentFilter:

  • Debe incluirse en los vínculos de paginación para mantener la configuración del filtro durante la paginación.
  • Debe restaurarse en el cuadro de texto cuando se vuelva a mostrar la página.

Si se cambia la cadena de búsqueda durante la paginación, la página se restablece a 1. La página debe restablecerse a 1 porque el nuevo filtro puede hacer que se muestren diferentes datos. Cuando se escribe un valor de búsqueda y se selecciona Submit:

  • La cadena de búsqueda cambia.
  • El parámetro searchString no es NULL.
if (searchString != null)
{
    pageIndex = 1;
}
else
{
    searchString = currentFilter;
}

El método PaginatedList.CreateAsync convierte la consulta del alumno en una sola página de alumnos de un tipo de colección que admita la paginación. Esa única página de alumnos se pasa a la página de Razor.

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

Los dos signos de interrogación en PaginatedList.CreateAsync representan el operador de uso combinado de NULL. El operador de uso combinado de NULL define un valor predeterminado para un tipo que acepta valores NULL. La expresión (pageIndex ?? 1) significa devolver el valor de pageIndex si tiene un valor. Devuelve 1 si pageIndex no tiene ningún valor.

Actualice el marcado en Students/Index.cshtml. Se resaltan los cambios:

@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>

Los vínculos del encabezado de la columna usan la cadena de consulta para pasar la cadena de búsqueda actual al método OnGetAsync, de modo que el usuario pueda ordenar los resultados del filtro:

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

Los botones de paginación se muestran mediante asistentes de etiquetas:


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

Ejecute la aplicación y vaya a la página Students.

  • Para comprobar que la paginación funciona correctamente, haga clic en los vínculos de paginación en distintos criterios de ordenación.
  • Para comprobar que la paginación también funciona correctamente con filtrado y ordenación, escriba una cadena de búsqueda e intente llevar a cabo la paginación de nuevo.

students index page with paging links

Para comprender mejor el código:

  • En Students/Index.cshtml.cs, establezca un punto de interrupción en switch (sortOrder).
  • Agregue una inspección para NameSort, DateSort, CurrentSort y Model.Student.PageIndex.
  • En Students/Index.cshtml, establezca un punto de interrupción en @Html.DisplayNameFor(model => model.Student[0].LastName).

Ejecute paso a paso el depurador.

Actualizar la página About para mostrar las estadísticas de los alumnos

En este paso, se actualiza Pages/About.cshtml para mostrar cuántos alumnos se han inscrito por cada fecha de inscripción. La actualización usa la agrupación e incluye los siguientes pasos:

  • Cree un modelo de vista para los datos usados por la página About.
  • Actualice la página About para usar el modelo de vista.

Creación del modelo de vista

Cree una carpeta SchoolViewModels en la carpeta Models.

En la carpeta SchoolViewModels, agregue EnrollmentDateGroup.cs con el código siguiente:

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

Actualizar el modelo de la página About

Las plantillas web de ASP.NET Core 2.2 no incluyen la página About. Si usa ASP.NET Core 2.2, cree la página de Razor About.

Actualice el archivo Pages/About.cshtml.cs con el código siguiente:

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

La instrucción LINQ agrupa las entidades de alumnos por fecha de inscripción, calcula la cantidad de entidades que se incluyen en cada grupo y almacena los resultados en una colección de objetos de modelo de la vista EnrollmentDateGroup.

Modificación de la página de Razor About

Reemplace el código del archivo Pages/About.cshtml por el código siguiente:

@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>

Ejecute la aplicación y vaya a la página About. En una tabla se muestra el número de alumnos para cada fecha de inscripción.

Si experimenta problemas que no puede resolver, descargue la aplicación completada para esta fase.

About page

Recursos adicionales

En el tutorial siguiente, la aplicación usa las migraciones para actualizar el modelo de datos.