Ext JS 5 の ViewController

Ext JS 5 にはアプリケーションアーキテクチャで使用できる改良がいくつか加えられています。ViewModel と MVVM に加えて ViewController のサポートを追加し、MVC アプリケーションを強化しています。とりわけ、これらは相互排他的ではないため、これらの機能を徐々に導入したり、さらには組み合わせたりすることができます。

コントローラの復習

Ext JS 4 では、Controller は Ext.app.Controller から派生したクラスです。これらの Controller は CSS のようなセレクタ(「コンポーネントクエリ」と呼ばれる)を使用してコンポーネントを一致させ、イベントに応答します。また、“refs” を使用してコンポーネントインスタンスの選択、取得を行います。

これらのコントローラはアプリケーションの起動時に作成され、アプリケーションがある限り存在します。その期間、コントローラへのビューは表示されたりされなかったりします。コントローラが管理するビューのインスタンスは複数あることもあります。

課題

大規模アプリケーションでは、これらの手法は課題を生み出すことがあります。

このような環境では、ビューとコントローラは複数の開発チームが作成し、最終アプリケーションに統合することができます。コントローラが意図したビューにのみ反応するようにすることは困難です。さらに、開発者がアプリケーション起動時に作成されるコントローラの数を制限したいと考えることは一般的です。コントローラをゆっくりと作成することはできますが、破棄できないので、不要になっても残ります。

ViewController

Ext JS 5 は現在のコントローラと下位互換性がありますが、これらの課題に対応するために新しいタイプのコントローラを導入しています。Ext.app.ViewControllerViewController は次のように行います。

  • “listeners” と “reference” コンフィグを使用してビューへの接続をシンプルにします。
  • ビューのライフサイクルを活用してその関連 ViewController を自動的に管理します。
  • 管理ビューとの1対1の関係に基づいて ViewController の複雑さを削減します。
  • ビューのネストの信頼性を高めるカプセル化を提供します。
  • コンポーネントを選択し、イベントを関連ビュー以下のレベルでリッスンする機能を保持します。

Listeners

listeners コンフィグは新しくはありませんが、Ext JS 5 ではいくつか新機能が用意されています。ViewController では、例を2つだけ見ていきましょう。最初の例では、ビューの子アイテムに対する listeners コンフィグの基本的な使用方法を示します。

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 の使用では、特定の「スコープ」のない名前付きイベントハンドラ(“onBarChange”)を示します。内部的に、イベントシステムは Bar テキストフィールドのデフォルトのスコープを所有する ViewController に解決します。

今までは、listeners コンフィグはコンポーネントの作成者が使用するように予約されていたので、ビューが専用イベントや基本クラスが発火したイベントをどのようにリッスンしていたでしょうか。その答えは、明示的なスコープを使用する必要があります。

Ext.define('MyApp.view.foo.Foo', {
    extend: 'Ext.panel.Panel',
    xtype: 'foo',
    controller: 'foo',

    listeners: {
        collapse: 'onCollapse',
        scope: 'controller'
    },

    items: [{
        ...
    }]
});

上記の例では、名前付きスコープと宣言リスナの Ext JS 5 の2つの新しい機能を使用しています。ここでは名前付きスコープに焦点を当てます。名前付きスコープには2つの有効な値があります。“this” と “controller” です。MVC アプリケーションを記述する場合、ほとんど常に “controller” を使用しますが、これは(インスタンスを作成したビューの ViewController ではなく)そのビューの ViewController を見ることになります。

ビューは Ext.Component のタイプなので、このビューに “xtype” を割り当て、他のビューがビューのインスタンスをこのビューがテキストフィールドを作成したのと同じ方法で作成できるようにしています。この様子については、これを使用するビューを考えてみてください。例:

Ext.define('MyApp.view.bar.Bar', {
    extend: 'Ext.panel.Panel',
    xtype: 'bar',
    controller: 'bar',

    items: [{
        xtype: 'foo',
        listeners: {
            collapse: 'onCollapse'
        }
    }]
});

この場合、Bar ビューは Foo ビューのインスタンスをそのアイテムの1つとして作成します。さらに、Foo ビューのように collapse イベントをリッスンします。旧バージョンの Ext JS と Sencha Touch では、これらの宣言は競合していました。ところが、Ext JS 5 ではこれが期待通りに解決されています。Foo ビューで宣言されるリスナは Foo の ViewController で発火し、Bar ビューで宣言されたリスナは Bar の ViewController で発火します。

リファレンス

コントローラロジックを記述する場合の未解決事項の1つが、特定のアクションを完了するために必要なコンポーネントを取得することです。次のようなシンプルなものです。

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” を使用して取得するだけです。

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 が存在するかで判定)。第2に、lookupReference メソッドはキャッシュにリファレンスを更新する必要があるか問い合わるだけです(コンテナでの追加または削除など)。何も問題なければ、キャッシュからリファレンスを返すだけです。または、疑似コードで行います。

