Поделиться через


Руководство. Добавление разбиения по страницам в результаты поиска с помощью пакета SDK для .NET

Узнайте, как реализовать две разные системы разбиения на страницы: первую на основе номеров страниц и вторую на основе бесконечной прокрутки. Обе системы разбиения по страницам широко используются, и выбор правильного зависит от пользовательского интерфейса, который вы хотите использовать с результатами.

Из этого руководства вы узнаете, как:

  • Расширьте приложение с нумерованной пагинацией
  • Расширение приложения с помощью бесконечной прокрутки

Обзор

В этом руководстве в уже созданный проект, описанный в руководстве Создайте свое первое поисковое приложение, добавляется система разбиения по страницам.

Готовые версии кода, который вы будете разрабатывать в этом руководстве, можно найти в следующих проектах:

Предпосылки

  • Проект 1-basic-search-page (GitHub). Этот проект может быть собственной версией, созданной из предыдущего руководства, или копией из GitHub.

Расширьте своё приложение с нумерованной пагинацией

Нумерованное разбиение на страницы — это система разбиения на страницы для основных коммерческих поисковых систем и многих других веб-сайтов поиска. Нумерованное разбиение на страницы обычно включает параметр "next" и "previous" в дополнение к диапазону фактических номеров страниц. Также может быть доступен параметр "первая страница" и "последняя страница". Эти параметры, безусловно, позволяют пользователю контролировать навигацию по результатам, отображаемым на страницах.

В этом руководстве вы добавите систему, содержащую первые, предыдущие, следующие и последние параметры, а также номера страниц, которые не начинаются с 1, но вместо этого окружают текущую страницу, на которую находится пользователь (например, если пользователь смотрит на страницу 10, возможно, номера страниц 8, 9, 10, 11 и 12 отображаются).

Система будет достаточно гибкой, чтобы позволить задать количество видимых номеров страниц в глобальной переменной.

Система будет обрабатывать кнопки номера страниц слева и справа как специальные, то есть они будут активировать изменение диапазона отображаемых номеров страниц. Например, если отображаются номера страниц 8, 9, 10, 11 и 12, а пользователь щелкает на 8, то диапазон номеров страниц изменяется на 6, 7, 8, 9 и 10. И есть аналогичный сдвиг вправо, если они выбрали 12.

Добавление полей разбиения по страницам в модель

Откройте готовую базовую страницу поиска.

  1. Откройте файл модели SearchData.cs.

  2. Добавьте глобальные переменные для поддержки разбиения на страницы. В MVC глобальные переменные объявляются в собственном статичном классе. ResultsPerPage задает количество результатов на страницу. MaxPageRange определяет количество видимых номеров страниц в представлении. PageRangeDelta определяет, сколько страниц следует перемещать влево или вправо, когда выбран самый левый или самый правый номер страницы. Обычно это последнее число составляет около половины MaxPageRange. Добавьте следующий код в пространство имен.

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

    Подсказка

    Если вы используете этот проект на устройстве с меньшим экраном, например ноутбуком, попробуйте изменить ResultsPerPage на 2.

  3. Добавьте свойства разбиения на страницы в класс SearchData после свойства 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; }
    

Добавление таблицы параметров разбиения по страницам в представление

  1. Откройте файл index.cshtml и добавьте следующий код прямо перед закрывающим <тегом /body> . В этом новом коде представлена таблица параметров разбиения по страницам: сначала, 1, 2, 3, 4, 5, далее, последняя.

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

    Мы используем html-таблицу для точного выравнивания вещей. Однако все действие происходит из @Html.ActionLink инструкций, каждая из которых вызывает контроллер с новой моделью, созданной с разными параметрами для свойства разбиения страниц, которое мы добавили ранее.

    Параметры первой и последней страницы не отправляют такие строки, как "first" и "last", а вместо этого отправляют правильные номера страниц.

  2. Добавьте классы разбиения на страницы в список стилей HTML в файле hotels.css. Выбранный класс pageSelected позволяет определить текущую страницу (применив полужирный формат к номеру страницы) в списке номеров страниц.

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

