倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

はじめての幻獣 Griffon 研 (31) : マルチ MVC への道 (8) : 短寿命の MVC グループ withMVC()

前回マルチ MVC は終了の予定でしたが、ちょっと追加記事(一覧)。

短寿命の MVC グループ


今まで作成した MVC グループはアプリケーション開始時、もしくはアクション実行時にインスタンスが作成され、View は全体として、もしくはその部分に埋め込まれて存在してました。 ただしこれは必須条件ではなく、ダイアログやウィザードのような短期間しか存在しない(使い捨ての)オブジェクトに対しても MVC グループを使用することもできます。

で、まぁ MVC グループの生成や破棄の方法は前回までにやったのと全く同じ方法でできますが、一連の

  1. MVC グループを生成する (buildMVCGroup())
  2. グループ内のインスタンスを用いて処理を行う
  3. MVC グループを破棄する (destroyMVCGroup())

という処理はクロージャを用いるとパターン化できます。 具体的には

    def withMVC(String type, String id, Map params, Closure closure) {  
        closure(buildMVCGroup(params, type, id))  
        destroyMVCGroup(id)  
    }

というメソッドを定義しておくと、以下のように

    def someAction = { evt ->
        Map params = ...
        withMVC('《MVC グループ名》', '《グループ ID》', params){ mvc ->
            // MVC グループのインスタンスを用いた処理
        }
    }

クロージャを使って処理だけを書くことが出来ます。 MVC グループの生成や破棄は気にしなくて OK。 クロージャ、すごいンジャ〜! ちなみに、以下のサイトを参考にしてます:

関数描画アプリケーション


では、例によって「関数描画アプリケーション」に機能を追加しましょう。 追加機能は、関数の詳細を設定するダイアログ(ウィザード)とします*1。 見た感じはこんなの:

単純なメッセージだけなら JOptionPane の static メソッドなどで事足りますが、ある程度複雑なコンテンツを持ち、ユーザーの複数の入力をオブジェクトとして取得したい場合などは、それ専用の MVC グループを作成する方がコーディングが楽かと思います。

新たに作成する MVC グループは「FunctionDetailWizardMVC グループとします。 また、今回作成(修正)するファイルは以下の通り:

  • FunctionPlotter/griffon-app/models/FunctionDetailWizardModel.groovy
  • FunctionPlotter/griffon-app/views/FunctionDetailWizardView.groovy
  • FunctionPlotter/griffon-app/controllers/FunctionDetailWizardController.groovy
  • FunctionPlotter/griffon-app/views/MonolineFunctionView.groovy (修正)・・・ボタンとアクションの追加だけなので省略
  • FunctionPlotter/griffon-app/controllers/MonolineFunctionController.groovy (修正)

FunctionDetailWizard MVC


まずは「FunctionDetailWizard」 MVC グループのコーディング。 このグループのコーディングは特に問題ないかと。 ちなみに、以下のコマンドで MVC グループを生成します:

griffon create-mvc FunctionDetailWizard

Model

Model では、設定する項目を保持しておくフィールドを定義しておきます。 ダイアログを閉じた後、ユーザーの入力値をこの Model から取得します(Controller にて)。

package functionplotter

class FunctionDetailWizardModel {
    @Bindable String name
    @Bindable String samples
}

MonolineFunctionModel と異なり、samples フィールドを String 型として宣言しています(この辺りは実装者の勝手)。 各フィールドの値はこの MVC グループの作成の際に初期化されるようにします(後述の MonolineFunctionController 参照)。

View

次は View。 この View では、ダイアログのメッセージ(コンテンツ)になる JPanel オブジェクトを定義します。 ダイアログ自体は Controller にて生成。 参照の必要がある Bean には id 属性を付加しておきます。

package functionplotter

panel(id:'content', border:emptyBorder(6)) {  
    migLayout(layoutConstraints:'wrap 3')

    label 'Name'
    label ' : '
    textField id:'nameField', columns:5, 
                 text: bind(source: model, sourceProperty:'name', mutual:true)

    label 'Samples'
    label ' : '
    textField id:'samplesField', columns:5, 
                 text: bind(source: model, sourceProperty:'samples', mutual:true)
}

