ASP.NET MVC 5

.NET 開発者向け Single Page Application 入門

Long Le

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

Microsoft .NET Framework 開発者のほとんどは、専門家としての時間の大半をサーバー側に費やし、C# や Visual Basic .NET のコーディングを行って、Web アプリケーションをビルドしています。もちろん、モーダル ウィンドウ、検証、AJAX 呼び出しといった単純なことには JavaScript を使用します。ただし、JavaScript は大部分がクライアント側コードにユーティリティ言語として使用され、アプリケーションの大部分がサーバー側で実行されます。

なめらかで応答性の高いユーザー エクスペリエンスを求めるユーザーの期待に応えるため、Web アプリケーション コードをサーバー側からクライアント側 (ブラウザー) へ移すのが最近の傾向です。こうした傾向の中、(特に企業内の) 多くの .NET 開発者は、JavaScript のベスト プラクティス、アーキテクチャ、単体テスト、保守の容易性、および近年爆発的にその種類を増やしている JavaScript ライブラリについて非常に多くの不安を抱えています。クライアント側に移行している現象の 1 つとして、Single Page Application (SPA) の使用が増加していることが挙げられます。SPA 開発には将来性があると言うだけでは、控えめすぎる表現かもしれません。Web 上の優れたアプリケーションには SPA を使ってなめらかなユーザー エクスペリエンスと応答性を実現し、ペイロード (トラフィック) とサーバーへのラウンドトリップを最小限に抑えているものがあります。

今回は、サーバー側から SPA に移行する際に抱く可能性のある不安に対処します。このような不安に対処する最善の方法は、C#、Visual Basic、.NET、Python などのあらゆる .NET 言語と同じ第一級の言語として JavaScript を使用することです。

JavaScript でアプリケーションを開発する際に無視または見過ごされがちな .NET 開発の基本原則を次に示します。

  • コード ベースを .NET で管理できる。クラスの境界、および実際にクラスを配置するプロジェク内の場所を決められます。
  • 懸念事項を分離できる。1 つのクラスが数百の役割を担い、複数のクラスの役割が重複するようなことがなくなります。
  • リポジトリ、クエリ、エンティティ (モデル)、データ ソースを再利用できる。
  • クラスやファイルに意味のあるわかりやすい名前を付けられる。
  • デザイン パターン、コーディング規約、編成を適切に使用できる。

今回は、これから SPA の世界に踏み込もうとしている .NET 開発者を対象としているため、フレームワークの使用を最小限に抑えて、優れたアーキテクチャを備えた管理可能な SPA をビルドします。

7 つの主要手順で SPA を作成する

Visual Studio 2013 に組み込まれている ASP.NET MVC テンプレートを使って新しい ASP.NET Web アプリケーションを作成し、このアプリケーションを SPA に変換します。この変換に使用するのが以下の 7 つの主要手順です (括弧内は今回のダウンロード可能コードに含めている該当プロジェクト ファイルへの参照です)。

  1. NuGet パッケージの RequireJS、RequireJS テキスト プラグイン、および Kendo UI Web をダウンロードおよびインストールする。
  2. 構成モジュールを追加する (Northwind.Web/Scripts/app/main.js)。
  3. アプリ モジュールを追加する (Northwind.Web/Scripts/app/app.js)。
  4. ルーター モジュールを追加する (Northwind.Web/Scripts/app/router.js)。
  5. どちらも Spa という名前のアクションとビューを追加する (Northwind.Web/Controllers/HomeController.cs and Northwind.Web/Views/Home/Spa.cshtml)。
  6. _ViewStart.cshtml ファイルを変更し、MVC が既定で _Layout.cshtml ファイルを使用しないでビューを読み込むようにする (Northwind.Web/Views/_ViewStart.cshtml)。
  7. SPA に適した新しい URL に対応するようにレイアウト ナビゲーション (メニュー) リンクを更新する (Northwind.Web/Views/Shared/_Layout.cshtml)。

上記の 7 手順を実行すると、Web アプリケーション プロジェクトの構造は図 1 のようになります。

ASP.NET MVC Project Structure
図 1 ASP.NET MVC プロジェクトの構造

