瞭解如何實作兩種不同的分頁系統,第一個是基於頁碼,第二個是無限滾動。 這兩個分頁系統都廣泛使用,而且選取正確的系統取決於您想要的結果用戶體驗。
在本教學課程中,您將了解如何:
- 使用編號分頁擴充您的應用程式
- 使用無限捲動擴充您的應用程式
概觀
本教學課程會將分頁系統重疊至先前建立的專案,如 建立您的第一個搜尋應用程式 教學課程中所述。
您可以在下列專案中找到您將在本教學課程中開發的已完成程式代碼版本:
先決條件
- 1-basic-search-page (GitHub) 專案。 此專案可以是您自己從上一個教學課程建置的版本,或是從 GitHub 建立的複本。
使用編號分頁擴充您的應用程式
編號分頁是主要商業 Web 搜尋引擎和其他許多搜尋網站選擇的分頁系統。 頁碼分頁通常除了實際頁碼範圍之外,還包含「下一頁」和「上一頁」選項。 您也可以使用「第一頁」和「最後一頁」選項。 這些選項當然可讓使用者控制瀏覽以頁面為基礎的結果。
在本教學課程中,您將新增一個系統,其中包含第一個、上一個、下一個和最後一個選項,以及不從 1 開始的頁碼,而是圍繞用戶開啟的目前頁面(例如,如果使用者正在查看第 10 頁,可能顯示頁碼 8、9、10、10、11 和 12)。
系統會有足夠的彈性,以允許全域變數中設定可見頁碼的數目。
系統會將最左邊和最右邊的頁碼按鈕視為特殊,這表示它們會觸發變更顯示的頁碼範圍。 例如,如果顯示頁碼 8、9、10、11 和 12,而使用者按兩下 8,則顯示頁碼的範圍會變更為 6、7、8、9 和 10。 如果他們選擇了12,則會有類似的向右轉變。
將分頁欄位新增至模型
開啟基本搜尋頁面方案。
開啟SearchData.cs模型檔案。
新增全域變數以支援分頁。 在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。
在 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; }
將Page動作新增至控制器
開啟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。
更新 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 方法需要修改,才能解決語法錯誤。 我們從 Skip 設定開始,使用 SearchOptions 類別的 Skip、Size 和 IncludeTotalCount 字段,只要求一頁值得的結果。 我們也需要計算我們檢視的分頁變數。 以下列程式代碼取代整個方法。
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 現在會包含一頁傳回的結果數目(在我們的範例中為 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 認知搜尋必須計算此總計。 使用複雜的數據集時,會有一個警告,指出傳回的值是 近似值。 因為旅館搜尋語料庫規模較小,所以會很準確。
編譯並執行應用程式
現在,選取 [啟動但不偵錯 ] (或按 F5 鍵)。
搜尋會傳回大量結果的字串(例如 “wifi” )。 您可以整齊地逐頁查看結果嗎?
請嘗試點擊最右邊的頁碼,然後點擊最左邊的頁碼。 頁碼是否會適當調整以確保您所在的頁面正好居中?
「first」和「last」這兩個選項有用嗎? 有些商業搜尋引擎會使用這些選項,有些則不使用。
移至結果的最後一頁。 最後一頁是唯一可能包含小於 ResultsPerPage 結果的頁面。
輸入 “town”,然後按兩下 [搜尋]。 如果結果少於一頁,則不會顯示分頁選項。
儲存此專案,並繼續下一節以取得替代的分頁形式。
使用無限捲動擴充您的應用程式
當使用者將垂直滾動條捲動至所顯示結果的最後一個時,就會觸發無限卷動。 在此事件中,將呼叫搜尋服務以獲取下一頁的結果。 如果沒有更多結果,則不會傳回任何結果,且垂直滾動條不會變更。 如果有更多結果,它們會附加至目前的頁面,而滾動條會變更以顯示有更多結果可供使用。
要注意的一個重要點是,目前的頁面不會被取代,而是會擴充以顯示其他結果。 使用者一律可以向上捲動至搜尋的第一個結果。
若要實作無限卷動,讓我們先從添加任何頁數捲動元素之前的專案開始。 在 GitHub 上,這是 FirstAzureSearchApp 解決方案。
將分頁欄位新增至模型
首先,將 分頁 屬性新增至 SearchData 類別(在SearchData.cs模型檔案中)。
// Record if the next page is requested. public string paging { get; set; }此變數是字串類型,用於指示是否要傳送下一頁的結果;如果需要,則為 "next";若為搜尋的第一頁,則為 Null。
在相同的檔案中,以及在命名空間內,新增具有一個屬性的全域變數類別。 在MVC中,全域變數會在自己的靜態類別中宣告。 ResultsPerPage 會設定每個頁面的結果數目。
public static class GlobalVariables { public static int ResultsPerPage { get { return 3; } } }
將垂直滾動條新增至檢視
找出顯示結果的 index.cshtml 檔案區段(其開頭為 @if (Model != null) 。
將區段取代為下列程序代碼。 新的 <div> 區段位於應該可捲動的區域周圍,並新增 溢位 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> 卷標後面<新增捲動函式。
<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 的動作呼叫 Home 控制器。 控制器不需要其他資訊,它會傳回下一頁的數據。 然後,此數據會使用與原始頁面相同的 HTML 樣式來格式化。 如果未傳回任何結果,則不會附加任何結果,且專案會保持原狀。
處理下一個動作
只有三個動作需要傳送至控制器:第一個執行的應用程式,它會呼叫 Index()、使用者第一次搜尋、呼叫 Index(model),然後透過 Next(model)呼叫更多結果的後續呼叫。
開啟主控制器檔案,並從原始教學課程中刪除 RunQueryAsync 方法。
以下列程序代碼取代 Index(model) 動作。 它現在會在分頁欄位為 Null 或設定為 「下一步」時處理 分頁 欄位,並處理對 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 和 Size 搜尋設定,只要求傳回我們需要的數據。
將 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); }如果您在 List<字串>上收到語法錯誤,請將下列 using 指示詞新增至控制器檔案的前端。
using System.Collections.Generic;
編譯並執行您的專案
現在,選取 [啟動但不偵錯 ] (或按 F5 鍵)。
輸入會提供大量結果的字詞(例如「集區」),然後測試垂直滾動條。 它會觸發結果的新頁面嗎?
小提示
為了確保滾動條出現在第一頁上,結果的第一頁必須略高於其所顯示區域的高度。 在我們的範例 中,.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()”。>
一路向下捲動至結果底部。 請注意,現在所有資訊都顯示在同一個檢視頁面上。 您可以一路捲動回到頂端,而不觸發任何伺服器呼叫。
更複雜的無限捲動系統可能會使用滑鼠滾輪或類似的其他機制來觸發結果新頁面的載入。 我們不會在這些教學課程中進一步探討無限捲動,但它具有一定的魅力,因為可以避免額外的滑鼠點擊,您可能會考慮更多地研究其他選擇。
外賣
請考慮這個專案的下列要點:
- 編號分頁對於結果順序比較隨機的搜尋非常有用,這意味著在之後的頁面上可能存在讓使用者感興趣的內容。
- 當結果的順序特別重要時,無限捲動很有用。 例如,如果結果是以距離目的地城市中心的距離排序。
- 編號的分頁允許更好的流覽。 例如,使用者可以記得有趣的結果是在第 6 頁,而無限捲動中沒有如此簡單的參考。
- 無限滾動有簡單的吸引力,不需要點擊頁碼,只需向上或向下滾動即可。
- 無限卷動的主要功能是將結果附加至現有的頁面,而不是取代該頁面,這是有效率的。
- 暫存記憶體只會保存一個呼叫,而且必須重設,才能在其他呼叫中存留。
後續步驟
分頁是搜尋過程的基礎。 隨著分頁功能的完善,下一步是透過新增自動完成搜尋功能來進一步提升用戶體驗。