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.
Aprenda a implementar dos sistemas de paginación diferentes, el primero basado en números de página y el segundo en desplazamiento infinito. Ambos sistemas de paginación se usan ampliamente y seleccionar el adecuado depende de la experiencia del usuario que desee con los resultados.
En este tutorial, aprenderá a:
- Extiende tu aplicación con paginación numerada
- Extiende tu aplicación con desplazamiento infinito
Información general
En este tutorial se superpone un sistema de paginación en un proyecto creado anteriormente descrito en el tutorial Creación de la primera aplicación de búsqueda .
Las versiones finalizadas del código que va a desarrollar en este tutorial se pueden encontrar en los siguientes proyectos:
Prerrequisitos
- Proyecto 1-basic-search-page (GitHub). Este proyecto puede ser su propia versión compilada a partir del tutorial anterior o una copia de GitHub.
Amplía tu aplicación con paginación numerada
La paginación numerada es el sistema de paginación elegido para los principales motores de búsqueda web comerciales y muchos otros sitios web de búsqueda. La paginación numerada normalmente incluye una opción "siguiente" y "anterior" además de un intervalo de números de página reales. También puede haber disponible una opción "primera página" y "última página". Sin duda, estas opciones proporcionan a un usuario control sobre cómo navegar por los resultados basados en páginas.
En este tutorial, agregará un sistema que incluya las opciones primero, anterior, siguiente y última, junto con los números de página que no comienzan desde 1, sino que rodeará la página actual en la que se encuentra el usuario (por ejemplo, si el usuario está viendo la página 10, quizás los números de página 8, 9, 10, 11 y 12 se muestran).
El sistema será lo suficientemente flexible como para permitir que el número de números de página visibles se establezca en una variable global.
El sistema tratará los botones de número de página más a la izquierda y de la derecha como especiales, lo que significa que desencadenarán el cambio del intervalo de números de página mostrados. Por ejemplo, si se muestran los números de página 8, 9, 10, 11 y 12, y el usuario hace clic en 8, el intervalo de números de página muestra cambios en 6, 7, 8, 9 y 10. Y hay un desplazamiento similar a la derecha si seleccionan 12.
Adición de campos de paginación al modelo
Asegúrese de tener abierta la solución de la página de búsqueda básica.
Abra el archivo de modelo de SearchData.cs.
Añada variables globales para soportar la paginación. En MVC, las variables globales se declaran en su propia clase estática. ResultsPerPage establece el número de resultados por página. MaxPageRange determina el número de números de página visibles en la vista. PageRangeDelta determina cuántas páginas se deben desplazar a la izquierda o a la derecha, cuando se selecciona el número de página más a la izquierda o de la derecha. Normalmente, este último número es aproximadamente la mitad de MaxPageRange. Agregue el siguiente código al namespace.
public static class GlobalVariables { public static int ResultsPerPage { get { return 3; } } public static int MaxPageRange { get { return 5; } } public static int PageRangeDelta { get { return 2; } } }Sugerencia
Si ejecuta este proyecto en un dispositivo con una pantalla más pequeña, como un portátil, considere la posibilidad de cambiar ResultsPerPage a 2.
Agregue propiedades de paginación a la clase SearchData , después de la propiedad searchText .
// The current page being displayed. public int currentPage { get; set; } // The total number of pages of results. public int pageCount { get; set; } // The left-most page number to display. public int leftMostPage { get; set; } // The number of page numbers to display - which can be less than MaxPageRange towards the end of the results. public int pageRange { get; set; } // Used when page numbers, or next or prev buttons, have been selected. public string paging { get; set; }
Agregar una tabla de opciones de paginación a la vista
Abra el archivo index.cshtml y agregue el siguiente código justo antes de la etiqueta de cierre </body>. Este nuevo código presenta una tabla de opciones de paginación: primero, anterior, 1, 2, 3, 4, 5, siguiente, último.
@if (Model != null && Model.pageCount > 1) { // If there is more than one page of results, show the paging buttons. <table> <tr> <td> @if (Model.currentPage > 0) { <p class="pageButton"> @Html.ActionLink("|<", "Page", "Home", new { paging = "0" }, null) </p> } else { <p class="pageButtonDisabled">|<</p> } </td> <td> @if (Model.currentPage > 0) { <p class="pageButton"> @Html.ActionLink("<", "PageAsync", "Home", new { paging = "prev" }, null) </p> } else { <p class="pageButtonDisabled"><</p> } </td> @for (var pn = Model.leftMostPage; pn < Model.leftMostPage + Model.pageRange; pn++) { <td> @if (Model.currentPage == pn) { // Convert displayed page numbers to 1-based and not 0-based. <p class="pageSelected">@(pn + 1)</p> } else { <p class="pageButton"> @Html.ActionLink((pn + 1).ToString(), "PageAsync", "Home", new { paging = @pn }, null) </p> } </td> } <td> @if (Model.currentPage < Model.pageCount - 1) { <p class="pageButton"> @Html.ActionLink(">", "PageAsync", "Home", new { paging = "next" }, null) </p> } else { <p class="pageButtonDisabled">></p> } </td> <td> @if (Model.currentPage < Model.pageCount - 1) { <p class="pageButton"> @Html.ActionLink(">|", "PageAsync", "Home", new { paging = Model.pageCount - 1 }, null) </p> } else { <p class="pageButtonDisabled">>|</p> } </td> </tr> </table> }Usamos una tabla HTML para alinear las cosas perfectamente. Sin embargo, toda la acción procede de las instrucciones , cada una de las @Html.ActionLink cuales llama al controlador con un nuevo modelo creado con entradas diferentes a la propiedad de paginación que hemos agregado anteriormente.
Las opciones de primera y última página no envían cadenas como "first" y "last", sino que envían los números de página correctos.
Agregue clases de paginación a la lista de estilos HTML en el archivo hotels.css. La clase pageSelected está ahí para identificar la página actual (aplicando un formato de negrita al número de página) en la lista de números de página.
.pageButton { border: none; color: darkblue; font-weight: normal; width: 50px; } .pageSelected { border: none; color: black; font-weight: bold; width: 50px; } .pageButtonDisabled { border: none; color: lightgray; font-weight: bold; width: 50px; }
Agregar una acción Page al controlador
Abra el archivo HomeController.cs y agregue la acción PageAsync . Esta acción responde a cualquiera de las opciones de página seleccionadas.
public async Task<ActionResult> PageAsync(SearchData model) { try { int page; switch (model.paging) { case "prev": page = (int)TempData["page"] - 1; break; case "next": page = (int)TempData["page"] + 1; break; default: page = int.Parse(model.paging); break; } // Recover the leftMostPage. int leftMostPage = (int)TempData["leftMostPage"]; // Recover the search text and search for the data for the new page. model.searchText = TempData["searchfor"].ToString(); await RunQueryAsync(model, page, leftMostPage); // Ensure Temp data is stored for next call, as TempData only stores for one call. TempData["page"] = (object)page; TempData["searchfor"] = model.searchText; TempData["leftMostPage"] = model.leftMostPage; } catch { return View("Error", new ErrorViewModel { RequestId = "2" }); } return View("Index", model); }El método RunQueryAsync mostrará ahora un error de sintaxis, debido al tercer parámetro, al que llegaremos un poco.
Nota:
Las llamadas a TempData almacenan un valor (un objeto) en el almacenamiento temporal, aunque este almacenamiento persiste solo para una llamada. Si almacenamos algo en datos temporales, estará disponible para la siguiente llamada a una acción del controlador, pero muy probablemente desaparecerá en la llamada subsiguiente. Debido a esta corta duración, almacenamos el texto de búsqueda y las propiedades de paginación en el almacenamiento temporal en cada llamada a PageAsync.
Actualice la acción Index(model) para almacenar variables temporales y para agregar el parámetro de página más a la izquierda a la llamada RunQueryAsync .
public async Task<ActionResult> Index(SearchData model) { try { // Ensure the search string is valid. if (model.searchText == null) { model.searchText = ""; } // Make the search call for the first page. await RunQueryAsync(model, 0, 0); // Ensure temporary data is stored for the next call. TempData["page"] = 0; TempData["leftMostPage"] = 0; TempData["searchfor"] = model.searchText; } catch { return View("Error", new ErrorViewModel { RequestId = "1" }); } return View(model); }El método RunQueryAsync , introducido en la lección anterior, necesita modificaciones para resolver el error de sintaxis. Usamos los campos Skip, Size e IncludeTotalCount de la clase SearchOptions para solicitar solo una página de resultados, empezando por la configuración Skip . También necesitamos calcular las variables de paginación de nuestra vista. Reemplace todo el método por el código siguiente.
private async Task<ActionResult> RunQueryAsync(SearchData model, int page, int leftMostPage) { InitSearch(); var options = new SearchOptions { // Skip past results that have already been returned. Skip = page * GlobalVariables.ResultsPerPage, // Take only the next page worth of results. Size = GlobalVariables.ResultsPerPage, // Include the total number of results. IncludeTotalCount = true }; // Add fields to include in the search results. options.Select.Add("HotelName"); options.Select.Add("Description"); // For efficiency, the search call should be asynchronous, so use SearchAsync rather than Search. model.resultList = await _searchClient.SearchAsync<Hotel>(model.searchText, options).ConfigureAwait(false); // This variable communicates the total number of pages to the view. model.pageCount = ((int)model.resultList.TotalCount + GlobalVariables.ResultsPerPage - 1) / GlobalVariables.ResultsPerPage; // This variable communicates the page number being displayed to the view. model.currentPage = page; // Calculate the range of page numbers to display. if (page == 0) { leftMostPage = 0; } else if (page <= leftMostPage) { // Trigger a switch to a lower page range. leftMostPage = Math.Max(page - GlobalVariables.PageRangeDelta, 0); } else if (page >= leftMostPage + GlobalVariables.MaxPageRange - 1) { // Trigger a switch to a higher page range. leftMostPage = Math.Min(page - GlobalVariables.PageRangeDelta, model.pageCount - GlobalVariables.MaxPageRange); } model.leftMostPage = leftMostPage; // Calculate the number of page numbers to display. model.pageRange = Math.Min(model.pageCount - leftMostPage, GlobalVariables.MaxPageRange); return View("Index", model); }Por último, realice un pequeño cambio en la vista. La variable resultList.Results.TotalCount contendrá ahora el número de resultados devueltos en una página (3 en nuestro ejemplo), no el número total. Dado que establecemos IncludeTotalCount en true, la variable resultList.TotalCount ahora contiene el número total de resultados. Por lo tanto, busque dónde se muestra el número de resultados en la vista y cámbielo al código siguiente.
// Show the result count. <p class="sampleText"> @Model.resultList.TotalCount Results </p> var results = Model.resultList.GetResults().ToList(); @for (var i = 0; i < results.Count; i++) { // Display the hotel name and description. @Html.TextAreaFor(m => results[i].Document.HotelName, new { @class = "box1" }) @Html.TextArea($"desc{1}", results[i].Document.Description, new { @class = "box2" }) }Nota:
Hay un impacto de rendimiento menor al establecer IncludeTotalCount en true, ya que azure Cognitive Search debe calcular este total. Con conjuntos de datos complejos, hay una advertencia de que el valor devuelto es una aproximación. Dado que el corpus de búsqueda del hotel es pequeño, será preciso.
Compilación y ejecución de la aplicación
Ahora seleccione Iniciar sin depurar (o presione la tecla F5).
Busque en una cadena que devuelva un montón de resultados (como "wifi"). ¿Puede navegar ordenadamente a través de los resultados?
Intente hacer clic en los números de página más a la derecha y, luego, más a la izquierda. ¿Los números de página se ajustan adecuadamente para que la página en la que estás aparezca centrada?
¿Son útiles las opciones "primera" y "última"? Algunos motores de búsqueda comercial usan estas opciones, y otros no.
Vaya a la última página de resultados. La última página es la única página que puede contener menos que los resultados de ResultsPerPage .
Escriba "town" y haga clic en buscar. No se muestran opciones de paginación si los resultados son menos de una página.
Guarde este proyecto y continúe con la sección siguiente para obtener una forma alternativa de paginación.
Extensión de la aplicación con desplazamiento infinito
El desplazamiento infinito se desencadena cuando un usuario desplaza una barra de desplazamiento vertical hasta el último de los resultados que se muestran. En este evento, se realiza una llamada al servicio de búsqueda para la siguiente página de resultados. Si no hay más resultados, no se devuelve nada y la barra de desplazamiento vertical no cambia. Si hay más resultados, se anexan a la página actual y la barra de desplazamiento cambia para mostrar que hay más resultados disponibles.
Un punto importante que hay que tener en cuenta es que la página actual no se reemplaza, sino que se amplía para mostrar los resultados adicionales. Un usuario siempre puede desplazarse hacia arriba hasta los primeros resultados de la búsqueda.
Para implementar el desplazamiento infinito, comencemos con el proyecto antes de agregar cualquiera de los elementos de desplazamiento de números de página. En GitHub, esta es la solución FirstAzureSearchApp .
Adición de campos de paginación al modelo
En primer lugar, agregue una propiedad de paginación a la clase SearchData (en el archivo de modelo SearchData.cs).
// Record if the next page is requested. public string paging { get; set; }Esta variable es una cadena que contiene "next" si se debe enviar la siguiente página de resultados, o será nula para la primera página de una búsqueda.
En el mismo archivo y dentro del espacio de nombres, agregue una clase de variable global con una propiedad. En MVC, las variables globales se declaran en su propia clase estática. ResultsPerPage establece el número de resultados por página.
public static class GlobalVariables { public static int ResultsPerPage { get { return 3; } } }
Agregar una barra de desplazamiento vertical a la vista
Busque la sección del archivo index.cshtml que muestra los resultados (comienza con el @if (Model != null)).
Reemplace la sección por el código siguiente. La nueva <sección div> está alrededor del área que se debe desplazar y agrega un atributo overflow-y y una llamada a una función onscroll denominada "scrolled()", como así.
@if (Model != null) { // Show the result count. <p class="sampleText"> @Model.resultList.TotalCount Results </p> var results = Model.resultList.GetResults().ToList(); <div id="myDiv" style="width: 800px; height: 450px; overflow-y: scroll;" onscroll="scrolled()"> <!-- Show the hotel data. --> @for (var i = 0; i < results.Count; i++) { // Display the hotel name and description. @Html.TextAreaFor(m => results[i].Document.HotelName, new { @class = "box1" }) @Html.TextArea($"desc{i}", results[i].Document.Description, new { @class = "box2" }) }Directamente debajo del bucle, después de la <etiqueta /div>, agregue la función scrolled.
<script> function scrolled() { if (myDiv.offsetHeight + myDiv.scrollTop >= myDiv.scrollHeight) { $.getJSON("/Home/NextAsync", function (data) { var div = document.getElementById('myDiv'); // Append the returned data to the current list of hotels. for (var i = 0; i < data.length; i += 2) { div.innerHTML += '\n<textarea class="box1">' + data[i] + '</textarea>'; div.innerHTML += '\n<textarea class="box2">' + data[i + 1] + '</textarea>'; } }); } } </script>La instrucción if del script anterior comprueba si el usuario se ha desplazado hasta la parte inferior de la barra de desplazamiento vertical. Si lo tienen, se realiza una llamada al controlador home a una acción denominada NextAsync. El controlador no necesita ninguna otra información, devolverá la siguiente página de datos. A continuación, estos datos tienen el formato de estilos HTML idénticos a la página original. Si no se devuelve ningún resultado, no se anexa nada y las cosas permanecen como están.
Gestionar la acción siguiente
Solo hay tres acciones que deben enviarse al controlador: la primera ejecución de la aplicación, que llama a Index(), la primera búsqueda por parte del usuario, que llama a Index(model) y, a continuación, las llamadas posteriores para obtener más resultados a través de Next(model).
Abra el archivo del controlador de inicio y elimine el método RunQueryAsync del tutorial original.
Reemplace la acción Index(model) por el código siguiente. Ahora controla el campo de paginación cuando es null o se establece en "next" y controla la llamada a Azure Cognitive Search.
public async Task<ActionResult> Index(SearchData model) { try { InitSearch(); int page; if (model.paging != null && model.paging == "next") { // Increment the page. page = (int)TempData["page"] + 1; // Recover the search text. model.searchText = TempData["searchfor"].ToString(); } else { // First call. Check for valid text input. if (model.searchText == null) { model.searchText = ""; } page = 0; } // Setup the search parameters. var options = new SearchOptions { SearchMode = SearchMode.All, // Skip past results that have already been returned. Skip = page * GlobalVariables.ResultsPerPage, // Take only the next page worth of results. Size = GlobalVariables.ResultsPerPage, // Include the total number of results. IncludeTotalCount = true }; // Specify which fields to include in results. options.Select.Add("HotelName"); options.Select.Add("Description"); // For efficiency, the search call should be asynchronous, so use SearchAsync rather than Search. model.resultList = await _searchClient.SearchAsync<Hotel>(model.searchText, options).ConfigureAwait(false); // Ensure TempData is stored for the next call. TempData["page"] = page; TempData["searchfor"] = model.searchText; } catch { return View("Error", new ErrorViewModel { RequestId = "1" }); } return View("Index", model); }De forma similar al método de paginación numerada, usamos la configuración de búsqueda Skip y Size para solicitar solo los datos que necesitamos.
Agregue la acción NextAsync al controlador principal. Observe cómo devuelve una lista, cada hotel agrega dos elementos a la lista: un nombre de hotel y una descripción del hotel. Este formato se establece para coincidir con el uso de la función de desplazamiento de los datos devueltos en la vista.
public async Task<ActionResult> NextAsync(SearchData model) { // Set the next page setting, and call the Index(model) action. model.paging = "next"; await Index(model).ConfigureAwait(false); // Create an empty list. var nextHotels = new List<string>(); // Add a hotel name, then description, to the list. await foreach (var searchResult in model.resultList.GetResultsAsync()) { nextHotels.Add(searchResult.Document.HotelName); nextHotels.Add(searchResult.Document.Description); } // Rather than return a view, return the list of data. return new JsonResult(nextHotels); }Si recibe un error de sintaxis en List<string>, agregue la siguiente directiva using al inicio del archivo del controlador.
using System.Collections.Generic;
Compilación y ejecución del proyecto
Ahora seleccione Iniciar sin depurar (o presione la tecla F5).
Escriba un término que proporcione muchos resultados (como "pool") y, a continuación, pruebe la barra de desplazamiento vertical. ¿Desencadena una nueva página de resultados?
Sugerencia
Para asegurarse de que aparece una barra de desplazamiento en la primera página, la primera página de resultados debe superar ligeramente el alto del área en la que se muestran. En nuestro ejemplo .box1 tiene un alto de 30 píxeles, .box2 tiene un alto de 100 píxeles y un margen inferior de 24 píxeles. Por lo tanto, cada entrada usa 154 píxeles. Tres entradas ocuparán 3 x 154 = 462 píxeles. Para asegurarse de que aparezca una barra de desplazamiento vertical, debe establecerse un alto en el área de visualización que sea inferior a 462 píxeles; incluso funciona 461. Este problema solo se produce en la primera página; después, es seguro que aparecerá una barra de desplazamiento. La línea que se va a actualizar es: <div id="myDiv" style="width: 800px; height: 450px; overflow-y: scroll;" onscroll="scrolled()">.
Desplácese hacia abajo hasta la parte inferior de los resultados. Observa cómo toda la información está ahora en una sola página de vista. Puede desplazarse hasta la parte superior sin desencadenar ninguna llamada de servidor.
Los sistemas de desplazamiento infinito más sofisticados pueden usar la rueda del mouse o un mecanismo similar para desencadenar la carga de una nueva página de resultados. No seguiremos con el desplazamiento infinito en estos tutoriales, pero tiene cierto encanto al evitar clics adicionales del ratón, y puede que quiera investigar otras opciones.
Conclusiones
Tenga en cuenta los siguientes aspectos de este proyecto:
- La paginación numerada es útil para las búsquedas en las que el orden de los resultados es algo arbitrario, lo que significa que puede haber algo de interés para los usuarios en las páginas posteriores.
- El desplazamiento infinito es útil cuando el orden de los resultados es especialmente importante. Por ejemplo, si los resultados se ordenan a la distancia desde el centro de una ciudad de destino.
- La paginación numerada permite una mejor navegación. Por ejemplo, un usuario puede recordar que un resultado interesante estaba en la página 6, mientras que no existe una referencia tan sencilla en desplazamiento infinito.
- El desplazamiento infinito tiene un atractivo sencillo, desplácese hacia arriba y hacia abajo sin números de página para hacer clic.
- Una característica clave del desplazamiento infinito es que los resultados se anexan a una página existente, no reemplazando esa página, lo que es eficaz.
- El almacenamiento temporal persiste solo para una llamada y debe restablecerse para sobrevivir a llamadas adicionales.
Pasos siguientes
La paginación es fundamental para una experiencia de búsqueda. Con la paginación bien cubierta, el siguiente paso es mejorar aún más la experiencia del usuario incorporando búsquedas anticipadas.