次の方法で共有


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

提供元: Microsoft

PDF のダウンロード

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

手順 8 では、ページング サポートを /Dinners URL に追加して、1000 件のディナーを一度に表示するのではなく、予定されているディナーを一度に 10 件だけ表示し、エンドユーザーが SEO フレンドリな方法で次の 10 件に進んだり、前の 10 件に戻ったりしてリスト全体を表示できるようにする方法を示します。

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

NerdDinner 手順 8: ページング サポート

このサイトが成功したら、今後何千ものディナーを持つことになります。 これらのすべてのディナーを処理できるように UI を拡張し、ユーザーがそれらを参照できるようにする必要があります。 これを行うには、ページング サポートを /Dinners URL に追加して、1000 件のディナーを一度に表示するのではなく、予定されているディナーを一度に 10 件だけ表示し、エンドユーザーが SEO フレンドリな方法で次の 10 件に進んだり、前の 10 件に戻ったりしてリスト全体を表示できるようにします。

Index() アクション メソッドの要約

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

//
// GET: /Dinners/

public ActionResult Index() {

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

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

Screenshot of the Nerd Dinner Upcoming Dinner list page.

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 は、データが要求されたときにデータベースに対して結合されたクエリを実行するのに十分な機能を備えています。

ページング ロジックを実装するには、返された IQueryable<Dinner> シーケンスに追加の "Skip" 演算子と "Take" 演算子を適用してから ToList() を呼び出すように、DinnersController の Index() アクション メソッドを更新できます。

//
// 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 クエリを構築できます。 つまり、データベースに今後何百万ものディナーが作成されたとしても、この要求の一部として、期待する 10 件のみを取得できます。これは、効率的であり、スケーラブルでもあります。

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

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

クエリ文字列値の使用

次のコードは、クエリ文字列パラメーターをサポートするように 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 行) を乗算します。 null 許容型を処理する場合に便利な C# null "coalescing" 演算子 (??) を使用しています。 上記のコードでは、ページ パラメーターが null の場合、ページに値 0 が割り当てられます。

埋め込み URL 値の使用

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

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

Screenshot of the Nerd Dinner navigation tree. Global dot a s a x is selected and highlighted.

次の routes.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() アクション メソッドにマップする必要があることを示します。

クエリ文字列シナリオでは、前とまったく同じ 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 件のディナーが表示されます。

Screenshot of the Nerd Dinners Upcoming Dinners list.

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

Screenshot of the next page of Upcoming Dinners list.

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

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

これを正しく実装するには、データベース内のディナーの合計数と、変換するデータのページ数を把握する必要があります。 次に、現在要求している "page" 値がデータの先頭または末尾にあるかどうかを計算し、それに応じて "戻る" と "次へ" 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>> ではなく ViewPage<NerdDinner.Helpers.PaginatedList<Dinner>> から継承するように \Views\Dinners\Index.aspx ビュー テンプレートを更新し、ビュー テンプレートの下部に次のコードを追加して、"次へ" と "戻る" ナビゲーション 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 を生成しているところです。 これにより、/Dinners/Page/{page} という形式の Index() アクション メソッドに対する URL が生成されます。ここで、{page} 値は、現在の PageIndex に基づいて上記で指定する変数です。

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

Screenshot of the Upcoming Dinners list on the Nerd Dinner page.

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

Screenshot of the Nerd Dinners page with Upcoming Dinners list.

その他のトピック: IQueryable <T> の影響を理解する
IQueryable <T> は、さまざまな興味深い遅延実行シナリオ (ページングやコンポジション ベースのクエリなど) を可能にする非常に強力な機能です。 すべての強力な機能と同様に、使用方法に注意を払い、悪用されないように気を付ける必要があります。 リポジトリから IQueryable <T> の結果を返すことで、コードを呼び出してチェーン演算子メソッドに追加し、最終的なクエリ実行に参加させることができることを理解するのは大切です。 呼び出し元コードにこの機能を提供しない場合は、IList<T> または IEnumerable<T> の結果 (既に実行されているクエリの結果を含む) を返す必要があります。 改ページ位置の自動修正シナリオでは、呼び出されるリポジトリ メソッドに実際のデータ改ページ ロジックをプッシュする必要があります。 このシナリオでは、PaginatedList を返すシグネチャを持つように、FindUpcomingDinners() Finder メソッドを更新できます (PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { } )。または、IList<Dinner> を返し、"totalCount" 出力パラメーターを使用して Dinners の合計カウントを返すこともできます (IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { })

次の手順

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