HOME > Learning Place >  No.4 コントローラー

コントローラー

コントローラーはユーザの入力などのイベントに応答し、それを処理する要素です。 主な部分は、ユーザーのインタラクションの処理を担当しますが、それだけではなく、モデルの変更のイベントや、システムのイベントの処理などにも対応します。 Sencha フレームワークでは、Ext.app.Controller クラスがこの要素を構成します。

前回まで作ってきたアプリケーションに、コントローラーを加えて、ユーザーの操作などのイベントに対応させましょう。

Ext.app.Applicationとの関係

アプリケーションには通常いくつかのコントローラーが存在し、それぞれがアプリケーションの特定の部分を制御します。

アプリケーションで使用するコントローラーは、Application オブジェクトのcontrollers コンフィグに配列で指定します。 そうすると Application は、自動的に各コントローラーをインスタンス化し、その参照を保持しますので、直接コントローラーをインスタンス化する必要はありません。 規則としては、SlView.controller.* という名前をつけます。 製品を扱うコントローラーであれば、SlView.controller.Products となります。

stores/models/views

コントローラーの stores/models/views の各コンフィグでは、コントローラーで使用する、ストア・モデル・ビューを定義します。

これらのコンフィグで指定されたクラスは、自動的に読み込まれます。 また、それぞれについてゲッターメソッドが定義されます。 stores で定義されたストアは、ロードされインスタンス化されますが、モデルやビューはクラス定義がロードされるだけで、インスタンスは作られません。

refs コンフィグ (コンポーネントの参照)

refsComponentQuery の文法を使って、ページ内のコンポーネントを簡単に指定できるパワフルな手法です。各コントローラーそれぞれに refs を定義することができます。 refs コンフィグは、Ext JS 4 と Sencha Touch 2 で記述方法に違いがあります。

Ext JS 4 の場合

ref はオブジェクトの配列で、各要素は、次のようなプロパティを持つオブジェクトです。

  • ref – 参照に使われる名前
  • selector – コンポーネントを見つけるための、ComponentQuery のセレクター式

refの値に指定した名前をベースにしたgetterメソッドが自動的に作成されます。

    refs: [{
        ref: 'nav',
        selector: '#mainNav'
    }],
Sencha Touch 2 の場合

refs は、オブジェクトで、そのキーが参照に使われる名前となり、値が ComponentQuery のセレクター式となります。

    refs: {
        'nav': '#mainNav'
    },
主な ComponentQuery 式
  • xtype: “panel“または”.panel“のようにCSSクラス的に指定します
  • itemId/id: “#mypanel“のようにidの前に”#”を付加して指定します
  • : “panel grid“のようにスペースでつなげて親子関係を指定します
  • プロパティ: “component[autoScroll]“や”panel[title="Test"]“のようにプロパティの指定も可能です
  • メソッド: “{isDisabled()}“のようにメソッドの戻り値で真の場合に選択するような指定も可能です

次の例は、Ext JS 4 での例で、mainNavというIDのコンポーネントを検索するnavというrefを定義しています。 そして、getNavメソッドを使って、そこにボタンを追加しています。

Ext.define('SlView.controller.Main', {
    extend: 'Ext.app.Controller',
 
    refs: [{
        ref: 'nav',
        selector: '#mainNav' 
    }],
 
    addLogoutButton: function() {
        this.getNav().add({
            text: 'Logout'
        });
    }
});

この例では、addLogoutButtonというメソッドの中で、生成されたgetNavメソッドを使っています。 このgetterメソッドの名前は、refsに基づいて生成され、refプロパティ (Touch の場合はプロパティのキー) の先頭を大文字にしたものの前にgetをつけたものになります。 この場合では、getNavtoolbarの参照を返すので、 そのtoolbarにログアウトボタンを追加しています。 ここで前提としているコンポーネントは次のようなものです。

Ext.create('Ext.Toolbar', {
    id: 'mainNav',
 
    items: [
        {
            text: 'Some Button'
        }
    ]
});

addLogoutButton メソッドが実行されると、 Some Button の後に二つ目のボタンが追加されます。

高度な refs

refsの各オブジェクトには、オプションのプロパティがあります。

  • autoCreate – trueにするとそのコンポーネントが存在しない場合に自動的に生成されます。
  • xtype – 自動生成するコンポーネントの xtype を指定します。
// Ext JS 4 の場合
refs: [{
    ref: 'foo',
    selector: '#foo',
    autoCreate: true,
    xtype: 'MyPanel'
}],
 
// Sencha Touch 2 の場合
refs: {
    foo: {
        selector: '#foo',
        autoCreate: true,
        xtype: 'MyPanel'
    }
},

control メソッド / control コンフィグ

controlメソッド (Ext JS 4) 、control コンフィグ (Sencha Touch 2) は、コンポーネントで発生したイベントをリッスンして何らかの処理をするための、リスナーを登録します。

Ext JS 4 の場合

Ext JS 4 の場合は、コントローラーの init メソッドの中で、control メソッドを呼び出すことで、リスナーをセットします。

