G*Magazine Vol.1

Griffon 不定期便 〜第2回 バインディング編〜

今回はGriffonでのバインディングについて紹介します。バインディングについては前回少し紹介しましたが、今回はもう少し深く紹介します。

Griffonのバインディングといっても基本的な機能はGroovyのSwingBuilderで提供されています。今回はGriffonというよりGroovyのSwingBuilderの話が中心になってしまいますが、SwingBuilderはGriffonでも重要な機能ですので、SwingBuilderを使った様々なバインディングを紹介したいと思います。

ビューからモデルへのバインディング

テキストフィールドに入力された値をモデルにバインディングさせてみましょう。次のようなサンプルコードでビューからモデルへのバインディングを確認します。SwingBuilderの機能しか使っていませんのでGroovyConsoleに貼付けて実行する事ができます。

import groovy.swing.SwingBuilder
import groovy.beans.Bindable

class Model {
    @Bindable name
}

def model = new Model()

new SwingBuilder().edt {
    frame(show:true, pack:true) {
        gridLayout(cols:1, rows:2)
        textField(text:bind(target:model, targetProperty:'name'))
        button('Hello', actionPerformed: {
            optionPane(message:"Hello, ${model.name}!").createDialog('Message').show()
        })
    }
}

このサンプルを実行すると次のようなアプリケーションが起動します。

このテキストフィールドに名前を入れてボタンをクリックするとダイアログが表示されます。

シンプルな例ですが、ポイントはModelクラスのプロパティで宣言している@Bindableアノテーションと、テキストフィールドのtextプロパティでbindメソッドを使用している2点です。この2点でテキストフィールドの値が変更されるとモデルに反映されるようになります。

このバインディングは次のように少し省略して書く事もできます。

textField(text:bind(target:model, 'name'))

モデルからビューへのバインディング

今度は逆にモデルからビューへのバインディングを見てみましょう。

ここではボタンをクリックすると、運勢を占ってくれるおみくじアプリを作ってみましょう。

このサンプルもGroovyConsole上で実行できます。

import groovy.swing.SwingBuilder
import groovy.beans.Bindable

def FORTUNE_TEXTS = ['大吉', '吉', '中吉', '小吉', '末吉', '凶']

class Model {
    @Bindable fortune
}

def model = new Model()

new SwingBuilder().edt {
    frame(title:'おみくじ', show:true, pack:true) {
        gridLayout(cols:1, rows:2)
        label(text:bind(source:model, sourceProperty:'fortune'))
        button('click', actionPerformed: {
            model.fortune = "今日の運勢 : ${FORTUNE_TEXTS[(int) (FORTUNE_TEXTS.size() * Math.random())]}"
        })
    }
}

実行結果は次のようになります。ボタンをクリックする事で運勢が表示されます。

最初のビューからモデルへのバインディングと似ていますが、今回はbindメソッドの引数にsourceとsourcePropertyを指定することでビューへのバインディングになっています。

label(text:bind(source:model, sourceProperty:'fortune'))

このコードも次のように省略形で記述する事ができます。

label(text:bind(source:model, 'fortune'))

bindメソッドでsourceを指定する場合は、クロージャを使用してさらに省略する事ができます。

label(text:bind { model.fortune })

双方向のバインディング

テキストフィールドなどの入力コンポーネントを使用していると同じプロパティを使ってモデルからビューへ、そしてビューからモデルへと双方向のバインディングが必要になる場合もあります。

これまでのサンプルでsourceとtargetプロパティを使用したので次のように記述すれば双方向のバインディングができるように思うかもしれません。

textField(text:bind(sourceProperty:'value', targetProperty:'value', target:model, source:model))

しかし、これでは期待した結果は得られません。モデルからビューへのバインディングとビューからモデルへのバインディングがループしてしまいStackOverflowErrorが発生してしまいます。

次のように、mutualプロパティを指定することで双方向のバインディングが可能になります。

textField(text:bind(targetProperty:'value', target:model, mutual:true))

ビューからビューへのバインディング

すべてのバインディングをモデルとビューの間で簡単にできるのは便利ですが、わざわざモデルにプロパティを持たせるまでもない(あるいは持たせたくない)ようなバインディングもあります。

たとえば、ラジオボタンの選択によって他の入力コンポーネントの状態を変える場合を考えてみます。

import groovy.swing.SwingBuilder
import groovy.beans.Bindable

class Model {
    @Bindable String text
}

def model = new Model()

new SwingBuilder().edt {
    frame(title:'アンケート', show:true, pack:true) {
        gridLayout(cols:1, rows:6)
        label '今回のG*Magazineは?'
        buttonGroup(id:'bg')
        radioButton 'とても良かった', buttonGroup:bg
        radioButton '良かった', buttonGroup:bg
        radioButton id:'other', 'その他', buttonGroup:bg
        textField(text:bind('text', target:model),
        editable:bind(source:other, sourceEvent:'itemStateChanged', sourceValue: {other.selected}))
    }
}