lookupReference: (reference) {
    var cache = this.references;
    if (!cache) {
        Ext.fixReferences(); // fix all references
        cache = this.references; // now the cache is valid
    }
    return cache[reference];
}

言い換えると、検索はなく、コンテナからアイテムを削除または追加したことでリンクが壊れた場合、必要に応じて1回で修正できます。以下から分かるように、このアプローチには効率性以外にもメリットがあります。

カプセル化

Ext JS 4 MVC の実装でセレクタは非常に柔軟に使用できましたが、同時にリスクもありました。これらのセレクタがコンポーネント階層のすべてのレベルで何でも「表示」するという事実は強力でミスを発生させやすいという両面を併せ持っていました。例えば、コントローラは隔離されて実行されていると 100% で動作しますが、セレクタに新しいビューとは好ましくない一致ができるため、他のビューが導入されるとすぐに失敗します。

これらの問題は特定の手順に従うと管理できますが、リスナやリファレンスを ViewController で使用する場合にはこれらの問題は発生しません。これは、listnners と reference コンフィグが所有する ViewController としか接続しないためです。ビューは自由にそのビュー内で一意のリファレンスの値を選択でき、これらの名前をビューの作成者に表示する必要がないことを理解しています。

同様に、リスナは所有する ViewController で解決できますが、errant セレクタで他のコントローラのイベントハンドラに偶然ディスパッチすることはありません。リスナはセレクタにすることが好ましいですが、2つのメカニズムはセレクタベースのアプローチが好ましい場合には効果があります。

このモデルを完了するには、ビューは所有するビューの ViewController が消費できるイベントを発火する必要があります。このために ViewController に helper メソッド(fireViewEvent)があります。例:

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);
    }
});

これによって、このビューの作成者は標準フォームのリスナを使用できます。

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 は標準「リスナ」と “control” メソッドを使用することができ、ビューへのスコープを暗示的に制限します。また、ビュー自体に合うように特殊セレクタを追加しています。

Ext.define('MyApp.view.foo.FooController', {
    extend: 'Ext.app.ViewController',
    alias: 'controller.foo',

    control: {
        '#': {  // matches the view itself
            collapse: 'onCollapse'
        },
        button: {
            click: 'onAnyButtonClick'
        }
    }
});

リスナとセレクタの主な違いは上記の通りです。“button” セレクタは深さに関係なくこのビューでのボタンまたは子ビューに一致します。これは、曾孫ビューに属する場合でもです。言い換えるなら、セレクタベースのハンドラはカプセル化範囲を維持しません。この動作は以前の Ext.app.Controller の動作と一致し、場合によっては便利なテクニックになります。

最後に、これらのイベントドメインはネストに従い、実質的にイベントをビュー階層に「バブル」アップします。つまり、イベントが発火されると、最初に標準リスナに配布されます。次に、所有する ViewController、階層が上の親 ViewController(ある場合)の順に配布されます。最終的に、イベント標準 “component” イベントドメインに配布され、Ext.app.Controller の派生コントローラによって処理されます。

ライフサイクル

大規模アプリケーションに共通の手法は、コントローラが最初に必要とされた場合に動的に作成することです。これによって、アプリケーションのロード時間が短縮され、すべての可能なコントローラを有効にしないことで実行時のパフォーマンスも向上します。旧バージョンでは、コントローラが作成されるとアプリケーション内で有効なままになるという制約がありました。コントローラを破棄してリソースを解放することはできませんでした。同様に、コントローラがいくつでも(ゼロを含め)関連ビューを持つことができるという事実は変わっていません。

ただし、ViewController はコンポーネントのライフサイクルの早い時期に作成され、ライフサイクル全体にわたってそのビューに結び付けられます。そのビューが破棄されると、ViewController も同様に破棄されます。これは、ViewController がビューがなかったり多かったりする状態を管理することを強制されなくなることを意味します。

この1対1の関係は、リファレンス追跡がシンプルになり、破棄されたコンポーネントのリークは発生しにくくなります。ViewController はこれらのメソッドを実装してライフサイクルの重要な時点で作業を実行できます。

  • beforeInit - このメソッドは initComponent メソッドを呼び出す前にビューで操作を行うためにオーバーライドすることができます。Component コンストラクタから initConfig が呼び出されている場合にコントローラが作成され、このメソッドがすぐに呼び出されます。
  • init - initComponent がビューで呼び出されるとすぐに呼び出されます。これは、一般的にビューが初期化され次にコントローラの初期化を行うタイミングです。
  • initViewModel - ビュー(定義されている場合)の ViewModel が作成されると作成されます。
  • destroy - クリーンアップとリソース(必ずcallParentを呼び出す必要があります)。

結論

ViewController は MVC アプリケーションを大幅に合理化すると考えています。また、ViewModel との相性も良いため、これらのアプローチやそれぞれの強みを組み合わせることもできます。次回リリースとアプリケーションでこれらが改良されることを楽しみにしています。

Last updated