データ ポイント

Knockout.js を使用して Web アプリケーションで OData をデータ バインドする

Julie Lerman

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

Julie Lermanデータを専門としている私は、非常に多くの時間をバックエンド コードの記述に費やし、クライアント側での楽しみの多くを経験し損ねています。以前このコラムを執筆していた John Papa は、現在はこのマガジンでクライアント テクノロジに関するコラムを執筆しており、Knockout.js と呼ばれる今話題の新しいクライアント側テクノロジを頻繁に取り上げています。Papa や他の人たちの Knockout.js に対する情熱に影響され、地元のユーザー グループ VTdotNET で MyWebGrocer.com (英語) の Jon Hoguet が Knockout に関するプレゼンテーションを行ってくれるという申し出に飛びつきました。そのミーティングには、普段より多くの参加者が集まり、.NET コミュニティ以外の開発者もいました。Hoguet がプレゼンテーションを進めていくうちに、なぜそれほど多くの Web 開発者が Knockout に惹かれるのかがはっきりわかりました。Knockout を使用すると、モデル - ビュー - ビューモデル (MVVM: Model-View-ViewModel) パターンの利用により、Web アプリケーションにおけるクライアント側のデータ バインドが簡単になるのです。データ バインドといえば、私が専門とするデータを使ってできることですね。翌日には、既に、Knockout.js とデータ アクセスを組み合わせる方法について学んでいました。それでは、今回学んだことをお伝えしましょう。

警告しておきますが、私の JavaScript スキルは非常に不足しているため、目的の達成には普通より少し時間がかかりました。ですが、このコラムの読者の中には、私と同様、JavaScript にあまり詳しくない方も大勢いらっしゃるでしょうから、私の少しずつの歩みを歓迎していただけるかもしれません。Knockout についての理解をさらに深めるには、Papa のコラムや、Papa が Pluralsight.com (英語) で提供しているすばらしいコースを活用してください。

今回の目標は、Knockout.js を使用して、WCF Data Services から取得したデータにバインドし、その後、そのデータを更新する方法を理解することです。今回のコラムでは、重要な機能を果たす部分を紹介します。各部分をどのように組み合わせるかは、付属のダウンロード サンプルで確認してください。

まず、既存の WCF Data Services を基礎として使用しました。実際のところ、2011 年 2 月号の「データ ポイント」コラム「WCF Data Services で Entity Framework の検証を処理する」(msdn.microsoft.com/magazine/hh580732、英語) で使用したのと同じサービスですが、最近リリースされた WCF Data Services 5 に更新してあります。

ちなみに、このときのデモウェア モデルは次のような 1 つのクラスで構成されていました。

public class Person
{
  public int PersonId { get; set; }
  [MaxLength(10)]
  public string IdentityCardNumber { get; set; }
  public string FirstName { get; set; }
  [Required]
  public string LastName { get; set; }
}

次のように、DbContext クラスによって DbSet 内の Person クラスが公開されます。

public class PersonModelContext : DbContext
{
  public DbSet<Person> People { get; set; }
}

その後、WCF Data Services によってその DbSet が読み取りと書き込み用に公開されます。

public class DataService : DataService<PersonModelContext>
{
  public static void InitializeService(DataServiceConfiguration config)
  {
    config.SetEntitySetAccessRule("People", EntitySetRights.All);
  }
}

Knockout.js を使用すると、クライアント側スクリプトを、データ バインドされたオブジェクトのプロパティ値の変化に反応させることができます。たとえば、スクリプト内で、オブジェクトを渡して Knockout の applyBindings メソッドを呼び出すと、データ バインドされている要素すべてに、そのオブジェクトのプロパティに加えられた更新が通知されます。Knockout が項目の格納や取り出しを監視しているコレクションについても、同様の動作を実現できます。こうした動作はすべてクライアントで行われ、イベント処理スクリプトを記述したり、支援を求めてサーバーに戻る必要はありません。

今回の目標を達成するには、次のようにいくつか作業を行う必要がありました。

  • Knockout 対応のビュー モデルを作成する。
  • OData を JSON 形式で取得する。
  • OData の結果をビュー モデル オブジェクトに格納する。
  • Knockout.js を使用してデータをバインドする。
  • 更新のため、ビュー モデルの値を OData 結果オブジェクトに戻す。

Knockout 対応のビュー モデルを作成する

このためには、Knockout がそのオブジェクトのプロパティを "監視" できる必要があります。オブジェクトのプロパティを定義する際に Knockout を使用すると、これが可能になります。でもちょっと待ってください。Knockout が値の変化を監視できるようにドメイン オブジェクトを UI 固有のロジックで "汚す" ことを勧めているわけではありません。ここで MVVM パターンの出番です。MVVM を使用すると、モデルの UI 固有バージョン (ビュー) を作成することができます。これが、MVVM の "VM" (ビューモデル) の部分です。つまり、好みの方法 (WCF Data Services に対するクエリ実行、サービスへのアクセス、新しい Web API 経由など) でデータを取得してアプリケーションに取り込んでから、ビューに合わせて結果の形式を変更できます。たとえば、今回使用しているデータ サービスは、FirstName、LastName、および IdentityCardNumber の情報を含む Person 型を返します。しかし、ビューで使用したいのは FirstName と LastName だけです。オブジェクトのビュー モデル バージョンで、ビュー固有のロジックを適用することも可能です。これにより、両方のメリットを利用することができます。つまり、データ ソースによって何が提供されるかにかかわらず、明確にビューに対象を限定したオブジェクトが手に入ります。

