Partilhar via


Tutorial: Adicionar paginação aos resultados da pesquisa usando o SDK do .NET

Saiba como implementar dois sistemas de paginação diferentes, o primeiro baseado em números de página e o segundo em rolagem infinita. Ambos os sistemas de paginação são amplamente utilizados, e selecionar o correto depende da experiência do usuário que você gostaria com os resultados.

Neste tutorial, saiba como:

  • Estenda seu aplicativo com paginação numerada
  • Estenda seu aplicativo com rolagem infinita

Visão geral

Este tutorial sobrepõe um sistema de paginação em um projeto criado anteriormente descrito no tutorial Criar seu primeiro aplicativo de pesquisa .

As versões concluídas do código que você desenvolverá neste tutorial podem ser encontradas nos seguintes projetos:

Pré-requisitos

Estenda seu aplicativo com paginação numerada

A paginação numerada é o sistema de paginação preferido dos principais motores de busca comerciais e de muitos outros sites de pesquisa. A paginação numerada normalmente inclui as opções "próximo" e "anterior", além de um intervalo de números de página efetivos. Também pode estar disponível uma opção de "primeira página" e "última página". Essas opções certamente dão ao usuário controle sobre a navegação pelos resultados baseados em página.

Neste tutorial, você adicionará um sistema que inclui as opções primeira, anterior, próxima e última, juntamente com números de página que não começam a partir de 1, mas cercam a página atual em que o usuário está (assim, por exemplo, se o usuário estiver olhando para a página 10, talvez os números de página 8, 9, 10, 11 e 12 sejam exibidos).

O sistema será suficientemente flexível para permitir que o número de números de páginas visíveis seja definido numa variável global.

O sistema tratará os botões de número de página mais à esquerda e mais à direita como especiais, o que significa que eles acionarão a alteração do intervalo de números de página exibidos. Por exemplo, se os números de página 8, 9, 10, 11 e 12 forem exibidos e o usuário clicar em 8, o intervalo de números de página exibidos será alterado para 6, 7, 8, 9 e 10. E há uma mudança semelhante para a direita se eles selecionaram 12.

Adicionar campos de paginação ao modelo

Tenha a solução de página de pesquisa básica aberta.

  1. Abra o arquivo de modelo SearchData.cs.

  2. Adicione variáveis globais para dar suporte à paginação. No MVC, as variáveis globais são declaradas em sua própria classe estática. ResultsPerPage define o número de resultados por página. MaxPageRange determina o número de números de página visíveis na exibição. PageRangeDelta determina quantas páginas devem ser deslocadas para a esquerda ou para a direita, quando o número de página mais à esquerda ou mais à direita é selecionado. Normalmente, este último número é cerca de metade do MaxPageRange. Adicione o seguinte código ao 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;
            }
        }
    }
    

    Sugestão

    Se você estiver executando este projeto em um dispositivo com uma tela menor, como um laptop, considere alterar ResultsPerPage para 2.

  3. Adicione propriedades de paginação à classe SearchData , após a propriedade 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; }
    

Adicionar uma tabela de opções de paginação à vista

  1. Abra o arquivo index.cshtml e adicione o seguinte código imediatamente antes da tag< /body> de encerramento. Este novo código apresenta uma tabela de opções de paginação: primeiro, anterior, 1, 2, 3, 4, 5, próximo, ú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">|&lt;</p>
                }
            </td>
    
            <td>
                @if (Model.currentPage > 0)
                {
                    <p class="pageButton">
                        @Html.ActionLink("<", "PageAsync", "Home", new { paging = "prev" }, null)
                    </p>
                }
                else
                {
                    <p class="pageButtonDisabled">&lt;</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">&gt;</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">&gt;|</p>
                }
            </td>
        </tr>
    </table>
    }
    

    Usamos uma tabela HTML para alinhar as coisas ordenadamente. No entanto, toda a ação vem das @Html.ActionLink instruções, cada uma chamando o controlador com um novo modelo criado com entradas diferentes para a propriedade de paginação que adicionámos anteriormente.

    As opções de primeira e última página não enviam cadeias de caracteres como "primeira" e "última", mas enviam os números de página corretos.

  2. Adicione classes de paginação à lista de estilos HTML no arquivo hotels.css. A classe pageSelected está lá para identificar a página atual (aplicando um formato em negrito ao número da página) na 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;
    }
    