control メソッドに渡す引数は一つのオブジェクトリテラルで、キーでコンポーネントを特定して、値部分にそのコンポーネントのリスナー設定を記述したオブジェクトを指定します。 オブジェクトのキーでコンポーネントを特定しますが、ここには ComponentQuery 式が指定できます。 オブジェクトの値には、オブジェクトリテラルで、そのコントロールのリスナーを記述します。 コントロールのリスナーは、キーがイベント名、値がそのイベントに割り当てられるメソッドです。 通常メソッド自身は、コントローラーに別途定義します。 記述すると次の様になります。

Ext.define('SlView.controller.Users', {
    init: function() {
        this.control({
            'useredit button[action=save]': {
                click: this.updateUser
            }
        });
    },
 
    updateUser: function(button) {
        console.log('clicked the Save button');
    }
});
Sencha Touch 2 の場合

Sencha Touch 2 の場合は、メソッドではなく control コンフィグでリスナーを指定します。 コントロールコンフィグにはオブジェクトを指定します。 オブジェクトのキーはコンポーネントを指定しますが、ComponentQuery 式あるいは、refs で定義したその参照名を指定できます。 オブジェクトの値は、オブジェクトでキーにイベント名、値がイベントリスナーの関数名を文字列で指定します。

Ext.define('SlView.controller.Users', {
    config: {
        refs: {
            saveButton: 'useredit button[action=save]'
        },
 
        control: {
            // refsでの定義名を指定できる
            saveButton: 'updateUser',
            // ComponentQuery 式も指定できる
            'mypanel button#foo': 'onFooClick'
        }
 
    },
 
    updateUser: function(button) {
        console.log('clicked the Save button');
    },
 
    onFooClick: function(button) {
        //
    }
});

Ext JS 4 と Sencha Touch 2 では記述方法が異なりますのでご注意下さい。

コントローラーの作成

前回までのサンプルに動きを加えて行きます。

グリッドのダブルクリック

プロジェクトにコントローラーを追加して、グリッドの行がダブルクリックされたら、フォームにその内容を表示するようにしましょう。

app/controller/Grid.js

Ext.define('SlView.controller.Grid', {
    extend: 'Ext.app.Controller',
 
    refs: [
        {ref: 'myForm', selector: 'myform'}
    ],
 
    init: function() {
        var me = this;
 
        me.control({
            'mygrid': {
                itemdblclick: me.onGridItemDblClick
            }
        });
    },
 
    onGridItemDblClick: function(view, record) {
        var me = this,
            form = me.getMyForm();
 
        form.expand();
        // この1行だけでフォームに値がセットできます
        form.getForm().loadRecord(record);
    }
});

app/Application.js

作ったコントローラーを Application に追加します。

    controllers: [
        'Main', 'Grid'
    ],

これをブラウザで表示してみると、グリッドがダブルクリックされたら、フォームが開いて、フォームに値がセットされるでしょう。

フォームでのデータ編集

フォームでデータを編集できるようにしましょう。

まず、フォームビューでは、ボタンのイベントをリッスンして、カスタムイベントを発火するようにします。 こうすることで、「foo ボタンが押された」という具体的なイベントから「保存したいと言っている」という抽象的なイベントに変換できます。 そして、UIが変わってユーザーの他の動作で保存をさせたくなった場合でも、コントローラーを修正せずに済みます。

Form.js の buttons コンフィグの部分を次の様に書き換えて下さい。

app/view/Form.js

buttons: [{
    text: '保存',
    formBind: true,
    handler: function(button) {
        var form = button.up('myform');
 
        form.fireEvent('saveCommand', form, form.getRecord());
        form.collapse();
    }
}, {
    text: 'キャンセル',
    handler: function(button) {
        var form = button.up('myform');
 
        form.collapse();
    }
}]

コントローラー側では、そのカスタムイベントをリッスンして、レコードにフォームのデータをセットします。

app/controller/Form.js

Ext.define('SlView.controller.Form', {
    extend: 'Ext.app.Controller',
 
    init: function() {
        var me = this;
 
        me.control({
            'myform': {
                saveCommand: me.doSave
            }
        });
    },
 
    doSave: function(form, record) {
        var me = this;
 
        // これだけで変更をレコードに反映できます
        form.updateRecord();
    }
});

このままだと空のフォームを開いてデータを保存できてしまうので、south リージョンのパネルのコンフィグに次のものを追加して、折りたたみ時にヘッダーを出さないようにします。

app/view/Main.js

collapseMode: 'mini',

作ったコントローラーを追加します。

Application.js

controllers: [
    'Main', 'Grid', 'Form'
],

ブラウザをリロードして動作を確認してみて下さい。フォームでの変更がグリッドに反映されるのがわかると思います。

データの追加と削除

更新ができるようになりましたので、データの追加と削除もできるようにしましょう。 まずは、グリッドのツールバーに、追加・削除ボタンを追加します。 次のコードを、title コンフィグの次ぐらいに挿入します。 冗長に書くと、dockedItems コンフィグに xtype: 'toolbar' のコンポーネントを追加するのですが、簡単に書ける tbar というコンフィグがありますので、ここではそれを利用しています。