Добавьте действие страницы в контроллер

  1. Откройте файл HomeController.cs и добавьте действие PageAsync . Это действие реагирует на любой из выбранных параметров страницы.

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

    Теперь метод RunQueryAsync будет отображать синтаксическую ошибку из-за третьего параметра, к которому мы перейдем немного.

    Замечание

    Вызовы TempData хранят значение ( объект) во временном хранилище, хотя это хранилище сохраняется только для одного вызова. Если мы сохраняем что-то во временных данных, они будут доступны для следующего вызова действия контроллера, но почти наверняка не будут доступны при следующем вызове. Из-за этого короткого срока существования мы сохраняем текст поиска и свойства пагинации во временном хранилище при каждом вызове PageAsync.

  2. Обновите действие Index(model) для хранения временных переменных и добавьте в вызов 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. Метод RunQueryAsync , представленный на предыдущем занятии, должен измениться для устранения синтаксической ошибки. Поля Skip, Size и IncludeTotalCount класса SearchOptions используются для запроса результатов только одной страницы, начиная с параметра Skip . Мы также должны вычислить переменные разбиения на страницы для нашего представления. Замените весь метод следующим кодом.

    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. Наконец, внесите небольшое изменение в представление. Переменная resultList.Results.TotalCount теперь будет содержать количество результатов, возвращаемых на одной странице (3 в нашем примере), а не общее число. Так как параметр IncludeTotalCount имеет значение true, переменная resultList.TotalCount теперь содержит общее количество результатов. Поэтому найдите, где отображается количество результатов в представлении, и измените его на следующий код.

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

    Замечание

    При установке IncludeTotalCount в значение true происходит незначительное снижение производительности, так как это общее количество должно быть вычислено службой Azure Cognitive Search. При использовании сложных наборов данных существует предупреждение о том, что возвращаемое значение является приблизительным. Поскольку поисковая база данных отелей невелика, она будет точной.

Компиляция и запуск приложения

Теперь выберите "Начать без отладки " (или нажмите клавишу F5).

  1. Поиск по строке, которая возвращает много результатов (например, "wifi"). Можете ли вы аккуратно просмотреть результаты?

    Нумерованная разбивка по страницам результатов пула

  2. Попробуйте щелкнуть правейший номер страницы, а затем левейший. Корректно ли настраиваются номера страниц так, чтобы текущая страница была по центру?

  3. Полезны ли варианты "первый" и "последний"? Некоторые коммерческие поисковые системы используют эти параметры, а другие — нет.

  4. Перейдите на последнюю страницу результатов. Последняя страница — единственная страница, которая может содержать меньше результатов ResultsPerPage .

    Изучение последней страницы

  5. Введите "город" и нажмите кнопку поиска. Параметры разбиения на страницы не отображаются, если результаты меньше одной страницы.

    Поиск

Сохраните этот проект и перейдите к следующему разделу для альтернативной формы разбиения по страницам.

Расширение приложения с помощью бесконечной прокрутки

Бесконечная прокрутка активируется, когда пользователь прокручивает вертикальную полосу прокрутки до конца отображаемых результатов. В этом случае вызов службы поиска выполняется для следующей страницы результатов. Если больше результатов нет, ничего не возвращается, и вертикальная полоса прокрутки не изменяется. Если есть больше результатов, они добавляются на текущую страницу, а полоса прокрутки изменится, чтобы показать, что доступны дополнительные результаты.

Важно отметить, что текущая страница не заменена, а скорее расширена для отображения дополнительных результатов. Пользователь всегда может прокрутить вверх к первым результатам поиска.

Чтобы реализовать бесконечную прокрутку, начнем проект с того этапа, на котором ещё не были добавлены элементы постраничной прокрутки. На сайте GitHub это решение FirstAzureSearchApp .

Добавление полей разбиения по страницам в модель

  1. Сначала добавьте свойство пагинации в класс SearchData (в файле модели SearchData.cs).

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

    Эта переменная представляет собой строку, содержащую "next", если следующая страница результатов должна быть отправлена или иметь значение NULL для первой страницы поиска.

  2. В том же файле и в пространстве имен добавьте глобальный класс переменной с одним свойством. В MVC глобальные переменные объявляются в собственном статичном классе. ResultsPerPage задает количество результатов на страницу.

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

Добавление вертикальной полосы прокрутки в представление

  1. Найдите раздел файла index.cshtml, отображающего результаты (начинается с @if (модель != null)).

  2. Замените раздел приведенным ниже кодом. Новый <раздел div> находится вокруг области, которая должна быть прокручиваемой, и добавляет как атрибут overflow-y, так и вызов функции onscroll с именем 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" })
            }
    
  3. Непосредственно под циклом, после тега </div>, добавьте функцию 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>
    

    Оператор if в приведенном выше скрипте проверяет, прокрутил ли пользователь до нижней части вертикальной полосы прокрутки. Если условие выполнено, вызывается контроллер Home для выполнения действия NextAsync. Никакие другие сведения не требуются контроллеру, он вернет следующую страницу данных. Эти данные затем форматируются с использованием идентичных HTML-стилей, как на исходной странице. Если результаты не возвращаются, ничего не добавляется и все остается как они.