Adicionar uma ação Página ao controlador

  1. Abra o arquivo HomeController.cs e adicione a ação PageAsync . Esta ação responde a qualquer uma das opções de página selecionadas.

    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);
    }
    

    O método RunQueryAsync agora mostrará um erro de sintaxe, devido ao terceiro parâmetro, ao qual chegaremos daqui a pouco.

    Observação

    As chamadas para TempData armazenam um valor (um objeto) no armazenamento temporário, embora esse armazenamento persista para apenas uma chamada. Se armazenarmos algo em dados temporários, ele estará disponível para a próxima chamada de uma ação do controlador, mas será provavelmente removido na chamada seguinte. Devido a esse curto período de vida, armazenamos o texto de pesquisa e as propriedades de paginação de volta no armazenamento temporário a cada chamada para PageAsync.

  2. Atualize a ação Index(model) para armazenar variáveis temporárias e adicionar o parâmetro de página mais à esquerda à chamada 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);
    }
    
  3. O método RunQueryAsync , introduzido na lição anterior, precisa de modificação para resolver o erro de sintaxe. Usamos os campos Skip, Size, e IncludeTotalCount da classe SearchOptions para solicitar apenas uma página de resultados, começando na configuração Skip . Também precisamos calcular as variáveis de paginação para nossa visualização. Substitua o método inteiro pelo código a seguir.

    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);
    }
    
  4. Por fim, faça uma pequena alteração na exibição. A variável resultList.Results.TotalCount agora conterá o número de resultados retornados em uma página (3 em nosso exemplo), não o número total. Como definimos IncludeTotalCount como true, a variável resultList.TotalCount agora contém o número total de resultados. Portanto, localize onde o número de resultados é exibido na exibição e altere-o para o código a seguir.

    // 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" })
    }
    

    Observação

    Há um pequeno impacto no desempenho ao definir IncludeTotalCount como true, pois esse total precisa ser calculado pela Pesquisa Cognitiva do Azure. Com conjuntos de dados complexos, há um aviso de que o valor retornado é uma aproximação. Como o corpus de pesquisa de hotéis é pequeno, ele será preciso.

Compilar e executar o aplicativo

Agora selecione Iniciar sem depuração (ou pressione a tecla F5).

  1. Pesquise em uma cadeia de caracteres que retorna muitos resultados (como "wifi"). Consegue folhear os resultados de forma organizada?

    Paginação numerada através dos resultados do

  2. Tente clicar nos números de página mais à direita e, mais tarde, mais à esquerda. Os números de página são ajustados adequadamente para centralizar a página em que você está?

  3. As opções "primeira" e "última" são úteis? Alguns motores de busca comerciais usam essas opções e outros não.

  4. Ir para a última página de resultados. A última página é a única que pode conter menos do que os resultados ResultsPerPage .

    Examinando a última página de

  5. Digite "cidade" e clique em pesquisar. Nenhuma opção de paginação será exibida se os resultados tiverem menos de uma página.

    À procura de

Salve este projeto e continue para a próxima seção para uma forma alternativa de paginação.

Estenda seu aplicativo com rolagem infinita

A rolagem infinita é acionada quando um usuário rola uma barra de rolagem vertical até o último dos resultados que estão sendo exibidos. Quando este evento ocorre, é feita uma chamada para o serviço de pesquisa para obter a próxima página de resultados. Se não houver mais resultados, nada será retornado e a barra de rolagem vertical não será alterada. Se houver mais resultados, eles serão anexados à página atual e a barra de rolagem será alterada para mostrar que mais resultados estão disponíveis.

Um ponto importante a ser observado é que a página atual não é substituída, mas sim estendida para mostrar os resultados adicionais. Um usuário sempre pode rolar de volta até os primeiros resultados da pesquisa.

Para implementar a rolagem infinita, vamos começar com o projeto antes que quaisquer elementos de rolagem por número de página fossem adicionados. No GitHub, esta é a solução FirstAzureSearchApp .

Adicionar campos de paginação ao modelo

  1. Primeiro, adicione uma propriedade de paginação à classe SearchData (no arquivo de modelo SearchData.cs).

    // Record if the next page is requested.
    public string paging { get; set; }
    

    Esta variável é uma cadeia de caracteres, que contém "next" se a próxima página de resultados deve ser enviada, ou ser nula para a primeira página de uma pesquisa.

  2. No mesmo arquivo e dentro do namespace, adicione uma classe de variável global com uma propriedade. No MVC, as variáveis globais são declaradas em sua própria classe estática. ResultsPerPage define o número de resultados por página.

    public static class GlobalVariables
    {
        public static int ResultsPerPage
        {
            get
            {
                return 3;
            }
        }
    }
    