ここからは NuGet から入手できる次の JavaScript ライブラリを使って ASP.NET MVC で優れた SPA をビルドする方法を紹介します。

  • RequireJS (requirejs.org、英語): JavaScript のファイルとモジュールのローダーです。RequireJS は、#include/import/require API と、依存関係の挿入 (DI) により入れ子になった依存関係を読み込む機能を提供します。RequireJS の設計手法では、コードのピースを使いやすい単位にカプセル化するのに役立つ、JavaScript モジュール用の Asynchronous Module Definition (AMD) API を使用します。また、コードの他の単位 (モジュール) を参照する直感的な方法も提供します。RequireJS モジュールは、モジュール パターンにも従います (bit.ly/18byc2Q、英語)。このパターンを簡単に実装するには、カプセル化のために JavaScript 関数を使用します。後ですべての JavaScript モジュールを define 関数または require 関数にまとめる際に、このパターンを実際に確認します。
  • DI や制御の反転 (IoC) といった考え方に精通している開発者は、これをクライアント側の DI フレームワークと考えることができます。現時点でまったくわからなくても大丈夫です。すべて腑に落ちるようにコードを使って説明します。
  • RequireJS 用テキスト プラグイン (bit.ly/1cd8lTZ、英語): HTML のブロック (ビュー) をリモートで SPA に読み込むために使用します。
  • Entity Framework (https://msdn.microsoft.com/ja-jp/data/ee712907): Entity Framework は非常にわかりやすく、今回は SPA に重点を置いているため、あまり詳しく触れません。Entity Framework については、利用可能な多数のドキュメントを参照してください。
  • Kendo UI Web (bit.ly/t4VkVp、英語): Web UI ウィジェット、DataSource、テンプレート、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターン、SPA、スタイルなどを含み、応答性と適合性を備えた見栄えの良いアプリケーションを提供するための、包括的な JavaScript/­HTML5 フレームワークです。

SPA インフラストラクチャを設定する

SPA インフラストラクチャの設定方法を紹介するため、まず RequireJS (config) モジュールの作成方法を説明します (Northwind.Web/Scripts/app/main.js)。このモジュールは、アプリケーション起動のエントリ ポイントになります。コンソール アプリケーションを作成したことがあれば、Program.cs の Main エントリ ポイントと考えてください。基本的には、SPA 起動時に最初に呼び出されるクラスとメソッドを含めます。main.js ファイルは基本的に SPA のマニフェストとして使用でき、ここで SPA のすべてと、存在する場合にはその依存関係を定義します。RequireJS 構成のコードを、図 2 に示します。

図 2 RequireJS 構成

 

require.config({
  paths: {
    // Packages
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router'
  },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
});
require([
  'app'
], function (app) {
  app.initialize();
});

図 2 では、すべてのモジュールの配置場所とその名前のリストを、paths プロパティに含めています。shim は、上記で定義したモジュールの名前です。shim プロパティには、モジュールが含む可能性のあるすべての依存関係を含めます。この場合、kendo というモジュールを読み込みます。kendo には jquery というモジュールへの依存関係があるため、なんらかのモジュールが kendo モジュールを要求すると、まず jquery を読み込みます。これは、jquery が kendo モジュールの依存関係として定義されているためです。

図 2 の require([], function(){}) というコードは、app という次のモジュールを読み込みます。モジュールには意図的に意味のある名前を付けるようにします。

では、SPA はこのモジュールを最初に呼び出すことをどのように知るのでしょう。SPA の最初のランディング ページを構成するには、RequireJS の script 参照タグで data-main 属性を使用します。今回は、main モジュール (main.js) を実行することを指定しています。main モジュールの読み込みに関連する面倒な作業はすべて RequireJS が処理するため、必要なのは最初に読み込むモジュールを指示することだけです。

SPA に読み込む SPA ビューは、標準の HTML (*.html) ページと ASP.NET MVC Razor (*.cshtml) ページのいずれかです。今回は .NET 開発者を対象としており、多くの企業がビュー内でサーバー側ライブラリとフレームワークの使用を継続するため、後者の Razor ビューを作成する方法を採用します。

まず、既に説明したように、ビューを追加して、Spa.cshtml という名前を付けます。このビューは、基本的にシェルまたは SPA のレイアウト用のすべての HTML を読み込みます。このビューで、"content" div のすべての HTML を置き換えるビューを交換することにより、ユーザーが SPA から移動するときに他のビュー (About.cshtml、Contact.cshtml、Index.cshtml など) を読み込みます。

SPA ランディング ページ (レイアウト) を作成する (Northwind.Web/Views/Spa.cshtml): Spa.cshtml ビューは他のすべてのビューを読み込む SPA のランディング ページなので、必要なスタイル シートの参照と RequireJS 以外には、マークアップはあまりありません。次のコードの data-main 属性で、最初に読み込むモジュールを RequireJS に指示しています。

@{
  ViewBag.Title = "Spa";
  Layout = "~/Views/Shared/_Layout.cshtml";
}
<link href=
  "~/Content/kendo/2013.3.1119/kendo.common.min.css" 
  rel="stylesheet" />
<link href=
  "~/Content/kendo/2013.3.1119/kendo.bootstrap.min.css" 
  rel="stylesheet" />
<script src=
  "@Url.Content("~/scripts/require.js")"
  data-main="/scripts/app/main"></script>
<div id="app"></div>

SPA レイアウト用のアクションを追加する (Northwind.Web/­Controllers/HomeController.cs): Spa.cshtml ビューを作成して読み込むため、アクションとビューを追加します。

public ActionResult Spa()
{
  return View();
}

アプリケーション モジュールを作成する (Northwind.Web/Scripts/app/app.js): 次に、Kendo UI ルーターを初期化して起動するアプリケーション モジュールを示します。

define([
    'router'
  ], function (router) {
    var initialize = function() {
      router.start();
    };
    return {
      initialize: initialize
    };
  });

ルーター モジュールを作成する (Northwind.Web/Scripts/app/router.js): これは、app.js から呼び出されます。ASP.NET MVC のルートをご存じであれば考え方は同じです。これらは、ビューの SPA ルートです。すべての SPA ビューの全ルートを定義し、ユーザーが SPA から移動する際に SPA に読み込まれるビューを Kendo UI ルーターに把握させます。付属ダウンロードのListing 1 を参照してください。

Kendo UI Router クラスは、アプリケーションの状態を追跡し、状態間を移動する役割を担います。ルーターは、フラグメント化した URL (#page) を使ってブラウザー履歴に統合され、アプリケーションの状態をブックマーク可能およびリンク可能にします。ルーティング可能な URL がクリックされると、ルーターが起動して、アプリケーション自体がルートにエンコーディングされた状態に戻ることをアプリケーションに伝えます。ルート定義は、ユーザーが確認したいアプリケーション状態を特定するため、使用されるパスを文字列で表したものです。ルート定義がブラウザーの URL ハッシュ フラグメントと一致すると、ルート ハンドラーが呼び出されます (図 3 参照)。

図 3 登録済みルート定義と対応する URL

登録済みルート (定義) 実際の完全な (ブックマーク可能) URL
/ localhost:25061/home/spa/home/index
/home/index localhost:25061/home/spa/#/home/index/home/about
/home/about localhost:25061/home/spa/#/home/about/home/contact
/home/contact localhost:25061/home/spa/#/home/contact/customer/index
/customer/index localhost:25061/home/spa/#/customer/index

Kendo UI レイアウト ウィジェットに関しては、その名前から機能がわかります。新しい ASP.NET MVC Web アプリケーションを作成するとプロジェクトに含まれる ASP.NET Web フォーム MasterPage (MVC レイアウト) についてはよくご存じでしょう。今回の SPA プロジェクトは、Northwind.Web/Views/Shared/_Layout.cshtml というパスに配置されます。Kendo UI レイアウトと MVC レイアウトに大きな違いはなく、Kendo UI レイアウトがクライアント側で実行される点のみが異なります。サーバー側のレイアウトでは MVC ランタイムがレイアウトのコンテンツを他のビューと置き換えますが、Kendo UI レイアウトもまったく同じ方法で機能します。showIn メソッドを使って Kendo UI レイアウトのビュー (コンテンツ) を置き換えます。View コンテンツ (HTML) は、content という ID で div に配置されます。この ID は初期化の際に Kendo UI レイアウトに渡されたものです。レイアウトを初期化したら、app という ID を持つ div 内にレンダリングします。これがランディング ページの div になります (Northwind.Web/Views/Home/Spa.cshtml)。これについて、簡単に説明します。

loadView ヘルパー メソッドはビュー モデルとビューを受け取ります。また、必要に応じて、ビューとビュー モデルのバインドを行った後に呼び出すコールバックも受け取ります。loadView メソッド内では、Kendo UI FX ライブラリを使用して、ビューの置き換えプロセスにシンプルなアニメーションを追加し、ユーザー エクスペリエンスの見栄えをよくします。そのため、現在読み込んでいるビューを左にスライドし、新しいビューをリモートで読み込み、新しく読み込んだビューを中央にスライドします。もちろん、Kendo UI FX ライブラリを使えば、さまざまなアニメーションに簡単に変更できます。Kendo UI レイアウトを使用する主なメリットの 1 つは、showIn メソッドを呼び出してビューを置き換えるときにわかります。ビューがアンロードされ、正しく破棄されて、ブラウザーの DOM から確実に削除されます。そのため、SPA はスケール変換が可能で、パフォーマンスも優れていることが保証されます。

_ViewStart.cshtml ビューを編集する (Northwind.Web/Views/­_ViewStart.cshtml): 以下に、すべてのビューが既定で ASP.NET MVC レイアウトを使用しないように構成する方法を示します。

@{
  Layout = null;
}

この時点で、SPA は機能します。メニューのナビゲーション リンクをクリックすると、Kendo UI ルーターと RequireJS によって AJAX を使って現在のコンテンツが置き換わるのを確認できます。

これら 7 つの手順で新しい ASP.NET Web アプリケーションを SPA に変換できるのは、そう悪くないと思いませんか。

これで SPA が起動して実行されるようになったので、先に進み、大部分の開発者が最終的に SPA で実行するであろうことを行います。つまり、作成、読み取り、更新、削除 (CRUD) 機能の追加です。

SPA に CRUD 機能を追加する

こからは、Customer グリッド ビューを SPA (および関連プロジェクト コード ファイル) に追加するのに必要な主要手順を示します。

  • CustomerController MVC コントローラーを追加する (Northwind.Web/Controllers/CustomerController.cs)。
  • REST OData Customer Web API コントローラーを追加する (Northwind.Web/Api/CustomerController.cs)。
  • Customer グリッド ビューを追加する (Northwind.Web/Views/­Customer/Index.cshtml)。
  • CustomerModel モジュールを追加する (Northwind.Web/Scripts/app/models/CustomerModel)。
  • Customer グリッド用の customerDatasource モジュールを追加する (Northwind.Web/Scripts/app/datasources/customer­Datasource.js)。
  • Customer グリッド ビュー用の indexViewModel モジュールを追加する (Northwind.Web/Scripts/app/viewModels/­indexViewModel.js)。

Entity Framework を使ってソリューション構造を設定する: 図 4 は、ソリューション構造を示しています。この中で、Northwind.Data (1)、Northwind.Entity (2)、および Northwind.Web (3) という 3 つのプロジェクトに注目します。この 3 つのプロジェクトと Entity Framework Power Tools について簡単に説明します。

  • Northwind.Data: Entity Framework オブジェクト リレーショナル マッピング (ORM) ツールに関連する、永続化を目的とするすべてのものが含まれています。
  • Northwind.Entity: 単純な従来の CLR オブジェクト (POCO: Plain Old CLR Object) クラスで構成されるドメイン エンティティが含まれています。これらはすべて、永続化に関係しないドメイン オブジェクトです。
  • Northwind.Web: プレゼンテーション層となる ASP.NET MVC 5 Web アプリケーションが含まれています。このプレゼンテーション層で Kendo UI と RequireJS という既に触れた 2 つのライブラリと、Entity Framework、Web API および OData という残りのサーバー側スタックで SPA を構築します。
  • Entity Framework Power Tools: 今回は、すべての POCO エンティティとマッピング (Database First) の作成に、Entity Framework チームの Entity Framework Power Tools を使用しました (bit.ly/1cdobhk、英語)。コード生成後、ここで行ったのは、懸念事項の分離のため、エンティティを別のプロジェクト (Northwind.Entity) にコピーしたことだけです。

A Best-Practice Solution Structure
図 4 ベスト プラクティス ソリューションの構造

注: Northwind SQL インストール スクリプトとデータベースのバックアップは、どちらも Northwind.Web/App_Data フォルダーのダウンロード可能ソース コードに含まれています (bit.ly/1cph5qc、英語)。

これでソリューションが設定されデータベースにアクセスできるようになったので、先に進み、MVC CustomerController.cs クラスを作成してインデックス ビューと編集ビューを提供します。このコントローラーの唯一の役割は、SPA 用の HTML ビューを提供することなので、コードは最小限になります。

MVC Customer コントローラーを作成する (Northwind.Web/­Controllers/CustomerController.cs): 以下に、インデックス ビューと編集ビューの操作を伴う Customer コントローラーを作成する方法を示します。

public class CustomerController : Controller
{
  public ActionResult Index()
  {
    return View();
  }
  public ActionResult Edit()
  {
    return View();
  }
}

Customers グリッドを備えたビューを作成する (Northwind.Web/­Views/Customers/Index.cshtml): 図 5 は、Customers グリッドを備えたビューを作成する方法を示しています。

図 5 のマークアップに慣れていなくても慌てることはありません。これは、Kendo UI MVVM (HTML) マークアップにすぎません。単純に 1 つの HTML 要素を構成します。この場合は、"grid" という ID を持つ div になります。後で Kendo UI MVVM フレームワークを使ってこのビューをビュー モデルにバインドする際、このマークアップを Kendo UI ウィジェットに変換することになります。詳細については、bit.ly/1d2Bgfj (英語) を参照してください。

図 5 MVVM ウィジェットおよびイベント バインドを伴う Customer グリッド ビュー マークアップ

<div class="demo-section">
  <div class="k-content" style="width: 100%">
    <div id="grid"
      data-role="grid"
      data-sortable="true"
      data-pageable="true"
      data-filterable="true"
      data-editable="inline"
      data-selectable="true"
      data-toolbar='[ { template: kendo.template($("#toolbar").html()) } ]'
      data-columns='[
        { field: "CustomerID", title: "ID", width: "75px" },
        { field: "CompanyName", title: "Company"},
        { field: "ContactName", title: "Contact" },
        { field: "ContactTitle", title: "Title" },
        { field: "Address" },
        { field: "City" },
        { field: "PostalCode" },
        { field: "Country" },
        { field: "Phone" },
        { field: "Fax" } ]'
      data-bind="source: dataSource, events:
        { change: onChange, dataBound: onDataBound }">
    </div>
    <style scoped>
    #grid .k-toolbar {
      padding: 15px;
    }
    .toolbar {
      float: right;
    }
    </style>
  </div>