今回 JavaScript オブジェクト内に定義した、クライアント側の PersonViewModel を以下に示します。

function PersonViewModel(model) {
  model = model || {};
  var self = this;
  self.FirstName = ko.observable(model.FirstName || ' ');
  self.LastName = ko.observable(model.LastName || ' ');
}

サービスから何が返されるかにかかわらず、ビューで使用したいのは姓と名だけなので、含めるプロパティはこの 2 つだけです。姓と名を、文字列ではなく Knockout の observable オブジェクトとして定義しているのがわかります。後で説明しますが、値を設定する際はこれを覚えておくことが重要です。

OData を JSON 形式で取得する

今回使用する OData クエリは、単に、データ サービスから最初の Person を返します。次のように、現在、その取得先は開発サーバーです。

http://localhost:43447/DataService.svc/People?$top=1

既定では、OData の結果は、(XML を使用して表現される) ATOM として返されます。ですが、Knockout.js で処理できるのは JSON データであり、OData は JSON データも提供することができます。したがって、JavaScript 内で直接処理を行っている場合は、XML を処理するより JSON 形式の結果を処理する方がはるかに簡単です。JavaScript 要求で、OData クエリにパラメーター ("$format=json") を追加して、結果が JSON として返されるよう指定します。ただし、そのためには、使用しているデータ サービスが format クエリ オプションの処理方法を認識している必要があります。今回使用しているデータ サービスは format クエリ オプションの処理方法を認識していません。この方法を使用する場合は (たとえば、AJAX を使用して OData 呼び出しを行う場合など)、JSON の出力をサポートするためにサービスで拡張機能を使用する必要があります (詳細については、bit.ly/mtzpN4 (英語) 参照)。

ただし、OData 用の datajs ツールキット (datajs.codeplex.com、英語) を使用しているため、これを気にする必要はありません。datajs ツールキットの既定の動作では、JSON 形式の結果が返されるように、要求にヘッダー情報が自動的に追加されます。したがって、自身でデータ サービスに JSONP 拡張機能を追加する必要はありません。datajs ツールキットの OData オブジェクトには次のような使い方ができる read メソッドがあり、このメソッドを使用すると、JSON 形式の結果を返すクエリを実行することができます。

OData.read({
  requestUri: http://localhost:43447/DataService.svc/People?$top=1"
  })

OData を PersonViewModel に格納する

結果が返されたら (今回の場合は、ドメイン モデルで定義されているとおり、1 つの Person 型です)、その結果を基に PersonViewModel インスタンスを作成します。今回作成した personToViewModel という JavaScript メソッドは、次のように、Person オブジェクトを受け取り、このオブジェクトの値を基に新しい PersonViewModel を作成して、PersonViewModel を返します。

function personToViewModel(person) {
  var vm=new PersonViewModel;
  vm.FirstName(person.FirstName);
  vm.LastName(person.LastName);
  return vm;
}

プロパティがメソッドであるかのように、新しい値を渡して値を設定していることに注目してください。当初、"vm.FirstName=person.FirstName" というコードを使用して値を設定していました。ですが、そうすると、FirstName が observable でなく文字列になってしまいました。なぜその後の値の変化が Knockout に認識されないのかを調べようとしばらく奮闘しましたが、結局あきらめて助けを求めました。プロパティは文字列でなく関数なので、プロパティの値はメソッド構文を使用して設定する必要があります。

クエリが実行されたら personToViewModel を実行したいと思っています。OData.read を使用すると、クエリが正常に実行された場合に使用するコールバック メソッドを通知できるので、これは可能です。今回は、mapResultsToViewModel というメソッドに結果を渡します。すると、このメソッドによって personToViewModel が呼び出されます (図 1 参照)。ソリューション内の他の場所で、peopleFeed 変数を "http://localhost:43447/DataService.svc/People" として定義済みです。

図 1 クエリの実行と応答の処理

OData.read({
  requestUri: peopleFeed + "?$top=1"
  },
  function (data, response) {
    mapResultsToViewModel(data.results);
  },
  function (err) {
    alert("Error occurred " + err.message);
  });
  function mapResultsToViewModel(results) {
    person = results[0];
    vm = personToViewModel(person)
    ko.applyBindings(vm);
}

HTML コントロールにバインドする

mapResultsToViewModel メソッド内の ko.applyBindings(vm) というコードに注目してください。これが、Knockout のしくみのもう 1 つの重要な側面です。ですが、何にバインドを適用しているのでしょう。それはマークアップ内で定義します。マークアップ コードでは、次のように、Knockout の data-bind 属性を使用して、PersonViewModel の値をいくつかの input 要素にバインドします。

