Tutorial: Adición de ordenación, filtrado y paginación: ASP.NET MVC con EF Core
En el tutorial anterior, implementamos un conjunto de páginas web para operaciones básicas de CRUD para las entidades Student. En este tutorial agregaremos la funcionalidad de ordenación, filtrado y paginación a la página de índice de Students. También crearemos una página que realice agrupaciones sencillas.
En la siguiente ilustración se muestra el aspecto que tendrá la página cuando haya terminado. Los encabezados de columna son vínculos en los que el usuario puede hacer clic para ordenar las columnas correspondientes. Si se hace clic de forma consecutiva en el encabezado de una columna, el criterio de ordenación alterna entre ascendente y descendente.
En este tutorial ha:
- Agrega vínculos de ordenación de columnas
- Agrega un cuadro de búsqueda
- Agrega paginación al índice de Students
- Agrega paginación al método Index
- Agrega vínculos de paginación
- Crea una página About
Requisitos previos
Agrega vínculos de ordenación de columnas
Para agregar ordenación a la página de índice de Student, deberá cambiar el método Index
del controlador de Students y agregar código a la vista de índice de Student.
Agregar la funcionalidad de ordenación al método Index
En StudentsController.cs
, reemplace el método Index
por el código siguiente:
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}
Este código recibe un parámetro sortOrder
de la cadena de consulta en la dirección URL. ASP.NET Core MVC proporciona el valor de la cadena de consulta como un parámetro al método de acción. El parámetro es una cadena que puede ser "Name" o "Date", seguido (opcionalmente) por un guión bajo y la cadena "desc" para especificar el orden descendente. El criterio de ordenación predeterminado es el ascendente.
La primera vez que se solicita la página de índice, no hay ninguna cadena de consulta. Los alumnos se muestran por apellido en orden ascendente, que es el valor predeterminado establecido por el caso desestimado en la instrucción switch
. Cuando el usuario hace clic en un hipervínculo de encabezado de columna, se proporciona el valor sortOrder
correspondiente en la cadena de consulta.
La vista usa los dos elementos ViewData
(NameSortParm y DateSortParm) para configurar los hipervínculos del encabezado de columna con los valores de cadena de consulta adecuados.
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var students = from s in _context.Students
select s;
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}
Estas son las instrucciones ternarias. La primera de ellas especifica que, si el parámetro sortOrder
es NULL o está vacío, NameSortParm debe establecerse en "name_desc"; en caso contrario, se debe establecer en una cadena vacía. Estas dos instrucciones habilitan la vista para establecer los hipervínculos de encabezado de columna de la forma siguiente:
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 crea una variable IQueryable
antes de la instrucción de cambio, la modifica en la instrucción de cambio y llama al método ToListAsync
después de la instrucción switch
. Al crear y modificar variables IQueryable
, no se envía ninguna consulta a la base de datos. La consulta no se ejecuta hasta que convierta el objeto IQueryable
en una colección mediante una llamada a un método, como ToListAsync
. Por lo tanto, este código produce una única consulta que no se ejecuta hasta la instrucción return View
.
Este código podría detallarse con un gran número de columnas. En el último tutorial de esta serie se muestra cómo escribir código que le permita pasar el nombre de la columna OrderBy
en una variable de cadenas.
Agregar hipervínculos del encabezado de columna a la vista de índice de Student
Reemplace el código de Views/Students/Index.cshtml
por el código siguiente para agregar los hipervínculos del encabezado de columna. Se resaltan las líneas modificadas.
@model IEnumerable<ContosoUniversity.Models.Student>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]">@Html.DisplayNameFor(model => model.LastName)</a>
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.EnrollmentDate)</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<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-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Este código usa la información que se incluye en las propiedades ViewData
para configurar hipervínculos con los valores de cadena de consulta adecuados.
Ejecute la aplicación, seleccione la ficha Students y haga clic en los encabezados de columna Last Name y Enrollment Date para comprobar que la ordenación funciona correctamente.
Agrega un cuadro de búsqueda
Para agregar filtrado a la página de índice de Students, agregue un cuadro de texto y un botón de envío a la vista y haga los cambios correspondientes en el método Index
. El cuadro de texto le permite escribir la cadena que quiera buscar en los campos de nombre y apellido.
Agregar la funcionalidad de filtrado al método Index
En StudentsController.cs
, reemplace el método Index
por el código siguiente (los cambios se resaltan).
public async Task<IActionResult> Index(string sortOrder, string searchString)
{
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
return View(await students.AsNoTracking().ToListAsync());
}
Ha agregado un parámetro searchString
al método Index
. El valor de la cadena de búsqueda se recibe desde un cuadro de texto que agregará a la vista de índice. También ha agregado a la instrucción LINQ una cláusula where que solo selecciona los alumnos cuyo nombre o apellido contienen la cadena de búsqueda. La instrucción que agrega la cláusula where solo se ejecuta si hay un valor que se tiene que buscar.
Nota
Aquí se llama al método Where
en un objeto IQueryable
y el filtro se procesa en el servidor. En algunos escenarios, puede hacer una llamada al método Where
como un método de extensión en una colección en memoria. (Por ejemplo, imagine que cambia la referencia a _context.Students
, de modo que, en lugar de hacer referencia a EF DbSet
, haga referencia a un método de repositorio que devuelva 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 del método Contains
realiza de forma predeterminada una comparación que diferencia entre mayúsculas y minúsculas, pero en SQL Server se determina por la configuración de intercalación de la instancia de SQL Server. De forma predeterminada, esta opción de configuración no diferencia entre mayúsculas y minúsculas. Podría hacer una llamada al método ToUpper
para hacer explícitamente que la prueba no diferenciara entre mayúsculas y minúsculas: >. Esto garantiza que los resultados permanezcan invariables aunque cambie el código más adelante para usar un repositorio que devuelva una colección IEnumerable
en vez de un objeto IQueryable
. (Al hacer una llamada al método Contains
en una colección IEnumerable
, obtendrá la implementación de .NET Framework; al hacer una llamada a un objeto IQueryable
, obtendrá la implementación del proveedor de base de datos). En cambio, el rendimiento de esta solución se ve reducido. El código ToUpper
tendría que poner una función en la cláusula WHERE de la instrucción SELECT de TSQL. Esto impediría que el optimizador usara un índice. Dado que principalmente SQL se instala de forma que no diferencia entre mayúsculas y minúsculas, es mejor que evite el código ToUpper
hasta que migre a un almacén de datos que distinga entre mayúsculas y minúsculas.
Agregar un cuadro de búsqueda a la vista de índice de Student
En Views/Student/Index.cshtml
, agregue el código resaltado justo antes de la etiqueta de apertura de tabla para crear un título, un cuadro de texto y un botón de búsqueda.
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-action="Index" method="get">
<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>
<table class="table">
Este código 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, lo que significa que los parámetros se pasan en el cuerpo del mensaje HTTP y no en la dirección URL como cadenas de consulta. Al especificar HTTP GET, los datos de formulario se pasan en la dirección URL como cadenas de consulta, lo que permite que los usuarios marquen la dirección URL. Las directrices de W3C recomiendan que use GET cuando la acción no produzca ninguna actualización.
Ejecute la aplicación, seleccione la ficha Students, escriba una cadena de búsqueda y haga clic en Search para comprobar que el filtrado funciona correctamente.
Fíjese en que la dirección URL contiene la cadena de búsqueda.
http://localhost:5813/Students?SearchString=an
Si marca esta página, obtendrá la lista filtrada al usar el marcador. El hecho de agregar method="get"
a la etiqueta form
es lo que ha provocado que se generara la cadena de consulta.
En esta fase, si hace clic en un vínculo de ordenación del encabezado de columna, el valor de filtro que especificó en el cuadro de búsqueda se perderá. Podrá corregirlo en la siguiente sección.
Agrega paginación al índice de Students
Para agregar paginación a la página de índice de Students, tendrá que crear una clase PaginatedList
que use las instrucciones Skip
y Take
para filtrar los datos en el servidor en lugar de recuperar siempre todas las filas de la tabla. A continuación, podrá realizar cambios adicionales en el método Index
y agregar botones de paginación a la vista Index
. La ilustración siguiente muestra los botones de paginación.
En la carpeta del proyecto, cree PaginatedList.cs
y después reemplace el código de plantilla por 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 este código toma el tamaño y el número de la página y aplica las instrucciones Skip
y Take
correspondientes a IQueryable
. Cuando se llama a ToListAsync
en IQueryable
, devuelve una lista que solo contiene la página solicitada. Las propiedades HasPreviousPage
y HasNextPage
se pueden usar para habilitar o deshabilitar los botones de página Previous y Next.
Para crear el objeto PaginatedList<T>
, se usa un método CreateAsync
en vez de un constructor, porque los constructores no pueden ejecutar código asincrónico.
Agrega paginación al método Index
En StudentsController.cs
, reemplace el método Index
por el código siguiente.
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
{
ViewData["CurrentSort"] = sortOrder;
ViewData["NameSortParm"] = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}
ViewData["CurrentFilter"] = searchString;
var students = from s in _context.Students
select s;
if (!String.IsNullOrEmpty(searchString))
{
students = students.Where(s => s.LastName.Contains(searchString)
|| s.FirstMidName.Contains(searchString));
}
switch (sortOrder)
{
case "name_desc":
students = students.OrderByDescending(s => s.LastName);
break;
case "Date":
students = students.OrderBy(s => s.EnrollmentDate);
break;
case "date_desc":
students = students.OrderByDescending(s => s.EnrollmentDate);
break;
default:
students = students.OrderBy(s => s.LastName);
break;
}
int pageSize = 3;
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
}
Este código agrega un parámetro de número de página, un parámetro de criterio de ordenación actual y un parámetro de filtro actual a la firma del método.
public async Task<IActionResult> Index(
string sortOrder,
string currentFilter,
string searchString,
int? pageNumber)
La primera vez que se muestra la página, o si el usuario no ha hecho clic en un vínculo de ordenación o paginación, todos los parámetros son nulos. Si se hace clic en un vínculo de paginación, la variable de página contiene el número de página que se tiene que mostrar.
El elemento ViewData
, denominado CurrentSort, proporciona la vista con el criterio de ordenación actual, que debe incluirse en los vínculos de paginación para mantener el criterio de ordenación durante la paginación.
El elemento ViewData
, denominado CurrentFilter, proporciona la vista con la cadena de filtro actual. Este valor debe incluirse en los vínculos de paginación para mantener la configuración de filtrado durante la paginación y debe restaurarse en el cuadro de texto cuando se vuelve a mostrar la página.
Si se cambia la cadena de búsqueda durante la paginación, la página debe restablecerse a 1, porque el nuevo filtro puede hacer que se muestren diferentes datos. La cadena de búsqueda cambia cuando se escribe un valor en el cuadro de texto y se presiona el botón Submit. En ese caso, el parámetro searchString
no es NULL.
if (searchString != null)
{
pageNumber = 1;
}
else
{
searchString = currentFilter;
}
Al final del método Index
, 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. Entonces, esa única página de alumnos pasa a la vista.
return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));
El método PaginatedList.CreateAsync
toma un número de página. Los dos signos de interrogación representan el operador de fusión de NULL. El operador de uso combinado de NULL define un valor predeterminado para un tipo que acepta valores NULL; la expresión (pageNumber ?? 1)
devuelve el valor de pageNumber
si tiene algún valor o devuelve 1 si pageNumber
es NULL.
Agrega vínculos de paginación
En Views/Students/Index.cshtml
, reemplace el código existente por el código siguiente. Los cambios aparecen resaltados.
@model PaginatedList<ContosoUniversity.Models.Student>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<form asp-action="Index" method="get">
<div class="form-actions no-color">
<p>
Find by name: <input type="text" name="SearchString" value="@ViewData["CurrentFilter"]" />
<input type="submit" value="Search" class="btn btn-default" /> |
<a asp-action="Index">Back to Full List</a>
</p>
</div>
</form>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["NameSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Last Name</a>
</th>
<th>
First Name
</th>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter="@ViewData["CurrentFilter"]">Enrollment Date</a>
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model)
{
<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-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
@{
var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";
var nextDisabled = !Model.HasNextPage ? "disabled" : "";
}
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex + 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @nextDisabled">
Next
</a>
La instrucción @model
de la parte superior de la página especifica que ahora la vista obtiene un objeto PaginatedList<T>
en lugar de un objeto List<T>
.
Los vínculos del encabezado de la columna usan la cadena de consulta para pasar la cadena de búsqueda actual al controlador, de modo que el usuario pueda ordenar los resultados del filtro:
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]" asp-route-currentFilter ="@ViewData["CurrentFilter"]">Enrollment Date</a>
Los botones de paginación se muestran mediante asistentes de etiquetas:
<a asp-action="Index"
asp-route-sortOrder="@ViewData["CurrentSort"]"
asp-route-pageNumber="@(Model.PageIndex - 1)"
asp-route-currentFilter="@ViewData["CurrentFilter"]"
class="btn btn-default @prevDisabled">
Previous
</a>
Ejecute la aplicación y vaya a la página Students.
Haga clic en los vínculos de paginación en distintos criterios de ordenación para comprobar que la paginación funciona correctamente. A continuación, escriba una cadena de búsqueda e intente llevar a cabo la paginación de nuevo, para comprobar que la paginación también funciona correctamente con filtrado y ordenación.
Crea una página About
En la página About del sitio web de Contoso University, se muestran cuántos alumnos se han inscrito en cada fecha de inscripción. Esto requiere realizar agrupaciones y cálculos sencillos en los grupos. Para conseguirlo, haga lo siguiente:
- Cree una clase de modelo de vista para los datos que necesita pasar a la vista.
- Cree el método About en el controlador Home.
- Cree la vista About.
Creación del modelo de vista
Cree una carpeta SchoolViewModels en la carpeta Models.
En la nueva carpeta, agregue un archivo de clase EnrollmentDateGroup.cs
y reemplace el código de plantilla 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; }
}
}
Modificación del controlador Home
En HomeController.cs
, agregue las siguientes instrucciones using al principio del archivo:
using Microsoft.EntityFrameworkCore;
using ContosoUniversity.Data;
using ContosoUniversity.Models.SchoolViewModels;
using Microsoft.Extensions.Logging;
Agregue una variable de clase para el contexto de base de datos inmediatamente después de la llave de apertura para la clase y obtenga una instancia del contexto de ASP.NET Core DI:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
private readonly SchoolContext _context;
public HomeController(ILogger<HomeController> logger, SchoolContext context)
{
_logger = logger;
_context = context;
}
Agregue un método About
en el código siguiente:
public async Task<ActionResult> About()
{
IQueryable<EnrollmentDateGroup> data =
from student in _context.Students
group student by student.EnrollmentDate into dateGroup
select new EnrollmentDateGroup()
{
EnrollmentDate = dateGroup.Key,
StudentCount = dateGroup.Count()
};
return View(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
.
Creación de la vista About
Agregue un archivo Views/Home/About.cshtml
con el código siguiente:
@model IEnumerable<ContosoUniversity.Models.SchoolViewModels.EnrollmentDateGroup>
@{
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)
{
<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.
Obtención del código
Descargue o vea la aplicación completa.
Pasos siguientes
En este tutorial ha:
- Agregado vínculos de ordenación de columnas
- Agregado un cuadro de búsqueda
- Agregado paginación al índice de Students
- Agregado paginación al método Index
- Agregado vínculos de paginación
- Creado una página About
Pase al tutorial siguiente para obtener información sobre cómo controlar los cambios en el modelo de datos mediante migraciones.