Tutorial: Incorporación de la paginación a los resultados de búsqueda mediante el SDK de .NET
Aprenda a implementar dos sistemas de paginación distintos, el primero basado en los números de página y el segundo en el desplazamiento infinito. Ambos sistemas de paginación se utilizan ampliamente y seleccionar el correcto depende de la experiencia de usuario que desee con los resultados.
En este tutorial, aprenderá a:
- Ampliar la aplicación con la paginación numerada
- Ampliar la aplicación con el desplazamiento infinito
Información general
En este tutorial se superpone un sistema de localización en un proyecto creado previamente que se describe en el tutorial Creación de la primera aplicación de búsqueda.
En los proyectos siguientes se pueden encontrar versiones finalizadas del código que va a desarrollar en este tutorial:
Prerrequisitos
- Proyecto 1-basic-search-page (GitHub). Este proyecto puede ser su propia versión, que se creó a partir del tutorial anterior o de una copia de GitHub.
Ampliar la aplicación con la paginación numerada
La paginación numerada es el sistema de localización favorito tanto de los principales motores de búsqueda web comerciales como de muchos otros sitios web de búsqueda. La paginación numerado normalmente incluye una opción "siguiente" y "anterior", además de un intervalo de números de página real. También pueden estar disponibles una opción de "última página" y de "primera página". Estas opciones le dan al usuario el control sobre el desplazamiento en los resultados de la página.
En este tutorial, agregará un sistema que incluya las opciones de primera página, anterior, siguiente y última, junto con los números de página que no se inicien en 1, sino usar la página actual en la que esté el usuario (por lo tanto, por ejemplo, si el usuario está viendo la página 10, podrían mostrarse los números de página 8, 9, 10, 11 y 12).
El sistema será lo suficientemente flexible como para permitir que la cantidad de números de página visibles se establezca en una variable global.
El sistema considerará los botones de número de página más a la izquierda y más a la derecha como especiales, lo que significa que desencadenarán el cambio del intervalo de números de página que se muestra. 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 los cambios a 6, 7, 8, 9 y 10. Y hay un cambio similar a la derecha si se selecciona 12.
Incorporación de campos de paginación al modelo
Tiene abierta la solución de la página de búsqueda básica.
Abra el archivo de modelo SearchData.cs.
Agregue variables globales que admitan 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 la cantidad de números de página visibles en la vista. PageRangeDelta determina el número de páginas que se deben desplazar a la izquierda o a la derecha cuando se selecciona el número de la página que está más a la izquierda o a la derecha. Normalmente, este último número es en torno a la mitad de MaxPageRange. Agregue el código siguiente en el espacio de nombres.
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 equipo portátil, considere la posibilidad de cambiar ResultsPerPage a 2.
Agregue las 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; }
Incorporación de una tabla de opciones de paginación a la vista
Abra el archivo index.cshtml y agregue el código siguiente justo antes de la etiqueta </body> de cierre. Este nuevo código presenta una tabla de opciones de paginación: primero, anterior, 1, 2, 3, 4, 5, siguiente, última.
@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> }
Se usa una tabla HTML para alinear los elementos ordenadamente. Aunque todas las acciones proceden de las instrucciones @Html.ActionLink, cada una invoca al controlador con un modelo nuevo creado con entradas diferentes para la propiedad paging agregada anteriormente.
Las opciones de primera y última página no envían cadenas como "first" y "last", sin que, en su lugar, envían los números de página correctos.
Agregue clases de paginación a la lista de estilos HTML del archivo hotels.css. La clase pageSelected está ahí para identificar la página actual (mediante la aplicación del formato 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; }
Incorporación de 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, sobre el que volveremos en un momento.
Nota
Las llamadas a TempData almacenan un valor (un objeto) en el almacenamiento temporal, aunque este almacenamiento dura solo una llamada. Si se almacena algo en los datos temporales, estará disponible para la siguiente llamada a una acción de la controladora, pero desaparecerá definitivamente tras la llamada. Debido a esta corta duración, almacenamos el texto de búsqueda y las propiedades de paginación en cada almacenamiento temporal y en cada llamada a PageAsync.
Actualice la acción Index(model) para almacenar variables temporales y para agregar el parámetro de la página que está más a la izquierda a la llamada a 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, que se presentó en la lección anterior, necesita una modificación para resolver el error de sintaxis. Se usan los campos Skip, Size e IncludeTotalCount de la clase SearchOptions para solicitar solo una página de resultados que merezcan la pena, empezando por el valor Skip. También es necesario calcular las variables de paginación para nuestra vista. Reemplace el método entero 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 ahora contendrá el número de resultados devueltos en una sola página (tres en nuestro ejemplo), no el número total. Dado que establecemos IncludeTotalResultCount en true, la variable resultList.TotalCount ahora contiene el número total de resultados. Por 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 pequeño aumento del rendimiento cuando IncludeTotalCount se establece en true, ya que es necesario que Azure Cognitive Search calcule este total. Con conjuntos de datos complejos, se advierte que el valor devuelto es una aproximación. Como el corpus de búsqueda de hoteles 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 una cadena que devuelva gran cantidad de resultados (como "wifi"). ¿Puede recorrer las páginas correctamente en los resultados?
Pruebe a hacer clic en los números de página de la izquierda, de la derecha y última. ¿Los números de página se ajustan adecuadamente al centro de la página en la que se encuentra?
¿Son útiles las opciones "primera" y "última"? Algunos motores de búsqueda comerciales usan estas opciones, mientras que otros no lo hacen.
Vaya a la última página de resultados. La última página es la única que puede contener menos de ResultsPerPage resultados.
Escriba "town" y haga clic en Buscar. Si los resultados ocupan menos de una página, no se muestran opciones de paginación.
Guarde este proyecto y pase a la sección siguiente para conocer otra forma de paginación.
Ampliar la aplicación con el desplazamiento infinito
El desplazamiento infinito se desencadena cuando un usuario se desplaza hasta el último de los resultados que se muestra con una barra de desplazamiento vertical. En este evento, se realiza una llamada al servicio de búsqueda para obtener la siguiente página de resultados. Si no hay ningún resultado más, 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 se debe indicar es que la página actual no se reemplaza, sino que se extiende para mostrar los resultados adicionales. Un usuario siempre puede desplazarse hasta los primeros resultados de la búsqueda.
Para implementar el desplazamiento infinito, comencemos con el proyecto antes de que se agregara cualquiera de los elementos de desplazamiento del número de página. En GitHub, esta es la solución FirstAzureSearchApp.
Incorporación de campos de paginación al modelo
En primer lugar, agregue una propiedad paging 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" (siguiente) si la página siguiente de resultados se debe enviar o es null 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; } } }
Incorporación de una barra de desplazamiento vertical a la vista
Busque la sección del archivo index.cshtml que muestra los resultados (comienza con @if (Model != null)).
Reemplace la sección por el código siguiente. La nueva sección <div> está alrededor del área que debe ser desplazable y agrega tanto un atributo overflow-y como una llamada a una función onscroll denominada "scrolled()".
@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 ha hecho, se realiza una llamada al controlador Home para una acción denominada NextAsync. El controlador no necesita ninguna otra información, devolverá la siguiente página de datos. A continuación, se da formato a estos datos con estilos HTML idénticos al de la página original. Si no se devuelve ningún resultado, no se anexa nada y todo permanece como esté.
Control de acción Next
Solo hay tres acciones que deban enviarse al controlador: la primera ejecución de la aplicación, que llama a Index() , la primera búsqueda del usuario, que llama a Index(model) y, a continuación, las siguientes llamadas para obtener más resultados mediante Next(model) .
Abra el archivo del controlador de home y elimine el método RunQueryAsync del tutorial original.
Reemplace la acción Index(model) con el código siguiente. Ahora controla el campo paging 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); }
Al igual que el método de paginación numerada, las opciones de búsqueda Skip y Size se usan para solicitar que se devuelvan solo los datos que necesitamos.
Agregue la acción NextAsync al controlador de inicio. Observe cómo devuelve una lista y cada hotel agrega dos elementos a la lista: un nombre y una descripción del hotel. Este formato se establece para que coincida con el uso de la función scrolled 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 la cadena List<> , agregue la siguiente directiva using al encabezado 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 una gran cantidad de resultados (por ejemplo, "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 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 pueden ocupar hasta 3 x 154 = 462 píxeles. Para asegurarse de que aparece una barra de desplazamiento vertical, se debe establecer un alto para el área de presentación menor que 462 píxeles, incluso 461 funciona. Este problema se produce únicamente en la primera página, después de que se tenga la seguridad de que la barra de desplazamiento aparecerá. La línea para 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. Tenga en cuenta cómo toda la información ya está en la página de una vista. Puede desplazarse remontándose a la parte superior sin desencadenar ninguna llamada al servidor.
Los sistemas más sofisticados de desplazamiento infinito podrían usar la rueda del mouse u otro mecanismo similar para desencadenar la carga de una nueva página de resultados. No adoptaremos el desplazamiento infinito más en estos tutoriales, aunque resulta atractivo, ya que evita clics adicionales del mouse y es posible que desee investigar otras opciones.
Puntos clave
Tenga en cuenta las siguientes conclusiones de este proyecto:
- La paginación numerada es útil para las búsquedas en las que el orden de los resultados es bastante 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 se ordenan según la distancia desde el centro de una ciudad de destino.
- La paginación numerada mejora la navegación. Por ejemplo, un usuario puede recordar que un resultado interesante estaba en la página 6, mientras que tal referencia fácil no existe en el desplazamiento infinito.
- El desplazamiento infinito tiene una apariencia sencilla y el desplazamiento se produce arriba y abajo sin que haya que hacer clic en los números de página.
- Una característica clave del desplazamiento infinito es que los resultados se anexan a una página existente, no reemplazan esa página, lo que resulta eficaz.
- El almacenamiento temporal se mantiene solo una llamada y debe reiniciarse para sobrevivir a las siguientes.
Pasos siguientes
La paginación es fundamental en una experiencia de búsqueda. Si la paginación se ha resuelto adecuadamente, el paso siguiente es mejorar aún más la experiencia del usuario incorporando búsquedas de escritura anticipada.