Visual Studio 2013 の Single Page Application (SPA) テンプレートを使った開発 (Knockout.js)


こんにちは。

もう 1 週間たってしまいましたが、先週の .NET Week で Web 開発のセッション (私の担当セッション) にご参加いただいた皆様、ありがとうございました。そして、またまた時間がない状況で失礼致しました。(さらにもう 1 本デモを用意してましたが、それはまた次回に。。。)

実は、あの当日の説明内容は、スライドも含め、チャックこと 井上章さん がすべて用意したものです。私は、ただ操り人形のように話していただけ、ということです。

セミナー資料では、随分と SPA (Single Page Application) への思いが熱い感じでしたが、特に、コンピューター リソースの活用がクライアント側へとシフトしてきている昨今では、この SPA (Single Page Application) は ASP.NET の中でも重要な存在になってきています。そんなチャックさんの思いが、あのスライドになっていたわけですね。(半分近く SPA の説明になっていました。私なら間違いなく、認証系で半分くらいスライドを使っていたことでしょう。。。)

さて、当日セミナーに参加いただいた方で、まだ資料をゴミ箱に捨ててない方は、資料の Appendix を見てください。以下のようなスライドが入っているはずです。(これも、当然、チャックさんが作ったものです。)

ここの下に小さく書かれていますが、セミナーでも説明したように、Visual Studio 2013 に既定で入っている SPA (Single Page Application) のプロジェクト テンプレートでは、ToDo のサンプルは含まれていないんです。そう、Knockout.js のライブラリーと、ログイン処理くらいしか入っておらず、全部自前でプログラミングする必要があるんですね。

さあ、大変です。まさしく Knockout という感じですが、ご安心ください。
当日のデモでご紹介したように、新しくなった Visual Studio 2013 を使用すると、簡単に SPA のアプリケーションが構築できてしまいます。Visual Studio 2012 の時の、あの完成度が高くて難解な ToDo サンプル (データ部分の model、接続などを管理する datacontext、それらをつなぐ viewmodel など、ずいぶんときれいに設計されていました) 以上に、Visual Studio 2013 が提供する「作りやすさ」のほうが欲しくなるはずです。

今回は、セッションで時間がなく飛ばしてしまった部分も含め、その内容をおさらいしてみましょう。

 

Web API の作成

UI を作る前に、データを用意して Web API で公開する必要があります。Visual Studio 2013 だと、そんなデータの公開もあっという間に終わってしまいます。

まずは、Visual Studio 2013 を起動して、[ASP.NET Single Page Application] のプロジェクトを新規作成してください。

今回は Code First で作成するため、ソリューション・エクスプローラーで [Models] を右クリックして、[追加] - [クラス] で以下の Product.cs を新規作成します。
セミナーで解説しましたが、今回は ProductId を内部的なキーとして使用したいので、下記の通り ScaffoldColumn(false) を指定しておきます。

. . .
using System.ComponentModel.DataAnnotations;
. . .

public class Product
{
  [ScaffoldColumn(false)]
  public int ProductId { get; set; }
  public string Name { get; set; }
  public int Price { get; set; }
  public int Count { get; set; }
}
. . .

このあとのデータ コンテキストの作成のため、いったんリビルドしておきましょう。

つぎに、このデータを使って、Web API を作成します。
ソリューション エクスプローラーで [Controllers] を右クリックして、[追加] - [新しくスキャフォールディングされたアイテム] (New Scaffold Item) を選択します。

すると、ダイアログ ボックスが表示されるので、[Entity Framework を使用したアクションがある Web API 2 コントローラー] を選択します。(なお、作成する Scaffolding アイテムのタイプも、MVC、Web API など、用意されているもの以外にアドイン可能です。)
つぎに、下図のようなダイアログ ボックスが表示されるので、ここで [非同期コントローラー アクションを使用します] をチェックして、モデル クラスとして、さきほど作成した Product クラスを選択します。今回は Entity Framework のデータ コンテキストも新規作成するので、下図の [新しいデータ コンテキスト] をクリックし、作成するデータ コンテキストの名前を指定します。

上図で [追加] ボタンを押すと、データ コンテキストと ASP.NET Web API が自動生成されます。試しに、作成された Controllers/ProductController.cs を見ると、下記のような CRUD の処理を持つ ApiController が生成されているのが確認できます。

public class ProductController : ApiController
{
  private ProductContext db = new ProductContext();

  // GET api/Product
  public IQueryable<Product> GetProducts()
  {
    return db.Products;
  }

  // GET api/Product/5
  [ResponseType(typeof(Product))]
  public async Task<IHttpActionResult> GetProduct(int id)
  {
    . . . (skip code) . . 
  }

  // PUT api/Product/5
  public async Task<IHttpActionResult> PutProduct(int id, Product product)
  {
    . . . (skip code) . . .
  }