Adicionar uma barra de rolagem vertical à exibição

  1. Localize a seção do arquivo index.cshtml que exibe os resultados (ele começa com o @if (Model != null)).

  2. Substitua a seção pelo código abaixo. A nova <seção div> abrange a área que deve ser rolável e adiciona tanto um atributo overflow-y como uma chamada para uma função onscroll chamada "scrolled()", assim.

    @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" })
            }
    
  3. Diretamente abaixo do loop, após a tag </div>, adicione a função 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>
    

    A instrução if no script acima testa se o usuário rolou para a parte inferior da barra de rolagem vertical. Se tiverem, uma chamada para o controlador Home é feita para uma ação chamada NextAsync. Nenhuma outra informação é necessária pelo controlador, ele retornará a próxima página de dados. Esses dados são formatados usando estilos HTML idênticos aos da página original. Se nenhum resultado for retornado, nada é anexado e as coisas permanecem como estão.

Lidar com a próxima ação

Há apenas três ações que precisam ser enviadas para o controlador: a primeira execução do aplicativo, que chama Index(), a primeira pesquisa pelo usuário, que chama Index(model) e, em seguida, as chamadas subsequentes para obter mais resultados via Next(model).

  1. Abra o arquivo do controlador inicial e exclua o método RunQueryAsync do tutorial original.

  2. Substitua a ação Index(model) pelo código a seguir. Ele agora manipula o campo de paginação quando ele é nulo, ou definido como "próximo", e lida com a chamada para a Pesquisa Cognitiva do Azure.

    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);
    }
    

    Semelhante ao método de paginação numerada, usamos as configurações de pesquisa Ignorar e Tamanho para solicitar que apenas os dados de que precisamos sejam retornados.

  3. Adicione a ação NextAsync ao controlador inicial. Observe como ele retorna uma lista, cada hotel adicionando dois elementos à lista: um nome de hotel e uma descrição do hotel. Esse formato é definido para corresponder ao uso da função de scroll dos dados retornados na visualização.

    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);
    }
    
  4. Se você receber um erro de sintaxe em List<string>, adicione a seguinte diretiva using ao cabeçalho do arquivo do controlador.

    using System.Collections.Generic;
    

Compile e execute seu projeto

Agora selecione Iniciar sem depuração (ou pressione a tecla F5).

  1. Insira um termo que dará muitos resultados (como "pool") e teste a barra de rolagem vertical. Desencadeia uma nova página de resultados?

    Rolagem infinita pelos resultados do

    Sugestão

    Para garantir que uma barra de rolagem apareça na primeira página, a primeira página de resultados deve exceder ligeiramente a altura da área em que estão sendo exibidos. No nosso exemplo, .box1 tem uma altura de 30 pixels, .box2 tem uma altura de 100 pixels e uma margem inferior de 24 pixels. Assim, cada entrada usa 154 pixels. Três entradas ocuparão 3 x 154 = 462 pixels. Para garantir que uma barra de rolagem vertical apareça, uma altura para a área de exibição deve ser definida que seja menor que 462 pixels, mesmo 461 funciona. Esse problema ocorre apenas na primeira página, depois disso uma barra de rolagem certamente aparecerá. A linha a atualizar é: <div id="myDiv" style="width: 800px; height: 450px; overflow-y: scroll;" onscroll="scrolled()">.

  2. Role para baixo até a parte inferior dos resultados. Observe como todas as informações estão agora na página de visualização única. Você pode rolar todo o caminho de volta para o topo sem disparar nenhuma chamada de servidor.

Sistemas de rolagem infinita mais sofisticados podem usar a roda do mouse, ou outro mecanismo semelhante, para acionar o carregamento de uma nova página de resultados. Não levaremos a rolagem infinita mais longe nesses tutoriais, mas ela tem um certo charme, pois evita cliques extras do mouse, e você pode querer investigar outras opções mais adiante.

Conclusões

Considere as seguintes conclusões deste projeto:

  • A paginação numerada é útil para pesquisas em que a ordem dos resultados é um pouco arbitrária, o que significa que pode haver algo de interesse para seus usuários nas páginas posteriores.
  • A rolagem infinita é útil quando a ordem dos resultados é particularmente importante. Por exemplo, se os resultados forem ordenados à distância do centro de uma cidade de destino.
  • A paginação numerada permite uma melhor navegação. Por exemplo, um usuário pode se lembrar que um resultado interessante estava na página 6, enquanto não existe uma referência tão fácil na rolagem infinita.
  • A rolagem infinita é atraente pela sua facilidade, permitindo navegar para cima e para baixo sem clicar em números de página.
  • Uma característica fundamental da rolagem infinita é que os resultados são anexados a uma página existente, não substituindo essa página, o que é eficiente.
  • O armazenamento temporário persiste para apenas uma chamada e precisa ser redefinido para sobreviver a chamadas adicionais.

Próximos passos

A paginação é fundamental para uma experiência de pesquisa. Com a paginação bem coberta, o próximo passo é melhorar ainda mais a experiência do utilizador, adicionando pesquisas preditivas.