ひだまりソケットは壊れない

ソフトウェア開発に関する話を書きます。 最近は主に Android アプリ、Windows アプリ (UWP アプリ)、Java 関係です。

まじめなことを書くつもりでやっています。 適当なことは 「一角獣は夜に啼く」 に書いています。

Windows ストアアプリ開発において UI 部品をページコントロールで実装する

Windows ストアアプリを JavaScript で開発する話です。 アプリを複数のページで構成する場合、基本的にはベースを単一の HTML ページにして、その上に次々とページコントロールを読み込むことでページ遷移を実現します。

ここで使用する ページコントロール (WinJS.UI.Pages.PageControl オブジェクト) ですが、名前からしてページ全体を表すためのもののように思えます。 しかし、実際にはページ全体に限らずページの一部を構成する UI 部品 (UI コンポーネント) をページコントロールで実装することもできます

UI 部品をページコントロールで実装する

1 つのページ内に複数の複雑な処理を行う UI 部品を配置する場合 (そして、それらの UI 部品における処理同士はそんなに密接に絡み合っていない場合)、保守することを考えるとそれらの UI 部品はそれぞれ別々のファイルにして実装したいところであります。 また、複数のページに表示したい UI 部品がある場合も、その UI 部品はページ全体のファイルとは別にして管理したいところです。

UI 部品を別ファイルで管理する方法としては色々な方法が考えられますが、最も簡単な方法はページコントロールとして作成することだと思います。 ページコントロールの中にページコントロールをレンダリングする場合にどんなコードを書けばいいのかを書いておきます

基本的なこと : ページコントロールの作り方とページ中へのレンダリングの仕方

一応基本を。 Visual Studio 2012 を使用して Windows ストアアプリを開発している場合、ページコントロールを作るには "ソリューションエクスプローラー" の中のページコントロールを作成したい場所のディレクトリを右クリックして、"追加" → "新しい項目" から "ページコントロール" を選択することでページコントロールのためのファイル (ページコントロールを構成する HTML と CSS, JavaScript の 3 つのファイル) が生成されます。

そして、そのページコントロールをページ中に表示するには以下のような方法があります。 (他にも方法はあります。) ただし、render メソッドやコンストラクタ呼び出しの直後からページの内容が使えるようになるわけではないことに注意が必要です。

// WinJS.UI.Pages.render メソッドを使う方法
var elem = document.getElementById("page-control-host");
WinJS.UI.Pages.render("/pages/pageControl.html", elem);

// PageControl オブジェクトコンストラクタを使う方法
var MyPageControl = WinJS.UI.Pages.get("/pages/pageControl.html");
var elem = document.getElementById("page-control-host");
new MyPageControl(elem);

ページコントロール中にページコントロールを埋め込む

さて、ページコントロールの初期化処理の途中で別のページコントロールを読み込む場合、そのページコントロールを埋め込むタイミングを考える必要があります。

ページコントロールのコンストラクタを呼び出して新たなインスタンスを生成する際、ページコントロールのインスタンスに定義されているいくつかのメソッドが順に呼ばれます。

  1. init : ページコントロールの内容がセットされる前に呼び出される
  2. load : もともとの HTML ファイルから作られた DOM 木のコピーを行う (普通はオーバーロードせずにデフォルトのまま使えばよい)
  3. processed : ページコントロールの内容がセットされ、WinJS.UI.processAll() の呼び出しも行われた後に呼び出される
  4. ready : ページコントロールのレンダリングの一連の流れの最後に呼び出される

これらのメソッドについては WinJS.UI.Pages.IPageControlMembers インターフェイスのドキュメント をご覧ください *1

これらの処理の中でページコントロール自身に別のページコントロールを読み込ませるために最適なタイミングは processed メソッドの中です *2。 また、読み込んだページコントロールの内容が揃う (読み込んだページコントロールの processed メソッドの処理が完了する) までは、ready メソッドが呼び出されないようにする必要もあります。 これは、processed メソッドの返り値として、子も含めてすべてのページコントロールの処理が完了した時に fulfilled される Promise オブジェクトを返すことで実現できます *3