  // POST api/Product
  [ResponseType(typeof(Product))]
  public async Task<IHttpActionResult> PostProduct(Product product)
  {
    . . . (skip code) . . .
  }

  // DELETE api/Product/5
  [ResponseType(typeof(Product))]
  public async Task<IHttpActionResult> DeleteProduct(int id)
  {
    . . . (skip code) . . .
  }
  . . .
}

たったこれだけの手順で、基本的なデータベースと Web API の作成は完了です。
ちゃんと Web API が動いていることを確認するため、試しに、デバッグ実行をおこない、Fiddler、Advanced REST Client (Chrome)、curl などを使ってデータを登録してみてください。

 

Entity Framework 6 のフル活用

セミナーでも簡単に説明しましたが、Visual Studio 2013 のテンプレートでは、既定で Entity Framework 6 が使用されています。そうです、以前「ASP.NET MVC で非同期 (Async) を乗りこなす」でも紹介した非同期に対応しています。

上記の ProductController.cs を見ていただくと、下記の通り、async の Web API と、async のデータベース アクセスが使用されています。今回から、Entity Framework を使ったデータベースの IO 待ちの間も、スレッドを消費することはありません。1 つのスレッドで、多くのスループットを処理することができます。

public class ProductController : ApiController
{
  private ProductContext db = new ProductContext();
  . . .

  // GET api/Product/5
  public async Task<IHttpActionResult> GetProduct(int id)
  {
    Product product = await db.Products.FindAsync(id);
    if (product == null)
    {
      return NotFound();
    }

    return Ok(product);
  }
  . . .

  // POST api/Product
  public async Task<IHttpActionResult> PostProduct(Product product)
  {
    if (!ModelState.IsValid)
    {
      return BadRequest(ModelState);
    }

    db.Products.Add(product);
    await db.SaveChangesAsync();

    return CreatedAtRoute("DefaultApi", new { id = product.ProductId }, product);
  }
  . . .

}

さて、そうなってくると、今度はデータベース側のチューニングをちゃんとしないと、ボトルネックになってしまいますね。なにせ、今まで以上に、どんどん軽快に処理がやってくるわけですから。。。
そんなときは、Entity Framework 6 の Logging を使用して、どのような SQL が投げられているか確認しておくと良いでしょう。下記の通り、コントローラーのコンストラクタに記述すれば OK です。

. . .
using System.Diagnostics;
. . .

public class ProductController : ApiController
{
  private ProductContext db = new ProductContext();