このサンプルを実行すると次のようになります。

よくあるアンケートの入力フォームですが、ラジオボタンで「その他」を選んだ場合だけテキストを自由に入力することができるようになります。

モデルにテキストフィールドの編集可能かどうかを判定するプロパティを持たせる事もできますが、モデルで持つほどのものでもないのでラジオボタンの選択によってテキストフィールドの状態を変えています。

sourceEvent、sourceValueという新しいプロパティをbindメソッドに指定しました。また、sourcePropertyは指定していません。

sourceEventを指定する事でsourceオブジェクトの任意のイベントでバインディングさせる事が可能になります。また、sourceValueを指定するとsourceとは異なるオブジェクトからバインディングさせる事が可能になります。

変換処理

バインディング時に変換処理を間に入れることも可能です。例えば、日付の年月日をそれぞれ別のコンポーネントに表示させるような場合、年月日の3つのプロパティをモデルに持たせるのは冗長で、コントローラなどからも扱いづらくなってしまいます。そんな場合はモデルに1つだけDate型のプロパティを定義しておきビューの各コンポーネントではconverterプロパティを指定することで変換した値を表示させる事ができます。

import groovy.swing.SwingBuilder
import groovy.beans.Bindable

class Model {
    @Bindable Date now
}

def model = new Model()
model.now = new Date()

new SwingBuilder().edt {
    frame(id:'frame', show:true, pack:true) {
        gridLayout(cols:6, rows:1)
        label(text:bind('now', source:model, converter: {it.format('yyyy')}))
        label('年')
        label(text:bind('now', source:model, converter: {it.format('MM')}))
        label('月')
        label(text:bind('now', source:model, converter: {it.format('dd')}))
        label('日')
    }
}

実行結果は次のとおりです。

converterはsourceValueと似ていますが、converterで指定するクロージャにはsourcePropertyの値が引数で渡されます。逆にsourceValueのクロージャには引数が渡されませんので任意の値を使用する事ができます。

バリデーション

変換処理が必要になってくると、ビューからモデルへのバインディング時に入力値のバリデーションも欲しくなってきます。モデルのすべてのプロパティをString型にしてしまう方法もありますが、とても扱いづらくなってしまうでしょう。バリデーションの実装も簡単にできます。

import groovy.swing.SwingBuilder
import groovy.beans.Bindable

class Model {
    @Bindable String postCode
}

def model = new Model()

new SwingBuilder().edt {
    frame(id:'frame', show:true, pack:true) {
        gridLayout(cols:1, rows:2)
        textField(text:bind('postCode', target:model, validator:{
            it ==~ /d{3}-d{4}/
        }))
        button('click', actionPerformed: {
            optionPane(message:model.postCode).createDialog('Message').show()
        })
    }
}

validatorプロパティで入力値を検証するクロージャを定義するだけです。このクロージャがfalseを返した場合、ビューの表示はそのままですがモデルには値が反映されません。入力値がエラーになる場合は何らかの通知をユーザに対して行う必要があります。

サンプルのバリデーションは簡単なものですが、実際にアプリケーションを作成するとGrailsのドメインクラスの様なバリデーションが欲しくなってくると思います。SwingBuilderではGrailsの様なバリデーションは提供されていませんが、GriffonのプラグインとしてValidationプラグインが提供されています。Validationプラグインについては後述します。

PropertyChangeListenerを追加する

ここまではSwingBuilderのバインディング機能を紹介してきましたが、Griffonではバージョン0.9からPropertyChangeListenerを追加するListenerアノテーションが提供されています。

Listenerアノテーションとリスナー用のクロージャを定義する事でPropertyChangeListenerを簡単に追加する事ができます。

package sample

import groovy.beans.Bindable
import griffon.beans.Listener

@Listener(loggingListener)
class ListenerSampleModel {

    @Bindable String value1
    @Bindable String value2

    def loggingListener = {
        println "${it.propertyName}:${it.oldValue} -> ${it.newValue}"
    }
}

Validationプラグイン

ここではGriffonのValidationプラグインを紹介します。バリデーションについてはSwingBuilderの機能でもできない事はありませんが、Grailsの様な豊富なバリデーションと入力エラーを通知するのにこのプラグインが利用できます。

アプリケーションの作成とプラグインのインストール

今のところValidationプラグインはGriffonのバージョン0.9にしか対応していませんので、Griffon 0.9でサンプルアプリケーションを作成していきます。

$ griffon create-app validationsample
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
Welcome to Griffon 0.9 - http://griffon.codehaus.org/
Licensed under Apache Standard License 2.0
Griffon home is set to: /usr/local/griffon/current
...

プラグインをインストールします。

$ cd validationsample
$ griffon install-plugin validation
...

モデルの編集

griffon-app/models/validationsample/ValidationsampleModel.groovyを編集していくつかのプロパティと制約を追加します。

package validationsample

import groovy.beans.Bindable

