ASP.NET WebGrid

ASP.NET MVC で WebGrid を最大限に活用する

Stuart Leeks

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

今年の初めにマイクロソフトは ASP.NET MVC 3 (asp.net/mvc、英語) と、WebMatrix という新製品 (asp.net/webmatrix、英語) をリリースしました。この WebMatrix リリースは、さまざまな生産性向上を支援するヘルパーを備え、グラフや表形式データのレンダリングなどのタスクを簡略化します。このヘルパーの 1 つが WebGrid で、非常に簡単な方法で表形式データをレンダリングできます。WebGrid は、AJAX を利用して、列の書式設定のカスタマイズやページ分割、非同期の更新をサポートします。

この記事では、WebGrid を紹介し、ASP.NET MVC 3 で使用する方法を説明して、ASP.NET MVC ソリューションで WebGrid を実際に活用する方法を示します (WebMatrix と、この記事で使用する Razor 構文の概要については、2011 年 4 月号の Clark Sell の記事「WebMatrix の概要」msdn.microsoft.com/magazine/gg983489) を参照してください。

ここでは、WebGrid コンポーネントを ASP.NET MVC 環境に組み込む方法と、表形式データをレンダリングする際の生産性を向上する方法について説明します。ここでは、ASP.NET MVC の観点から、WebGrid に注目します。具体的には、完全に IntelliSense を利用でき、厳密に型指定された WebGrid を作成し、サーバー側のページ分割に WebGrid サポートをフックし、スクリプトが無効な場合に適切に機能が制限される AJAX 機能を追加します。ここで使用するサンプルは、Entity Framework を利用して AdventureWorksLT データベースへのアクセスを提供するサービスを基盤に構築しています。このデータ アクセスを提供するコードは、コード ダウンロードに含めていますので、興味があれば確認してください。また、2011 年 3 月号の Julie Lerman の記事「Entity Framework と ASP.NET MVC 3 によるサーバー側でのページ切り替え」(msdn.microsoft.com/magazine/gg650669) も参考にしてください。

WebGrid 入門

WebGrid の簡単な例を紹介するため、IEnumerable<Product> をビューに渡すだけの ASP.NET MVC アクションを用意しました。この記事の大半で Razor ビュー エンジンを使用していますが、WebForms ビュー エンジンを使用する方法についても後で説明します。ProductController クラスには、次のアクションがあります。

public ActionResult List()
  {
    IEnumerable<Product> model =
      _productService.GetProducts();
 
    return View(model);
  }

List ビューには次の Razor コードが設定されています。このコードは、図 1 のグリッドをレンダリングします。

@model IEnumerable<MsdnMvcWebGrid.Domain.Product>
@{
  ViewBag.Title = "Basic Web Grid";
}
<h2>Basic Web Grid</h2>
<div>
@{
  var grid = new WebGrid(Model, defaultSort:"Name");
}
@grid.GetHtml()
</div>

(クリックすると拡大表示されます)

図 1 レンダリングされた基本 WebGrid

ビューの 1 行目は、モデルの型 (たとえば、ここではビューからアクセスする Model プロパティの型) を IEnumerable<Product> に指定しています。次に、div 要素内で WebGrid のインスタンスを作成し、モデル データを渡します。ここでは、これを @{...} コード ブロック内で行い、Razor によって結果がレンダリングされないようにしています。コンストラクターでも defaultSort パラメーターを "Name" に設定して、渡されたデータが Name によって既に並び替えられていることを WebGrid に伝達しています。最後に、@grid.GetHtml() を使用して、グリッドの HTML を生成し、この HTML をレンダリングして応答を生成しています。

これは、わずかな量のコードですが、高度なグリッド機能を提供しています。このグリッドでは、表示するデータの量を制限し、データ内を移動するためにページ切り替え用のリンクを含めています。また、列見出しを、並べ替えを可能にするリンクとして表示します。WebGrid コンストラクターと GetHtml メソッドの一連のオプションを指定して、この動作をカスタマイズすることもできます。これらのオプションでは、ページ切り替えや並べ替えの無効化、1 ページあたりの行数の変更、ページ切り替え用リンクのテキストの変更などが可能です。図 2 は WebGrid コンストラクターのパラメーターで、図 3 は GetHtml パラメーターです。

