HTML5

Apache Cordova を使って Windows Phone の HTML 5 アプリを開発する

Colin Eberhardt

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

今回は Apache Cordova を取り上げ、これを使って Windows Phone アプリを開発する方法を説明します。Apache Cordova は、HTML5 と JavaScript を使ってクロスプラットフォームのモバイル アプリを作成するためのフレームワークです。

Windows Phone とそのネイティブ開発プラットフォームを使用すると、見栄えの良い Metro スタイルのアプリを容易に作成できます。先日の Nokia とのパートナーシップ締結により、Windows Phone はますますその勢いを増しています。

調査会社の Gartner 社が最近発表したデータによると、マイクロソフトの OS はこの競争の激しい市場でかなりのシェアを占める可能性が高いと予測されています (bit.ly/h5Ic32、英語)。スマートフォン アプリの開発者がこの競争の激しい市場に参入するには、ターゲットにする OS を絞り込むか、それぞれのスマートフォンが必要とするさまざまな言語 (C#、Java、Objective-C) を使って同じアプリをターゲットごとに繰り返し作成することが求められます。

しかし、今回はもう 1 つの方法を取り上げます。デスクトップでは今でも古いブラウザーが使われることがありますが、スマートフォンはいずれも、同じシリーズのデスクトップ版よりもさまざまな点で優れた高機能ブラウザーを備えています。最新のスマートフォンをターゲットにする場合は、HTML5、JavaScript、CSS を組み合わせて、ブラウザー内で実行するアプリを作成します。これらの技術を使うと、ブラウザー ベースのアプリを 1 つ作成し、それを幅広いスマートフォン デバイスで実行することが、潜在的には可能になります。

Apache Cordova の概要

JavaScript と HTML5 コンテンツを備えたパブリックな Web ページを作成し、ユーザーをホスティング先の URL にリダイレクトすれば、HTML5 ベースのモバイル アプリを作成できます。しかし、この方法にはいくつか問題があります。最初の問題は、オンライン マーケットプレースやオンライン ストアを利用する配布モデルです。作成した Wep アプリをホストする URL をマーケットプレースへ提出することはできません。そのため、このアプリから収益を上げる方法が問題です。もう 1 つは、スマートフォンのハードウェアにアクセスする方法が問題です。スマートフォン上の連絡先、通知、カメラ、センサーなどにアクセスするための幅広いサポートを提供するブラウザー API はありません。Apache Cordova (以下、簡潔にするため Cordova とします) は、こうした問題を解決する無償のオープン ソース フレームワークです。

Cordova は Nitobi が開発した PhoneGap としてスタートしました。2011 年 10 月、Nitobi は Adobe System Inc. に買収され、PhoneGap フレームワークは Apache Software Foundation の下で、ブランドを Cordova と変えてオープンソースになりました。現在もまだ過渡期にあるツールです。

Cordova は、簡単なネイティブ ラッパー内で HTML5/JavaScript コンテンツをホストするための環境を提供します。Cordova はスマートフォンの OS ごとに、アプリのアセットを再頒布可能コードとしてバンドルし、ネイティブ ブラウザー コントロールを使ってアプリのコンテンツをレンダリングします。Windows Phone であれば、開発した HTML5 のアセットを XAP ファイルにパッケージ化し、Cordova アプリの開始時にこのアセットを分離ストレージに読み込みます。実行時には、WebBrowser コントロールがコンテンツをレンダリングし、JavaScript コードを実行します。

Cordova は、さまざまなスマートフォンに共通する機能にアクセスするための標準 API のセットも提供します。共通の機能には次のようなものがあります。

  • アプリ ライフサイクルのイベント
  • ストレージ (HTML5 ローカル ストレージとデータベース)
  • 連絡先
  • カメラ
  • 地理位置情報
  • 加速度計

これらの機能は JavaScript コードから使用できる JavaScript API として公開されます。Cordova は、コードが実行されるスマートフォンの OS を問わず同じ JavaScript API を使って作業できるように、必要なネイティブ実装の提供にかかわる面倒な作業をすべて行います (図 1 参照)。

Cordova Allows the Same HTML5 Application to Run Across a Range of Mobile OSes
図 1 Cordova によって同じ HTML5 アプリを幅広いモバイル OS で実行可能

今回は、Visual Studio で開発を行い、エミュレーターまたは実際のデバイスを使ってテストを行う、言わば Windows Phone の開発者の観点から Cordova について解説します。Cordova はクロスプラットフォーム テクノロジですが、通常、開発者は好みのエディターや統合開発環境 (IDE) を使って開発を行います。つまり、iOS の開発者は Xcode を使って Cordova アプリを開発し、Android の開発者は Eclipse を使います。

Cordova にも Build (build.phonegap.com、英語) というクラウド ベースのビルド サービスがあり、開発者はここから HTML5/JavaScript コンテンツを提出できます。提出してからしばらくすると、Cordova がサポートするほとんどのプラットフォーム向けの配布物が返されます。つまり、さまざまなプラットフォーム向けのアプリをビルドするために、プラットフォーム固有の IDE (または Mac コンピューター) は必要ありません。Build サービスは Adobe 社の所有物であり、現在はベータ版として無料で使用できます。オープンソース プロジェクトには引き続き無料で提供されます。

ツールを入手する

ここでは、Windows Phone の開発環境として、既に Visual Studio、Windows Phone SDK、Zune (オプション) を使用していることを前提としています。まだ手元にない場合は、Visual Studio 2010 Express for Windows Phone (bit.ly/dTsCH2、英語) をダウンロードして、ツールを無償で入手できます。

Cordova の開発者ツールの最新版は PhoneGap の Web サイト (phonegap.com、英語) から入手できます。今後のリリースは Apache (incubator.apache.org/cordova、英語) から配布される予定です。ダウンロードには、サポート対象のすべてのプラットフォーム向け Cordova アプリの開発に必要なテンプレート、ライブラリ、スクリプトが含まれます。今回はもちろん Windows Phone バージョンを使います。

Cordova ツールをダウンロードしたら、Windows Phone 向け入門ガイド (phonegap.com/start#wp、英語) に従って、Visual Studio のテンプレートをインストールします。"Hello World" スタイルのアプリは、提供されるテンプレートを基に新しいプロジェクトを作成するだけで、簡単に作成できます (図 2 参照)。

Cordova for Windows Phone Includes a Visual Studio Template
図 2 Cordova for Windows Phone は Visual Studio テンプレートを含む

テンプレートを基に作成したプロジェクトをビルドして、エミュレーターに配置すると、"Hello Cordova" メッセージが表示されます (図 3 参照)。

The Cordova Template Application Running on an Emulator
図 3 エミュレーターで実行中の Cordova テンプレート アプリ

Windows Phone の Cordova アプリのしくみ

Cordova アプリは、内部のしくみについての知識があまりなくても開発できますが、テンプレートが生成するさまざまなファイルについて理解しておくと役に立ちます (図 4 参照)。

The Cordova Template Folder Structure
図 4 Cordova のテンプレート フォルダー構造

図 4 に示す Cordova ファイルのみに注目して、上から順番に説明します。

  • GapLib/WP7CordovaClassLib.dll は Cordova アセンブリです。これには Cordova API の Windows Phone ネイティブ実装を含みます。
  • www は HTML5、JavaScript、CSS、画像などのアプリ アセットを配置するフォルダーです。テンプレートは、基本の index.html ファイルと master.css スタイルシートを生成します。
  • www/cordova-1.5.0.js は Cordova JavaScript API の Windows Phone 実装を提供します。これは WP7CordovaClassLib に含まれるネイティブ コードとのインターフェイスです。
  • BuildManifestProcessor.js はビルド後の手順で呼び出される JavaScript ファイルです。このファイルは CordovaSourceDictionary.xml ファイルを生成し、www フォルダーに追加したアセットがすべて分離ストレージに読み込まれるようにします。
  • CordovaSourceDictionary.xml はすべてのアプリ アセットを一覧するよう生成される XML ファイルです。この XML ファイルは、アプリの初回起動時に分離ストレージに読み込むファイルを指定します。

MainPage.xaml ファイルは、CordovaView コントロールのインスタンスを含みます。このコントロールは、WebBrowser コントロールを含むユーザー コントロールです。

<Grid x:Name="LayoutRoot">
    <my:CordovaView Name="PGView" />
  </Grid>

アプリの開始時に、CordovaView コントロールはアプリのアセットをローカル ストレージに読み込み、www/index.html ファイルに移動してアプリを起動します。この XAML を編集して、ページに他の Silverlight コントロールを配置することも、もちろん可能ですが、お勧めしません。HTML5 アプリを作成している場合の目的は、おそらくクロスプラットフォームで動作させることでしょう。MainPage.xaml にコントロールを追加すると、当然 Windows Phone ビルド固有になってしまいます。

Cordova アプリを開発する

HTML、JavaScript、CSS の各ファイルを www フォルダーに追加し、[ビルド アクション] を [コンテンツ] に設定すれば、これらのファイルがプロジェクトに含まれ、アプリの実行時にブラウザー コントロールからアクセスできます。スマートフォンのブラウザーとの互換性があれば、標準の JavaScript/HTML5 ライブラリやフレームワークを Cordova アプリで使用できます。

Cordova API については、Cordova の Web サイトにドキュメントが用意されているため、ここでは詳しく触れません。重要な点は、deviceready イベントを待ってから API の他のメソッドを使う必要があることです。テンプレートから生成された index.html ファイルを調べると、デバイスの準備が整ってから UI を更新しているのがわかります。

<script type="text/javascript">
  document.addEventListener("deviceready",onDeviceReady,false);
  function onDeviceReady()
  {
    document.getElementById("welcomeMsg").innerHTML
      += "Cordova is ready! version=" + window.device.cordova;
    console.log(
      "onDeviceReady. You should see this " +
        "message in Visual Studio's output window.");
  }
</script>

上記のコードで使用しているコンソール オブジェクトによって、アプリにデバッグ出力を追加できます。デバッグ メッセージは Cordova から Visual Studio コンソールに送られます。

単一ページと複数ページのアプリ アーキテクチャ

Cordova アプリをビルドするときには、2 とおりのパターンを採用できます。

  • 複数ページのアプリ: 複数ページのアプリでは、アプリのさまざまな画面を表示するために、複数の HTML ページを使います。ページ間の移動は、アンカー タグで定義するリンクによって、ブラウザーが備える標準のしくみを使います。各 HTML ページは Cordova JavaScript コードとアプリの JavaScript へのスクリプト参照を含みます。
  • 単一ページのアプリ: 単一ページのアプリでは、1 つの HTML ファイルから Cordova とアプリの JavaScript を参照します。アプリのさまざまなページ間の移動は、レンダリングする HTML を動的に更新することで実現します。スマートフォンのブラウザーの観点からは、URL は同じままで、ページ間の移動は行われません。

この 2 とおりのパターンのいずれを選択するかによって、作成するコードの構造に大きな影響があります。

一般に、複数ページのパターンは、ほとんどが静的コンテンツから構成されるアプリに適しています。この方法では、現在 Web サイトで使用している HTML/CSS/JavaScript を使い、Cordova によってそれをパッケージ化し、アプリとしてスマートフォンに配信します。しかし、複数ページの方法にはいくつかデメリットがあります。まず、あるページから次のページにブラウザーを使って移動すると、ブラウザーは新しいページに関連付けられたすべての JavaScript を再度読み込み、解析する必要があります。Cordova のライフサイクルが実行され、JavaScript API と対応する C# の間のリンクを作成するため、かなりの時間一時停止します。次に、JavaScript コードが再度読み込まれるため、アプリの状態がすべて失われます。

単一ページのパターンでは、複数ページの方法にまつわる問題を解消できます。Cordova とアプリの JavaScript コードの読み込みは一度だけなので、UI の応答性に優れ、アプリの状態をあるページから次のページへ受け渡す必要もありません。この方法の唯一のデメリットは、ページ移動が行われるときに UI を更新する JavaScript コードが必要となり、複雑性が増すことです。

今回説明するデモ アプリは単一ページのパターンを使います。複数ページの方法を使った例については、DemoGAP CodePlex プロジェクト (demogap.codeplex.com、英語) の参照をお勧めします。この例では、Windows Phone アプリ内での Cordova API 機能の簡単なデモを示しています。

デモ アプリ

ここからは、1 つ以上のキーワードを使ってユーザーが Twitter を検索できる、簡単な Windows Phone アプリの "Cordova Twitter 検索" (図 5 参照) について説明します。

The Cordova Twitter Search Demo Application
図 5 Cordova の Twitter 検索デモ アプリ

このアプリでは Cordova と次のフレームワークを使用します。

  • jQuery と jQuery テンプレート: jQuery はブラウザーのドキュメント オブジェクト モデル (DOM) を操作するための事実上の標準フレームワークになりました。jQuery テンプレートはマイクロソフトが開発したプラグイン (bit.ly/8ZO2V1、英語) で、DOM にレンダリングできる再利用可能な HTML テンプレートを容易に作成できます。Twitter 検索ではこの jQuery テンプレートを使って、アプリ内のさまざまなページの UI を定義します。
  • Knockout JS: Knockout は Silverlight 開発者になじみのある方法でビューモデルを容易に構築し、それを絶えずビューと同期できる、モデル - ビュー -ビューモデル (MVVM: Model-View-ViewModel) フレームワークです。他にも多数ある JavaScript UI フレームワークの中から Knockout を選んだのは、この使いやすさが理由です。

ここでは Knockout の詳細には触れません。このフレームワークについてさらに詳しく知りたい方は、John Papa による最近の記事「Knockout 入門」(msdn.microsoft.com/magazine/hh781029、英語) の一読をお勧めします。MVVM パターンについて詳しくない方は (恥ずかしがることはありません)、Josh Smith の優れた記事「Model-View-ViewModel (MVVM) デザイン パターンによる WPF アプリケーション」(msdn.microsoft.com/magazine/dd419663) の一読をお勧めします。

Visual Studio を使用して JavaScript アプリを開発する

アプリを詳しく説明する前に、JavaScript アプリ開発についての注意点をいくつか添えておきます。JavaScript 開発者にとっての課題の 1 つは、言語の動的性質です。JavaScript を使うと厳密な型システムに制約されず、オブジェクトを動的にビルドできます。このことが JavaScript のエディターや IDE の開発者にとっての課題をもたらします。C# や Java などの厳密に型指定される言語では、型情報をコードのナビゲーションの拡張、リファクタリングや IntelliSense に使用できます。一方 JavaScript では、型情報がないことにより、通常、IDE から開発者に提供されるサポートが少なくなります。

さいわい、これは改善されてきており、Visual Studio 2010 では JavaScript コードの擬似実行を行って各オブジェクトの "形状" を決定することで、JavaScript に IntelliSense を提供しています。IntelliSense のサポートを最大限に活用するには、擬似実行にどのファイルを含めるかという "参照" のヒントをいくつか IDE に提供する必要があります。このデモ プロジェクトでは、すべてのファイルは参照コメントで始まり、intellisense.js ファイルをインクルードするよう IDE に指示しています。このファイルの内容は、下記に示すように、IDE がアプリの重要な JavaScript ファイルをすべてインクルードできるようにするための単なる参照のリストであり、これによってアプリ全体にわたって高品質な IntelliSense のサポートを提供できるようにしています。

/// Ensure IntelliSense includes all the files from this project.
///
/// <reference path="app.js" />
/// <reference path="viewModel/ApplicationViewModel.js" />
/// <reference path="viewModel/SearchResultsViewModel.js" />
/// <reference path="viewModel/TweetViewModel.js" />
/// <reference path="viewModel/TwitterSearchViewModel.js" />
/// <reference path="lib/jquery-1.6.4.js" />
/// <reference path="lib/cordova-1.5.0.js" />
/// <reference path="lib/knockout-1.2.1.js" />

JavaScript は厳密ではなく寛容な言語であり、値の強制やセミコロンの挿入などの機能によって、スクリプト環境で使いやすくなっています。しかし、大量のコードを管理する場合には、これらの機能が問題になることもあります。このため、JavaScript に厳密なコーディング標準を適用するためのツールである JSLint の使用を強く推奨します。よく使用される Visual Studio の拡張機能により JSLint のサポートを追加でき (jslint4vs2010.codeplex.com、英語)、lint エラーをエラー コンソールに出力することができます。Twitter 検索アプリでは、これまで関わってきた他のすべての JavaScript プロジェクトと同様に、JSLint を使用しています。

プロジェクトの各 JavaScript ファイルは "globals" コメントで始まっています。JSLint によって、誤って var キーワードを忘れたため、変数が意図せずにグローバル スコープになってしまうのを防ぐことができます。JSLint で "globals" コメントを使うと、どの変数がグローバル スコープになることができるかを正しく定義できます。

MVVM アプリ構造

Twitter 検索アプリは、スマートフォンのブラウザー コントロールの観点からは単一ページ アプリです。逆にユーザーの観点からは、図 5 に示すように複数のページを備えています。これをサポートするため、Knockout ビューモデルは、アプリ内のページをそれぞれ表すビューモデルのインスタンスのスタックを含むように構成します。ユーザーが新しいページに移動すると、対応するビューモデルがこのスタックに追加され、ユーザーがページを戻ると、最上位のビューモデルがスタックから消去されます (図 6 参照)。

図 6 Knockout の ApplicationViewModel

/// <reference path="..//intellisense.js" />
/*globals ko*/
function ApplicationViewModel() {
  /// <summary>
  /// The ViewModel that manages the ViewModel back-stack.
  /// </summary>
  // --- properties
  this.viewModelBackStack = ko.observableArray();
  // --- functions
  this.navigateTo = function (viewModel) {
    this.viewModelBackStack.push(viewModel);
  };
  this.back = function () {
    this.viewModelBackStack.pop();
  };
  this.templateSelector = function (viewModel) {
    return viewModel.template;
  }
}

アプリ開始時に、ApplicationViewModel のインスタンスを作成し、Knockout を使って UI にバインドします。

document.addEventListener("deviceready", initializeViewModel, false);
var application;
function initializeViewModel() {
  application = new ApplicationViewModel();
  ko.applyBindings(application);
}

UI 自体は簡単なもので、ビューモデル スタックをレンダリングするようにバインドされている Knockout テンプレートを使う div 要素を含みます。

<body>
  <h1>Cordova Twitter Search</h1>
  <div class="app"
    data-bind="template: {name: templateSelector,
                          foreach: viewModelBackStack}">
  </div>
</body>

Knockout テンプレートのバインドは、ビューモデル インスタンスの配列にバインドし、テンプレートによって各ビューの生成を行うという点で、Silverlight ItemsControl に似た方法で機能します。この場合、ApplicationViewModel の templateSelector 関数を呼び出し、各ビューモデルの名前付きテンプレートを決定します。

このアプリを実行すると、実際には何も実行されないことがわかります。それはアプリのページを表すビューモデルがないためです。

TwitterSearchViewModel

最初のビューモデルとして、アプリの最初のページを表す TwitterSearchViewModel について説明します。このビューモデルは、ユーザー入力フィールドにバインドされている searchTerm、Twitter API が HTTP によりクエリされるときに検索ボタンを無効にする監視可能なブール型である isSearching といった、UI をサポートする単純な監視可能プロパティをいくつか公開します。また検索ボタンにバインドされる検索機能も公開します。これは ICommand を Silverlight のボタンにバインドするのと同じ方法です (図 7 参照)。

図 7 TwitterSearchViewModel

/// <reference path="..//intellisense.js" />
/*globals $ application ko localStorage SearchResultsViewModel TweetViewModel*/
function TwitterSearchViewModel() {
  /// <summary>
  /// A ViewModel for searching Twitter for a given term.
  /// </summary>
  // --- properties
  this.template = "twitterSearchView";
  this.isSearching = ko.observable(false);
  this.searchTerm = ko.observable("");
  // --- public functions
  this.search = function () {
    /// <summary>
    /// Searches Twitter for the current search term.
    /// </summary>
    // implementation detailed later in this article ...
  };
}

ビューモデルのテンプレート プロパティによって、このビューモデルに関連付けられるビューに名前を付けます。このビューは index.html ファイル内で jQuery テンプレートとして記述されます。

<script type=text/x-jquery-tmpl" charset="utf-8" id="twitterSearchView" 
  <div>
    <form data-bind="submit: search">
      <input type="text"
        data-bind="value: searchTerm, valueUpdate: 'afterkeydown'" />
      <button type="submit"
        data-bind="enable: searchTerm().length > 0 &&
          isSearching() == false">Go</button> 
    </form>     
  </div>
</script>

TwitterSearchViewModel のインスタンスをアプリのビューモデル スタックに追加すると、アプリが最初のページを表示するようになります (図 8 参照)。

The TwitterSearchViewModel Rendered via the twitterSearchView Template
図 8 twitterSearchView テンプレートによりレンダリングされた TwitterSearchViewModel

CSS を使って Metro UI を作成する

Windows Phone OS の最も特筆すべき機能の 1 つは、ルック アンド フィールのすべての指針を提供する Metro デザイン言語です。このデザイン言語は、クロムよりもコンテンツを重視し、目を楽しませるだけでなく実用的であり、スマートフォンの小さなフォーム ファクター上でも非常に読みやすいインターフェイスを提供します。

図 8 に示す現在の UI は、標準ブラウザーのスタイルを使用しているため、あまり目を楽しませるものではありません。HTML と CSS を使って見た目も美しいモバイル UI を作成するための定評のあるフレームワークは、既にいくつか存在します。たとえば、jQuery Mobile (jquerymobile.com、英語) です。現在のところ、これらのフレームワークは iOS のルック アンド フィールのエミュレーションを行うことに注力しているようです。jQuery Mobile を使って Windows Phone の Cordova アプリのスタイルを作成することも可能ですが、OS の全体的な外観にそぐわないため、よく思わないユーザーもいるでしょう。

さいわい、クロムを使わない Metro のテーマは、HTML と CSS を使って非常に容易に再現できます。実際、Windows 8 は HTML を最優遇しており、Windows ランタイム API を使って HTML5 Metro アプリを開発できます。

簡単な CSS によって正しいフォント、フォント サイズおよび色を適用することにより (図 9)、Metro テーマに忠実な UI を作成できます (図 10)。

図 9 Metro テーマに沿ったコード

body
{
  background: #000 none repeat scroll 0 0;
  color: #ccc;
  font-family: Segoe WP, sans-serif;
}
h1
{
  font-weight: normal;
  font-size: 42.667px; /* PhoneFontSizeExtraLarge */
}
button
{
  background: black;
  color: white;
  border-color: white;
  border-style: solid;
  padding: 4px 10px;
  border-width: 3px; /* PhoneBorderThickness */
  font-size: 25.333px; /* PhoneFontSizeMediumLarge */
}
input[type="text"]
{
  width: 150px;
  height: 34px;
  padding: 4px;
}

The Twitter Search Application with a Metro CSS Style Applied
図 10 Metro CSS スタイルを適用した Twitter 検索アプリ

またネイティブ アプリでなく HTML5 アプリであることから、ユーザーは "ピンチ" を使った UI の拡大や縮小が可能になってしまいます。これは次のメタ プロパティを index.html ページに追加することによって、部分的に防ぐことができます。

<meta name="viewport" content="user-scalable=no" />

これによってブラウザーは、ユーザーがレンダリングされたコンテンツを拡大縮小できないようにします。残念ながら、Windows Phone のブラウザーに実装すると、ユーザーはコンテンツを拡大縮小できてしまい、操作が終わると元に戻ります。これはあまり美しくありません。

WebBrowser コントロールのビジュアル ツリーを調べたところ、Manipulation イベントにハンドラーをアタッチして、HTML5 コンテンツをレンダリングするネイティブの TileHost にイベントがバブルアップしないようにできることがわかりました。これを実現する簡単なユーティリティ クラスを含むブログを投稿しておきます (bit.ly/vU2o1q、英語)。しかしこれは、Windows Phone OS の将来のバージョンで変更される可能性のある WebBrowser コントロールの内部構造に依存するため、使用する場合には注意が必要です。

Twitter の検索を行う

TwitterSearchViewModel の検索機能を詳細に見ると、jQuery の "ajax" 関数を使って Twitter API をクエリし、JSONP 応答を返しています。1 つの TweetViewModel インスタンスは返された各ツイートから構成され、それらが SearchResultsViewModel インスタンスを構成しています (図 11 参照)。

図 11 TwitterSearchViewModel の検索関数

this.search = function () {
  /// <summary>
  /// Searches Twitter for the current search term.
  /// </summary>
  this.isSearching(true);
  var url = "http://search.twitter.com/search.json?q=" +
    encodeURIComponent(that.searchTerm());
  var that = this;
  $.ajax({
    dataType: "jsonp",
    url: url,
    success: function (response) {
      // Create an array to hold the results.
      var tweetViewModels = [];
      // Add the new items.
      $.each(response.results, function () {
        var tweet = new TweetViewModel(this);
        tweetViewModels.push(tweet);
      });
      // Navigate to the results ViewModel.
      application.navigateTo(new SearchResultsViewModel(tweetViewModels));
      that.isSearching(false);
    }
  });
};

SearchResultsViewModel はツイートのリストだけを含んでいます。

/// <reference path="..//intellisense.js" />
/*globals ko*/
function SearchResultsViewModel(tweetViewModels) {
  /// <summary>
  /// A ViewModel that renders the results of a twitter search.
  /// </summary>
  /// <param name="tweetViewModels">An array of TweetViewModel instances</param>
  // --- properties
  this.template = "searchResultsView";
  this.tweets = ko.observableArray(tweetViewModels);
}

TweetViewModel は各ツイートのプロパティと、各ツイートのビューに移動する選択関数を公開します (図 12 参照)。

図 12 TweetViewModel

/// <reference path="..//intellisense.js" />
/*globals application*/
function TweetViewModel(tweet) {
  /// <summary>
  /// A ViewModel that represents a single tweet
  /// </summary>
  /// <param name="tweet">A tweet as returned by the twitter search API</param>
  // --- properties
  this.template = "tweetDetailView";
  this.author = tweet.from_user;
  this.text = tweet.text;
  this.id = tweet.id;
  this.time = tweet.created_at;
  this.thumbnail = tweet.profile_image_url;
  // --- public functions
  this.select = function () {
    /// <summary>
    /// Selects this tweet, causing the application to navigate to a tweet-view.
    /// </summary>
    application.navigateTo(this);
  };
}

繰り返しになりますが、これらのビューモデルごとにビューを記述するテンプレートは index.html ファイルに追加します (図 13 参照)。

図 13 index.html ファイルにテンプレートを追加

<script type=text/x-jquery-tmpl" charset="utf-8" id="searchResultsView">
  <div>
    <ul data-bind="template: {name: 'tweetView',
                              foreach: tweets}"> </ul>
  </div>
