データ ポイント

Entity Framework と ASP.NET MVC 3 によるサーバー側でのページ切り替え

Julie Lerman

コード サンプルのダウンロード

image: Julie Lerman2 月号の「データ ポイント」コラムでは、jQuery の DataTables プラグインと、クライアント側で大量データをシームレスに処理する、このプラグインの機能について紹介しました。この手法は、大量データを分析する必要がある Web アプリケーションで役に立ちます。今月は、もう少し小さなペイロードを返すクエリを使用して、さまざまな種類のデータ操作を可能にする方法に注目します。モバイル アプリケーション向け開発を行うときは、この手法が特に重要です。

今月は、ASP.NET MVC 3 から導入された機能を利用して、Entity Framework に対する効率的なサーバー側ページ切り替えと組み合わせて使用する方法について例を挙げて説明します。この手法には 2 つの課題があります。1 つは、Entity Framework クエリに正しいページ切り替えパラメーターを提供することです。もう 1 つは、取得するデータが他にもあることを示す視覚的な手掛かりと、データを取得するためのリンクを表示して、クライアント側でのページ切り替え機能を模倣することです。

ASP.NET MVC 3 には、新しい Razor ビュー エンジン、検証の強化、大量に追加された JavaScript 機能など、多数の新機能が備わっています。MVC のトップ ページは asp.net/mvc (英語) からアクセスでき、ここでは ASP.NET MVC 3 をダウンロードしたり、MVC の理解に役に立つブログ記事やトレーニング ビデオへのリンクを参照したりすることができます。今回使用する新機能の 1 つは、ViewBag です。ASP.NET MVC の使用経験がある方向けに説明すると、ViewBag は ViewData クラスを強化した機能で、動的に作成したプロパティを使用できます。

ASP.NET MVC 3 で導入された新機能には、特殊な System.Web.Helpers.WebGrid もあります。グリッドにはページ切り替え機能があり、今回の例では新しいグリッドを使用しますが、そのページ切り替え機能は使用しません。これは、グリッドのページ切り替え機能がクライアント側で実行されるため、つまり、DataTables プラグインと同様、一連のデータを取得してからページ切り替えが実行されるためです。今回はサーバー側でページ切り替えを行うことを考えています。

アプリケーションには、操作対象の Entity Data Model が必要です。今回は、Microsoft AdventureWorksLT サンプル データベースから作成したモデルを使用しますが、必要なのはこのモデルに含まれる Customer テーブルと SalesOrderHeaders だけです。ここでは Customer の rowguid プロパティ、PasswordHash プロパティ、および PasswordSalt プロパティを別々のエンティティに移動して、編集時にこれらの関係を考慮する必要がないようにしました。このわずかな変更以外は、モデルを既定の状態から変更していません。

ここでは、既定の ASP.NET MVC 3 プロジェクト テンプレートを使用してプロジェクトを作成しました。このテンプレートを使用すると、多数のコントローラーやビューがあらかじめ設定されます。既定の HomeController は Customers に配置したままにしておきます。

単純な DataAccess クラスを使用して、モデル、コンテキスト、および最終的にはデータベースを操作できるようにします。このクラスでは、独自の GetPagedCustomers メソッドでサーバー側でのページ切り替えを行います。ASP.NET MVC アプリケーションの目標が、ユーザーがすべての顧客を操作できるようにすることだとすれば、多数の顧客を 1 回のクエリで返して、ブラウザーで管理したはずです。しかし、今回は一度に 10 行ずつ表示するため、GetPagedCustomers メソッドではそのためのフィルターを提供します。最終的に実行する必要があるクエリは、次のようなクエリです。

context.Customers.Where(c => 
c.SalesOrderHeaders.Any()).Skip(skip).Take(take).ToList()

