Tutorial: Adición de ordenación, filtrado y paginación con Entity Framework en una aplicación de ASP.NET MVC

En el tutorial anterior, se implementó un conjunto de páginas web para operaciones básicas de CRUD para entidades Student. En este tutorial agregaremos la funcionalidad de ordenación, filtrado y paginación a la página Students Index. También se crea una página de agrupación sencilla.

En la siguiente imagen 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.

Students_Index_page_with_paging

En este tutorial ha:

  • Agrega vínculos de ordenación de columnas
  • Agrega un cuadro de búsqueda
  • Adición de paginación
  • Crea una página About

Requisitos previos

Para agregar ordenación a la página Student Index, se debe cambiar el método Index del controlador Student y agregar código a la vista Student Index.

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

  • En Controllers\StudentController.cs, reemplace el método Index por el código siguiente:

    public ActionResult Index(string sortOrder)
    {
       ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
       ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
       var students = from s in db.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(students.ToList());
    }
    

Este código recibe un parámetro sortOrder de la cadena de consulta en la dirección URL. ASP.NET 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 en orden ascendente por LastName, 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.

Las dos variables ViewBag se usan para que la vista pueda configurar los hipervínculos de encabezado de columna con los valores de cadena de consulta adecuados:

ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";

Estas son las instrucciones ternarias. La primera de ellas especifica que, si el parámetro sortOrder es NULL o está vacío, ViewBag.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<T> antes de la instrucción switch, la modifica en la instrucción switch y llama al método ToList 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 se convierte el objeto IQueryable en una colección mediante una llamada a un método como ToList. Por lo tanto, este código produce una única consulta que no se ejecuta hasta la instrucción return View.

Como alternativa a escribir diferentes instrucciones LINQ para cada criterio de ordenación, puede crear dinámicamente una instrucción LINQ. Para obtener información sobre LINQ dinámico, vea LinQ dinámico.

  1. En Views\Student\Index.cshtml, reemplace los elementos <tr> y <th> de la fila de encabezado por el código resaltado:

    <p>
        @Html.ActionLink("Create New", "Create")
    </p>
    <table class="table">
        <tr>
            <th>
                @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm })
            </th>
            <th>First Name
            </th>
            <th>
                @Html.ActionLink("Enrollment Date", "Index", new { sortOrder = ViewBag.DateSortParm })
            </th>
            <th></th>
        </tr>
    
    @foreach (var item in Model) {
    

    Este código usa la información de las propiedades ViewBag para configurar hipervínculos con los valores de cadena de consulta adecuados.

  2. Ejecute la página y haga clic en los encabezados de columna Last Name y Enrollment Date para comprobar que la ordenación funciona.

    Después de hacer clic en el encabezado Last Name, los alumnos se muestran según el orden descendente de los apellidos.

Para agregar filtrado a la página Students Index, 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 Controllers\StudentController.cs, reemplace el método Index por el código siguiente (los cambios están resaltados):

    public ViewResult Index(string sortOrder, string searchString)
    {
        ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
        ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
        var students = from s in db.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(students.ToList());
    }
    

El código agrega 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 agrega una cláusula where a la instrucción LINQ que solo selecciona 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 buscar.

Nota:

En muchos casos, puede llamar al mismo método en un conjunto de entidades de Entity Framework o como un método de extensión en una colección en memoria. Los resultados suelen ser los mismos, pero en algunos casos pueden ser diferentes.

Por ejemplo, la implementación de .NET Framework del método Contains devuelve todas las filas cuando se pasa una cadena vacía, pero el proveedor de Entity Framework para SQL Server Compact 4.0 devuelve cero filas para cadenas vacías. Por lo tanto, el código del ejemplo (que coloca la instrucción Where dentro de una instrucción if) asegura que obtiene los mismos resultados para todas las versiones de SQL Server. Además, la implementación de .NET Framework del método Contains realiza una comparación que distingue mayúsculas de minúsculas de forma predeterminada, pero los proveedores de SQL Server de Entity Framework realizan comparaciones que no distinguen mayúsculas de minúsculas de forma predeterminada. Por lo tanto, llamar al método ToUpper para hacer que la prueba no distinga mayúsculas de minúsculas explícitamente garantiza que los resultados no cambien cuando cambie el código más adelante para usar un repositorio, lo que devolverá una colección IEnumerable en lugar 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).