</script>
<script type="text/x-jquery-tmpl" charset="utf-8" id="tweetView">
  <li class="tweet"
      data-bind="click: select">
    <div class="thumbnailColumn">
      <img data-bind="attr: {src: thumbnail}"
                             class="thumbnail"/>
    </div>
    <div class="detailsColumn">
      <div class="author"
           data-bind="text: author"/>
      <div class="text"
           data-bind="text: text"/>
      <div class="time"
           data-bind="text: time"/>
    </div>
  </li>
</script>

このコードがあるため、ユーザーが "go" ボタンを押して Twitter を検索すると、新しい SearchResultsViewModel がビューモデル スタックに追加されます。これにより自動的に、searchResultsView テンプレートが "アプリの" div 内にレンダリングされることになります。しかし、ビューモデル スタックは foreach テンプレート バインドによりレンダリングされるため、twitterSearchView テンプレート インスタンスは非表示とならず、それらは重なって表示されます。これは次のように簡単な CSS ルールを追加して解決できます。

.app>div
{
  display: none;
}
.app>*:last-child
{
  display: block;
}

最初のセレクターは app クラスで指定された div の直接の子要素をすべて非表示にし、優先度の高い 2 つ目のセレクターによって最後の子要素が表示されるようにします。

この CSS ルールによって、Twitter 検索アプリは完全に機能し操作することができます。

