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.
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
aStudents
. - 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.
Agregar hipervínculos de encabezado de columna a la página de índice de Student
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
yEnrollmentDate
. - Usa la información de
NameSort
yDateSort
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
porModel.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étodoOnGetAsync
y guarda el valor del parámetro en la propiedadCurrentFilter
. 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áusulaWhere
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 IQueryable
suele 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.
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 configuración appsettings.json
:
{
"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
deIList<Student>
aPaginatedList<Student>
. - Agrega el índice de la página, el elemento
sortOrder
actual y el elementocurrentFilter
a la firma del métodoOnGetAsync
. - 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.
Agrega vínculos de paginación
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.
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.
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.
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 sortOrder
no 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.
Agregar hipervínculos de encabezado de columna a la página de índice de Student
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
yEnrollmentDate
. - Usa la información de
NameSort
yDateSort
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 enswitch (sortOrder)
. - Agregue una inspección para
NameSort
yDateSort
. - 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étodoOnGetAsync
. 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áusulaWhere
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:
- Todas las filas se devuelven desde el servidor de base de datos.
- 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.
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.
Incorporación de vínculos de paginación a la página de Razor de alumnos
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.
Para comprender mejor el código:
- En
Students/Index.cshtml.cs
, establezca un punto de interrupción enswitch (sortOrder)
. - Agregue una inspección para
NameSort
,DateSort
,CurrentSort
yModel.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.
Recursos adicionales
En el tutorial siguiente, la aplicación usa las migraciones para actualizar el modelo de datos.