ビューは要求対象のページを認識し、そのページについての情報をコントローラーに提供します。コントローラーは、1 ページあたりに提供する行数を把握します。続いて、ページ番号と 1 ページあたりの行数を使用して、skip の値を計算します。GetPagedCustomers メソッドを呼び出す際、計算した skip 値と 1 ページあたりの行数 (take 値) をコントローラーからメソッドに渡します。たとえば、1 ページあたり 10 行を表示し、4 ページ目を表示するとしたら、skip 値は 40、take 値は 10 になります。

ページ切り替えクエリでは、まず、SalesOrderHeaders の値がある顧客だけを要求するフィルターを作成します。次に、LINQ の Skip メソッドと Take メソッドを使用すると、最終的なデータは最初の条件からさらに絞り込まれた顧客になります。ページ切り替えを含め、クエリ全体がデータベースで実行されます。データベースからは、Take メソッドで指定した数の行だけが返されます。

後で機能を追加できるように、クエリはいくつかに分けて構成します。HomeController から呼び出される GetPagedCustomers メソッドの初期状態は、次のとおりです。

public static List<Customer> GetPagedCustomers(int skip, int take)
    {
      using (var context = new AdventureWorksLTEntities())
      {
        var query = context.Customers.Include("SalesOrderHeaders")
          .Where(c => c.SalesOrderHeaders.Any())
          .OrderBy(c => c.CompanyName + c.LastName + c.FirstName);

        return query.Skip(skip).Take(take).ToList();
      }
    }

このメソッドを呼び出すコントローラーの Index メソッドでは、pageSize という変数を使用して、返す行数を特定します。この変数は、Take メソッドに渡す値にもなります。また、パラメーターとして渡されるページ番号に基づいて、返すデータの開始位置も指定します。

public ActionResult Index(int? page)
    {
      const int pageSize = 10;
      var customers=DataAccess.GetPagedCustomers((page ?? 0)*pageSize, pageSize);
      return View(customers);
    }

これで、ほとんどの作業は完了です。サーバー側でのページ切り替えに必要な処理はすべて構築しました。Index ビューのマークアップの WebGrid により、GetPagedCustomers メソッドから返される顧客を表示できます。マークアップでは、グリッドを宣言し、Model 変数を渡してインスタンスを作成する必要があります。Model 変数は、コントローラーがビューを作成したときに提供された List<Customer> オブジェクトを表します。続いて、WebGrid の GetHtml メソッドを使用すると、表示する列を指定して、グリッドの書式を設定できます。ここでは、Customer のプロパティのうち、CompanyName プロパティ、FirstName プロパティ、および LastName プロパティの 3 つだけを表示します。すばらしいことに、このマークアップを ASPX に関連付けられた構文で入力する場合でも、新しい MVC 3 Razor ビュー エンジンの構文で入力する場合 (以下の例参照) でも、IntelliSense が完全にサポートされます。最初の列に、Edit という ActionLink オブジェクトを追加して、表示されている任意の Customer オブジェクトをユーザーが編集できるようにします。

@{
  var grid = new WebGrid(Model); 
}
<div id="customergrid">
  @grid.GetHtml(columns: grid.Columns(
    grid.Column(format: (item) => Html.ActionLink
      ("Edit", "Edit", new { customerId = item.CustomerID })),
  grid.Column("CompanyName", "Company"), 
  grid.Column("FirstName", "First Name"),
  grid.Column("LastName", "Last Name")
   ))
</div>

この結果を図 1 に示します。

image: Providing Edit ActionLinks in the WebGrid

図 1 WebGrid に表示された Edit という ActionLink