図 2 WebGrid コンストラクターのパラメーター

名前 備考
source IEnumerable<dynamic> レンダリング対象のデータ。
columnNames IEnumerable<string> レンダリングされる列をフィルター選択します。
defaultSort string 並べ替えの基準にする既定の列を指定します。
rowsPerPage int 1 ページ当たりに表示する行数を制御します (既定値は 10)。
canPage bool データのページ切り替えを有効または無効にします。
canSort bool データの並べ替えを有効または無効にします。
ajaxUpdateContainerId string グリッドに含まれる要素の ID。これにより、AJAX のサポートが可能になります。
ajaxUpdateCallback string AJAX の更新が完了した時点で呼び出されるクライアント側の関数。
fieldNamePrefix string 複数のグリッドをサポートするためのクエリ文字列フィールドのプレフィックス。
pageFieldName string ページ番号のクエリ文字列フィールド名。
selectionFieldName string 選択した行番号のクエリ文字列フィールド名。
sortFieldName string 並べ替え列のクエリ文字列フィールド名。
sortDirectionFieldName string 並べ替えの方向のクエリ文字列フィールド名。

図 3 WebGrid.GetHtml のパラメーター

名前 備考
tableStyle string 表のスタイル指定に使うクラス。
headerStyle string ヘッダー行のスタイル指定に使うクラス。
footerStyle string フッター行のスタイル指定に使うクラス。
rowStyle string 行のスタイル指定に使うクラス (奇数行のみ)。
alternatingRowStyle string 行のスタイル指定に使うクラス (偶数行のみ)。
selectedRowStyle string 選択した行のスタイル指定に使うクラス。
caption string 表見出しとして表示される文字列。
displayHeader bool ヘッダー行を表示するかどうかを指定します。
fillEmptyRows bool rowsPerPage の行数を確保するために、表に空の行を追加できるかどうかを指定します。
emptyRowCellValue string 空の行に設定される値。fillEmptyRows が設定されている場合にのみ使用されます。
columns IEnumerable<WebGridColumn> 列のレンダリングをカスタマイズするための列モデル。
exclusions IEnumerable<string> 列の値が自動で設定される場合に除外する列。
mode WebGridPagerModes ページ切り替えレンダリングのモード (既定値は NextPrevious および Numeric)。
firstText string 先頭ページへのリンクのテキスト。
previousText string 前ページへのリンクのテキスト。
nextText string 次ページへのリンクのテキスト。
lastText string 最終ページへのリンクのテキスト。
numericLinksCount int 表示する数字リンクの数 (既定値は 5)。
htmlAttributes object 要素に設定する HTML 属性を保持します。

前出の Razor コードでは、各行のすべてのプロパティが表示されますが、表示する列を制限したい場合があります。それには、さまざまな方法があります。1 つ目の (最も簡単な) 方法は、WebGrid コンストラクターに列のセットを渡すことです。たとえば、以下のコードでは、Name プロパティと ListPrice プロパティだけがレンダリングされます。

var grid = new WebGrid(Model, columnNames: new[] {"Name", "ListPrice"});

また、コンストラクターではなく GetHtml の呼び出しに列を指定することもできます。これは、少しコードが長くなりますが、列の表示方法について追加情報を指定できるメリットがあります。次の例では、ListPrice 列をユーザーにとって使いやすくするために、header プロパティを指定しています。

@grid.GetHtml(columns: grid.Columns(
 grid.Column("Name"),
 grid.Column("ListPrice", header:"List Price")
 )
)

アイテムの一覧をレンダリングする場合は、通常、アイテムをクリックしてその Details ビューに移動できるようにします。Column メソッドの format パラメーターを使用すると、データ アイテムのレンダリングをカスタマイズできます。次のコードは、名前のレンダリングを変更してアイテムの Details ビューへのリンクを出力する方法と、通貨値の表示では小数点以下 2 桁まで定価を出力する方法を示しています。この結果の出力を図 4 に示します。