これらのことを踏まえたうえで、別のページコントロール (下の例では ChildUI) を読み込むページコントロール (下の例では ParentUI) を定義する最低限の JavaScript の記述は以下のようになります。

(function () {
    "use strict";

    // このページコントロールの中に読み込む別のページコントロール (子のページコントロール)
    var ChildUI = WinJS.UI.Pages.get("/pages/ChildUI.html");

    WinJS.UI.Pages.define("/pages/ParentUI.html", {
        /// <field type="HTMLElement" domElement="true" hidden="true">このページコントロールの最上位要素</field>
        element: null,
        /// <field type="Array">child ui のリスト</field>
        _childUIs: null,
            // ここにプロパティを書いておくと IntelliSense の恩恵を受けられる (値は null でいい)

        init: function (element, options) {
            // 通常はコンストラクタで行うような処理をここに書けばよい
            this._childUIs = [];
        },
        processed: function (element, options) {
            // ページコントロール ChildUI をレンダリングする先の HTML 要素を取得する
            // (この例では複数個読み込む)
            var childUIContainers = element.getElementsByClassName("child-ui-container");
            // 読み込んだページコントロールの初期化処理が processed まで完了したら fulfilled される
            // Promise オブジェクトを入れておくための配列
            var promisesToBeWaited = [];
            for (var i = 0, len = childUIContainers.length; i < len; ++i) {
                var childUI = new ChildUI(childUIContainers.item(i));
                this._childUIs.push(childUI);
                promisesToBeWaited.push(childUI.renderComplete);
            }
            // 全ての ChildUI の processed が終わるのを待つ
            return WinJS.Promise.join(promisesToBeWaited).then(function () {
                // ChildUI の ready よりも自身の ready を後に実行する様に, イベントループのキューに入っている別の処理を先に実行させる
                return WinJS.Promise.timeout(0);
            });
        },
        ready: function (element, options) {
            /// <summary>processed の処理が全て終わった後にページコントロールの仕組みによって自動的に呼び出される処理</summary>
            // イベントリスナの設定とかはここでやればいい
        },
        unload: function () {
            /// <summary>この UI の終了処理を行う</summary>
            // 子のページコントロールの unload メソッドを呼び出す
            this._childUIs.forEach(function (childUI) {
                if (childUI.unload) childUI.unload();
            });
        }
    });
})();

しかしよく考えてみたら

読み込むページコントロールを静的に決定できる場合は、以下のように HTML 側で data-win-control 属性に入れておけばいいですね (下の例は読み込むページコントロールを ChildUI でグローバルに参照できる場合)。 上のような面倒なことしなくていいですし。 unload の呼び出しを忘れないようにしないといけないので、その点だけ注意が必要です。

<div class="child-ui" data-win-control="ChildUI"></div>

読み込むページコントロールを静的には決定できない場合などは JavaScript で読み込む必要があるので、そういう場合は上で述べた方法を使うのがいいのではないでしょうか。

*1:ちなみに、これらのメソッドはデフォルトで定義されていますが、WinJS.UI.Pages.define メソッドを使って新たなページコントロールのコンストラクタを定義する際に、第 2 引数として渡すオブジェクトにこれらを定義しておくことでオーバーライドできます。

*2:ready メソッドは、それが呼び出された時点ですでにページコントロールの中身は使用可能な状態になっている必要があるので、ready メソッドの中では遅すぎます。 また、init メソッドの中ではページコントロールのコンテンツが何もない状態ですので、子のページコントロールを読み込む先の HTML 要素もまだ存在しません。 よって、init メソッドの中では早すぎます。

*3:ページコントロールの初期化処理の中で、processed メソッドが Promise オブジェクトを返した場合、それが fulfilled されるまで ready メソッドは呼び出されないため。