  public ProductController()
  {
    db.Database.Log = (l) =>
      {
        Debug.Write(l);
      };
  }
  . . .

デバッグ時の出力画面を使って、下図のように、どんな SQL が投げられているか確認できますので、SQL チューニングに役立ててください。

Entity Framework 6 には、この他にも、Windows Azure SQL Database における Connection timeout への対応など (retry の対応など) 数多くの必須の機能がありますので、是非活用してください。

 

ページの作成 (Single Page Application - Knockout.js)

データと Web API の準備が完了しましたので、さいごに、この Web API を使ってページ (UI, HTML) を作成します。

今回は、既に作成されている Home/Index.cshtml のソースをいったん全部削除して、ここに簡単に記述してみましょう。(ソースを削除して、「html5」と入力して Tab キーを押すと、HTML5 の DOCTYPE などソースのひな形が挿入できます。)

まず、ページ (View) と関連付ける (Bind する) ための View Model を JavaScript で作成します。
View Model は、Knockout (ko) の Observable オブジェクトなどを使って下記の通り定義します。ちょっと大変ですが、ちゃんとインテリセンスが効きますので、ここだけ頑張って記述してみてください。

<!DOCTYPE html>
<html>
<head>
  <title></title>
  @Scripts.Render("~/bundles/jquery")
  @Scripts.Render("~/bundles/knockout")
  <script type="text/javascript">
    $(document).ready(function () {
      function Product() {
        this.productId = ko.observable();
        this.name = ko.observable();
        this.price = ko.observable();
        this.count = ko.observable();
        this.total = ko.computed(function () {
          return this.price() * this.count();
        }, this);
      };
      var productViewModel = {
        products: ko.observableArray(),
        newproduct: {
          name: ko.observable(""),
          price: ko.observable(0),
          count: ko.observable(0)
        }
      };
      productViewModel.loadProducts = function () {
        this.products([]); // reset
        $.ajax({
          type: "GET",
          url: "/api/Product/",
          dataType: "json"
        })
        .done(function (result) {
          $.each(result, function (i, p) {
            productViewModel.products.push(new Product()
              .productId(p.productId)
              .name(p.name)
              .price(p.price)
              .count(p.count)
            );
          });
        });
      };
      productViewModel.addProduct = function () {
        var dat = {
          name: this.newproduct.name(),
          price: this.newproduct.price(),
          count: this.newproduct.count()
        };
        $.ajax({
          type: "POST",
          url: "/api/Product/",
          contentType: "application/json",
          dataType: "json",
          data: JSON.stringify(dat)
        })
        .done(function (result) {
          productViewModel.products.push(new Product()
            .productId(result.productId)
            .name(result.name)
            .price(result.price)
            .count(result.count)
          );
        })
      };
      ko.applyBindings(productViewModel);
    });
  </script>
</head>
<body></body>
</html>

やっている内容は簡単で、最終的にバインド (applyBindings) する productViewModel オブジェクトに、必要なプロパティ (オブジェクト) やメソッド (正確には「関数」) を定義しています。
特に、Product の一覧を取得する loadProducts 関数と、Product を 1 件追加する addProduct 関数では、上記で作成した Web API を呼び出している点に注意してください。

さて、バインドするオブジェクト (View Model) の定義さえできてしまったら、あとは HTML を Knockout のマークアップを使って記述するだけです。
例えば、以下のように記述してみましょう。(プログラマーの方なら、特に説明は不要ですね。。。) 今回は、あえて、下記のコードのコピー/ペーストはおこなわずに記載してみてください。

. . .

<body>
  <ul data-bind="foreach: products">
    <li>
      <div data-bind="text: name" />
    </li>
  </ul>
  <button data-bind="click: loadProducts">Load</button>
</body>
. . .

このコードでは、ページを表示して「Load」ボタンを押すと、下図の通り、登録されているデータ (Product) が検索され、その Name の一覧が表示されます。

コードの編集中に気付いたかと思いますが、下図のように、Knockoout のマークアップでもインテリセンス (Intellisense) が使用可能で、ほとんど文字を入力することなくコードがすいすいと書けてしまいます。(実際には稀にインテリセンスが効かない箇所があり不安になりますが、まあ、そこはちょっと我慢してください。。。)

こうしたインテリセンスの機能は、同様に AngularJS のマークアップ (「ng-」など) でも使用できます。

Visual Studio 2013 では、もちろん、Knockout のマークアップ以外にも、HTML 編集の補助機能がさらに進化していますので、下記のような多少複雑なマークアップのアプリケーションも、そこそこ快適に作成できてしまいます。(ごめんなさい、UI がタコい点はサンプルなのでご容赦を。。。)

. . .
<body>
  <div class="header">
    <span data-bind="text: products().length"></span>
    <span>Products</span>
    <br />
    <button data-bind="click: loadProducts">Load all</button>
  </div>
  <div class="results">
    <table>
      <thead>
        <tr>
          <th>Name</th>
          <th>Price</th>
          <th>Count</th>
          <th>Total</th>
        </tr>
      </thead>
      <tbody data-bind="foreach: products">
        <tr>
          <td data-bind="text: name"></td>
          <td data-bind="text: price"></td>
          <td data-bind="text: count"></td>
          <td data-bind="text: total"></td>
        </tr>
      </tbody>
    </table>
  </div>
  <div class="newitem" style="border:dotted;">
    <form data-bind="submit: addProduct">
      <input type="submit" value="New" /><br />
      Name:<input type="text" data-bind="value: newproduct.name" /><br />
      Price:<input type="text" data-bind="value: newproduct.price" /><br />
      Count:<input type="text" data-bind="value: newproduct.count" /><br />
    </form>
  </div>
</body>
. . .

 

なお、今回はサンプルとしてすべて自作しなおしましたが、既定のテンプレートでは ToDo テンプレートこそ無いものの、いろいろと便利な実装が入っていますので確認してみてください。
例えば、複数のビュー (既定のプロジェクト テンプレートでは Partial View を使用) を Single Page で切り替えるために (かつ、ビューごとに異なる View Model を関連付けるために)、knockout の with と computed を使用したエレガントな手法を使っています。このため、Scripts/app/home.viewmodel.js の HomeViewModel に必要な View Model を追加し、Views/Home/_Home.cshtml の Partial View を自身で構築することで、プロジェクト テンプレートで提供されているログイン処理などをそのまま利用しながら、アプリケーション本体のページのみを knockout でデザインできます。(2013/11/07 追記 : この手法については、「Knockout.js で Multiple View (Partial View) をエレガントに切り替える方法」に記載しました。)
また、サーバー側の Session の代わりとして sessionStorage や localStorage を使用して必要な情報を維持したり、History の実装などもされています。個人ユーザー アカウント (Individual User Accounts) を使用した際のログイン処理も、従来のフォーム認証のような Cookie は使用せず、今風な感じになっています。(sessionStorage や localStorage に access token を記録し、このトークンを HTTP ヘッダーに設定して Web API を呼び出します。)
実際の開発では間違いなくこうした手法が必要になると思いますので、どんなことをやっているか見ておくと良いでしょう。

Knockout そのものをこれから学びたい方は、Build Insider「3つのMVC系人気フレームワーク、Backbone.js/AngularJS/Knockout.js」で大変わかりやすく解説してくれていますので、こちらも参考にしてみてください。

 

Comments (0)

Skip to main content