@grid.GetHtml(columns: grid.Columns(
 grid.Column("Name", format: @<text>@Html.ActionLink((string)item.Name,
            "Details", "Product", new {id=item.ProductId}, null)</text>),
 grid.Column("ListPrice", header:"List Price", 
             format: @<text>@item.ListPrice.ToString("0.00")</text>)
 )
)

A Basic Grid with Custom Columns

図 4 カスタム列と基本グリッド

format を指定すると何か魔法が働いているかのように見えますが、実は、format のパラメーターは Func<dynamic,object> です。これは、動的パラメーターを受け取り、オブジェクトを返すデリゲートです。Razor エンジンが format パラメーターに指定されたスニペットを受け取り、デリゲートに返します。このデリゲートは動的パラメーターの名前が付いたアイテムを受け取ります。これが、format のスニペットで使用されるアイテムの変数です。これらのデリゲートの動作の詳細については、Phil Haack のブログ bit.ly/h0Q0Oz (英語) を参照してください。

item パラメーターは動的な型であるため、コードの記述時に IntelliSense やコンパイラー チェック機能はまったく提供されません (2011 年 2 月号の Alexandra Rusina による動的な型についての記事 msdn.microsoft.com/magazine/gg598922 を参照してください)。また、動的パラメーターを使用した拡張メソッドの呼び出しは、サポートされていません。つまり、拡張メソッドを呼び出す場合は、必ず静的な型を使用する必要があります。前出のコードで、Html.ActionLink 拡張メソッドを呼び出すときに、item.Name の型を string にキャストしているのはこのためです。ASP.NET MVC で使用される拡張メソッドの範囲を考えると、この動的型と拡張メソッドを共に使用できないための処理は面倒になる場合があります (T4MVC (bit.ly/9GMoup、英語) などを使用する場合はなおさらです)。

厳密な型指定を追加する

おそらく WebMatrix には動的な型指定が適していますが、厳密に型指定したビューにはメリットがあります。厳密に型指定したビュー作成するには、WebGrid<T> の派生型を作成します (図 5 参照)。これは非常に軽量なラッパーです。

図 5 WebGrid の派生型の作成

public class WebGrid<T> : WebGrid
  {
    public WebGrid(
      IEnumerable<T> source = null,
      ... parameter list omitted for brevity)
    : base(
      source.SafeCast<object>(), 
      ... parameter list omitted for brevity)
    { }
  public WebGridColumn Column(
              string columnName = null, 
              string header = null, 
              Func<T, object> format = null, 
              string style = null, 
              bool canSort = true)
    {
      Func<dynamic, object> wrappedFormat = null;
      if (format != null)
      {
        wrappedFormat = o => format((T)o.Value);
      }
      WebGridColumn column = base.Column(
                    columnName, header, 
                    wrappedFormat, style, canSort);
      return column;
    }
    public WebGrid<T> Bind(
            IEnumerable<T> source, 
            IEnumerable<string> columnNames = null, 
            bool autoSortAndPage = true, 
            int rowCount = -1)
    {
      base.Bind(
           source.SafeCast<object>(), 
           columnNames, 
           autoSortAndPage, 
           rowCount);
      return this;
    }
  }

  public static class WebGridExtensions
  {
    public static WebGrid<T> Grid<T>(
             this HtmlHelper htmlHelper,
             ... parameter list omitted for brevity)
    {
      return new WebGrid<T>(
        source, 
        ... parameter list omitted for brevity);
    }
  }

では、このコードはどのような働きをするのでしょう。新しい WebGrid<T> 実装では、format パラメーターに Func<T, object> を受け取る新しい Column メソッドを追加しています。つまり、拡張メソッドの呼び出し時にキャストは不要です。また、IntelliSense やコンパイラー チェック機能も利用できるようになりました (プロジェクト ファイルで MvcBuildViews が有効であることが前提です。これは既定では無効です)。

Grid 拡張メソッドは、コンパイラーのジェネリック パラメーター用の型インターフェイスを利用できるようにします。したがって、この例では、新しい WebGrid<Product>(Model) ではなく、Html.Grid(Model) と記述できます。どちらの場合も、戻り値の型は WebGrid<Product> です。

ページ切り替えと並べ替えを追加する

