Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
por Microsoft
Este es el paso 8 de un tutorial gratuito de aplicación "NerdDinner" que le guía a través de cómo compilar una aplicación web pequeña, pero completa con ASP.NET MVC 1.
El paso 8 muestra cómo agregar compatibilidad de paginación a nuestra dirección URL /Dinners para que, en lugar de mostrar miles de cenas a la vez, solo mostremos 10 próximas cenas, y permitir que los usuarios finales naveguen hacia atrás y hacia adelante a través de toda la lista de forma amigable para SEO.
Si usas ASP.NET MVC 3, te recomendamos seguir los tutoriales introducción a MVC 3 o MVC Music Store .
NerdDinner Paso 8: Soporte de paginación
Si nuestro sitio es exitoso, tendrá miles de próximas cenas. Debemos asegurarnos de que nuestra interfaz de usuario escale para gestionar todas estas cenas y permita a los usuarios navegarlas. Para habilitar esto, agregaremos soporte de paginación a nuestra dirección URL /Dinners de modo que, en lugar de mostrar miles de cenas a la vez, solo mostraremos 10 próximas cenas a la vez, y permitiremos que los usuarios finales se desplacen hacia atrás y hacia delante por toda la lista de una manera amigable para el SEO.
Repaso del Método de Acción Index()
El método de acción Index() dentro 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 URL /Dinners, recupera una lista de las próximas cenas y luego muestra un listado de todas ellas.
Descripción de IQueryable<T>
IQueryable<T> es una interfaz que se introdujo 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, estamos devolviendo una secuencia IQueryable<Dinner> de 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. Importantemente, no ejecutará la consulta en la base de datos hasta que intentemos acceder o iterar los datos de la consulta, o hasta que llamemos al método ToList() en ella. El código que llama al método FindUpcomingDinners() puede elegir opcionalmente agregar filtros o 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() en ella:
//
// 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 realiza 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 los 10 que queremos como parte de esta solicitud (lo que hace que sea eficaz y escalable).
Adición de un valor de "página" a la dirección URL
En lugar de codificar de forma rígida un intervalo de página específico, queremos que nuestras direcciones URL incluyan un parámetro "page" que indica qué intervalo de cenas solicita un usuario.
Uso de un valor querystring
El código siguiente muestra cómo podemos actualizar nuestro método de acción Index() para admitir un parámetro querystring 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 denominado "page". El parámetro se declara como un entero que puede ser nulo (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 de parámetro. La dirección URL de /Dinners (sin un valor querystring) hará que se pase un valor NULL.
Multiplicamos el valor de la página por el tamaño de página (en este caso, 10 filas) para determinar cuántas cenas se van a omitir. Usamos el operador "coalescing" 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 URL incrustados
Una alternativa al uso de un valor querystring 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. ASP.NET MVC 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 necesitamos hacer es abrir el archivo Global.asax en nuestro proyecto.
A continuación, registre una nueva regla de asignación mediante el método auxiliar MapRoute() como en la primera llamada a routes.MapRoute() mostrada 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 denominada "UpcomingDinners". Estamos indicando que tiene el formato de dirección URL "Dinners/Page/{page}", donde {page} es un valor de parámetro incrustado 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() en la clase DinnersController.
Podemos usar el mismo código index() exacto que teníamos antes con nuestro escenario querystring: excepto 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:
Y cuando escribamos /Dinners/Page/1 veremos la siguiente página de cenas:
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 omitan fácilmente los datos de la cena.
Para implementar esto correctamente, es necesario conocer el número total de cenas en la base de datos, así como el número de páginas de datos a las que se traduce. A continuación, tendremos que calcular si el valor de "página" 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 encapsula 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 funcione con los resultados de IQueryable<Dinner>, pero podría usarse con la misma facilidad contra 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 una PaginatedList<Dinner> a partir de nuestro resultado DinnerRepository.FindUpcomingDinners() y pasárselo 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);
}
A continuación, 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 estamos usando 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 estamos generando la dirección URL mediante la regla de enrutamiento "UpcomingDinners" que configuramos en nuestro archivo Global.asax. Esto garantiza que generaremos direcciones URL a 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 de pageIndex actual.
Y ahora, cuando vuelvamos a ejecutar nuestra aplicación, veremos 10 cenas a la vez en nuestro navegador:
También tenemos <<< y >>> la interfaz de usuario de navegación en la parte inferior de la página que nos permite navegar hacia adelante y hacia atrás por nuestros datos utilizando URLs accesibles a motores de búsqueda.
| 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 diferidos interesantes (como las consultas basadas en paginación y composición). Al igual que con todas las características potentes, quiere tener cuidado al usarlas y asegurarse de que no se abusen. Es importante reconocer que devolver un resultado de IQueryable<T> desde su repositorio permite al código cliente anexar métodos de operador encadenados y así participar en la ejecución final de la consulta. Si no desea proporcionar al código de llamada esta capacidad, debe devolver los resultados de 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 que tenga una firma que devuelva una PaginatedList: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } O que devuelva una IList<Dinner>, y use un parámetro de salida "totalCount" para devolver el recuento total de Dinner: IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { } |
Paso siguiente
Ahora veremos cómo podemos agregar compatibilidad con autenticación y autorización a nuestra aplicación.