かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

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