El control de valores NULL también puede ser diferente para distintos proveedores de bases de datos o cuando se usa un objeto IQueryable en comparación con cuando se usa una colección IEnumerable. Por ejemplo, en algunos escenarios, una condición Where como table.Column != 0 puede no devolver columnas que tengan null como valor. De forma predeterminada, EF genera operadores SQL adicionales para que la igualdad entre los valores NULL funcione en la base de datos como funciona en la memoria, pero se puede establecer la marca UseDatabaseNullSemantics en EF6 o llamar al método UseRelationalNulls en EF Core para configurar este comportamiento.

Agregar un cuadro de búsqueda a la vista Student Index

  1. En Views\Student\Index.cshtml, agregue el código resaltado inmediatamente antes de la etiqueta de apertura table para crear un título, un cuadro de texto y un botón Buscar.

    <p>
        @Html.ActionLink("Create New", "Create")
    </p>
    
    @using (Html.BeginForm())
    {
        <p>
            Find by name: @Html.TextBox("SearchString")  
            <input type="submit" value="Search" /></p>
    }
    
    <table>
        <tr>
    
  2. Ejecute la página, escriba una cadena de búsqueda y haga clic en Buscar para comprobar que el filtrado funciona.

    Observe que la dirección URL no contiene la cadena de búsqueda "an", lo que significa que, si marca esta página, no obtendrá la lista filtrada cuando use el marcador. Esto también se aplica a los vínculos de ordenación de columna, ya que ordenarán toda la lista. Cambiará el botón Buscar para usar cadenas de consulta para los criterios de filtro más adelante en el tutorial.

Adición de paginación

Para agregar paginación a la página Students Index, comenzará instalando el paquete NuGet PagedList.Mvc. A continuación, realizará cambios adicionales en el método Index y agregará vínculos de paginación a la vista Index. PagedList.Mvc es uno de los muchos paquetes de paginación y ordenación para ASP.NET MVC, y se usa aquí solo como ejemplo, no para recomendarlo para otras opciones.

Instalación del paquete NuGet PagedList.MVC

El paquete PagedList.Mvc de NuGet instala automáticamente el paquete PagedList como dependencia. El paquete PagedList instala un tipo de colección PagedList y métodos de extensión para las colecciones IQueryable y IEnumerable. Los métodos de extensión crean una sola página de datos en una colección PagedList de IQueryable o IEnumerable, y la colección PagedList proporciona varias propiedades y métodos que facilitan la paginación. El paquete PagedList.Mvc instala un asistente de paginación que muestra los botones de paginación.

  1. En el menú Herramientas, seleccione Administrador de paquetes NuGet y, después, Consola del administrador de paquetes.

  2. En la ventana Consola del Administrador de paquetes, asegúrese de que Origen del paquete es nuget.org y Proyecto predeterminado es ContosoUniversity y, a continuación, escriba el siguiente comando:

    Install-Package PagedList.Mvc
    
  3. Compile el proyecto.

Nota:

Ya no se mantiene el paquete PageList. Por lo tanto, para los proyectos actuales es mejor usar el paquete X.PagedList. La principal diferencia es que X.PagedList es un ensamblado portátil. Esto significa que el paquete es multiplataforma y se puede usar para proyectos web, así como para otros proyectos de .NET. El nuevo paquete no debe causar problemas de compatibilidad, ya que se ha migrado a .NET 6 desde la versión 8.4.

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

  1. En Controllers\StudentController.cs, agregue una instrucción using para el espacio de nombres PagedList:

    using PagedList;
    
  2. Reemplace el método Index con el código siguiente:

    public ViewResult Index(string sortOrder, string currentFilter, string searchString, int? page)
    {
       ViewBag.CurrentSort = sortOrder;
       ViewBag.NameSortParm = String.IsNullOrEmpty(sortOrder) ? "name_desc" : "";
       ViewBag.DateSortParm = sortOrder == "Date" ? "date_desc" : "Date";
    
       if (searchString != null)
       {
          page = 1;
       }
       else
       {
          searchString = currentFilter;
       }
    
       ViewBag.CurrentFilter = searchString;
    
       var students = from s in db.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:  // Name ascending 
             students = students.OrderBy(s => s.LastName);
             break;
       }
    
       int pageSize = 3;
       int pageNumber = (page ?? 1);
       return View(students.ToPagedList(pageNumber, pageSize));
    }
    

    Este código agrega un parámetro page, un parámetro de criterio de ordenación actual y un parámetro de filtro actual a la firma del método:

    public ActionResult Index(string sortOrder, string currentFilter, string searchString, int? page)
    

    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 NULL. Cuando se hace clic en un vínculo de paginación, la variable page contiene el número de página que se va a mostrar.

    Una propiedad ViewBag proporciona la vista con el criterio de ordenación actual, ya que debe incluirse en los vínculos de paginación para mantener el criterio de ordenación durante la paginación:

    ViewBag.CurrentSort = sortOrder;
    

    Otra propiedad, ViewBag.CurrentFilter, proporciona a la vista 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 enviar. En ese caso, el parámetro searchString no es NULL.

    if (searchString != null)
    {
        page = 1;
    }
    else
    {
        searchString = currentFilter;
    }
    

    Al final del método, el método de extensión ToPagedList del objeto IQueryable de los alumnos convierte la consulta de alumnos en una sola página de alumnos de un tipo de colección que admite paginación. Entonces, esa única página de alumnos pasa a la vista:

    int pageSize = 3;
    int pageNumber = (page ?? 1);
    return View(students.ToPagedList(pageNumber, pageSize));
    

    El método ToPagedList 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 (page ?? 1) devuelve el valor de page si tiene algún valor o devuelve 1 si page es NULL.

  1. En Views\Students\Index.cshtml, reemplace el código existente por el código siguiente. Los cambios aparecen resaltados.

    @model PagedList.IPagedList<ContosoUniversity.Models.Student>
    @using PagedList.Mvc;
    <link href="~/Content/PagedList.css" rel="stylesheet" type="text/css" />
    
    @{
        ViewBag.Title = "Students";
    }
    
    <h2>Students</h2>
    
    <p>
        @Html.ActionLink("Create New", "Create")
    </p>
    @using (Html.BeginForm("Index", "Student", FormMethod.Get))
    {
        <p>
            Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
            <input type="submit" value="Search" />
        </p>
    }
    <table class="table">
        <tr>
            <th>
                @Html.ActionLink("Last Name", "Index", new { sortOrder = ViewBag.NameSortParm, currentFilter=ViewBag.CurrentFilter })
            </th>
            <th>
                First Name
            </th>
            <th>
                @Html.ActionLink("Enrollment Date", "Index", new { sortOrder = ViewBag.DateSortParm, currentFilter=ViewBag.CurrentFilter })
            </th>
            <th></th>
        </tr>
    
    @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>
                @Html.ActionLink("Edit", "Edit", new { id=item.ID }) |
                @Html.ActionLink("Details", "Details", new { id=item.ID }) |
                @Html.ActionLink("Delete", "Delete", new { id=item.ID })
            </td>
        </tr>
    }
    
    </table>
    <br />
    Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount
    
    @Html.PagedListPager(Model, page => Url.Action("Index", 
        new { page, sortOrder = ViewBag.CurrentSort, currentFilter = ViewBag.CurrentFilter }))
    

    La instrucción @model de la parte superior de la página especifica que ahora la vista obtiene un objeto PagedList en lugar de un objeto List.

    La instrucción using para PagedList.Mvc proporciona acceso al asistente de MVC para los botones de paginación.

    El código usa una sobrecarga de BeginForm que le permite especificar FormMethod.Get.

    @using (Html.BeginForm("Index", "Student", FormMethod.Get))
    {
        <p>
            Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)  
            <input type="submit" value="Search" />
        </p>
    }
    

    El BeginForm predeterminado 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 para usar HTTP GET recomiendan usar GET cuando la acción no produzca ninguna actualización.

    El cuadro de texto se inicializa con la cadena de búsqueda actual, por lo que al hacer clic en una nueva página puede ver la cadena de búsqueda actual.

    Find by name: @Html.TextBox("SearchString", ViewBag.CurrentFilter as string)
    

    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:

    @Html.ActionLink("Last Name", "Index", new { sortOrder=ViewBag.NameSortParm, currentFilter=ViewBag.CurrentFilter })
    

    Se muestran la página actual y el número total de páginas.

    Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount
    

    Si no hay páginas que mostrar, se muestra "Página 0 de 0". (En ese caso, el número de página es mayor que el recuento de páginas porque Model.PageNumber es 1 y Model.PageCount es 0).

    Los botones de paginación se muestran mediante el asistente de PagedListPager:

    @Html.PagedListPager( Model, page => Url.Action("Index", new { page }) )
    

    El asistente PagedListPager proporciona una serie de opciones que puede personalizar, incluidas las direcciones URL y el estilo. Para obtener más información, consulte TroyGoode / PagedList en el sitio de GitHub.

  2. Ejecute la página.

    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.
  • Modifique el método About en el controlador Home.
  • Modifique la vista About.