ここまでは順調ですね。しかし、この状態では、ユーザーは別のページのデータに移動できません。別のページに移動する方法はたくさんありますが、その 1 つは、URI でページ番号を指定する方法です (http://adventureworksmvc.com/Page/3 など)。もちろん、このような操作をするようエンド ユーザーに求めることは、お勧めできません。もっとよく使用されるのは、ページ番号のリンク ([1]、[2]、[3]、[4]、[5] など) や、前後のページを示すリンク ([<<] と [>>] など) のようなページ切り替えコントロールを用意する方法です。

ページ切り替えリンクを有効にするうえでの現在の問題は、取得する顧客データが他にもあることを、Index ビュー ページが認識していないことです。顧客の総数は、現在ビューに表示している 10 人だけだと認識しています。データ アクセス層にロジックを追加し、コントローラーを使用してこのロジックをビューに渡すと、この問題を解決できます。まずは、データ アクセスのロジックを説明しましょう。

顧客の現在のセットよりも多くのレコードが存在することを認識するには、10 人ずつのページ切り替えを行わない場合にクエリから返される最大顧客数を把握する必要があります。GetPagedCustomers メソッドでのクエリ作成に効果があるのは、このような場合です。次のように、最初のクエリが、_customerQuery というクラス レベルで宣言した変数に返されることに注意してください。

_customerQuery = context.Customers.Where(c => c.SalesOrderHeaders.Any());

Count メソッドをクエリの末尾に追加すると、ページ切り替えの適用前に、クエリに一致する Customers の総数を取得できます。Count メソッドは、比較的単純なクエリをすぐに実行します。SQL Server で実行されるクエリを以下に示します。このクエリからは、応答として 1 つの値が返されます。

    SELECT 
    [GroupBy1].[A1] AS [C1]
    FROM ( SELECT 
           COUNT(1) AS [A1]
           FROM [SalesLT].[Customer] AS [Extent1]
           WHERE  EXISTS (SELECT 
                  1 AS [C1]
                  FROM [SalesLT].[SalesOrderHeader] AS [Extent2]
                  WHERE [Extent1].[CustomerID] = [Extent2].[CustomerID]
           )
    )  AS [GroupBy1]

顧客総数を特定したら、現在の顧客ページが最初のページ、最後のページ、または中間のページのいずれに当たるのか判断できます。続いて、このロジックを使用して、表示するリンクを決定できます。たとえば、最初の顧客ページ以外を表示しているのであれば、前のページへのリンク ([<<] など) を表示して、前のページの顧客データにアクセスできるようにするのが論理的です。

このロジックを表す値をデータ アクセス クラスで計算して、顧客と共にラッパー クラスで公開できます。使用する新しいクラスは、次のとおりです。

public class PagedList<T>
  {
    public bool HasNext { get; set; }
    public bool HasPrevious { get; set; }
    public List<T> Entities { get; set; }
  }

GetPagedCustomers メソッドは、List クラスではなく PagedList クラスを返すようになります。図 2 は、新しいバージョンの GetPagedCustomers メソッドです。

図 2 新しいバージョンの GetPagedCustomers メソッド

public static PagedList<Customer> GetPagedCustomers(int skip, int take)
    {
      using (var context = new AdventureWorksLTEntities())
      {
        var query = context.Customers.Include("SalesOrderHeaders")
          .Where(c => c.SalesOrderHeaders.Any())
          .OrderBy(c => c.CompanyName + c.LastName + c.FirstName);

        var customerCount = query.Count();

        var customers = query.Skip(skip).Take(take).ToList();
      
        return new PagedList<Customer>
        {
          Entities = customers,
          HasNext = (skip + 10 < customerCount),
          HasPrevious = (skip > 0)
        };
      }
    }

設定した新しい変数を使用して、HomeController の Index メソッドからこれらの変数をビューに渡す方法を見てみましょう。Index メソッドでは、新しい ViewBag を使用できます。このバージョンでも顧客に関するクエリの結果をビューに返しますが、さらに、前や次に移動するリンクを表すマークアップの外観を指定するうえで役に立つ値も ViewBag クラスで設定できます。設定した値は、実行時にビューで使用できます。

public ActionResult Index(int? page)
    {
      const int pageSize = 10;
      var customers=DataAccess.GetPagedCustomers((page ?? 0)*pageSize, pageSize);
      ViewBag.HasPrevious = DataAccess.HasPreviousCustomers;
      ViewBag.HasMore = DataAccess.HasMoreCustomers;
      ViewBag.CurrentPage = (page ?? 0);
      return View(customers);
    }

ViewBag クラスが厳密に型指定されず動的である点について、理解することが重要です。HasPrevious プロパティと HasMore プロパティは、実際には ViewBag クラスに付属していません。今回のコードの作成時に、独自に作成したプロパティです。ですから、IntelliSense の候補に表示されなくても、あわてないでください。必要に応じて、動的プロパティを自由に作成できます。

以前に ViewPage.ViewData ディクショナリの使用経験があり、ViewBag との違いに興味がある方のために説明すると、ViewBag でも、まったく同じ処理を実行します。ただし、ViewBag ではコードが少し便利になるだけでなく、プロパティが型指定されます。たとえば、HasMore プロパティは dynamic{bool} 型で、CurrentPage プロパティは dynamic{int} 型です。後で取得する際に、値のキャストが不要になります。

マークアップでは、このバージョンでも Model 変数に格納された顧客のリストを使用していますが、ViewBag 型の変数も使用できます。動的プロパティをマークアップに入力する際は、IntelliSense を使用せずに自力で行う必要があります。ツールヒントには、プロパティが動的であることが示されます (図 3 参照)。

image: ViewBag Properties Aren’t Available Through IntelliSense Because They’re Dynamic

図 3 ViewBag のプロパティは動的なため IntelliSense の候補に表示されない

ViewBag 型の変数を使用してナビゲーション リンクを表示するかどうか決定するマークアップを、次に示します。

@{ if (ViewBag.HasPrevious)
  {
    @Html.ActionLink("<<", "Index", new { page = (ViewBag.CurrentPage - 1) })
  }
}

@{ if (ViewBag.HasMore)
   { @Html.ActionLink(">>", "Index", new { page = (ViewBag.CurrentPage + 1) }) 
  }
}

このロジックは、NerdDinner アプリケーション チュートリアルで使用されているマークアップに手を加えたものです。このチュートリアルは、www.atmarkit.co.jp/fdotnet/scottgublog/nerddinner/intro.html で公開されています。

この段階でアプリケーションを実行すると、ある顧客ページから次の顧客ページに移動できます。

最初のページを表示していれば、次のページに移動するリンクは表示されますが、前にはページがないため、前のページに移動するリンクは表示されません (図 4 参照)。

image: The First Page of Customer Has Only a Link to Navigate to the Next Page

図 4 最初の顧客ページには、次のページに移動するリンクだけが表示される

リンクをクリックして次のページに移動すると、前のページと次のページに移動するリンクが表示されるようになります (図 5 参照)。

image: A Single Page of Customers with Navigation Links to Go to Previous or Next Page of Customers

図 5 前の顧客ページと次の顧客ページに移動するナビゲーション リンクが表示される顧客ページ

次の段階は、もちろん、デザイナーを使用してこのページ切り替えの外観を魅力的に整えることですね。

開発者にとって非常に重要な手段

まとめると、クライアント側でのページ切り替えの効率を高めるツールは多数存在しますが (jQuery の DataTables 拡張や ASP.NET MVC 3 の新機能である WebGrid など)、大量データを取得することになり、アプリケーションの要件に沿ったメリットが得られないことがあります。サーバー側で効率的にページ切り替えができれば、開発者にとって非常に重要な手段となります。Entity Framework と ASP.NET MVC を連携させると、優れたユーザー エクスペリエンスを実現できるだけでなく、その開発作業も容易になります。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女が執筆した『Programming Entity Framework』(O'Reilly Media、2009 年) は絶賛を浴びました。彼女には Twitter.com/julielerman (英語) から連絡できます。

この記事のレビューに協力してくれた技術スタッフの Vishal Joshi に心より感謝いたします。