Partilhar via


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

Saiba como implementar dois sistemas de paginação diferentes, o primeiro com base em números de página e o segundo em deslocamento infinito. Ambos os sistemas de paginação são amplamente utilizados e selecionar o correto depende da experiência de utilizador que pretende com os resultados.

Neste tutorial, ficará a saber como:

  • Expandir a aplicação com paginação numerada
  • Expandir a aplicação com deslocamento infinito

Descrição Geral

Este tutorial sobrepõe um sistema de paginação num projeto criado anteriormente descrito no tutorial Criar a sua primeira aplicação de pesquisa .

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

Pré-requisitos

Expandir a aplicação com paginação numerada

A paginação numerada é o sistema de paginação escolhido para os principais motores de busca web comerciais e muitos outros sites de pesquisa. Normalmente, a paginação numerada inclui uma opção "seguinte" e "anterior", além de um intervalo de números de página reais. Também pode estar disponível uma opção de "primeira página" e "última página". Estas opções dão certamente a um utilizador controlo sobre a navegação através de resultados baseados em páginas.

Neste tutorial, irá adicionar um sistema que inclui as primeiras, anteriores, seguintes e últimas opções, juntamente com números de página que não começam a partir de 1, mas que rodeiam a página atual em que o utilizador se encontra (por exemplo, se o utilizador estiver a olhar para a página 10, talvez sejam apresentados os números de página 8, 9, 10, 11 e 12).

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

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

Adicionar campos de paginação ao modelo

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

  1. Abra o ficheiro de modelo SearchData.cs.

  2. Adicione variáveis globais para suportar a paginação. No MVC, as variáveis globais são declaradas na 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 vista. PageRangeDelta determina quantas páginas devem ser movidas para a esquerda ou para a direita quando o número de página mais à esquerda ou para a direita está selecionado. Normalmente, este último número é cerca de metade de MaxPageRange. Adicione o seguinte código ao espaço de nomes.

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

    Dica

    Se estiver a executar este projeto num dispositivo com um ecrã mais pequeno, como um portátil, considere alterar o 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 ficheiro index.cshtml e adicione o seguinte código imediatamente antes de fechar </body> tag. Este novo código apresenta uma tabela de opções de paginação: primeiro, anterior, 1, 2, 3, 4, 5, seguinte, ú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>
    }
    

    Utilizamos uma tabela HTML para alinhar as coisas corretamente. No entanto, toda a ação provém 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 como "primeiro" e "último", mas enviam os números de página corretos.

  2. Adicione classes de paginação à lista de estilos HTML no ficheiro hotels.css. A classe pageSelected está lá para identificar a página atual (ao aplicar um formato a negrito ao número de 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 ficheiro 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 irá agora mostrar um erro de sintaxe, devido ao terceiro parâmetro, ao qual vamos chegar dentro de momentos.

    Nota

    As chamadas para TempData armazenam um valor (um objeto) no armazenamento temporário, embora este armazenamento persista apenas para uma chamada. Se armazenarmos algo em dados temporários, este estará disponível para a próxima chamada para uma ação de controlador, mas será definitivamente efetuada pela chamada depois disso. Devido a esta curta duração, armazenamos o texto de pesquisa e as propriedades de paginação novamente no armazenamento temporário, cada chamada para PageAsync.

  2. Atualize a ação Index(model) para armazenar variáveis temporárias e para 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. Utilizamos os campos Ignorar, Tamanho e IncluirTotalCount da classe SearchOptions para pedir apenas uma página de resultados, começando na definição Ignorar . Também precisamos de calcular as variáveis de paginação para a nossa vista. Substitua todo o método pelo seguinte código.

    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 à vista. A variável resultList.Results.TotalCount conterá agora o número de resultados devolvidos numa página (3 no nosso exemplo) e não o número total. Uma vez que definimos IncludeTotalCount como verdadeiro, a variável resultList.TotalCount contém agora o número total de resultados. Por isso, localize onde é apresentado o número de resultados na vista e altere-o para o seguinte código.

    // 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

    Ocorre um pequeno impacto no desempenho ao definir IncludeTotalCount como verdadeiro, uma vez que este total tem de ser calculado por Azure Cognitive Search. Com conjuntos de dados complexos, existe um aviso de que o valor devolvido é uma aproximação. Como o corpus de pesquisa do hotel é pequeno, será preciso.

Compilar e executar a aplicação

Agora, selecione Iniciar Sem Depuração (ou prima a tecla F5).

  1. Pesquise numa cadeia que devolve muitos resultados (como "wi-fi"). Pode analisar cuidadosamente os resultados?

    Paginação numerada através dos resultados do

  2. Experimente clicar nos números de página mais à direita e posteriores à esquerda. Os números de página ajustam-se adequadamente para centrar a página em que se encontra?

  3. As opções "primeiro" e "último" são úteis? Alguns motores de busca comerciais utilizam estas opções e outros não.

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

    Examinar a última página de

  5. Escreva "cidade" e clique em procurar. Não são apresentadas opções de paginação se os resultados forem inferiores a uma página.

    Procurar

Guarde este projeto e avance para a secção seguinte para obter uma forma alternativa de paginação.

Expandir a aplicação com deslocamento infinito

O deslocamento infinito é acionado quando um utilizador desloca uma barra de deslocamento vertical até ao último dos resultados que estão a ser apresentados. Neste evento, é feita uma chamada para o serviço de pesquisa para a próxima página de resultados. Se não existirem mais resultados, nada é devolvido e a barra de deslocamento vertical não é alterada. Se existirem mais resultados, estes são anexados à página atual e a barra de deslocamento muda para mostrar que estão disponíveis mais resultados.

Um ponto importante a ter em atenção é que a página atual não é substituída, mas sim expandida para mostrar os resultados adicionais. Um utilizador pode sempre deslocar-se para cima até aos primeiros resultados da pesquisa.

Para implementar o deslocamento infinito, vamos começar com o projeto antes de qualquer um dos elementos de deslocamento do número de página ter sido adicionado. 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 ficheiro de modelo SearchData.cs).

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

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

  2. No mesmo ficheiro e no espaço de nomes, adicione uma classe de variável global com uma propriedade. No MVC, as variáveis globais são declaradas na 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 deslocamento vertical à vista

  1. Localize a secção do ficheiro index.cshtml que apresenta os resultados (começa com o @if (Modelo != nulo)).

  2. Substitua a secção pelo código abaixo. A nova <secção div> está à volta da área que deve ser percorrível e adiciona um atributo overflow-y e uma chamada a uma função onscroll chamada "scrolled()", da mesma forma.

    @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 por baixo do ciclo, depois da <etiqueta /div> , adicione a função deslocada .

    <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 utilizador se deslocou para a parte inferior da barra de deslocamento vertical. Se o tiverem feito, será efetuada uma chamada para o controlador Base para uma ação chamada NextAsync. Não são necessárias outras informações pelo controlador. Irá devolver a próxima página de dados. Estes dados são então formatados com estilos HTML idênticos à página original. Se não forem devolvidos resultados, nada é acrescentado e as coisas permanecem como estão.

