NerdDinnerチュートリアル

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

Scott Guthrie 著/Chica
2010/02/26

 本記事は、Microsoftの本社副社長であり、ASP.NETやSilverlightなどの開発チームを率いるScott Guthrie氏が公開している「NerdDinner Tutorial」を翻訳したものです。氏の許可を得て転載しています。

[これは無償の"NerdDinner"アプリケーション・チュートリアルのステップ8で、ASP.NET MVCを使用して、小さいながらも完全なWebアプリケーションを構築する手順を紹介しています。]

 もしこのサイトが成功したら、何千もの新しい夕食会ができるでしょう。それらの夕食会をすべて処理できるようなUIにして、ユーザーがそれらを一覧できるようにしなければなりません。これを可能にするには、ページングのサポートを/Dinners URLに追加して、一度に1000個の夕食会を表示する代わりに、新しい夕食会を10個ずつ表示させ、SEOフレンドリな方法でエンドユーザーが全一覧を前後しながらページ表示できるようにしましょう。

Indexアクション・メソッドのおさらい

 DinnersControllerクラス内のIndexアクション・メソッドは現在、以下のようになっています。

//
// GET: /Dinners/

public ActionResult Index() {

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

 /Dinners URLに要求があったとき、すべての未開催の夕食会を取得し、それらすべての一覧を描画します。


図1

IQuerable<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アクション・メソッドを更新して、ToListメソッドがその上で呼び出される前に、返された一連のIQueryable<Dinner>へ“Skip”や“Take”演算子の追加を適用します。

//
// 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”の値を追加

 特定のページの範囲をハードコーディングする代わりに、ユーザーが要求している夕食会の範囲を示す“page”パラメータが含まれたURLにしましょう。

■クエリ文字列の値を使用

 以下のコードは、クエリ文字列パラメータをサポートし、/Dinners?page=2のようなURLが有効になるように、Indexアクション・メソッドを更新する方法を示しています。

//
// 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行)、スキップする夕食会の数を決定します。C#のnull合体演算子(??)を使用しています。これはnull許容型を処理するときに便利です。上記のコードでは、ページのパラメータがnullの場合、0をページに割り当てます。

■埋め込みURL値を使用

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

 すべての受け取ったURLあるいはURLのフォーマットを、必要となるコントローラ・クラスまたはアクション・メソッドにマッピングする独自のルーティング・ルールを登録できます。それにはまず、プロジェクトにあるGlobal.asaxファイルを開きます。


図2

 そして、以下のroutes.MapRouteメソッドへの最初の呼び出しのように、MapRouteヘルパー・メソッドを使用する新しいマッピング・ルールを登録します。


public void RegisterRoutes(RouteCollection routes) {

   routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

  routes.MapRoute(
    "UpcomingDinners",                 // ルート名
    "Dinners/Page/{page}",             // パラメータ付きURL
    new { controller = "Dinners", action = "Index" } // デフォルト・パラメータ
  );

  routes.MapRoute(
    "Default",                       // ルート名
    "{controller}/{action}/{id}",    // パラメータ付きURL
    new { controller="Home", action="Index",id="" }  // デフォルト・パラメータ
  );
}

void Application_Start() {
  RegisterRoutes(RouteTable.Routes);
}

 上記では、“UpcomingDinners”という名前の新しいルーティング・ルールを登録しています。“Dinners/Page/{page}”というURLフォーマットを取ることを示しています。その中の{page}は、URL内に埋め込まれるパラメータ値です。MapRouteメソッドへの3つ目のパラメータは、DinnersControllerクラス上のIndexアクション・メソッドへ、このフォーマットと合致したURLをマッピングするべきであることを示しています。

 以前にクエリ文字列のケースで使った、まったく同じ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個の未開催の夕食会が表示されます。


図3

 /Dinners/Page/1を入力すると、次のページの夕食会が表示されます。


図4

ページ操作のUIを追加

 ページングのシナリオを完成させる最後の手順は、ビュー・テンプレート内に“次”と“前”の操作用の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”などのプロパティをどのように計算して公開しているか確認してください。また、“HasPreviousPage”と“HasNextPage”の2つのヘルパー・メソッドも公開しており、これらはコレクションのデータのページが、データの最初か最後かを示しています。上記のコードは、実行すると2つのSQLクエリを生成します。1つ目はDinnerオブジェクトの合計数を取得し(これはオブジェクトを返しません。Integer値を返す“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);
}

 そして、\Views\Dinners\Index.aspxビュー・テンプレートを更新して、ViewPage<IEnumerable<Dinner>>ではなく、ViewPage<NerdDinner.Helpers.PaginatedList<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を生成しているところです。これにより、/Dinners/Page/{page}のフォーマットを持つIndexアクション・メソッドに対して、URLが生成されるようになります。その{page}値は、現在のPageIndexに基づいて上記で設定している変数です。

 いまアプリケーションを再実行すると、10個の夕食会がブラウザで同時に表示されます。


図5

 <<<と>>>の操作用UIもページの最後にあり、これにより検索エンジンもアクセス可能なURLを使いながら、データを前後にスキップできます。


図6

サイド・トピック:IQueryable<T>の実装の理解

 IQueryable<T>は非常に強力な機能で、さまざまな興味深い遅延実行のシナリオが可能です(ページングや組み立てベースのクエリなど)。すべての強力な機能において、その使い方に気を付け、乱用しないようにする必要があります。

 リポジトリから返されたIQueryable<T>結果は、呼び出しているコードが、それに対して連鎖的な操作メソッドを追加できるため、最終的なクエリの実行に関与することを認識していることが重要です。もし呼び出しコードにこの機能を提供したくないなら、IList<T>またはIEnumerable<T>の結果を返すべきです。これにはすでに実行されたクエリの結果が含まれています。

 ページングのシナリオでは、呼び出されるリポジトリ・メソッドに、実際のデータをページングするロジックを入れ込む必要があります。このシナリオでは、FindUpcomingDinners()の検索メソッドを更新して、PaginatedListを返すシグネチャを持つようにします。

PaginatedList< Dinner> FindUpcomingDinners(int pageIndex, int pageSize) { }

 あるいは、IList<Dinner>を返し、“totalCount”出力パラメータを使って夕食会の合計数を返します。

IList<Dinner> FindUpcomingDinners(int pageIndex, int pageSize, out int totalCount) { }

次のステップ

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

[注:NerdDinnerアプリケーションの完成版はhttp://nerddinner.codeplex.com/からダウンロードできます。] End of Article

 
インデックス・ページヘ  「NerdDinnerチュートリアル」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間