効率的なデータ ページングを実装する

マイクロソフトより

PDF をダウンロードする

これは、MVC 1 を使用して小規模で完全な Web アプリケーションを構築する方法を説明する無料の "NerdDinner" アプリケーション チュートリアル ASP.NET 手順 8 です。

手順 8 では、ページングのサポートを /Dinners URL に追加して、1000 回のディナーを一度に表示する代わりに、予定されているディナーを一度に 10 回だけ表示し、エンド ユーザーが SEO に優しい方法でリスト全体をページングして転送できるようにする方法を示します。

MVC 3 ASP.NET 使用している場合は、MVC 3 または MVC ミュージック ストア概要に関するチュートリアルに従うことをお勧めします。

NerdDinner ステップ 8: ページングサポート

私たちのサイトが成功した場合、今後何千ものディナーが開催されることになります。 これらのすべてのディナーを処理し、ユーザーがそれらを参照できるように UI がスケーリングされることを確認する必要があります。 これを有効にするには、ページングのサポートを /Dinners URL に追加して、1000 回のディナーを一度に表示するのではなく、予定されているディナーを一度に 10 回だけ表示し、エンドユーザーが SEO に優しい方法でリスト全体をページングして転送できるようにします。

Index() Action メソッドの要約

DinnersController クラス内の Index() アクション メソッドは、現在次のようになります。

//
// GET: /Dinners/

public ActionResult Index() {

    var dinners = dinnerRepository.FindUpcomingDinners().ToList();
    return View(dinners);
}

/Dinners URL に対する要求が行われると、今後予定されているすべてのディナーの一覧が取得され、そのすべての一覧がレンダリングされます。

Nerd Dinner の今後のディナー リスト ページのスクリーンショット。

IQueryable<T について>

IQueryable<T> は、.NET 3.5 の一部として LINQ で導入されたインターフェイスです。 これにより、ページングのサポートを実装するために利用できる強力な "遅延実行" シナリオが可能になります。

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() アクション メソッドを更新して、返された IQueryable<Dinner> シーケンスに "Skip" 演算子と "Take" 演算子を追加してから ToList() を呼び出すようにします。

//
// GET: /Dinners/

public ActionResult Index() {

    var upcomingDinners = dinnerRepository.FindUpcomingDinners();
    var paginatedDinners = upcomingDinners.Skip(10).Take(20).ToList();

    return View(paginatedDinners);
}

上記のコードでは、データベース内の予定されている最初の 10 回のディナーをスキップし、20 回のディナーを返します。 LINQ to SQL は、Web サーバーではなく、SQL データベースでこのスキップ ロジックを実行する最適化された SQL クエリを構築するのに十分なスマートです。 つまり、データベースに今後何百万もの Dinner がある場合でも、この要求の一部として取得されるのは 10 個だけです (効率的でスケーラブルです)。

URL への "ページ" 値の追加

特定のページ範囲をハードコーディングする代わりに、ユーザーが要求している Dinner 範囲を示す "ページ" パラメーターを URL に含める必要があります。

Querystring 値の使用

次のコードは、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 (querystring 値なし) では、null 値が渡されます。

スキップするディナーの数を決定するために、ページ値にページ サイズ (この場合は 10 行) を乗算します。 C# null "coalescing" 演算子 (??) は、Null 許容型を扱う際に便利です。 上記のコードでは、ページ パラメーターが null の場合、ページに値 0 が割り当てられます。

埋め込み URL 値の使用

クエリ文字列値を使用する代わりに、ページ パラメーターを実際の URL 自体に埋め込む方法もあります。 例: /Dinners/Page/2 または /Dinners/2。 ASP.NET MVC には、このようなシナリオを簡単にサポートできる強力な URL ルーティング エンジンが含まれています。

任意の受信 URL または URL 形式を任意のコントローラー クラスまたはアクション メソッドにマップするカスタム ルーティング規則を登録できます。 必要なのは、プロジェクト内で Global.asax ファイルを開くことです。

Nerd Dinner ナビゲーション ツリーのスクリーンショット。グローバル ドット a x が選択され、強調表示されます。

次に、ルートの最初の呼び出しのような MapRoute() ヘルパー メソッドを使用して、新しいマッピング 規則を登録します。以下の 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() メソッドの 3 番目のパラメーターは、この形式に一致する URL を DinnersController クラスの Index() アクション メソッドにマップする必要があることを示します。