class ValidationsampleModel {
    @Bindable String requiredText
    @Bindable String url
    @Bindable String email
    @Bindable String host

    static constraints = {
        requiredText(blank:false)
        url(url:true)
        email(email:true)
        host(inetAddress:true)
    }
}

制約の指定方法はGrailsのドメインクラスと同じです。

ビューの編集

作成したモデルをテストするためにgriffon-app/views/validationsample/ValidationsampleView.groovyを変更しましょう。

package validationsample

import net.sourceforge.gvalidation.swing.ErrorMessagePanel

application(title: 'Validation Sample',
  size: [600,300],
  locationByPlatform:true,
  iconImage: imageIcon('/griffon-icon-48x48.png').image,
  iconImages: [imageIcon('/griffon-icon-48x48.png').image,
               imageIcon('/griffon-icon-32x32.png').image,
               imageIcon('/griffon-icon-16x16.png').image]) {
    tableLayout {
        tr {
            td(colspan:2) {
                widget(new ErrorMessagePanel(messageSource),
                    id: 'errorMessagePanel', errors: bind(source: model, 'errors'))
            }
        }
        tr {
            td(align:'right') { label 'Not blank' }
            td { textField(text:bind('requiredText', target:model), columns:30) }
        }
        tr {
            td(align:'right') { label 'URL' }
            td { textField(text:bind('url', target:model), columns:30) }
        }
        tr {
            td(align:'right') { label 'Email' }
            td { textField(text:bind('email', target:model), columns:30) }
        }
        tr {
            td(align:'right') { label 'Host' }
            td { textField(text:bind('host', target:model), columns:30) }
        }
        tr {
            td(colspan:2) { button('validate', actionPerformed:{model.validate()}) }
        }
    }
}

ErrorMessagePanelクラスはValidationプラグインで提供されているエラーを通知するための簡単なコンポーネントです。ボタンをクリックしたときにモデルのvalidateメソッドを呼び出してバリデーションを行います。このvalidateメソッドはValidationプラグインによって拡張されたもので、モデルにエラーがあった場合、errorsというプロパティにエラー情報が格納されます。ErrorMessagePanelにモデルのerrorsをバインディングする事でエラー情報をビューで通知する事が可能になります。

エラーメッセージの追加

griffon-app/i18n/messages.propertiesにエラーメッセージを追加します。今回はValidationプラグインのドキュメントページのエラーメッセージをそのまま追加します。

default.matches.message=Property [{0}] of class [{1}] with value [{2}] does not match the required pattern [{3}]
default.url.message=Property [{0}] of class [{1}] with value [{2}] is not a valid URL
default.creditCard.message=Property [{0}] of class [{1}] with value [{2}] is not a valid credit card number
default.email.message=Property [{0}] of class [{1}] with value [{2}] is not a valid e-mail address
default.range.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid range
default.size.message=Property [{0}] of class [{1}] with value [{2}] does not fall within the valid size
default.max.message=Property [{0}] of class [{1}] with value [{2}] exceeds maximum value [{3}]
default.min.message=Property [{0}] of class [{1}] with value [{2}] is less than minimum value [{3}]
default.maxSize.message=Property [{0}] of class [{1}] with value [{2}] exceeds the maximum size of [{3}]
default.minSize.message=Property [{0}] of class [{1}] with value [{2}] is less than the minimum size of [{3}]
default.validator.message=Property [{0}] of class [{1}] with value [{2}] does not pass custom validation
default.inList.message=Property [{0}] of class [{1}] with value [{2}] is not contained within the list [{3}]
default.blank.message=Property [{0}] of class [{1}] cannot be blank
default.notEqual.message=Property [{0}] of class [{1}] with value [{2}] cannot equal [{3}]
default.nullable.message=Property [{0}] of class [{1}] cannot be null

アプリケーションの実行

それではアプリケーションを実行してみましょう。

griffon run-app

実行すると以下のようになります。

ボタンをクリックしてエラーがあると以下のようにエラーメッセージが表示されます。

まとめ

今回はGriffonでよく使うバインディングを紹介しました。バインディングを理解する事でMVC間での情報のやりとりも、よりスマートな形で実装する事ができるようになります。次回はスレッドについて紹介したいと思います。それではまた、お会いしましょう。

著者紹介

  • 奥 清隆( おく きよたか)
  • 仕事でもときどきGroovy と戯れるプログラマ。日本Grails/Groovy ユーザーグループ関西支部長。著書:『Seasar2 によるWeb アプリケーションスーパーサンプル』

G*Magazine Vol.1

Grailsをコントロールせよ! Part 1
Griffon 不定期便 〜第2回 バインディング編〜
CodeNarcを利用してGROOVYのコード品質を上げる ~第1回 CodeNarcとは~
GebではじめるWebテスト 〜第1回 導入編〜
Grails Plugin 探訪 第2回 ~Spock プラグイン~
リリース情報 2011.02.11
ぐるーびーたん 第1話