ここまでで、開発者が何もしなくても、WebGrid によってページ切り替え機能と並べ替え機能が提供されることを説明しました。また、(コンストラクター内または Html.Grid ヘルパーから) rowsPerPage パラメーターを利用してページ サイズを構成し、グリッドに自動的に 1 ページのデータを表示し、ページ間の移動ができるようにページ切り替えコントロールをレンダリングする方法も説明しました。ただし、既定の動作が、目的の動作と同じだとは限りません。これを説明するため、グリッドのレンダリング後に、データ ソースのアイテム数をレンダリングするコードを追加しました (図 6 参照)。

The Number of Items in the Data Source

図 6 データ ソース内のアイテム数

ご覧のとおり、渡されているデータには全商品の一覧が含まれています (この例では 295 点ですが、これよりも多くのデータが取得されるシナリオは想像に難くありません)。返されるデータの量が増えるに従い、サービスとデータベースにかかる負荷も増えますが、表示されるデータ ページは 1 ページのままです。しかし、これよりもよい方法があります。サーバー側のページ切り替えです。この場合、現在のページに表示する必要があるデータのみ (たとえば、5 行のみ) を取得します。

WebGrid 用にサーバー側のページ切り替えを実装するには、まず、データ ソースから取得するデータを制限します。それには、正しいデータ ページを取得できるように、どのページを要求するかを把握する必要があります。WebGrid がページ切り替えリンクをレンダリングする場合、http://localhost:27617/Product/DefaultPagingAndSorting?page=3 のように、ページ URL を再利用し、クエリ文字列パラメーターにページ番号を付加します (クエリ文字列パラメーター名は、ヘルパー パラメーターから構成可能です。これは、ページ上で複数のグリッドの改ページ位置の自動修正をサポートする場合に便利です)。つまり、アクション メソッドでパラメーターにより呼び出されるページを取得でき、このページにはクエリ文字列値が設定されます。

既存のコードを変更して 1 ページ分のデータのみを WebGrid に渡すと、WebGrid には 1 データ ページしか表示されません。他にもページがあることが認識されないため、ページ切り替えコントロールは表示されなくなります。さいわい、WebGrid には、データの指定に使用できる Bind というメソッドもあります。Bind には、データを受け取るだけでなく、総行数を取得するパラメーターもあるため、ページ数を計算できます。このメソッドを使用するには、ビューに渡す追加情報を取得するように、List アクションを更新する必要があります (図 7 参照)。

図 7 List アクションの更新

public ActionResult List(int page = 1)
{
  const int pageSize = 5;
 
  int totalRecords;
  IEnumerable<Product> products = productService.GetProducts(
    out totalRecords, pageSize:pageSize, pageIndex:page-1);
            
  PagedProductsModel model = new PagedProductsModel
                                 {
                                   PageSize= pageSize,
                                   PageNumber = page,
                                   Products = products,
                                   TotalRows = totalRecords
                                 };
  return View(model);
}

この追加情報により、WebGrid Bind メソッドを使用するようにビューを更新できます。Bind への呼び出しによって、レンダリングするデータと総行数が提供されるだけでなく、autoSortAndPage パラメーターが false に設定されます。autoSortAndPage パラメーターは、ページ切り替えを List アクション メソッドで処理するため、ページ切り替えを適用する必要がないことを WebGrid に伝達します。これを次のコードに示します。

<div>
@{
  var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize, 
    defaultSort:"Name");
  grid.Bind(Model.Products, rowCount: Model.TotalRows, autoSortAndPage: false);
}
@grid.GetHtml(columns: grid.Columns(
 grid.Column("Name", format: @<text>@Html.ActionLink(item.Name, 
   "Details", "Product", new { id = item.ProductId }, null)</text>),
  grid.Column("ListPrice", header: "List Price", 
    format: @<text>@item.ListPrice.ToString("0.00")</text>)
  )
 )
 
</div>

これらの変更を組み込むと、WebGrid は元に戻り、ページ切り替えコントロールが表示されるようになりますが、ページ切り替えはビューではなくサービスで処理されます。ただし、autoSortAndPage を無効にすると、並べ替え機能が適切に動作しません。WebGrid では、クエリ文字列パラメーターを使用して並べ替えの列と方向を渡しますが、ここでは WebGrid によって並べ替えを実行しないように指定しています。解決策は、アクション メソッドに sort パラメーターと sortDir パラメーターを追加し、これらをサービスに渡して、サービスが必要な並べ替えを実行できるようにします (図 8 参照)。