Querystring シナリオでは、前とまったく同じ Index() コードを使用できます。ただし、現在の "page" パラメーターは、querystring ではなく 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 回のディナーが表示されます。

今後のディナーリスト(Nerd Dinners)のスクリーンショット。

/ Dinners/Page/1 と入力すると、ディナーの次のページが表示されます。

[Upcoming Dinners]\(今後のディナー\) リストの次のページのスクリーンショット。

ページ ナビゲーション UI の追加

ページング シナリオを完了する最後の手順は、ユーザーが Dinner データを簡単にスキップできるように、ビュー テンプレート内に "next" および "previous" ナビゲーション UI を実装することです。

これを正しく実装するには、データベース内の Dinner の合計数と、それが変換されるデータのページ数を把握する必要があります。 次に、現在要求されている "ページ" 値がデータの先頭または末尾にあるかどうかを計算し、それに応じて "previous" と "next" UI を表示または非表示にする必要があります。 このロジックは、Index() アクション メソッド内に実装できます。 または、このロジックをより再利用可能な方法でカプセル化するヘルパー クラスをプロジェクトに追加することもできます。

以下は、.NET Framework に組み込まれている List<T> コレクション クラスから派生する単純な "PaginatedList" ヘルパー クラスです。 これは、IQueryable データの任意のシーケンスを改ページ処理するために使用できる再利用可能なコレクション クラスを実装します。 NerdDinner アプリケーションでは、IQueryable<Dinner> の結果に対して機能しますが、他のアプリケーションのシナリオでも、IQueryable<Product> や IQueryable<Customer> に対して同様に簡単に使用できます。

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" などのプロパティを計算して公開する方法に注目してください。 また、コレクション内のデータのページが元のシーケンスの先頭または末尾にあるかどうかを示す 2 つのヘルパー プロパティ "HasPreviousPage" と "HasNextPage" も公開します。 上記のコードでは、2 つの SQL クエリが実行されます。1 つ目は Dinner オブジェクトの合計数を取得します (オブジェクトは返されません。整数を返す "SELECT COUNT" ステートメントを実行します)。2 つ目は、現在のデータ ページのデータベースから必要なデータ行のみを取得します。

その後、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);
}

その後、ViewPage<IEnumerable<Dinner>> の代わりに ViewPageNerdDinner.Helpers.PaginatedList<<Dinner>> から継承するように \Views\Dinners\Index.aspx ビュー テンプレートを更新し、次のコードをビュー テンプレートの下部に追加して、次のナビゲーション UI と前のナビゲーション 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 を生成することです。 これにより、URL が Index() アクション メソッドに対して生成され、形式は次の通りです: /Dinners/Page/{page}。ここで、{page} の値は現在の PageIndex に基づいて上記で指定する変数です。

アプリケーションをもう一度実行すると、ブラウザーに一度に 10 回のディナーが表示されます。

[Nerd Dinner] ページの [Upcoming Dinners]\(今後のディナー\) リストのスクリーンショット。

また、ページの下部に <<< と >>> ナビゲーション UI があり、検索エンジンのアクセス可能な URL を使用してデータの前後をスキップできます。

[Nerd Dinners] ページのスクリーンショット。[Upcoming Dinners]\(今後のディナー\) の一覧が表示されています。

サイド トピック: IQueryable<T の影響について>
IQueryable<T> は、さまざまな興味深い遅延実行シナリオ (ページングやコンポジション ベースのクエリなど) を可能にする非常に強力な機能です。 すべての強力な機能と同様に、使用方法に注意し、悪用されないようにする必要があります。 リポジトリから IQueryable<T> の結果を返すことは、呼び出し元のコードがその結果にチェーンされた演算子メソッドを追加し、最終的なクエリの実行に参加できるようにするために重要です。 呼び出し元コードにこの機能を提供しない場合は、IList<T> または IEnumerable<T> の結果 (既に実行されているクエリの結果を含む) を返す必要があります。 改ページ処理のシナリオでは、実際のデータページネーションのロジックを呼び出すリポジトリメソッドに組み込む必要があります。 このシナリオでは、FindUpcomingDinners() ファインダーメソッドを更新して、以下のいずれかを返すように署名を変更します: PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } または、IList<Dinner> を返し、メソッドの "totalCount" out パラメータを使用して Dinners の合計数を返します: IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

次のステップ

次に、アプリケーションに認証と承認のサポートを追加する方法を見てみましょう。