Ext JS 5 の ViewController を使う
こんにちは、ゼノフィnakamuraです。
はじめに
Ext JS 5 は、アプリケーションのアーキテクチャを使う上で、いくつかのエキサイティングな進化を遂げています。
ViewModel と MVVM とともに ViewController も MVC アプリケーションを拡張するためにサポートされるようになりました。
大変良いことに、これら選択することは互いに排他的ではないので、これらの機能を徐々に導入したり、ブレンドして使ったりできると言うことです。
Controller の要約
Ext JS 4 では、Controller は Ext.app.Controller というクラスでした。 これらの Controller は、コンポーネントにイベントをセットするときに、CSSライクなセレクター (コンポーネントクエリ) を使います。このクエリはまた、”refs” でコンポーネントのインスタンスを取得するときにも使います。
これらのコントローラーは、アプリケーションの起動時に生成され、アプリケーションが終了するまで存在し続けます。 その間コントローラに関係するビューは作成されたり廃棄されたりします。 また複数のビューをコントローラーが管理することもあります。
難問
大きなアプリケーションにおいては、これらのテクニックは、特定の問題を引き起こすことがあります。
そのような環境では、ビューやコントローラーは複数の開発者チームによって作成され、最終的にアプリケーションの中に統合されます。 コントローラーが対象とするビューにだけ反応するようにするのは少々難しいことです。 また、アプリケーションの起動時に生成するコントローラーの数を制限したいと思う開発者も多いことでしょう。 コントローラーの遅延生成は少し頑張れば可能ですが、それを廃棄することはできませんので、必要がなくなっても残り続けることになります。
ViewController
Ext JS 5 は現在のコントローラーとの後方互換性がありますが、これらの難問を解決するためにデザインされた、新しいタイプのコントローラー (Ext.app.ViewController) が導入されました。 ViewController はこれを次の様な方法で実現します。
- “listeners” と “reference” コンフィグを使って簡単にビューと接続します。
- ビューのライフサイクルによって自動的に関連する ViewController を管理します。
- ViewController は管理するビューと一対一で対応するので、複雑さを回避できます。
- ネストしたビューの信頼性を高めるカプセル化を提供します。
- コンポーネントを選択する機能を保ち、関連するビューのあらゆるレベルのイベントをリッスンできます。
listeners コンフィグ
listeners コンフィグは新しいものではありませんが、Ext JS 5 ではいくつかの新機能が追加されています。 新しい listeners 機能のより詳しい説明はこの後に発表される記事 “Declarative Listeners in Ext JS 5 (Ext JS 5 における宣言型リスナー)” に書かれることになるでしょう。 ViewController の用途について、二つのサンプルのみを提示しましょう。 一つ目は、ビューの子アイテムでの listeners コンフィグの基本的な使い方です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | Ext.define('MyApp.view.foo.Foo', { extend: 'Ext.panel.Panel', xtype: 'foo', controller: 'foo', items: [{ xtype: 'textfield', fieldLabel: 'Bar', listeners: { change: 'onBarChange' // no scope given here } }] }); Ext.define('MyApp.view.foo.FooController', { extend: 'Ext.app.ViewController', alias: 'controller.foo', onBarChange: function (barTextField) { // called by 'change' event } }); |
この listeners の使い方は、”scope” を指定しない名前付きのイベントハンドラー (“onBarChange”) の例です。 内部的に、イベントシステムは Bar テキストフィールドのデフォルトスコープにはオーナーの ViewController を割り当てます。
歴史的に、listeners コンフィグはコンポーネントの生成元によって使われるようになっていました。ビューが自身のイベントをリッスンし、そのベースクラスによって発火させるにはどうしたらいいのでしょう。 そうするには、明示的なスコープ指定を使う必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | Ext.define('MyApp.view.foo.Foo', { extend: 'Ext.panel.Panel', xtype: 'foo', controller: 'foo', listeners: { collapse: 'onCollapse', scope: 'controller' }, items: [{ ... }] }); |
上記のサンプルは Ext JS 5 の二つの新機能 (名前付きスコープと宣言的リスナー) の例です。 ここでは、名前付きスコープに焦点を当てます。 名前付きスコープには二つの値を設定できます。”this” と “controller” です。 MVCアプリケーションを記述するとき、たいていはそのビューの ViewController (インスタンスをつくったビューの ViewController でない)を探した明らかな結果がある「コントローラ」を使います。
ビューは Ext.Component の一種なので、このビューが textfield をつくったのと同じ方法で、他のビューがビューのインスタンスをつくることができる “xtype” を、このビューに割り当てています。 これがどのように動作するかは、これを使うビューをよくご覧ください。 たとえば:
1 2 3 4 5 6 7 8 9 10 11 12 | Ext.define('MyApp.view.bar.Bar', { extend: 'Ext.panel.Panel', xtype: 'bar', controller: 'bar', items: [{ xtype: 'foo', listeners: { collapse: 'onCollapse' } }] }); |
この場合、Bar ビューは、そのアイテムの一つとして、Foo ビューのインスタンスを生成します。 さらに、collapse イベントを Foo ビューのときようにリスニングします。 Ext JS の以前のバージョンや Sencha Touch であれば、このような定義は衝突を起こします。 Ext JS 5 では、期待通りに解決されます。 Foo ビューで定義されたリスナーは、Foo の ViewController で発火し、Bar で定義されたリスナーは Bar の ViewController で発火します。
Reference
最も共通な未解決の問題は、 特定のアクションを完了するために必要なコンポーネントを取得するコントローラーロジックをいつ書くのか ということです。 簡単に書くと、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | Ext.define('MyApp.view.foo.Foo', { extend: 'Ext.panel.Panel', xtype: 'foo', controller: 'foo', tbar: [{ xtype: 'button', text: 'Add', handler: 'onAdd' }], items: [{ xtype: 'grid', ... }] }); Ext.define('MyApp.view.foo.FooController', { extend: 'Ext.app.ViewController', alias: 'controller.foo', onAdd: function () { // ... get the grid and add a record ... } }); |
しかしどうやってグリッドコンポーネントを取得しましょうか? Ext JS 4 では “refs” コンフィグなどの方法でコンポーネントを参照しました。 どのテクニックでも、グリッドにそれを特定できるような識別可能なプロパティを配置する必要がありました。 古いテクニックでは、”id” コンフィグ (と Ext.getCmp) もしくは “itemId” コンフィグ (“refs” やコンポーネントクエリメソッドを使う) を使いました。 “id” の利点は素早く解決できることですが、アプリケーションや DOM 全体でユニークでなければなりません。これはあまり望ましいことではありません。 “itemId” とコンポーネントクエリを使うのはよりフレキシブルですが、希望のコンポーネントを探すために検索する必要がありました。
Ext JS 5 の新しい reference コンフィグでは、”reference” コンフィグをグリッドに追加し、”lookupReference” メソッドを使うだけで、それを取得できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | Ext.define('MyApp.view.foo.Foo', { extend: 'Ext.panel.Panel', xtype: 'foo', controller: 'foo', tbar: [{ xtype: 'button', text: 'Add', handler: 'onAdd' }], items: [{ xtype: 'grid', reference: 'fooGrid' ... }] }); Ext.define('MyApp.view.foo.FooController', { extend: 'Ext.app.ViewController', alias: 'controller.foo', onAdd: function () { var grid = this.lookupReference('fooGrid'); } }); |
これは、”fooGrid” という itemId を割り当てて、”this.down(‘#fooGrid’)” とするのとよく似ています。 しかし内部的には非常に大きな違いがあります。 まず、reference コンフィグは、所有しているビュー(この場合ViewControllerの存在によって特定される) とともに自身を登録するようにコンポーネントに指示します。 次に、lookupReference メソッドは reference をリフレッシュする必要があるかどうか(コンテナーに追加や削除があったかどうか) キャッシュに問い合わせます。 すべてOKならば、キャッシュを返すだけです。さもなくば次の擬似コードのようにします。
1 2 3 4 5 6 7 8 | lookupReference: (reference) { var cache = this.references; if (!cache) { Ext.fixReferences(); // fix all references cache = this.references; // now the cache is valid } return cache[reference]; } |
言い換えると、検索する必要もありませんし、必要に応じてコンテナにアイテムを追加したり削除したりすることにより、リンクが壊れてしまうこともありません。 次に示す様に、このやり方には効率以外にも利点があります。
カプセル化
Ext JS 4 の MVC 実装でのセレクターの利用は非常にフレキシブルでしたが、同時に特定のリスクもありました。 これらのセレクターはコンポーネントの階層構造の全てのレベルが「見える」ため、パワフルでしたがミスが発生することも多かったのです。 例えば、コントローラーは独立した状態で 100% 動作しているとしても、ビューが追加され、セレクターがその新しいビューで望ましくないものと一致することになると、うまく動作しなくなります。
これらの問題は特定のやり方で解決できます。 しかし ViewController と listeners と references を使うと これらの問題は簡単に起こらなくなります。 なぜかというと、Listeners と reference コンフィグは、所有する ViewController とだけ接続されるからです。 ビューは、そのビューの中でユニークであるどんな reference の値でも自由に選べます。これらの名前はビューを生成したものには公開されません。
同様に、listeners は 所有する ViewController 上で解決され、誤ったセレクターで他のコントローラーのイベントハンドラーにディスパッチされてしまうことがありません。 listeners はセレクターよりも好ましくはありますが、セレクターベースのアプローチが望ましいときには、二つのメカニズムは一緒に使うことができます。
このモデルを完成させるには、ビューはビューの ViewController によって処理されるイベントを発火させる必要があります。 ViewController にはそのためのfireViewEventというヘルパーメソッドがあります。例えば、
1 2 3 4 5 6 7 8 9 10 11 12 | Ext.define('MyApp.view.foo.FooController', { extend: 'Ext.app.ViewController', alias: 'controller.foo', onAdd: function () { var record = new MyApp.model.Thing(); var grid = this.lookupReference('fooGrid'); grid.store.add(record); this.fireViewEvent('addrecord', this, record); } }); |
これにより、ビューを作成する方では、標準のリスナー形式を使うことができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 | Ext.define('MyApp.view.bar.Bar', { extend: 'Ext.panel.Panel', xtype: 'bar', controller: 'bar', items: [{ xtype: 'foo', listeners: { collapse: 'onCollapse', addrecord: 'onAddRecord' } }] }); |
リスナーとイベントドメイン
Ext JS 4.2 では、イベントドメインの導入とともにMVC イベントディスパッチャが一般化されました。 これらのイベントドメインは、発火されたイベントを傍受して、セレクタが一致することでコントローラーに対して、ディスパッチします。 他のドメインはより限定的なセレクターですが、コンポーネントイベントドメインには、完全なコンポーネントクエリーのセレクターがあります。
Ext JS 5 では、各 ViewController は “view” イベントドメインと呼ばれる、新しいタイプのイベントドメインインスタンスを生成します。 このイベントドメインは、そのビューに対して暗黙的にそのスコープに限定した ViewController に、標準の “listen” や “control” メソッドを使えるようにします。 ビュー自身に一致する特別なセレクターも追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | Ext.define('MyApp.view.foo.FooController', { extend: 'Ext.app.ViewController', alias: 'controller.foo', control: { '#': { // matches the view itself collapse: 'onCollapse' }, button: { click: 'onAnyButtonClick' } } }); |
listeners とセレクターの主な違いはこのようなものです。 “button” のセレクターは階層に関わりなく、たとえ「ひ孫」であろうとも、このビューやその子供のビューにある全てのボタンに一致します。 言い換えると、セレクターベースのハンドラーはカプセル化の境界を守りません。 この振る舞いは、以前の Ext.app.Controller での動きで、 限定されたシチュエーションでは便利なテクニックではあります。
最後に、これらのイベントドメインはネスティングを考慮し、ビュー階層上で効果的にイベントを「バブリング」する事ができます。 イベントが発火すると、最初に標準リスナーに通知されます。 次に、所有する ViewController に通知され、続いて階層構造を遡って (親があれば) 親の ViewController に通知されます。 最後に、イベントは標準の “コンポーネント” イベントドメインに通知され、Ext.app.Controller コントローラーによって処理されます。
ライフサイクル
大きなアプリケーションでの共通のテクニックは、必要に応じて動的にコントローラーを生成する事です。 これによりアプリケーションのロード時間を短くし、全てのコントローラーを動作させるわけではないので、実行時のパフォーマンスを上げることができます。 この点について前バージョンでは、一旦コントローラーが生成されると、アプリケーションが実行中はアクティブであり続けるという制限がありました。 それらを廃棄してそのリソースを解放することはできませんでした。 同様に、コントローラーが関連するビューがいくつあっても (あるいは無くても) いいという現実は変わりませんでした。
ViewController は、コンポーネントのライフサイクルの最初の方で生成され、ライフサイクル全体はそれらのビューと密接に結びつきます。 ビューが廃棄されると、ViewController は同様に廃棄されます。 これは、ViewControllerがビューが存在しないとか多くのビューがある状態を管理することを強制されないということを意味します。
この一対一の関係によって、参照を追跡するのが簡単になり、廃棄されたコンポーネントがメモリリークを起こす心配が無くなります。 ViewController はそのライフサイクルのキーポイントでタスクを実行する為の次のようなメソッドを実装することができます。
- beforeInit — このメソッドをオーバーライドして、initComponent メソッドが呼び出される前に操作することができます。 このメソッドはコントローラーが生成された直後、initConfig がコンポーネントのコンストラクターの呼び出しに続いて呼び出されます。
- init — ビュー上で、initComponent が呼び出された直後に呼び出されます。 ここでは一般的にビューが初期化された後、コントローラーの初期化処理を実行します。
- initViewModel — ビューの ViewModel が定義されていたらそれが生成された後に呼び出されます。
- destroy — 全てのリソースをクリーンアップします。(callParent しましょう)
まとめ
ViewController は MVC アプリケーションを劇的に合理化します。 また、ViewModel と組み合わせて動作するので、これらのアプローチとそれぞれの強みを組み合わせられます。 Ext JS 5 が近々リリースされ、あなたのアプリケーションでこれらの進化が動作するのを見るのが楽しみです。