</div>
<script type="text/x-kendo-template" id="toolbar">
  <div>
    <div class="toolbar">
      <span data-role="button" data-bind="click: edit">
        <span class="k-icon k-i-tick"></span>Edit</span>
      <span data-role="button" data-bind="click: destroy">
        <span class="k-icon k-i-tick"></span>Delete</span>
      <span data-role="button" data-bind="click: details">
        <span class="k-icon k-i-tick"></span>Edit Details</span>
    </div>
    <div class="toolbar" style="display:none">
      <span data-role="button" data-bind="click: save">
        <span class="k-icon k-i-tick"></span>Save</span>
      <span data-role="button" data-bind="click: cancel">
        <span class="k-icon k-i-tick"></span>Cancel</span>
    </div>
  </div>
</script>

MVC (OData) Web API Customer コントローラーを作成する (Northwind.Web/Api/CustomerController.cs): 次に、MVC (OData) Web API Customer コントローラーの作成方法を示します。OData は、CRUD 操作でデータ セットをクエリおよび操作するための統一した方法を提供する Web 用のデータ アクセス プロトコルです。ASP.NET Web API を使うと、OData エンドポイントを簡単に作成でき、どの OData 操作を公開するかを管理できます。また、OData 以外のエンドポイントと共に複数の OData エンドポイントをホストすることも可能なうえ、データ モデル、バックエンドのビジネス ロジック、およびデータ層を完全に管理できます。図 6 は、Customer Web API OData コントローラー用のコードを示しています。