戻るスタックを管理する

現在の Twitter 検索アプリでは、検索結果ページから個別のツイートのページに移動できますが、スマートフォンの [戻る] ボタンを押すと、アプリは直ちに終了してしまいます。これはアプリのナビゲーションが、完全にブラウザー コントロール内で行われているためです。Silverlight フレームワークの観点からは、アプリには単一ページしかありません。これではユーザー エクスペリエンスとして望ましくないだけでなく、アプリを Windows Phone Marketplace に提出しても、却下されるでしょう。

さいわい、この問題の解決は簡単です。ApplicationViewModel はビューモデルのインスタンスのスタックを保持します。このスタックに 2 つ以上のビューモデル インスタンスがある場合には、ハードウェアの [戻る] ボタンの押下に応じて、このスタックから最上位のビューモデルをを取り出します。これを行わないと、Silverlight フレームワークにより、アプリが終了します。

これをサポートするため、ビューモデルに依存関係のある監視可能な backButtonRequired を追加します。

function ApplicationViewModel() {
  // --- properties
  this.viewModelBackStack = ko.observableArray();
  this.backButtonRequired = ko.dependentObservable(function () {   
    return this.viewModelBackStack().length > 1;
  }, this);
  // --- functions
  // ...
}

ビューモデルの初期化時に、この監視可能なプロパティへの変更を処理し、Cordova が提供する backbutton イベントをサブスクライブできます。イベントが発生すると、ApplicationViewModel の back 関数が起動され、スタックから最上位のビューモデルを取り出します。Knockout テンプレートのバインドがビューモデルに関連付けられたビューを UI から削除し、CSS のスタイルにより最上位のビューが表示されるようになります (図 14 参照)。

