ページ番号に基づく 1 つ目と無限スクロール時の 2 つの異なるページング システムを実装する方法について説明します。 どちらのページング システムも広く使用されており、適切なシステムを選択するかどうかは、結果に必要なユーザー エクスペリエンスによって異なります。
このチュートリアルでは、次の方法について説明します。
- 番号付きページングを使用してアプリを拡張する
- 無限スクロールでアプリを拡張する
概要
このチュートリアルでは、「 最初の検索アプリの作成 」チュートリアルで説明した、以前に作成したプロジェクトにページング システムをオーバーレイします。
このチュートリアルで開発するコードの完成版は、次のプロジェクトにあります。
[前提条件]
- 1-basic-search-page (GitHub) プロジェクト。 このプロジェクトは、前のチュートリアルからビルドされた独自のバージョンか、GitHub からのコピーのいずれかになります。
番号付きページングを使用してアプリを拡張する
番号付きページングは、主要な商用 Web 検索エンジンやその他の多くの検索 Web サイトで選択されるページング システムです。 番号付きページングには、通常、実際のページ番号の範囲に加えて、"next" オプションと "previous" オプションが含まれます。 また、"最初のページ" と "最後のページ" オプションも使用できます。 これらのオプションを使用すると、ユーザーはページ ベースの結果間を移動できます。
このチュートリアルでは、最初、前、次、最後のオプションと、1 から始まっていないページ番号を含むシステムを追加します。代わりに、ユーザーが現在表示されているページを囲みます (たとえば、ユーザーがページ 10 を見ている場合は、ページ番号 8、9、10、11、12 が表示されます)。
システムは、表示されるページ番号の数をグローバル変数に設定するのに十分な柔軟性を備えます。
システムは、左端と右端のページ番号ボタンを特別なボタンとして扱います。つまり、表示されるページ番号の範囲の変更がトリガーされます。 たとえば、ページ番号 8、9、10、11、12 が表示され、ユーザーが 8 をクリックすると、表示されるページ番号の範囲が 6、7、8、9、10 に変わります。 そして、12を選択した場合、右にも同様のシフトがあります。
モデルにページング フィールドを追加する
基本的な検索ページ ソリューションを開きます。
SearchData.cs モデル ファイルを開きます。
ページネーションをサポートするためにグローバル変数を追加します。 MVC では、グローバル変数は独自の静的クラスで宣言されます。 ResultsPerPage は 、1 ページあたりの結果の数を設定します。 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 に変更することを検討してください。
searchText プロパティの後に、SearchData クラスにページング プロパティを追加します。
// 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; }
ページング オプションのテーブルをビューに追加する
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">|<</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> }HTML テーブルを使用して、物事をきちんと揃えます。 ただし、すべてのアクションは @Html.ActionLink ステートメントから取得され、それぞれが、前に追加したページング プロパティに対して異なるエントリを使用して作成された 新しい モデルでコントローラー を 呼び出します。
最初と最後のページのオプションは、"first" や "last" などの文字列を送信するのではなく、正しいページ番号を送信します。
hotels.css ファイル内の HTML スタイルの一覧にページング クラスを追加します。 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; }
ページ アクションをコントローラーに追加する
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 メソッドでは、3 番目のパラメーターが原因で構文エラーが表示されます。これは少し後で説明します。
注
TempData の呼び出しでは値 (オブジェクト) が一時ストレージに格納されますが、このストレージは 1 回の呼び出しでのみ保持されます。 一時的なデータに何かを格納すると、コントローラーアクションの次の呼び出しで使用できるようになりますが、その後の呼び出しによって最も間違いなく消えます。 この短い有効期間のため、検索テキストとページング プロパティは 、PageAsync の呼び出しごとに一時ストレージに格納されます。
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); }前のレッスンで導入した RunQueryAsync メソッドでは、構文エラーを解決するために変更が必要です。 SearchOptions クラスの Skip、Size、IncludeTotalCount フィールドを使用して、Skip 設定から始まる 1 ページ分の結果のみを要求します。 ビューのページング変数も計算する必要があります。 メソッド全体を次のコードに置き換えます。
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); }最後に、ビューに小さな変更を加えます。 変数 resultList.Results.TotalCount には、合計数ではなく、1 ページ (この例では 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 キーを押します)。
多くの結果を返す文字列 ("wifi" など) を検索します。 結果をきちんと見やすく整理できますか?
右端と後の左端のページ番号をクリックしてみてください。 ページ番号は、使用しているページの中央に合わせて適切に調整されますか?
"first" オプションと "last" オプションは便利ですか? 商用検索エンジンの中には、これらのオプションを使用するものもあれば、使用しないものもあります。
結果の最後のページに移動します。 最後のページは、 ResultsPerPage の 結果より小さい場合がある唯一のページです。
「town」と入力し、[検索] をクリックします。 結果が 1 ページ未満の場合、ページング オプションは表示されません。
このプロジェクトを保存し、次のセクションに進み、別の形式のページングを行います。
無限スクロールでアプリを拡張する
無限スクロールは、ユーザーが垂直スクロール バーを表示される結果の最後までスクロールしたときにトリガーされます。 このイベントでは、結果の次のページに対して検索サービスの呼び出しが行われます。 それ以上の結果がない場合、何も返されず、垂直スクロール バーは変更されません。 さらに多くの結果がある場合は、現在のページに追加され、スクロール バーが変化して、より多くの結果が使用可能であることを示します。
注意すべき重要な点は、現在のページが置き換えられるのではなく、追加の結果を表示するように拡張されていることです。 ユーザーは常に、検索の最初の結果までスクロール バックできます。
無限スクロールを実装するには、ページ番号のスクロール要素が追加される前に、プロジェクトから始めましょう。 GitHub では、これは FirstAzureSearchApp ソリューションです 。
モデルにページング フィールドを追加する
まず、(SearchData.cs モデル ファイル内の) SearchData クラスにページング プロパティを追加します。
// Record if the next page is requested. public string paging { get; set; }この変数は文字列で、結果の次のページを送信する場合は "next" を保持し、検索の最初のページでは null になります。
同じファイル内と名前空間内に、1 つのプロパティを持つグローバル変数クラスを追加します。 MVC では、グローバル変数は独自の静的クラスで宣言されます。 ResultsPerPage は 、1 ページあたりの結果の数を設定します。
public static class GlobalVariables { public static int ResultsPerPage { get { return 3; } } }
ビューに垂直スクロール バーを追加する
結果を表示する index.cshtml ファイルのセクションを見つけます ( @if (Model != null) で始まります)。
セクションを次のコードに置き換えます。 新しい <div> セクションは、スクロール可能な領域の周囲にあり、 overflow-y 属性と "scrolled()" という名前の onscroll 関数の呼び出しの両方を追加します。
@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" }) }ループのすぐ下で、</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 ステートメントは、ユーザーが垂直スクロール バーの一番下までスクロールしたかどうかをテストします。 存在する場合は、NextAsync と呼ばれるアクションに対してホーム コントローラーの呼び出しが行われます。 コントローラーが他の情報を必要とせず、データの次のページが返されます。 このデータは、元のページと同じ HTML スタイルを使用して書式設定されます。 結果が返されない場合は、何も追加されないため、そのまま残ります。
次のアクションを処理する
コントローラーに送信する必要があるアクションは、 Index() を呼び出すアプリの最初の実行、ユーザーによる最初の検索、 Index(model) の呼び出し、次に Next(model) を使用してさらに多くの結果を呼び出すアクションの 3 つのみです。
ホーム コントローラー ファイルを開き、元のチュートリアルから RunQueryAsync メソッドを削除します。
Index(model) アクションを次のコードに置き換えます。 これで、 ページング フィールドが null の場合、または "next" に設定されたときに処理され、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); }番号付きページングメソッドと同様に、 Skip と Size の検索設定を使用して、必要なデータのみを返すように要求します。
NextAsync アクションをホーム コントローラーに追加します。 リストがどのように返されるかに注目してください。各ホテルは、ホテル名とホテルの説明という 2 つの要素をリストに追加します。 この形式は、ビューで返 される データのスクロール関数の使用に合わせて設定されます。
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); }List<string> で構文エラーが発生した場合は、コントローラー ファイルの先頭に次の using ディレクティブを追加します。
using System.Collections.Generic;
プロジェクトをコンパイルして実行する
[ デバッグなしで開始] を選択します (または F5 キーを押します)。
多くの結果を得る用語 ("pool" など) を入力し、垂直スクロール バーをテストします。 結果の新しいページがトリガーされますか?
ヒント
最初のページにスクロール バーが表示されるようにするには、結果の最初のページが表示されている領域の高さをわずかに超える必要があります。 この例の .box1 の高さは 30 ピクセルで、 .box2 の高さは 100 ピクセル 、 下余白は 24 ピクセルです。 したがって、各エントリは 154 ピクセルを使用します。 3 つのエントリは、3 x 154 = 462 ピクセルを占めます。 垂直スクロール バーが表示されるようにするには、表示領域の高さを 462 ピクセルより小さく設定する必要があります。461 でも動作します。 この問題は、最初のページでのみ発生します。その後、スクロール バーが表示されます。 更新する行は次のとおりです。 <div id="myDiv" style="width: 800px; height: 450px; overflow-y: scroll;" onscroll="scrolled()">。
結果の一番下まで下にスクロールします。 すべての情報が 1 つのビュー ページにどのように表示されているかに注目してください。 サーバー呼び出しをトリガーすることなく、一番上までスクロールできます。
より高度な無限スクロール システムでは、マウス ホイールまたはその他の同様のメカニズムを使用して、結果の新しいページの読み込みをトリガーできます。 これらのチュートリアルでは無限スクロールを行いませんが、余分なマウスクリックを回避し、他のオプションをさらに調査したい場合があるため、それには特定の魅力があります。
学んだこと
このプロジェクトの次の点について考えてみましょう。
- 番号付きページングは、結果の順序がやや任意の検索に役立ちます。つまり、後のページでユーザーにとって関心のあるものがある可能性があります。
- 無限スクロールは、結果の順序が特に重要な場合に便利です。 たとえば、結果が目的地都市の中心からの距離に基づいて並べ替えられた場合です。
- 番号付きページングを使用すると、ナビゲーションが向上します。 たとえば、ユーザーは興味深い結果が 6 ページに表示されたのに対し、無限スクロールにはそのような簡単な参照が存在しないことを覚えることができます。
- 無限スクロールは、クリックするページ番号なしで上下にスクロールする、簡単な魅力を持っています。
- 無限スクロールの主な機能は、結果が既存のページに追加され、そのページが置き換えられないという点です。これは効率的です。
- 一時ストレージは 1 回の呼び出しに対してのみ保持され、追加の呼び出しを継続するにはリセットする必要があります。
次のステップ
ページングは、検索エクスペリエンスの基本です。 ページングが十分にカバーされている次の手順では、先行入力検索を追加することで、ユーザー エクスペリエンスをさらに向上させます。