図 6 のコードは、単純に OData Web API コントローラーを作成して Northwind データベースから Customer データを公開します。コントローラーを作成したらプロジェクトを実行でき、Fiddler (無料 Web デバッガー (fiddler2.com、英語) や INQPad などのツールを使って、実際に Customer データをクエリできます。

図 6 Customer Web API OData コントローラー

public class CustomerController : EntitySetController<Customer, string>
{
  private readonly NorthwindContext _northwindContext;
  public CustomerController()
  {
    _northwindContext = new NorthwindContext();
  }
  public override IQueryable<Customer> Get()
  {
    return _northwindContext.Customers;
  }
  protected override Customer GetEntityByKey(string key)
  {
    return _northwindContext.Customers.Find(key);
  }
  protected override Customer UpdateEntity(string key, Customer update)
  {
    _northwindContext.Customers.AddOrUpdate(update);
    _northwindContext.SaveChanges();
    return update;
  }
  public override void Delete(string key)
  {
    var customer = _northwindContext.Customers.Find(key);
    _northwindContext.Customers.Remove(customer);
    _northwindContext.SaveChanges();
  }
}

グリッド用の Customer テーブルの OData を構成して公開する (Northwind.Web/App_Start/WebApiConfig.cs): 図 7 は、グリッド用の Customer テーブルの OData を構成して公開するコードを示しています。

LINQPad を使って OData Web API をクエリする: LINQPad (linqpad.net、英語) をまだ使用したことがなければ、是非このツールを開発者向けのツールキットに追加してください。このツールは必携ですが、無料版を利用できます。図 8 は、Web API OData (localhost:2501/odata) と接続した LINQPad を示しており、"Customer.Take (100)" という LINQ クエリの結果を示しています。

図 7 OData 用の ASP.NET MVC Web API ルートの構成

public static void Register(HttpConfiguration config)
{
  // Web API configuration and services
  ODataModelBuilder modelBuilder = new ODataConventionModelBuilder();
  var customerEntitySetConfiguration =
    modelBuilder.EntitySet<Customer>("Customer");
  customerEntitySetConfiguration.EntityType.Ignore(t => t.Orders);
  customerEntitySetConfiguration.EntityType.Ignore(t =>
     t.CustomerDemographics);
  var model = modelBuilder.GetEdmModel();
  config.Routes.MapODataRoute("ODataRoute", "odata", model);
  config.EnableQuerySupport();
  // Web API routes
  config.MapHttpAttributeRoutes();
  config.Routes.MapHttpRoute(
    "DefaultApi", "api/{controller}/{id}",
    new {id = RouteParameter.Optional});
}

Querying the Customer Controller Web API OData Via a LINQPad Query
図 8 LINQPad クエリによる Customer コントローラー Web API OData のクエリ

(監視可能な) Customer モデルを作成する (Northwind.Web/­Scripts/app/models/customerModel.js): 次に、(Kendo UI の監視可能な) Customer モデルを作成します。これは、クライアント側の Customer エンティティ ドメイン モデルと考えることができます。Customer モデルを作成し、Customer グリッド ビューと編集ビューの両方から簡単に再利用できるようにします。図 9 にそのコードを示します。

図 9 Customer (Kendo UI の監視可能) モデルの作成

define(['kendo'],
  function (kendo) {
    var customerModel = kendo.data.Model.define({
      id: "CustomerID",
      fields: {
        CustomerID: { type: "string", editable: false, nullable: false },
        CompanyName: { title: "Company", type: "string" },
        ContactName: { title: "Contact", type: "string" },
        ContactTitle: { title: "Title", type: "string" },
        Address: { type: "string" },
        City: { type: "string" },
        PostalCode: { type: "string" },
        Country: { type: "string" },
        Phone: { type: "string" },
        Fax: { type: "string" },
        State: { type: "string" }
      }
    });
    return customerModel;
  });

Customers グリッド用の DataSource を作成する (Northwind.Web/Scripts/app/datasources/customersDatasource.js): ASP.NET Web フォームのデータ ソースに慣れていれば、ここでも同じ考え方で Customers グリッド用のデータ ソースを作成します (Northwind.Web/Scripts/app/datasources/customersDatasource.js)。Kendo UI DataSource (bit.ly/1d0Ycvd、英語) の構成要素は、ローカル (JavaScript オブジェクトの配列) またはリモート (XML、JSON、JSONP) データを使用するために抽象化したものです。これは、CRUD データ操作を完全にサポートしており、ローカルとサーバー側の両方の並べ替え、ページング、フィルター処理、グループ化、および集計をサポートします。

Customers グリッド ビュー用のビュー モデルを作成する: Windows Presentation Foundation (WPF) または Silverlight の MVVM に慣れていれば、クライアント側で実行する以外は、まったく同じ考え方です (このプロジェクトの Northwind.Web/Scripts/ViewModels/­Customer/indexViewModel.cs を参照)。MVVM は、ビューをそのデータおよびビジネス ロジックから分離するために使用するアーキテクチャの分離パターンです。すべてのデータ、ビジネス ロジックなどはビュー モデルに収め、ビューには純粋な HTML (プレゼンテーション) のみを収めています。図 10 に Customer グリッド ビュー モデルのコードを示します。

図 10 Customer グリッド ビュー モデル

define(['kendo', 'customerDatasource'],
  function (kendo, customerDatasource) {
    var lastSelectedDataItem = null;
    var onClick = function (event, delegate) {
      event.preventDefault();
      var grid = $("#grid").data("kendoGrid");
      var selectedRow = grid.select();
      var dataItem = grid.dataItem(selectedRow);
      if (selectedRow.length > 0)
        delegate(grid, selectedRow, dataItem);
      else
        alert("Please select a row.");
      };
      var indexViewModel = new kendo.data.ObservableObject({
        save: function (event) {
          onClick(event, function (grid) {
            grid.saveRow();
            $(".toolbar").toggle();
          });
        },
        cancel: function (event) {
          onClick(event, function (grid) {
            grid.cancelRow();
            $(".toolbar").toggle();
          });
        },
        details: function (event) {
          onClick(event, function (grid, row, dataItem) {
            router.navigate('/customer/edit/' + dataItem.CustomerID);
          });
        },
        edit: function (event) {
          onClick(event, function (grid, row) {
            grid.editRow(row);
            $(".toolbar").toggle();
          });
        },
        destroy: function (event) {
          onClick(event, function (grid, row, dataItem) {
            grid.dataSource.remove(dataItem);
            grid.dataSource.sync();
          });
        },
        onChange: function (arg) {
          var grid = arg.sender;
          lastSelectedDataItem = grid.dataItem(grid.select());
        },
        dataSource: customerDatasource,
        onDataBound: function (arg) {
          // Check if a row was selected
          if (lastSelectedDataItem == null) return;
          // Get all the rows     
          var view = this.dataSource.view();
          // Iterate through rows
          for (var i = 0; i < view.length; i++) {
          // Find row with the lastSelectedProduct
            if (view[i].CustomerID == lastSelectedDataItem.CustomerID) {
              // Get the grid
              var grid = arg.sender;
              // Set the selected row
              grid.select(grid.table.find("tr[data-uid='" + view[i].uid + "']"));
              break;
            }
          }
        },
      });
      return indexViewModel;
  });

図 10 のコードに含まれるさまざまな構成要素を簡単に説明します。

  • onClick (ヘルパー): Customer グリッドのインスタンス、現在選択されている行、およびその選択されている行の Customer を表す JSON モデルを取得するヘルパー関数。
  • save: Customer のインライン編集を実行したときに変更点を保存する。
  • cancel: インライン編集モードをキャンセルする。
  • details: SPA を Customer 編集ビューに移動させ、URL に Customer の ID を付加する。
  • edit: 現在選択されている Customer のインライン編集を有効にする。
  • destroy: 現在選択されている Customer を削除する。
  • onChange (イベント): Customer が選択されるたびに発生する。最後に選択した Customer を保存し、状態を維持する。更新を実行するか Customer グリッドから移動した後、グリッドに戻ると、最後に選択した Customer を再選択する。

次に RequireJS 構成に customerModel モジュール、indexViewModel モジュール、および customersDatasource モジュールを追加します (Northwind.Web/Scripts/app/main.js)。図 11 にそのコードを示します。

図 11 RequireJS 構成の追加

paths: {
  // Packages
  'jquery': '/scripts/jquery-2.0.3.min',
  'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
  'text': '/scripts/text',
  'router': '/scripts/app/router',
  // Models
  'customerModel': '/scripts/app/models/customerModel',
  // View models
  'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
  'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
  // Data sources
  'customerDatasource': '/scripts/app/datasources/customerDatasource',
  // Utils
  'util': '/scripts/util'
}

新しい Customers グリッド ビューにルートを追加する: loadView コールバック (Northwind.Web/Scripts/app/router.js) で、初期化され、MVVM バインドが実行されてから、グリッドのツール バーをバインドします。これは、最初にグリッドをバインドするとき、ツール バーがグリッドに存在しておらず、初期化されていないためです。グリッドを最初に MVVM で初期化するときに、Kendo UI テンプレートからツール バーが読み込まれます。グリッドに読み込まれる際、ツール バーのみビュー モデルにバインドし、ツール バーのボタンはビュー モデルの save メソッドと cancel メソッドにバインドします。以下に、Customer 編集ビューのルート定義を登録する関連コードを示します。

router.route("/customer/index", function () {
  require(['customer-indexViewModel', 'text!/customer/index'],
    function (viewModel, view) {
      loadView(viewModel, view, function () {
        kendo.bind($("#grid").find(".k-grid-toolbar"), viewModel);
      });
    });
});

これで完全な機能を備えた Customers グリッド ビューが完成です。ブラウザーの localhost:25061/Home/Spa#/customer/index (ポート番号はコンピューターによって変わる場合があります) を読み込むと、図 12 のようになります。

The Customer Grid View with MVVM Using the Index View Model
図 12 Index ビュー モデルを使用する MVVM による Customer グリッド ビュー

Customers 編集ビューを追加する: Customer 編集ビューを SPA に追加する主な手順は、次のとおりです。

  • MVVM により Customer モデルにバインドされる Customer 編集ビューを作成する (Northwind.Web/Views/Customer/Edit.cshtml)。
  • Customer 編集ビュー用の編集ビュー モデル モジュールを追加する (Northwind.Web/Scripts/app/viewModels/­editViewModel.js)。
  • URL から ID を取得するためのユーティリティ ヘルパー モジュールを追加する (Northwind.Web/Scripts/app/util.js)。

Kendo UI フレームワークを使用しているため、Kendo UI スタイルで編集ビューのスタイルを設定します。詳細については、bit.ly/1f3zWuC (英語) を参照してください。図 13 は、MVVM ウィジェットとイベント バインドを行った編集ビュー マークアップを示しています。

図 13 MVVM ウィジェットとイベント バインドを行った編集ビュー マークアップ

<div class="demo-section">
  <div class="k-block" style="padding: 20px">
    <div class="k-block k-info-colored">
      <strong>Note: </strong>Please fill out all of the fields in this form.
    </div>
    <div>
      <dl>
        <dt>
          <label for="companyName">Company Name:</label>
        </dt>
        <dd>
          <input id="companyName" type="text"
            data-bind="value: Customer.CompanyName" class="k-textbox" />
        </dd>
        <dt>
          <label for="contactName">Contact:</label>
        </dt>
        <dd>
          <input id="contactName" type="text"
            data-bind="value: Customer.ContactName" class="k-textbox" />
        </dd>
        <dt>
          <label for="title">Title:</label>
        </dt>
        <dd>
          <input id="title" type="text"
            data-bind="value: Customer.ContactTitle" class="k-textbox" />
        </dd>
        <dt>
          <label for="address">Address:</label>
        </dt>
        <dd>
          <input id="address" type="text"
            data-bind="value: Customer.Address" class="k-textbox" />
        </dd>
        <dt>
          <label for="city">City:</label>
        </dt>
        <dd>
          <input id="city" type="text"
            data-bind="value: Customer.City" class="k-textbox" />
        </dd>
        <dt>
          <label for="zip">Zip:</label>
        </dt>
        <dd>
          <input id="zip" type="text"
            data-bind="value: Customer.PostalCode" class="k-textbox" />
        </dd>
        <dt>
          <label for="country">Country:</label>
        </dt>
        <dd>
          <input id="country" type="text"
          data-bind="value: Customer.Country" class="k-textbox" />
        </dd>
        <dt>
          <label for="phone">Phone:</label>
        </dt>
        <dd>
          <input id="phone" type="text"
            data-bind="value: Customer.Phone" class="k-textbox" />
        </dd>
        <dt>
          <label for="fax">Fax:</label>
        </dt>
        <dd>
          <input id="fax" type="text"
            data-bind="value: Customer.Fax" class="k-textbox" />
        </dd>
      </dl>
      <button data-role="button"
        data-bind="click: saveCustomer"
        data-sprite-css-class="k-icon k-i-tick">Save</button>
      <button data-role="button" data-bind="click: cancel">Cancel</button>
      <style scoped>
        dd
        {
          margin: 0px 0px 20px 0px;
          width: 100%;
        }
        label
        {
          font-size: small;
          font-weight: normal;
        }
        .k-textbox
        {
          width: 100%;
        }
        .k-info-colored
        {
          padding: 10px;
          margin: 10px;
        }
      </style>
    </div>
  </div>
</div>

URL から Customer の ID を取得するユーティリティを作成する: ここでは懸案事項の適切に分離するための明確な境界を持った簡潔なモジュールを作成します。ここでは、すべてのユーティリティ ヘルパー関数を配置した Util モジュールの作成方法をデモします。Customer DataSource (Northwind.Web/Scripts/app/datasources/customerDatasource.js) 用の URL のカスタマー ID を取得できるユーティリティ メソッドから始めます (図 14 参照)。

図 14 Utility モジュール

define([],
  function () {
    var util;
    util = {
      getId:
      function () {
        var array = window.location.href.split('/');
        var id = array[array.length - 1];
        return id;
      }
    };
    return util;
  });

RequireJS 構成に編集ビュー モデルとユーティリティ モジュールを追加する (Northwind.Web/Scripts/app/main.js): 図 15 は、Customer 編集モジュールの RequireJS 構成の追加を示しています。

図 15 Customer 編集モジュール用の RequireJS 構成の追加

require.config({
  paths: {
    // Packages
    'jquery': '/scripts/jquery-2.0.3.min',
    'kendo': '/scripts/kendo/2013.3.1119/kendo.web.min',
    'text': '/scripts/text',
    'router': '/scripts/app/router',
    // Models
    'customerModel': '/scripts/app/models/customerModel',
    // View models
    'customer-indexViewModel': '/scripts/app/viewmodels/customer/indexViewModel',
    'customer-editViewModel': '/scripts/app/viewmodels/customer/editViewModel',
    // Data sources
    'customerDatasource': '/scripts/app/datasources/customerDatasource',
    // Utils
    'util': '/scripts/util'
    },
  shim : {
    'kendo' : ['jquery']
  },
  priority: ['text', 'router', 'app'],
  jquery: '2.0.3',
  waitSeconds: 30
  });
require([
  'app'
], function (app) {
  app.initialize();
});

Customer 編集ビュー モデルを追加する (Northwind.Web/Scripts/app/viewModels/editViewModel.js): 図 16 は、Customer 編集ビュー モデルの追加方法を示しています。

図 16 Customer ビュー用の Customer 編集ビュー モデル モジュール

define(['customerDatasource', 'customerModel', 'util'],
  function (customerDatasource, customerModel, util) {
    var editViewModel = new kendo.data.ObservableObject({
      loadData: function () {
        var viewModel = new kendo.data.ObservableObject({
          saveCustomer: function (s) {
            customerDatasource.sync();
            customerDatasource.filter({});
            router.navigate('/customer/index');
          },
          cancel: function (s) {
            customerDatasource.filter({});
            router.navigate('/customer/index');
          }
        });
        customerDatasource.filter({
          field: "CustomerID",
          operator: "equals",
          value: util.getId()
        });
        customerDatasource.fetch(function () {
          console.log('editViewModel fetching');
          if (customerDatasource.view().length > 0) {
            viewModel.set("Customer", customerDatasource.at(0));
          } else
            viewModel.set("Customer", new customerModel());
        });
        return viewModel;
      },
    });
    return editViewModel;
  });

図 16 のコードに含まれるさまざまな構成要素を簡単に説明します。

  • saveCustomer: すべての変更を Customer に保存し、DataSource のフィルターをリセットして、グリッドにすべての Customer を流し込む。
  • cancel: SPA を Customer グリッド ビューに移動して戻し、DataSource のフィルターをリセットして、グリッドにすべての Customer を流し込む。
  • filter: DataSource のフィルター メソッドを呼び出し、URL の ID で特定の Customer をクエリする。
  • fetch: フィルターの設定後、DataSource のフェッチ メソッドを呼び出す。フェッチのコールバックで、ビュー モデルの Customer プロパティに DataSource フェッチから返された Customer を設定する。これは、Customer 編集ビューをバインドするために使用する。

RequireJS がモジュールを読み込むときに、define メソッド本体内のコードが一度だけ呼び出されます (つまり、RequireJS がそのモジュールを読み込む際)。そのため、メソッド (loadData) を編集ビュー モデルで公開し、編集ビュー モデル モジュールが読み込まれた後にデータが読み込まれるようにします (Northwind.Web/­Scripts/app/router.js 参照)。

新しい Customer 編集ビューにルートを追加する (Northwind.Web/Scripts/app/router.js): 以下にルーターを追加するための関連コードを示します。

 

router.route("/customer/edit/:id",
        function () {
    require(['customer-editViewModel',
          'text!/customer/edit'],
      function (viewModel, view) {
      loadView(viewModel.loadData(), view);
    });
  });

 

Customer 編集ビュー モデルが RequireJS から要求された際、ビュー モデルから loadData メソッドを呼び出すことで Customer を取得できます。この方法で、Customer 編集ビューが読み込まれるたびに、URL の ID に基づき適切な Customer データを読み込むことができます。ルートは、単なるハードコーディングされた文字列にする必要はなく、バックエンド サーバー ルーターなどのパラメーターも含めることができます (Ruby on Rails、ASP.NET MVC、Django など)。これを行うため、ルート セグメント名前を変数名の前にコロンを付けた名前にします。

これで、ブラウザーで Customer 編集ビューを読み込めるようになり (localhost:25061/Home/Spa#/customer/edit/ANATR)、図 17 のような画面が表示されます。

The Customer Edit View図 17 Customer 編集ビュー

注: Customer グリッド ビューの削除 (破棄) 機能が設定しましたが、ツール バーの [Delete] (削除) ボタン (図 18 参照) をクリックすると、図 19 のような例外が発生する場合があります。

The Customer Grid View図 18 Customer グリッド ビュー

Expected Exception When Deleting a Customer Due to CustomerID Foreign Key Referential Integrity図 19 CustomerID 外部キーの参照整合性により Customer を削除する際に予想される例外

これは、Orders、Invoices など、大部分の Customer ID は他のテーブルの外部キーであるためで、この例外は想定内です。Customer ID が外部キーとなっている全テーブルのすべてのレコードを削除するため、連鎖削除を行う必要があります。何も削除することはできませんが、delete 機能の手順とコードだけは紹介しました。

以上です。今回は、RequireJS と Kendo UI を使って、ASP.NET Web アプリケーションを迅速かつ容易に SPA に変換できることをデモしました。その後、CRUD のような機能を SPA に簡単に追加できることを示しました。

プロジェクトのライブ デモは bit.ly/1bkMAlK (英語) で、CodePlex プロジェクト サイト (および、ダウンロード可能コード) は easyspa.codeplex.com (英語) で確認できます。

それでは、コーディングを楽しんでください。

Long Le は、CBRE Inc. の主任 .NET アプリ/開発アーキテクトで、Telerik/Kendo UI MVP です。彼は、フレームワークとアプリケーション ブロックの開発、ベスト プラクティスとパターンのガイダンスの提供、およびエンタープライズ テクノロジ スタックの標準化に大部分の時間を費やしています。彼は、10 年以上にわたって Microsoft テクノロジに取り組んでいます。余暇には、ブログ記事の執筆 (blog.longle.net、英語) と Call of Duty を楽しんでいます。彼の Twitter (twitter.com/LeLong37、英語) を見つけてフォローしてください。

この記事のレビューに協力してくれた技術スタッフの Derick Bailey (Telerik) および Mike Wasson (マイクロソフト) に心より感謝いたします。