図 14 [戻る] ボタン押下の処理

function initializeViewModel() {
  application = new ApplicationViewModel();
  ko.applyBindings(application);
  // Handle the back button.
  application.backButtonRequired.subscribe(function (backButtonRequired) {
    if (backButtonRequired) {
      document.addEventListener("backbutton", onBackButton, false);
    } else {
      document.removeEventListener("backbutton", onBackButton, false);
    }
  });
  var viewModel = new TwitterSearchViewModel();
  application.navigateTo(viewModel);
}
function onBackButton() {
  application.back();
}

backbutton イベントは Cordova が提供しているため、別のスマートフォン OS を使って実行しても図 14 のコードは (ハードウェアの [戻る] ボタンがあるスマートフォンである限り) うまく動きます。

状態の永続化

Twitter 検索アプリに最後の機能を追加します。検索がうまく結果を返した場合、その検索語句を最近の検索のリストに追加します (図 15)。

図 15 検索語句を最近の検索のリストに追加

function TwitterSearchViewModel() {
  /// <summary>
  /// A ViewModel for searching Twitter for a given term.
  /// </summary>
  // --- properties
  // ... some properties omitted for clarity ...
  this.recentSearches = ko.observableArray();
  // --- functions
  // ... some functions omitted for clarity ...
  this.loadState = function () {
    /// <summary>
    /// Loads the persisted ViewModel state from local storage.
    /// </summary>
    var state = localStorage.getItem("state");
    if (typeof (state) === 'string') {
      $.each(state.split(","), function (index, item) {
        if (item.trim() !== "") {
          that.recentSearches.push(item);
        }
      });
    }
  };
  function saveState() {
    /// <summary>
    /// Saves the ViewModel state to local storage.
    /// </summary>
    localStorage.setItem("state", that.recentSearches().toString());
  }
  function addSearchTermToRecentSearches() {
    /// <summary>
    /// Adds the current search term to the search history.
    /// </summary>
    that.recentSearches.unshift(that.searchTerm());
    saveState();
  }
}