図 8 並べ替えパラメーターのアクション メソッドへの追加

public ActionResult List(
           int page = 1, 
           string sort = "Name", 
           string sortDir = "Ascending" )
{
  const int pageSize = 5;
 
  int totalRecords;
  IEnumerable<Product> products =
    _productService.GetProducts(out totalRecords,
                                pageSize: pageSize,
                                pageIndex: page - 1,
                                sort:sort,
                                sortOrder:GetSortDirection(sortDir)
                                );
 
  PagedProductsModel model = new PagedProductsModel
  {
    PageSize = pageSize,
    PageNumber = page,
    Products = products,
    TotalRows = totalRecords
  };
  return View(model);
}

AJAX: クライアント側の変更

WebGrid は、AJAX を使用したグリッド コンテンツの非同期更新をサポートします。これを利用するには、グリッドが含まれる div に確実に id を設定し、ajaxUpdateContainerId パラメーターによりこの id をグリッドのコンストラクターに渡すだけです。また、jQuery への参照も必要ですが、これは既にレイアウト表示に含まれています。ajaxUpdateContainerId が指定されると、WebGrid はその動作を変更して、ページ切り替えと並べ替えのリンクの更新に AJAX が使用されるようにします。

<div id="grid">
 
@{
  var grid = new WebGrid<Product>(null, rowsPerPage: Model.PageSize, 
  defaultSort: "Name", ajaxUpdateContainerId: "grid");
  grid.Bind(Model.Products, autoSortAndPage: false, rowCount: Model.TotalRows);
}
@grid.GetHtml(columns: grid.Columns(
 grid.Column("Name", format: @<text>@Html.ActionLink(item.Name, 
   "Details", "Product", new { id = item.ProductId }, null)</text>),
 grid.Column("ListPrice", header: "List Price", 
   format: @<text>@item.ListPrice.ToString("0.00")</text>)
 )
)
 
</div>

AJAX を使用するための組み込みの機能は便利ですが、スクリプトが無効にされている場合、生成された出力は機能しません。これは、AJAX モードでは、WebGrid は href に "#" を設定してアンカー タグをレンダリングし、onclick ハンドラーを利用して AJAX 動作を挿入するためです。

私は必ず、スクリプトが無効な場合に適切に機能を制限するページを作成するように意識しています。一般に、これは "プログレッシブ エンハンスメント" によって行うのが最適です (基本的に、スクリプトなしで機能し、スクリプトを追加することで機能が強化されるページを用意します)。それには、AJAX を利用していない WebGrid に戻し、図 9 のスクリプトを作成して、AJAX 動作を再度適用します。

図 9 AJAX 動作の再適用

$(document).ready(function () {
 
  function updateGrid(e) {
    e.preventDefault();
    var url = $(this).attr('href');
    var grid = $(this).parents('.ajaxGrid'); 
    var id = grid.attr('id');
    grid.load(url + ' #' + id);
  };
  $('.ajaxGrid table thead tr a').live('click', updateGrid);
  $('.ajaxGrid table tfoot tr a').live('click', updateGrid);
 });

このスクリプトが WebGrid にのみ適用されるように、jQuery セレクターを使用して、ajaxGrid クラス セットによって要素を特定します。このスクリプトでは、jQuery live メソッド (api.jquery.com/live、英語) を使用して、(グリッド コンテナー内の表のヘッダーまたはフッターにより特定された) 並べ替えおよびページ切り替え用のリンクのクリック ハンドラーを作成します。これで、セレクターに対応する既存の要素と将来の要素のイベント ハンドラーが設定されます。これは、スクリプトがコンテンツを置き換えることを考えると便利です。

updateGrid メソッドをイベント ハンドラーとして設定します。このメソッドでは、まず、preventDefault を呼び出して、既定の動作を抑制します。その後、(アンカー タグの href 属性から) 使用する URL を取得し、AJAX 呼び出しを実行して、更新されたコンテンツをコンテナー要素に読み込みます。この方法を使用するには、既定の WebGrid AJAX 動作を無効にし、ajaxGrid class をコンテナーの div に追加して、図 9 のスクリプトを設定します。