Creación del modelo de vista

Cree una carpeta ViewModels en la carpeta del proyecto. En esa 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.ViewModels
{
    public class EnrollmentDateGroup
    {
        [DataType(DataType.Date)]
        public DateTime? EnrollmentDate { get; set; }

        public int StudentCount { get; set; }
    }
}

Modificación del controlador Home

  1. En HomeController.cs, agregue las siguientes instrucciones using en la parte superior del archivo:

    using ContosoUniversity.DAL;
    using ContosoUniversity.ViewModels;
    
  2. Agregue una variable de clase para el contexto de base de datos inmediatamente después de la llave de apertura de la clase:

    public class HomeController : Controller
    {
        private SchoolContext db = new SchoolContext();
    
  3. Reemplace el método About con el código siguiente:

    public ActionResult About()
    {
        IQueryable<EnrollmentDateGroup> data = from student in db.Students
                   group student by student.EnrollmentDate into dateGroup
                   select new EnrollmentDateGroup()
                   {
                       EnrollmentDate = dateGroup.Key,
                       StudentCount = dateGroup.Count()
                   };
        return View(data.ToList());
    }
    

    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.

  4. Agregue un método Dispose:

    protected override void Dispose(bool disposing)
    {
        db.Dispose();
        base.Dispose(disposing);
    }
    

Modificación de la vista About

  1. Reemplace el código del archivo Views/Home/About.cshtml por el código siguiente:

    @model IEnumerable<ContosoUniversity.ViewModels.EnrollmentDateGroup>
               
    @{
        ViewBag.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>
    
  2. Ejecute la aplicación y haga clic en el vínculo Acerca de.

    El recuento de alumnos para cada fecha de inscripción se muestra en una tabla.

    About_page

Obtención del código

Descargar el proyecto completado

Recursos adicionales

Encontrará vínculos a otros recursos de Entity Framework en Acceso a datos de ASP.NET: Recursos recomendados.

Pasos siguientes

En este tutorial ha:

  • Agrega vínculos de ordenación de columnas
  • Agrega un cuadro de búsqueda
  • Adición de paginación
  • Crea una página About

Pase al siguiente artículo para aprender a usar la resistencia de conexión y la interceptación de comandos.