addSearchTermToRecentSearches 関数は、Knockout の便利な関数 unshift を使って項目を配列の最初に追加します。最近の検索が追加されると、常に、HTML5 のローカル ストレージを使って状態を保存します。ここでは、状態の配列を toString 関数によりコンマ区切りのリストに変換して保存し、split によってリストから取り出します。より複雑なビューモデルでは、複数のプロパティの値を JSON 形式で保存する場合もあります。

興味深いことですが、Windows Phone のブラウザーはローカル ストレージをサポートしますが、ブラウザーが分離ストレージからページをレンダリングする場合にはこの機能は無効となります。前述のコードをうまく実行するため、Cordova チームは、スマートフォンの分離ストレージ内に状態を保存して橋渡しを行うような、ローカル ストレージ API の実装を作成する必要がありました。

この最後の変更によって、Twitter 検索アプリは完全に機能するようになりました。

移植性の証明: iPhone で実行する

ここまで説明してきたように、Cordova フレームワークにより、Windows Phone 向けに HTML5 ベースのアプリを作成できます。Knockout などのフレームワークによって、開発コードを適切に構築することもできますが、簡単な HTML5 と CSS の手法を使って、ネイティブの Metro に外観を合わせることも可能です。

今回は Windows Phone アプリの作成を中心に扱いましたが、Twitter 検索アプリは移植可能で、iPhone や Android フォンでも変更することなく実行できます。しかし Metro スタイルのアプリが iPhone や Android フォンに合うでしょうか。

今回の手法の多用性を示す最後のデモとして、jQuery Mobile を使って iOS ネイティブの外観に似せた Twitter 検索アプリの iOS 版を作成しました。これにはビューだけを変更すればよく、ビューモデルのロジックはまったく同じであることから、MVVM パターンの有用性がわかります。クラウド ベースの Build サービスを使い、iOS の "ipa" パッケージの作成と、iPhone へのインストールを、すべて 1 台の Windows マシンから行うことができました。図 16 に 2 つのアプリ実行されているところを並べて示します。

Twitter Search Running on an iPhone and a Windows Phone Device
図 16 iPhone と Windows Phone デバイスで実行中の Twitter 検索

アプリの iOS 版と Windows Phone 版のすべてのソース コードをこの記事の付属に含めています。

Colin Eberhardt は Scott Logic Ltd のテクニカル アーキテクトで、幅広い Microsoft .NET フレームワーク テクノロジ向けにグラフ コントロールを提供する Visiblox (visiblox.com、英語) のリード アーキテクトです。Twitter (twitter.com/ColinEberhardt) で彼をフォローしてください。

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