Compartir a través de


Implementar la paginación de datos eficaz

Por Microsoft

Descargar PDF

Este es el paso 8 de un tutorial de la aplicación "NerdDinner" gratuito que le guía en el proceso de creación de una aplicación web pequeña, pero completa, con MVC 1 de ASP.NET.

El paso 8 muestra cómo agregar compatibilidad con la paginación a nuestra dirección URL /Dinners para que, en lugar de mostrar miles de cenas a la vez, solo se muestren las 10 próximas cenas a la vez, y permitir a los usuarios finales avanzar y retroceder por toda la lista de una forma compatible con SEO.

Si usa MVC 3 de ASP.NET, le recomendamos que siga los tutoriales Introducción a MVC 3 o Almacén de música MVC.

Paso 8 de NerdDinner: Compatibilidad con la paginación

Si nuestro sitio tiene éxito, tendrá miles de próximas cenas. Debemos asegurarnos de que nuestra interfaz de usuario se escale para controlar todas estas cenas y permitir a los usuarios examinarlas. Para habilitar esto, vamos a agregar compatibilidad con la paginación a nuestra dirección URL /Dinners para que, en lugar de mostrar miles de cenas a la vez, solo se muestren las 10 próximas cenas a la vez, y permitir a los usuarios finales avanzar y retroceder por toda la lista de una forma compatible con SEO.

Recapitulación del método de acción Index()

Actualmente, el método de acción Index() de nuestra clase DinnersController tiene un aspecto similar al siguiente:

//
// GET: /Dinners/

