かずきのBlog@hatena

日本マイクロソフトに勤めています。XAML + C#の組み合わせをメインに、たまにASP.NETやJavaなどの.NET系以外のことも書いています。掲載内容は個人の見解であり、所属する企業を代表するものではありません。

Beans Bindingその2「ConverterとValidator」

さっきの続き。
ちょっとBindingクラスのメンバを眺めてみるとJSFみたいにConverterやValidatorを仕掛けることが出来るっぽい。
Converterはjavax.beans.binding.BindingConverteを継承して作る。
Validatorはjavax.beans.binding.BindingValidatorを継承して作る。

どういう風に動くのかをトレースしてみた。
トレースのために下みたいなコンバータとバリデータを作った。

// ----------
// PersonConverter.java
package okazuki;

import javax.beans.binding.BindingConverter;

public class PersonConverter extends BindingConverter {

    public Object sourceToTarget(Object source) {
        System.out.println(">> converter: source = " + source);
        return source;
    }

    @Override
    public Object targetToSource(Object target) {
        System.out.println(">> converter: target = " + target);
        return target;
    }
}

// ----------
// PersonValidator.java
package okazuki;

import javax.beans.binding.Binding;
import javax.beans.binding.BindingValidator;
import javax.beans.binding.ValidationResult;

public class PersonValidator extends BindingValidator {

    public PersonValidator() {
    }

    public ValidationResult validate(Binding binding, Object value) {
        System.out.println(">> validator: value = " + value);
        return null;
    }
}

そして、さっきのmainを下のように書き換えて実行した。

package okazuki;

import javax.beans.binding.Binding;
import javax.beans.binding.BindingContext;

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person();
        Person p2 = new Person();
        
        BindingContext ctx = new BindingContext();
        // Bindingオブジェクトに対してコンバータとバリデータを仕掛ける
        Binding binding = ctx.addBinding(p1, "${name}", p2, "name");
        binding.setConverter(new PersonConverter());
        binding.setValidator(new PersonValidator());
        ctx.bind();
        
        System.out.println("p1.name change");
        p1.setName("太郎");
        System.out.println("p1.name = " + p1.getName());
        System.out.println("p2.name = " + p2.getName());
        
        System.out.println("p2.name change");
        p2.setName("二郎");
        System.out.println("p1.name = " + p1.getName());
        System.out.println("p2.name = " + p2.getName());
    }
}

実行すると結果は下のようになった。

p1.name change
>> converter: source = 太郎
p1.name = 太郎
p2.name = 太郎
p2.name change
>> converter: target = 二郎
>> validator: value = 二郎
p1.name = 二郎
p2.name = 二郎

今更だけど、addBindingのときに最初に渡してるクラスがソースで、後に渡してるクラスがターゲットというらしい。
動きを見てるとこんな感じのサイクルで動いてるのかな。

  • Source to Targetの場合
  1. sourceの値が書き換わる
  2. ConverterのsourceToTargetメソッド呼び出し
  3. sourceToTargetの結果をtargetに設定
  • Target to Sourceの場合
  1. targetの値が書き換わる
  2. ConverterのtargetToSourceメソッド呼び出し
  3. Validatorのvalidateメソッドの呼び出し
  4. validateメソッドの結果に応じて動作が変わる
    1. nullを返した時は普通にsourceの値が書き換わる
    2. javax.beans.binding.ValidationResultを返した時は、javax.beans.binding.ValidationResultのコンストラクタに設定したActionが実行される

ちなみにValidatorのvalidateメソッドで返すValidationResultに設定するアクションは、デフォルトで下の2つが提供されてる。

  • 何もしない
    • ValidationResult.Action.DO_NOTHING
  • ソースの値をターゲットに設定する
    • ValidationResult.Action.SET_TARGET_FROM_SOURCE

Converter/Validatorお試し

PersonクラスにbirthdayというDate型のプロパティを追加して、それをテキストフィールドにバインドする場合の例を書いてみた。
解説はめんどいので、コードを読み解いてもらうといいかも!?

// Person.java
package okazuki;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

public class Person {
    private String name;
    private Date birthday;
    private List<PropertyChangeListener> propertyChangeListeners = new LinkedList<PropertyChangeListener>();
    public Person() {
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        String oldName = this.name;
        this.name = name;
        firePropertyChange("name", oldName, name);
    }

    public Date getBirthday() {
        return birthday;
    }

    public void setBirthday(Date birthday) {
        Date old = this.birthday;
        this.birthday = birthday;
        firePropertyChange("birthday", old, birthday);
    }
    
    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeListeners.add(l);
    }
    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeListeners.remove(l);
    }
    protected void firePropertyChange(String name, Object oldValue, Object newValue) {
        PropertyChangeEvent evt = new PropertyChangeEvent(this, name, oldValue, newValue);
        for (PropertyChangeListener l : propertyChangeListeners) {
            l.propertyChange(evt);
        }
    }
}

んでメインは下のように書き換える

package okazuki;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.beans.binding.Binding;
import javax.beans.binding.BindingContext;
import javax.beans.binding.BindingConverter;
import javax.beans.binding.BindingValidator;
import javax.beans.binding.ValidationResult;
import javax.swing.JTextField;

public class Main {

    public static void main(String[] args) {
        Person p = new Person();
        JTextField text = new JTextField();

        BindingContext ctx = new BindingContext();
        // 誕生日とテキストフィールドのテキストのバインド
        Binding binding = ctx.addBinding(p, "${birthday}", text, "text");
        binding.setConverter(new BindingConverter(){
            private SimpleDateFormat fmt = new SimpleDateFormat("yyyy/MM/dd");
            public Object sourceToTarget(Object source) {
                // nullは空文字
                if (source == null) return "";
                // あとはフォーマットする
                return fmt.format((Date) source);
            }
            @Override
            public Object targetToSource(Object target) {
                try {
                    return fmt.parse((java.lang.String) target);
                } catch (ParseException ex) {
                    return null;
                }
            }
        });
        binding.setValidator(new BindingValidator(){
            public ValidationResult validate(Binding binding, Object value) {
                // nullは無効なフォーマットが入力された証!
                if (value == null) {
                    return new ValidationResult(ValidationResult.Action.SET_TARGET_FROM_SOURCE);
                }
                // nullじゃなければOK
                return null;
            }
        });
        ctx.bind();
        
        System.out.println("誕生日に今日の日付をセット");
        p.setBirthday(new Date());
        System.out.println("textField.text = " + text.getText());
        System.out.println("p.birthday = " + p.getBirthday());
        
        System.out.println("テキストフィールドにでたらめな値をセット");
        text.setText("hoge");
        binding.setSourceValueFromTargetValue();
        System.out.println("textField.text = " + text.getText());
        System.out.println("p.birthday = " + p.getBirthday());
        
        System.out.println("誕生日にnullをセット");
        p.setBirthday(null);
        System.out.println("textField.text = " + text.getText());
        System.out.println("p.birthday = " + p.getBirthday());
    }
}

実行結果は下の通り

誕生日に今日の日付をセット
textField.text = 2007/07/07
p.birthday = Sat Jul 07 14:39:45 JST 2007
テキストフィールドにでたらめな値をセット
textField.text = 2007/07/07
p.birthday = Sat Jul 07 14:39:45 JST 2007
誕生日にnullをセット
textField.text = 
p.birthday = null