テキストフィールドの内容と Model のフィールドをバインドしていますが、「mutual:true」と指定することによって、相互に値の変更を反映することができます。 これはナカナカ便利。 「mutual:true」にする場合、バインドには source を指定するようですが、target を指定したらどうなるのかは試してません*2

各テキストフィールドの初期テキストは、「mutual:true」によって Model のフィールドの値にセットされます。

Controller

最後は Controller。 ここでは View をメッセージとして(message:view.content)、optionPane を介してダイアログを生成・表示しています。 ダイアログの種類は「OK / Cancel」ダイアログとしています()。

package functionplotter

import javax.swing.JOptionPane

class FunctionDetailWizardController {

    def model
    def view
    def builder

    def show = { evt = null ->
        def pane = builder.optionPane(
                    message:view.content,
                    messageType:JOptionPane.PLAIN_MESSAGE,
                    optionType:JOptionPane.OK_CANCEL_OPTION)
        def dialog = pane.createDialog(app.windowManager.windows[0], 'Function Detail')
        dialog.visible = true

        if(pane.value == JOptionPane.OK_OPTION)
            return model
        else
            return null
    }
}

ユーザーが「OK (了解)」を選択すると、Model 自体を返すようにしています。 それ以外(「Cancel (取消し)」もしくは「閉じる」ボタンをクリック)なら null を返します。

MonolineFunctionController


さて、上記の「FunctionDetailWizard」 MVC グループの実装を踏まえて、MonolineFunctionController に上記のダイアログ(ウィザード)を表示してユーザーの入力を取得する処理を追加しましょう。 View のコーディングは一辺倒なので省略。 処理は detail として Controller に記述します。

package functionplotter

import javax.swing.JOptionPane

class MonolineFunctionController {

    def model
    def view

    def detail = { evt = null ->
        def params = [name:model.name, samples:model.samples.toString()]
        withMVC('FunctionDetailWizard', 'functionDetail', params){ mvc ->

            // 「FunctionDetailWizard」 MVC グループのインスタンスを使った処理。
            def result = mvc.controller.show()

            if(result != null){
                model.with{
                    name = result.name
                    samples = result.samples as int
                }
            }
        }

        app.groups.FunctionPlotter.view.controlPanel.updateUI()
    }

    def withMVC(String type, String id, Map params, Closure closure) {  
        closure(buildMVCGroup(params, type, id))  
        destroyMVCGroup(id)  
    }
}
  • withMVC() メソッドは記事の最初に説明した通り。 3つ目の引数として渡している Map は FunctionDetailWizardModel のフィールドを初期化するのに使われます。
  • detail 処理の内部では、生成された MVC インスタンスを利用してダイアログ(ウィザード)を介してユーザーの入力を取得し、Model (MonolineFunctionModel) のフィールドに設定しています。 実際には入力値の妥当性検証なども必要ですが、ここでは省略。

これでコーディング完了。

上記で実装した内容を新たな MVC グループを作らずに(例えばダイアログだけで)行おうとするとかなり大変かと。 一方、今回見てきた方法では、ほとんど単なる MVC グループの作成だけでストレートフォワードにコーディングできてますネ。 Model を介したユーザー入力の取得とスクリプトによる View の構築はすごく簡単で柔軟性もあるし、Controller の処理はほとんど定型文といってもいい内容。 ん〜、MVC パターン、ハマるとやめられませんなぁ〜。

追記


Griffon 0.9.3-beta-1 から withMVC() メソッドがデフォルトで使えるようになりました。
Griffon in Action

Griffon in Action


Groovyイン・アクション

Groovyイン・アクション

*1:Griffon のプラグインに「Wizard Plugin」というのがありますが、Griffon 0.9.2-beta-3 では動きませんでした。

*2:他にも converter を指定して自動で型変換するようにしてるとどうなるかとかなんてのも気になる(けど試さない:-P)。