public ActionResult Index() {

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

Cuando se realiza una solicitud a la dirección URL /Dinners, se recupera una lista de todas las próximas cenas y, a continuación, se representa una lista de todas ellas:

Screenshot of the Nerd Dinner Upcoming Dinner list page.

Descripción de IQueryable<T>

IQueryable<T> es una interfaz que se presentó con LINQ como parte de .NET 3.5. Permite escenarios eficaces de "ejecución diferida" que podemos aprovechar para implementar la compatibilidad con la paginación.

En nuestro DinnerRepository, devolvemos una secuencia IQueryable<Dinner> desde nuestro método FindUpcomingDinners():

public class DinnerRepository {

    private NerdDinnerDataContext db = new NerdDinnerDataContext();

    //
    // Query Methods

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

El objeto IQueryable<Dinner> devuelto por nuestro método FindUpcomingDinners() encapsula una consulta para recuperar objetos Dinner de nuestra base de datos mediante LINQ to SQL. Es importante destacar que no ejecutará la consulta en la base de datos hasta que intentemos acceder o iterar sobre los datos de la consulta, o hasta que llamemos al método ToList(). El código que llama al método FindUpcomingDinners() puede elegir opcionalmente agregar filtros y operaciones "encadenadas" adicionales al objeto IQueryable<Dinner> antes de ejecutar la consulta. LINQ to SQL es lo suficientemente inteligente como para ejecutar la consulta combinada en la base de datos cuando se solicitan los datos.

Para implementar la lógica de paginación, podemos actualizar el método de acción Index() de DinnersController para que aplique operadores adicionales "Skip" y "Take" a la secuencia IQueryable<Dinner> devuelta antes de llamar a ToList():

//
// GET: /Dinners/

public ActionResult Index() {

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

    return View(paginatedDinners);
}

El código anterior omite las primeras 10 próximas cenas de la base de datos y, a continuación, devuelve 20 cenas. LINQ to SQL es lo suficientemente inteligente como para construir una consulta SQL optimizada que lleva a cabo esta lógica de omisión en la base de datos SQL, y no en el servidor web. Esto significa que incluso si tenemos millones de próximas cenas en la base de datos, solo se recuperarán las 10 que queremos como parte de esta solicitud (lo que hace que sea eficaz y escalable).

Adición de un valor "page" a la dirección URL

En lugar de codificar de forma rígida un intervalo de páginas específico, queremos que nuestras direcciones URL incluyan un parámetro "page" que indique qué intervalo de cenas solicita un usuario.

Uso de un valor de cadena de consulta

El código siguiente muestra cómo podemos actualizar nuestro método de acción Index() para admitir un parámetro de cadena de consulta y habilitar direcciones URL como /Dinners?page=2:

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

public ActionResult Index(int? page) {

    const int pageSize = 10;

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();

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

    return View(paginatedDinners);
}

El método de acción Index() anterior tiene un parámetro llamado "page". El parámetro se declara como un entero que admite un valor NULL (eso es lo que indica int?). Esto significa que la dirección URL /Dinners?page=2 hará que se pase un valor de "2" como valor del parámetro. La dirección URL /Dinners (sin un valor de cadena de consulta) hará que se pase un valor null.

Multiplicamos el valor de la página por el tamaño de la página (en este caso, 10 filas) para determinar cuántas cenas se van a omitir. Usamos el operador "de combinación" null de C# (??), que es útil cuando se trabaja con tipos que aceptan valores NULL. El código anterior asigna a la página el valor de 0 si el parámetro page es null.

Uso de valores de dirección URL insertados

Una alternativa al uso de un valor de cadena de consulta sería insertar el parámetro de página dentro de la propia dirección URL real. Por ejemplo: /Dinners/Page/2 o /Dinners/2. MVC de ASP.NET incluye un potente motor de enrutamiento de direcciones URL que facilita la compatibilidad con escenarios como este.

Podemos registrar reglas de enrutamiento personalizadas que asignen cualquier formato de dirección URL entrante a cualquier clase de controlador o método de acción que deseemos. Todo lo que tenemos que hacer es abrir el archivo Global.asax en nuestro proyecto:

Screenshot of the Nerd Dinner navigation tree. Global dot a s a x is selected and highlighted.

Después, registre una nueva regla de asignación mediante el método auxiliar MapRoute() como la primera llamada a routes.MapRoute() que se muestra a continuación:

public void RegisterRoutes(RouteCollection routes) {

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

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

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

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

Anteriormente registramos una nueva regla de enrutamiento llamada "UpcomingDinners". Indicamos que tiene el formato de dirección URL "Dinners/Page/{page}", donde {page} es un valor de parámetro insertado dentro de la dirección URL. El tercer parámetro para el método MapRoute() indica que debemos asignar direcciones URL que coincidan con este formato al método de acción Index() de la clase DinnersController.

Podemos usar el mismo código de Index() exacto que teníamos antes con nuestro escenario de cadena de consulta, excepto que ahora nuestro parámetro "page" provendrá de la dirección URL y no de la cadena de consulta:

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

public ActionResult Index(int? page) {

    const int pageSize = 10;

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

    return View(paginatedDinners);
}

Y ahora, cuando ejecutemos la aplicación y escribamos /Dinners veremos las primeras 10 próximas cenas:

Screenshot of the Nerd Dinners Upcoming Dinners list.

Y cuando escribamos /Dinners/Page/1 veremos la siguiente página de cenas:

Screenshot of the next page of Upcoming Dinners list.

Adición de la interfaz de usuario de navegación de página

El último paso para completar nuestro escenario de paginación será implementar la interfaz de usuario de navegación "siguiente" y "anterior" dentro de nuestra plantilla de vista para permitir que los usuarios se desplacen fácilmente por los datos de las cenas.

Para implementar esto correctamente, es necesario conocer el número total de cenas que hay en la base de datos, así como el número de páginas de datos en las que se traduce. A continuación, tendremos que calcular si el valor de "page" solicitado actualmente está al principio o al final de los datos, y mostrar u ocultar la interfaz de usuario "anterior" y "siguiente" en consecuencia. Podríamos implementar esta lógica dentro del método de acción Index(). Como alternativa, podemos agregar una clase auxiliar a nuestro proyecto que encapsule esta lógica de forma más fácil de usar.

A continuación, se muestra una clase auxiliar "PaginatedList" sencilla que deriva de la clase de colección List<T> integrada en .NET Framework. Implementa una clase de colección reutilizable que se puede usar para paginar cualquier secuencia de datos IQueryable. En nuestra aplicación NerdDinner, haremos que trabaje con los resultados de IQueryable<Dinner>, pero podría usarse con facilidad en los resultados de IQueryable<Product>o IQueryable<Customer> en otros escenarios de aplicación:

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

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

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

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

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

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

Observe cómo calcula y expone propiedades como "PageIndex", "PageSize", "TotalCount" y "TotalPages". También expone dos propiedades auxiliares, "HasPreviousPage" y "HasNextPage", que indican si la página de datos de la colección está al principio o al final de la secuencia original. El código anterior hará que se ejecuten dos consultas SQL: la primera para recuperar el recuento del número total de objetos Dinner (esto no devuelve los objetos, sino que realiza una instrucción "SELECT COUNT" que devuelve un entero) y la segunda para recuperar solo las filas de datos que necesitamos de nuestra base de datos para la página actual de datos.

A continuación, podemos actualizar nuestro método auxiliar DinnersController.Index() para crear un elemento PaginatedList<Dinner> a partir del resultado de DinnerRepository.FindUpcomingDinners() y pasarlo a nuestra plantilla de vista:

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

public ActionResult Index(int? page) {

    const int pageSize = 10;

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

    return View(paginatedDinners);
}

Después, podemos actualizar la plantilla de vista \Views\Dinners\Index.aspx para heredar de ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>> en lugar de ViewPage<IEnumerable<Dinner>> y, a continuación, agregar el código siguiente a la parte inferior de nuestra plantilla de vista para mostrar u ocultar la interfaz de usuario de navegación siguiente y anterior:

<% if (Model.HasPreviousPage) { %>

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

<% } %>

<% if (Model.HasNextPage) {  %>

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

<% } %>

Observe sobre cómo usamos el método auxiliar Html.RouteLink() para generar nuestros hipervínculos. Este método es similar al método auxiliar Html.ActionLink() que hemos usado anteriormente. La diferencia es que generamos la dirección URL mediante la regla de enrutamiento "UpcomingDinners" que configuramos en el archivo Global.asax. Esto garantiza que generaremos direcciones URL para nuestro método de acción Index() con el formato: /Dinners/Page/{page}, donde el valor {page} es una variable que proporcionamos anteriormente en función del elemento PageIndex actual.

Y ahora, cuando volvamos a ejecutar la aplicación, veremos 10 cenas a la vez en nuestro explorador:

Screenshot of the Upcoming Dinners list on the Nerd Dinner page.

También tenemos la interfaz de usuario de navegación <<< y >>> en la parte inferior de la página, que nos permite saltar hacia delante y hacia atrás sobre los datos mediante direcciones URL accesibles para los motores de búsqueda:

Screenshot of the Nerd Dinners page with Upcoming Dinners list.

Tema secundario: Descripción de las implicaciones de IQueryable<T>
IQueryable<T> es una característica muy eficaz que permite una variedad de escenarios de ejecución diferida interesantes (como la paginación y las consultas basadas en composición). Al igual que con todas las características eficaces, querrá tener cuidado con cómo se usa y asegurarse de que no se abuse. Es importante reconocer que devolver un resultado IQueryable<T> del repositorio permite al código que realiza la llamada anexarlo a los métodos de operador encadenados y, por tanto, participar en la ejecución final de la consulta. Si no desea proporcionar esta capacidad al código que realiza la llamada, debe devolver resultados IList<T> o IEnumerable<T>, que contienen los resultados de una consulta que ya se ha ejecutado. En escenarios de paginación, esto requeriría insertar la lógica real de paginación de datos en el método del repositorio al que se llama. En este escenario, podríamos actualizar nuestro método de búsqueda FindUpcomingDinners() para tener una firma que haya devuelto un elemento PaginatedList:PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } o devolver un elemento IList<Dinner> y usar un parámetro de salida "totalCount" para devolver el recuento total de Cenas: IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

siguiente paso

Ahora, veremos cómo podemos agregar compatibilidad con la autenticación y la autorización a nuestra aplicación.