實作有效率的資料分頁
由 Microsoft 提供
這是免費的 "NerdDinner" 應用程式教學課程的第 8 個步驟,詳細介紹了如何使用 ASP.NET MVC 1 建置一個小型但完整的 Web 應用程式。
步驟 8 示範如何將分頁支援新增至 /Dinners URL,讓系統只顯示 10 個即將推出的 Dinners 而不是一次顯示 1000 個,並允許終端使用者以對 SEO 易用的方式向前或向後翻頁瀏覽整個清單。
如果使用 ASP.NET MVC 3,建議遵循 MVC 3 使用者入門或 MVC Music 市集教學課程。
NerdDinner 步驟 8:分頁支援
如果我們的網站成功,將會有數千個即將推出的 Dinners。 我們需要確保 UI 縮放來處理所有這些 Dinners,並允許使用者瀏覽它們。 為此,我們將分頁支援新增至 /Dinners URL,讓系統一次只顯示 10 個即將推出的 Dinners 而不是一次顯示 1000 個,並允許終端使用者以對 SEO 易用的方式向前或向後翻頁瀏覽整個清單。
Index() 動作方法回顧
DinnersController 類別內的 Index() 動作方法,目前的外觀如下所示:
//
// GET: /Dinners/
public ActionResult Index() {
var dinners = dinnerRepository.FindUpcomingDinners().ToList();
return View(dinners);
}
對 /Dinners URL 提出要求時,它會擷取所有即將推出的 Dinners 清單,然後轉譯出所有 Dinners 的清單:
了解 IQueryable<T>
IQueryable<T> 是隨著 LINQ 在 .NET 3.5 中導入的一個介面。 它可啟用強大的「延後執行」案例,我們可以利用這些案例來實作分頁支援。
在我們的 DinnerRepository 中,我們會從 FindUpcomingDinners() 方法傳回 IQueryable<Dinner> 序列:
public class DinnerRepository {
private NerdDinnerDataContext db = new NerdDinnerDataContext();
//
// Query Methods
public IQueryable<Dinner> FindUpcomingDinners() {
return from dinner in db.Dinners
where dinner.EventDate > DateTime.Now
orderby dinner.EventDate
select dinner;
}
FindUpcomingDinners() 方法所傳回的 IQueryable<Dinner> 物件會封裝查詢,以使用 LINQ to SQL 從資料庫擷取 Dinner 物件。 重要的是,除非我們嘗試存取/逐一查看查詢中的資料,或直到我們對其呼叫 ToList() 方法,否則不會查詢資料庫。 呼叫 FindUpcomingDinners() 方法的程式碼可以選擇在執行查詢之前,先將其他「鏈結」作業/篩選新增至 IQueryable<Dinner> 物件。 LINQ to SQL 接著就能夠聰明地在要求資料時對資料庫執行合併的查詢。
若要實作分頁邏輯,我們可以更新 DinnersController 的 Index() 動作方法,以便在呼叫 ToList() 之前,將額外的 “Skip” 和 “Take” 運算子套用至傳回的 IQueryable<Dinner> 序列:
//
// GET: /Dinners/
public ActionResult Index() {
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();
return View(paginatedDinners);
}
上述程式碼會跳過資料庫中前 10 個即將推出的 Dinners,然後傳回 20 個 Dinners。 LINQ to SQL 能夠聰明地建構最佳化 SQL 查詢,以在 SQL 資料庫中執行此跳過邏輯,而不是在網頁伺服器中執行。 這表示,即使在資料庫中有數百萬個即將推出的 Dinners,在此要求中也只會擷取我們想要的 10 個 (使其有效率且可調整)。
將「頁面」值新增至 URL
我們不會對特定頁面範圍進行硬式編碼,而是要讓 URL 包含「頁面」參數,指出使用者要求哪個 Dinner 範圍。
使用 Querystring 值
下列程式碼示範如何更新 Index() 動作方法來支援查詢字串參數,並啟用 /Dinners?page=2 之類的 URL:
//
// GET: /Dinners/
// /Dinners?page=2
public ActionResult Index(int? page) {
const int pageSize = 10;
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
.Take(pageSize)
.ToList();
return View(paginatedDinners);
}
上述 Index() 動作方法具有名為 "page" 的參數。 參數被宣告為可為 Null 的整數 (這就是 int? 所代表的意思)。 這表示 /Dinners?page=2 URL 會導致值 “2 ” 被當做參數值傳遞。 /Dinners URL (不含查詢字串值) 會導致傳遞 Null 值。
我們會將頁面值乘以頁面大小 (在此案例中為 10 個資料列),以判斷要跳過多少 Dinners。 我們使用 C# Null 「聯合」運算子 (??),這在處理可為 Null 的類型時很有用。 如果頁面參數為 null,上述程式碼會將 0 值指派給頁面。
使用內嵌 URL 值
使用查詢字串值的替代方法是將頁面參數內嵌在實際的 URL 上。 例如:/Dinners/Page/2 或 /Dinners/2。 ASP.NET MVC 包含功能強大的 URL 路由引擎,可輕鬆支援這類案例。
我們可以註冊自訂路由規則,將任何傳入 URL 或 URL 格式對應至我們所要的任何控制器類別或動作方法。 我們只需要在專案中開啟 Global.asax 檔案:
然後使用 MapRoute() 協助程式方法註冊新的對應規則,例如下方第一個對 routes.MapRoute() 的叫用所示:
public void RegisterRoutes(RouteCollection routes) {
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"UpcomingDinners", // Route name
"Dinners/Page/{page}", // URL with params
new { controller = "Dinners", action = "Index" } // Param defaults
);
routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with params
new { controller="Home", action="Index",id="" } // Param defaults
);
}
void Application_Start() {
RegisterRoutes(RouteTable.Routes);
}
上述內容中,我們註冊了一個名為 "UpcomingDinners" 的新路由規則。 我們指出其 URL 格式為 “Dinners/Page/{page}” – 其中 {page} 是內嵌在 URL 中的參數值。 MapRoute() 方法的第三個參數,指出應該將符合此格式的 URL 對應至 DinnersController 類別上的 Index() 動作方法。
我們可以在 Querystring 案例中使用與之前完全相同的 Index() 程式碼 ,但現在的 “page” 參數會由 URL 而非查詢字串提供:
//
// GET: /Dinners/
// /Dinners/Page/2
public ActionResult Index(int? page) {
const int pageSize = 10;
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = upcomingDinners.Skip((page ?? 0) * pageSize)
.Take(pageSize)
.ToList();
return View(paginatedDinners);
}
現在,當我們執行應用程式並在 /Dinners 中輸入時,我們將會看到前 10 個即將推出的 Dinners:
當我們輸入 /Dinners/Page/1 時,我們將會看到下一頁的 Dinners:
新增頁面導覽 UI
完成分頁案例的最後一個步驟是在檢視範本中實作「下一個」和「上一個」導覽 UI,讓使用者輕鬆跳過 Dinner 資料。
若要正確實作此步驟,我們必須知道資料庫中的 Dinners 總數,以及這要轉譯的資料頁數。 然後,我們需要計算目前要求的「頁面」值是否在資料的開頭或結尾,並據以顯示或隱藏「上一個」和「下一個」UI。 我們可以在 Index() 動作方法內實作此邏輯。 也可以將協助程式類別新增至專案,以更可重複使用的方式封裝此邏輯。
以下是衍生自 .NET Framework 內建的 List<T> 集合類別的簡單 “PaginatedList” 協助程式類別。 它會實作可重複使用的集合類別,可用來對任何 IQueryable 資料序列進行分頁。 在我們的 NerdDinner 應用程式中,我們會讓它透過 IQueryable <Dinner> 結果運作,但它可以輕鬆地應用於其他應用程式案例中的 IQueryable <產品>或 IQueryable <客戶>結果:
public class PaginatedList<T> : List<T> {
public int PageIndex { get; private set; }
public int PageSize { get; private set; }
public int TotalCount { get; private set; }
public int TotalPages { get; private set; }
public PaginatedList(IQueryable<T> source, int pageIndex, int pageSize) {
PageIndex = pageIndex;
PageSize = pageSize;
TotalCount = source.Count();
TotalPages = (int) Math.Ceiling(TotalCount / (double)PageSize);
this.AddRange(source.Skip(PageIndex * PageSize).Take(PageSize));
}
public bool HasPreviousPage {
get {
return (PageIndex > 0);
}
}
public bool HasNextPage {
get {
return (PageIndex+1 < TotalPages);
}
}
}
請注意上述計算方式,然後公開屬性,例如 “PageIndex”、“PageSize”、“TotalCount” 和 “TotalPages”。 它接著也會公開兩個協助程式屬性 "HasPreviousPage" 和 "HasNextPage",指出集合中的資料頁面在原始序列的開頭或結尾。 上述程式碼會導致執行兩個 SQL 查詢,第一個查詢會擷取 Dinner 物件總數的計數 (這不會傳回物件 ,而是執行傳回整數的 “SELECT COUNT” 陳述式),而第二個查詢只會擷取資料庫中目前資料頁所需的資料列。
然後,我們可以更新 DinnersController.Index() 協助程式方法,從 DinnerRepository.FindUpcomingDinners () 結果建立 PaginatedList<Dinner>,並將其傳遞至我們的檢視範本:
//
// GET: /Dinners/
// /Dinners/Page/2
public ActionResult Index(int? page) {
const int pageSize = 10;
var upcomingDinners = dinnerRepository.FindUpcomingDinners();
var paginatedDinners = new PaginatedList<Dinner>(upcomingDinners, page ?? 0, pageSize);
return View(paginatedDinners);
}
然後,我們可以更新 \Views\Dinners\Index.aspx 檢視範本,以繼承自 ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>>,而不是 ViewPage<IEnumerable<Dinner>>,然後將下列程式碼新增至檢視範本底部,以顯示或隱藏下一個和上一個導覽 UI:
<% if (Model.HasPreviousPage) { %>
<%= Html.RouteLink("<<<", "UpcomingDinners", new { page = (Model.PageIndex-1) }) %>
<% } %>
<% if (Model.HasNextPage) { %>
<%= Html.RouteLink(">>>", "UpcomingDinners", new { page = (Model.PageIndex + 1) }) %>
<% } %>
請注意上述如何使用 Html.RouteLink() 協助程式方法來產生超連結。 此方法類似於我們先前使用的 Html.ActionLink() 協助程式方法。 差別在於,我們會使用我們在 Global.asax 檔案內設定的 “UpcomingDinners” 路由規則來產生 URL。 這可確保我們將產生 Index() 動作方法的 URL,其格式為:/Dinners/Page/{page} – 其中 {page} 值是我們根據目前 PageIndex 提供的變數。
現在,當我們再次執行應用程式時,瀏覽器一次會顯示 10 個 Dinners:
在頁面底部也有 <<< 和 >>> 導覽 UI,可讓我們使用搜尋引擎可存取的 URL 來向前和向後跳過資料:
側邊主題:了解 IQueryable<T> 的影響 |
---|
IQueryable<T> 是一項非常強大的功能,可啟用各種有趣的延遲執行案例 (例如分頁和組合型查詢)。 像所有強大的功能一樣,請謹慎使用,並確保不會被濫用。 請務必辨識從存放庫傳回 IQueryable<T> 結果,可讓呼叫程式碼將鏈結運算子方法附加至該存放庫,因此參與最終的查詢執行。 如果不要提供呼叫程式碼這個功能,則應該傳回 IList<T> 或 IEnumerable<T> 結果,其中包含已執行的查詢結果。 如果是分頁案例,請將實際的資料分頁邏輯推送至所呼叫的存放庫方法。 在此案例中,我們可能會更新 FindUpcomingDinners() 搜尋工具方法,以具有傳回 PaginatedList 的簽章:PaginatedList <Dinner> FindUpcomingDinners (int pageIndex, int pageSize) { } 或傳回 IList<Dinner>,並使用 “totalCount” out param 傳回 Dinners 的總計數:IList<Dinner> FindUpcomingDinners (int pageIndex, int pageSize, out int totalCount) { } |
後續步驟
現在讓我們看看如何將驗證和授權支援新增至應用程式。