Processar a ação Seguinte

Existem apenas três ações que têm de ser enviadas para o controlador: a primeira execução da aplicação, que chama Índice(), a primeira pesquisa pelo utilizador, que chama Índice(modelo) e, em seguida, o subsequente pede mais resultados através de Next(model).

  1. Abra o ficheiro do controlador principal e elimine o método RunQueryAsync do tutorial original.

  2. Substitua a ação Índice(modelo) pelo seguinte código. Agora, processa o campo de paginação quando é nulo ou está definido como "seguinte" e processa a chamada para 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);
    }
    

    Semelhante ao método de paginação numerado, utilizamos as definições de pesquisa Ignorar e Tamanho para pedir apenas os dados de que precisamos.

  3. Adicione a ação NextAsync ao controlador principal. Repare como devolve uma lista, cada hotel adiciona dois elementos à lista: um nome de hotel e uma descrição do hotel. Este formato está definido para corresponder à utilização da função deslocada dos dados devolvidos na 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);
    }
    
  4. Se receber um erro de sintaxe na cadeia Lista<>, adicione a seguinte diretiva ao cabeçalho do ficheiro do controlador.

    using System.Collections.Generic;
    

Compilar e executar o projeto

Agora, selecione Iniciar Sem Depuração (ou prima a tecla F5).

  1. Introduza um termo que dará muitos resultados (como "conjunto") e, em seguida, teste a barra de deslocamento vertical. Aciona uma nova página de resultados?

    Deslocamento infinito através dos resultados do

    Dica

    Para garantir que é apresentada uma barra de deslocamento na primeira página, a primeira página de resultados tem de exceder ligeiramente a altura da área em que estão a ser apresentados. No nosso exemplo .box1 tem uma altura de 30 pixéis, .box2 tem uma altura de 100 pixéis e uma margem inferior de 24 píxeis. Assim, cada entrada utiliza 154 píxeis. Três entradas ocuparão 3 x 154 = 462 píxeis. Para garantir que é apresentada uma barra de deslocamento vertical, tem de definir uma altura para a área de apresentação inferior a 462 pixels, até mesmo 461 funciona. Este problema só ocorre na primeira página, depois de ser apresentada uma barra de deslocamento. A linha a atualizar é: <div id="myDiv" style="width: 800px; height: 450px; overflow-y: scroll;" onscroll="scrolled()">.

  2. Desloque-se para baixo até à parte inferior dos resultados. Repare como todas as informações estão agora na página de uma vista. Pode deslocar-se até à parte superior sem acionar chamadas de servidor.

Os sistemas de deslocamento infinitos mais sofisticados podem utilizar a roda do rato, ou outro mecanismo semelhante, para acionar o carregamento de uma nova página de resultados. Não vamos continuar a deslocar-nos infinitamente nestes tutoriais, mas tem um certo atalho, uma vez que evita cliques de rato adicionais e poderá querer investigar mais outras opções.

Conclusões

Considere as seguintes conclusões deste projeto:

  • A paginação numerada é útil para pesquisas em que a ordem dos resultados é algo arbitrária, o que significa que pode muito bem haver algo de interesse para os seus utilizadores nas páginas posteriores.
  • Deslocamento infinito é ú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 utilizador pode lembrar-se de que um resultado interessante foi na página 6, enquanto que não existe uma referência tão fácil em deslocamento infinito.
  • O deslocamento infinito tem um apelo fácil, deslocando-se para cima e para baixo sem números de página para clicar.
  • Uma funcionalidade-chave do deslocamento infinito é que os resultados são acrescentados 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 de ser reposto para sobreviver a chamadas adicionais.

Passos seguintes

A paginação é fundamental para uma experiência de pesquisa. Com a paginação bem abrangida, o próximo passo é melhorar ainda mais a experiência do utilizador ao adicionar pesquisas com antecedência.