app/view/Grid.js

tbar: [{
    text: '追加',
    itemId: 'addButton'
}, {
    text: '削除',
    itemId: 'deleteButton'
}],

フォームでは、データの更新の他、新規追加も発生することになるので、モードを保持するコンフィグオプションを追加します。 それと同時に保存ボタンのオリジナルイベントで、そのモードも送信するように変更します。

まず、フォームのビューに config を追加し、モードを保存するプロパティを作ります。

app/view/Form.js

config:{
    addMode: false
},

buttons コンフィグの handler メソッドの中身を少し書き換えて、イベントの引数にモードもつけるようにします。

app/view/Form.js

    buttons: [{
        text: '保存',
        formBind: true,
        handler: function(button) {
            var form = button.up('myform');
 
            // モードもつけて発火
            form.fireEvent('saveCommand', form, form.getRecord(), form.getAddMode());
            form.collapse();
        }
    }, {

次にグリッドのコントローラーで、イベントをハンドリングします。

まず、グリッドの参照をするので、refsmygrid への参照を記述します。

app/controller/Grid.js

refs: [
    {ref: 'myForm', selector: 'myform'},
    {ref: 'myGrid', selector: 'mygrid'}
],

モデルとストアのgetterメソッドを使うために、 models コンフィグと stores コンフィグを追加します。

app/controller/Grid.js

stores: ['Employees'],
models: ['Employee'],

control メソッドにイベントリスナーを追加します。

app/controller/Grid.js

me.control({
    :
    :
    'button#addButton': {
        click: me.onAddButtonClick
    },
    'button#deleteButton': {
        click: me.onDeleteButtonClick
    }
});

次にそれぞれのイベントリスナーを記述します。まず、レコードの追加では、新しいレコードのインスタンスを作って、それをフォームにセットします。

app/controller/Grid.js

onAddButtonClick: function(button) {
    var me = this,
        form = me.getMyForm(),
        newRec = me.getEmployeeModel().create();
 
    form.setAddMode(true);
    form.expand();
    form.getForm().loadRecord(newRec);
},

レコードの削除では、現在選択されているレコードを削除します。

app/controller/Grid.js

onDeleteButtonClick: function(button) {
    var me = this,
        records = me.getMyGrid().getSelectionModel().getSelection(),
        store = me.getEmployeesStore();
 
    Ext.iterate( records, function(record) {
        store.remove(record);
    });
 
}

行のダブルクリックのイベントリスナーを変更して、モードを設定するようにします。

app/controller/Grid.js

onGridItemDblClick: function(view, record) {
    var me = this,
        form = me.getMyForm();
 
    form.expand();
    form.setAddMode(false); // この行を追加
    form.getForm().loadRecord(record);
},

次に、フォームのコントローラーの保存イベントのリスナーを新規追加に対応させます。

ストアのgetterを使いたいので、stores コンフィグを指定します。 これにより、getEmployeesStore メソッドが使えるようになります。

app/controller/Form.js

stores: ['Employees'],

保存のイベントハンドラーでは、追加モードの場合には、ストアにレコードを追加します。

app/controller/Form.js

doSave: function(form, record, addMode) {
    var me = this,
        store = me.getEmployeesStore();
 
    record.set(form.getValues());
    if( addMode ) {
        store.add(record);
    }
}

変更箇所がわりと多かったですが、これらの変更をして、追加/削除/変更の作業をしてみて下さい。

このようにビューのイベントに対して、コントローラーにリスナーを記述していきます。

コントローラー間の通信

コントローラーが別のコントローラーと通信をする方法を考えてみましょう。 コントローラーには、getController() というメソッドがあり、それを使って他のコントローラーを参照できます。

var anotherController = this.getController('someController');

しかし基本的に、コントローラーのインスタンスを取得して、そのメソッドをコールするというやり方は、コントローラー同士の結合が強くなってしまいます。(他のコントローラーがないと、そのコントローラーは動作しない)

そこでコントローラー間で通信を実現するにはイベントを使います。 コントローラー A でイベントを発火し、コントローラー B でそのイベントをリッスンします。

イベントを発火させるオブジェクトは、関連するビューにあるコンポーネントでもいいですし、Application を使うこともできます。

// コンポーネントで発火
this.getMyForm().fireEvent('newEvent');
// アプリケーションで発火
this.getApplication().fireEvent('newEvent');

イベントをリッスンする側のコントローラーでは、リスナーを設定します。

// コンポーネントで発火させた場合
init: function() [
    this.control({
        'myForm': {
            newEvent: 'onNewEvent'
        }
    });
}

Application でイベントを発火させた場合は、initメソッドでApplicationのonメソッドなどでリスナーをセットします。

init: function () {
    this.getApplication().on('newEvent', this.onNewEvent);
}

今回はコントローラーについて解説しました。次回はモデル/ストアをサーバーのデータソースと接続する方法をご紹介します。

Learning Placeトップに戻る

PAGETOP