<body>
  <input data-bind="value: FirstName"></input>
  <input data-bind="value: LastName"></input>
  <input id="save" type="button" value="Save" onclick="save();"></input>
</body>

データを表示したいだけであれば、label 要素を使用し、値へのデータ バインドではなくテキストへのバインドを行うという方法もあります。たとえば、次のような形です。

<label data-bind="text: FirstName"></label>

しかし、今回は編集を行う必要があるため、単に input 要素を使用するだけでなく、Knockout の data-bind 属性で、こうしたプロパティの値にバインドすることも指定します。

Knockout が今回のソリューションに提供する主な要素は、ビュー モデルの監視可能なプロパティ、マークアップ要素の data-bind 属性、そして、プロパティ値が変化する際にこうしたマークアップ要素への通知に必要な実行時ロジックを追加する applyBindings メソッドです。

ここまでの段階のアプリケーションを実行すると、クエリから返される人物をデバッグ モードで確認できます (図 2 参照)。

Person Data from the OData Service
図 2 OData サービスから返された Person データ

図 3 は、ページに表示された PersonViewModel プロパティの値を示しています。

The PersonViewModel Object Bound to Input Controls
図 3 入力コントロールにバインドされた PersonViewModel オブジェクト

データベースに戻して保存する

Knockout のおかげで、保存の際に input 要素から値を抽出する必要はありません。フォームにバインドされた PersonViewModel オブジェクトは Knockout によって既に更新されています。save メソッドでは、PersonViewModel の値を (サービスから返された) person オブジェクトに格納し、その後、サービスを通じてこうした変更をデータベースに保存します。コード サンプルをダウンロードすると、もともと OData クエリから返された person インスタンスを保持しておき、ここでそのオブジェクトを使用しているのがわかります。viewModeltoPerson メソッドを使用して person を更新したら、更新した person を要求オブジェクトの一部として OData.request に渡すことができます (図 4 参照)。要求オブジェクトは 1 つ目のパラメーターで、URI、メソッド、およびデータで構成します。request メソッドの詳細については、datajs のドキュメント (bit.ly/FPTkZ5、英語) を参照してください。person インスタンスのバインド先の URI が person インスタンスの __metadata.uri プロパティに格納されているのを利用しているのがわかります。このプロパティを使用すると、"http://localhost:43447/DataService.svc/People(1)" という URI をハードコードする必要がありません。

図 4 変更をデータベースに保存

function save() {
  viewModeltoPerson(vm, person);
  OData.request(
    {
      requestUri: person.__metadata.uri,
      method: "PUT",
      data: person
    },
    success,
    saveError
    );
  }
  function success(data, response) {
    alert("Saved");
  }
  function saveError(error) {
    alert("Error occurred " + error.message);
  }
}
function viewModeltoPerson(vm,person) {
  person.FirstName = vm.FirstName();
  person.LastName = vm.LastName();
}

データに変更を加え (たとえば、Julia を Julie に変更するなど)、[Save] ボタンをクリックすると、エラーが発生していないことを示す "Saved" というメッセージが表示されるだけでなく、次のように、プロファイラーでデータベースの更新を確認することができます。

    exec sp_executesql N'update [dbo].[People]
    set [FirstName] = @0
    where ([PersonId] = @1)
    ',N'@0 nvarchar(max) ,@1 int',@0=N'Julie',@1=1

大衆向けの Knockout.js と WCF Data Services

Knockout.js について学んでいたら、.NET プラットフォームを使用する開発者だけでなくあらゆるタイプの開発者が使用できるいくつかの新しいツールについて学ぶことにもなりました。また、さびついていた JavaScript スキルを発揮することにもなりましたが、今回記述したコードは、コントロールとのやり取りという単調で骨の折れる作業ではなく、オブジェクト操作というなじみのあるタスクに的を絞ったものです。また、Knockout.js について学んだことにより、MVVM アプローチを利用して、モデル オブジェクトと UI に表示するものとを区別することになり、優れたアーキテクチャの採用にもつながりました。Knockout.js でできることは、ほかにもたくさんあります (特に、反応の良い Web UI の作成に関しては)。WCF Web API (bit.ly/kthOgY、英語) などの優れたツールを使用してデータ ソースを作成することもできます。私は、専門家の方々から今後さらにいろいろ学び、クライアント側での経験を得るための口実を見つけられることを楽しみにしています。

Julie Lerman は、バーモント ヒルズ在住の Microsoft MVP、.NET の指導者、およびコンサルタントです。世界中のユーザー グループやカンファレンスで、データ アクセスなどの Microsoft .NET トピックについてプレゼンテーションを行っています。彼女のブログは thedatafarm.com/blog (英語) で、彼女は『Programming Entity Framework』(O’Reilly Media、2010 年) および『Programming Entity Framework: Code First』(O’Reilly Media、2011 年) の著者でもあります。Twitter (twitter.com/julielerman、英語) で彼女をフォローしてください。

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