共用方式為


實作有效率的資料分頁

Microsoft 提供

下載 PDF

這是免費的 "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 的清單:

Nerd Dinner Upcoming Dinner 清單頁面的螢幕擷取畫面。

了解 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 檔案:

Nerd Dinner 導覽樹狀目錄的螢幕擷取畫面。選取並醒目提示 Global dot a s a x。

然後使用 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:

Nerd Dinners Upcoming Dinners 清單頁面的螢幕擷取畫面。

當我們輸入 /Dinners/Page/1 時,我們將會看到下一頁的 Dinners:

即將推出的 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:

Nerd Dinner 頁面上即將推出的 Dinners 清單的螢幕擷取畫面。

在頁面底部也有 <<< 和 >>> 導覽 UI,可讓我們使用搜尋引擎可存取的 URL 來向前和向後跳過資料:

顯示即將推出的 Dinners 清單的 Nerd Dinners 頁面的螢幕擷取畫面。

側邊主題:了解 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) { }

後續步驟

現在讓我們看看如何將驗證和授權支援新增至應用程式。