AJAX: サーバー側の変更

注意すべきもう一つのポイントは、スクリプトが jQuery の load メソッドの機能を使用して、返されたドキュメントからフラグメントを分離していることです。単に load(‘http://example.com/someurl’) を呼び出すと、その URL のコンテンツが読み込まれます。しかし、load(‘http://example.com/someurl #someId’) の場合は、指定された URL のコンテンツが読み込まれたうえで、id が "someId" であるフラグメントが返されます。これは、WebGrid の既定の AJAX 動作を反映しています。つまり、部分的なレンダリングの動作を追加するために、サーバー コードを更新する必要はありません。WebGrid がページ全体を読み込んだうえで、そこから新しいグリッドを抽出します。

AJAX 機能をすぐに使えるという意味では、これは非常に便利ですが、必要以上のデータを転送することになり、必要以上にサーバーのデータを検索している可能性もあります。さいわい、ASP.NET MVC を使用すると、この処理を非常に簡単にできます。基本的な考え方は、AJAX 要求と AJAX 以外の要求で共有するレンダリングを 1 つの部分ビューに抽出することです。そのうえで、コントローラーの List アクションが、AJAX 呼び出しの部分ビューのみをレンダリングするか、AJAX 以外の呼び出し用の完全ビューをレンダリングできます (完全ビューはレンダリングされると部分ビューを使用します)。

この方法は非常にシンプルで、アクション メソッド内で Request.IsAjaxRequest 拡張メソッドの結果をテストするだけです。これは、AJAX コード パスと AJAX 以外のコード パス間でわずかな違いしかない場合に、うまくいく可能性があります。ただし、通常は、両者の違いはこれよりも大きくなります (たとえば、完全なレンダリングでは、部分的なレンダリングよりも多くのデータが必要です)。このようなシナリオでは、おそらく AjaxAttribute を記述して、別々のメソッドを記述し、要求が AJAX 要求であるかどうかに従って、MVC フレームワークによって正しいメソッドが選択されるようにできます (HttpGet と HttpPost attributes と同じ動作)。この例については、私のブログ bit.ly/eMlIxU (英語) を参照してください。

WebGrid と WebForms ビュー エンジン

これまで説明した例は、いずれも Razor ビュー エンジンを使用していました。最も簡単な場合では、何の変更もしなくても WebGrid を WebForms ビュー エンジンで使用できます (ビュー エンジン構文の違いは除く)。前出の例では、format パラメーターを使用して行データの表示をカスタマイズする方法を説明しました。

grid.Column("Name", 
  format: @<text>@Html.ActionLink((string)item.Name, 
  "Details", "Product", new { id = item.ProductId }, null)</text>),

format パラメーターは実際には Func ですが、Razor ビュー エンジンではこのことが隠蔽されます。ただし、Func を渡してもかまいません。たとえば、次のようにラムダ式を使用できます

grid.Column("Name", 
  format: item => Html.ActionLink((string)item.Name, 
  "Details", "Product", new { id = item.ProductId }, null)),

この簡単な変更だけで、WebGrid を WebForms ビュー エンジンでも簡単に利用できるようになりました。

まとめ

この記事では、いくつかの簡単な変更だけで、厳密な型指定、IntelliSense、またはサーバー側のページ切り替え機能を犠牲にすることなく、WebGrid が提供する機能を利用できることを説明しました。WebGrid には、表形式データのレンダリングが必要な場合に、生産性の向上に役立つすばらしい機能があります。ASP.NET MVC アプリケーションで WebGrid を最大限に活用するにはどうしたらよいか、この記事で雰囲気をお伝えできたらさいわいです。

Stuart Leeks は、英国の開発チームのプレミア サポートで、アプリケーション開発マネージャーを務めています。キーボード ショートカットが、異常なほど好きです。ブログは blogs.msdn.com/b/stuartleeks (英語) です。このブログでは、興味のある技術的な話題 (ASP.NET MVC、Entity Framework、LINQ など) について書いています。

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