Обработка следующего действия

Существует только три действия, которые должны быть отправлены контроллеру: первый запуск приложения, который вызывает Index(), первый поиск пользователем, который вызывает Index(model), а затем последующие вызовы дополнительных результатов через Next(model).

  1. Откройте файл контроллера дома и удалите метод RunQueryAsync из исходного руководства.

  2. Замените действие Index(model) следующим кодом. Теперь он обрабатывает поле пагинации, если оно принимает значение NULL или устанавливается в "next", и обрабатывает вызов когнитивного поиска 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);
    }
    

    Как и в случае с нумерованным методом разбиения на страницы, мы используем параметры поиска skip and Size , чтобы запрашивать только нужные данные.

  3. Добавьте действие NextAsync к домашнему контроллеру. Обратите внимание, как он возвращает список, каждый отель добавляет два элемента в список: имя отеля и описание отеля. Этот формат установлен для соответствия использованию возвращенных данных функцией прокрутки в представлении.

    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. Если в строке> списка< возникает синтаксическая ошибка, добавьте следующую директиву using в голову файла контроллера.

    using System.Collections.Generic;
    

Компиляция и запуск проекта

Теперь выберите "Начать без отладки " (или нажмите клавишу F5).

  1. Введите термин, который даст много результатов (например, пул), а затем протестируйте вертикальную полосу прокрутки. Активирует ли она новую страницу результатов?

    Бесконечная прокрутка по результатам

    Подсказка

    Чтобы убедиться, что полоса прокрутки отображается на первой странице, первая страница результатов должна немного превышать высоту области, в которой они отображаются. В нашем примере .box1 имеет высоту 30 пикселей, .box2 имеет высоту 100 пикселей и нижнее поле 24 пикселей. Поэтому каждая запись использует 154 пикселя. Три записи будут занимать 3 x 154 = 462 пикселя. Чтобы обеспечить отображение вертикальной полосы прокрутки, необходимо задать высоту области отображения, которая меньше 462 пикселей, даже 461 работает. Эта проблема возникает только на первой странице, после этого полоса прокрутки обязательно появится. Строка для обновления: <div id="myDiv" style="width: 800px; height: 450px; overflow-y: scroll;" onscroll="scrolled()">.

  2. Прокрутите до конца результатов. Обратите внимание на то, как все сведения теперь отображаются на одной странице представления. Вы можете прокручивать весь путь обратно в верхнюю часть без активации вызовов сервера.

Более сложные системы бесконечной прокрутки могут использовать колесико мыши или аналогичный другой механизм для активации загрузки новой страницы результатов. Мы не будем продолжать использовать бесконечную прокрутку в этих руководствах, но у неё есть определенный шарм, так как она позволяет избежать дополнительных щелчков мыши, и, возможно, вы захотите изучить другие варианты.

Общие выводы

Рассмотрим следующие выносы из этого проекта:

  • Нумерованное разбиение по страницам полезно для поиска, где порядок результатов несколько произвольный, то есть может быть что-то интересное для пользователей на последующих страницах.
  • Бесконечная прокрутка полезна, когда особенно важен порядок результатов. Например, если результаты упорядочены по расстоянию от центра города назначения.
  • Нумерованное разбиение на страницы обеспечивает лучшую навигацию. Например, пользователь может вспомнить, что интересный результат был на странице 6, в то время как такая простая ссылка не существует при бесконечной прокрутке.
  • Бесконечная прокрутка имеет простую привлекательность, позволяя перемещаться вверх и вниз без необходимости нажимать на номера страниц.
  • Ключевой особенностью бесконечной прокрутки является добавление результатов на существующую страницу, а не замена этой страницы, которая эффективна.
  • Временное хранилище сохраняется только в течение одного вызова, и его необходимо сбросить, чтобы оно продолжало существовать в других вызовах.

Дальнейшие действия

Разбиение по страницам является фундаментальным для интерфейса поиска. При использовании подробного разбиения на страницах следующий шаг — улучшить взаимодействие с пользователем, добавив поисковые запросы вперед по типу.