BeansBinding1.2.1でButtonGroupを扱う(読み出しのみ)

BeansBinding1.2.1でバインディングを使ってButtonGroupから選択されているボタンの読み出しに成功したので、記録しておく。
前回の失敗はこれ

BeansBindingの拡張機能

BeansBindingには、一般的なJavaBeansの規格に沿わないクラスに対してバインディングを定義するような拡張機能が用意されています。それがorg.jdesktop.beansbinding.extパッケージ内にあるクラス、BeanAdapterFactoryクラスとBeanAdapterProviderインターフェースです。
今回はこの拡張機能を使ってButtonGroupの読み出しを行います。

BeanAdapterFactoryクラス

監視対象のオブジェクトとプロパティに応じて、適切なBeanAdapterProviderを返すクラスらしいです。独自のアダプタを使うには、BeanAdapterFactoryクラスにアダプタを登録する必要があります。
アダプタクラスを登録するには、「META-INF/services/」というリソースパスに「org.jdesktop.beansbinding.ext.BeanAdapterProvider」というファイルを用意して、その中にBeanAdapterProviderを実装したクラスの完全修飾クラス名を記述すればいいらしいです。(beansbinding.jarの中身は書き換えなくてもOK。)
 eclipseNetBeansで実行する場合は、ソースフォルダにMETA-INF/servicesというフォルダを作って、ファイルをおいてやればOK
仕組みとしては、JARのサービスプロバイダー機能とほぼ同じになっているようです。(独自実装だけど。)

BeanAdapterProviderインターフェース

Beanアダプタが実装しなければいけないインターフェースです。以下のメソッドを見てもらえば分かるように、渡された監視対象のオブジェクトの型に応じて、そのアダプタが対応しているかどうかを返したり、アダプタインスタンスを作ったりすれば良いようです。

createAdapter(Object source, String property)メソッド
監視対象のオブジェクトを BeansBindingからバインドするためのアダプタインスタンスを作成して返す。
getAdapterClass(Class type)メソッド
監視対象のオブジェクトの型を引数に渡すと、それに応じてアダプタクラスを返す。該当しない場合はnullを返す。
providesAdapter(Class type, String property)メソッド
監視対象のオブジェクトに対してアダプタが提供できるかどうかを返す。

ソースコード

BeansBindingのソースコードを参考にしています。

プロバイダ構成ファイル(META-INF/services/org.jdesktop.beansbinding.ext.BeanAdapterProvider)
cnaos.beansbinding.sample.ButtonGroupAdapterProvider
ButtonGroupAdapterProvider.java

内部クラスとしてAdapter, そのまた内部クラスとしてHandlerが宣言されている事に注意してください。

package cnaos.beansbinding.sample;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeListener;
import java.util.Enumeration;
import javax.swing.AbstractButton;
import javax.swing.ButtonGroup;
import javax.swing.ButtonModel;
import org.jdesktop.beansbinding.ext.BeanAdapterProvider;
import org.jdesktop.swingbinding.adapters.BeanAdapterBase;

/**
 *
 * @author cnaos
 */
public class ButtonGroupAdapterProvider implements BeanAdapterProvider {
    /** プロパティ名 */
    private static final String SELECTION_P = "selection";

    /**
     * アダプタクラス
     *
     */
    public static final class Adapter extends BeanAdapterBase {
        /** 監視対象のボタングループ */
        private ButtonGroup buttonGroup;
        /** 選択したボタンのキャッシュ */
        private Object cachedItem;
        /** 監視対象オブジェクトが操作されたときに呼び出されるハンドラ */
        private Handler handler;

        /**
         *
         * @param buttonGroup 監視対象とするオブジェクト
         */
        private Adapter(ButtonGroup buttonGroup) {
            super(SELECTION_P);
            this.buttonGroup = buttonGroup;
        }

        /**
         * ボタンが選択されているかどうかを返す
         * @param m
         * @return
         */
        public boolean isSelected(ButtonModel m){
            return buttonGroup.isSelected(m);
        }
        /**
         * 選択されたボタンのモデルを返します。
         * @return
         */
        public ButtonModel getSelection() {
            return buttonGroup.getSelection();
        }

        /**
         *  ButtonModel に対して選択された値を設定します。
         * @param m
         * @param b
         */
        public void setSelected(ButtonModel m, boolean b) {
            buttonGroup.setSelected(m,b);
        }

        @Override
        protected void listeningStarted() {
            handler = new Handler();
            cachedItem = buttonGroup.getSelection();
            // ボタングループ内の全てのボタンに対してハンドラを追加
            for (Enumeration e = buttonGroup.getElements(); e.hasMoreElements();) {
                AbstractButton button = (AbstractButton) e.nextElement();
                button.addActionListener(handler);
            }
        }

        @Override
        protected void listeningStopped() {
            // ボタングループ内の全てのボタンからハンドラを除去
            for (Enumeration e = buttonGroup.getElements(); e.hasMoreElements();) {
                AbstractButton button = (AbstractButton) e.nextElement();
                button.removeActionListener(handler);
            }
            handler = null;
            cachedItem = null;
        }

        /**
         * ハンドラクラス
         * 読み込みだけなので、ActionListenerのみ。
         */
        private class Handler implements ActionListener {
            private void buttonGroupSelectionChanged() {
                Object oldValue = cachedItem;
                cachedItem = getSelection();
                firePropertyChange(oldValue, cachedItem);
            }

            public void actionPerformed(ActionEvent ae) {
                buttonGroupSelectionChanged();
            }
        }
    }

    /**
     * 提供しているアダプタが、監視対象のオブジェクトに適用出来るかどうかを返す
     * @param type 監視対象のオブジェクトの型
     * @param property 監視対象のオブジェクトのプロパティ
     * @return trueならアダプタが適用出来る。
     */
    public boolean providesAdapter(Class<?> type, String property) {
        return ButtonGroup.class.isAssignableFrom(type) && property.intern() == SELECTION_P;
    }

    /**
     * アダプタのインスタンスを作って返す
     * @param source 監視対象オブジェクト(ButtonGroupを想定)
     * @param property 監視対象オブジェクトのプロパティ
     * @return アダプタ
     */
    public Object createAdapter(Object source, String property) {
        if (!providesAdapter(source.getClass(), property)) {
            throw new IllegalArgumentException();
        }

        return new Adapter((ButtonGroup)source);
    }

    /**
     * アダプタクラスを返す
     * @param type 監視対象のオブジェクトの型
     * @return アダプタクラス。監視対象でない場合、nullが返される。
     */
    public Class<?> getAdapterClass(Class<?> type) {
        return ButtonGroup.class.isAssignableFrom(type) ?
            ButtonGroupAdapterProvider.Adapter.class :
            null;
    }
}

説明

ButtonGroupAdapterProviderクラス

BeansBindingのフレームワークから、監視対象のオブジェクトに対して適切なアダプタが存在するか問い合わせを受けて、その適切なアダプタを作って返すクラスです。
実際に値を読み出す処理を記述するのはアダプタクラスの方です。

必要なモノ

  • アダプタのBeanプロパティのプロパティ名として使う文字列定数
  • インターフェースで宣言されているメソッド
    • createAdapter(Object source, String property)メソッド
    • getAdapterClass(Class type)メソッド
    • providesAdapter(Class type, String property)メソッド
  • アダプタクラスを内部クラスAdapterとして宣言
    • BeanAdapterBaseクラスを継承する
Adapterクラス

BeanAdapterBaseクラスを継承して作成します。
BeanAdapterBaseクラスでは、listeningStarted()メソッドとlisteningStopped()メソッドの実装が空になっているので、これをオーバーライドして初期化処理と後始末の処理を実装してやればよいです。
listeningStarted()メソッドはバインド(Binding.bind())をしたときに呼ばれ、listeningStopped()メソッドはアンバインド(Binding.unbind())したときによばれます。(たぶん)

BeanAdapterBaseのコンストラクタで渡している引数はBeanのプロパティ名っぽいです。これはNetBeansGUIエディタ(Matisse)でButtonGroupのバインドを選んだときに出てきた「selection」という文字列を指定しています。

必要なモノ

  • 監視対象のオブジェクトを保持するフィールド(ButtonGroup)
  • 選択されたラジオボタンをキャッシュするためのフィールド(Object)
    • 何が何に変更されたのかを通知する firePropertyChange()メソッドのために必要。
  • 監視対象のオブジェクトが操作されたときに呼び出されるハンドラ(Handler)
  • listeningStarted()メソッド
    • ButtonGroupに属している各ボタンに対してアクションリスナを登録。ラジオボタンが選択されたときにハンドラクラス(Handler)を呼び出すように設定する。
  • listeningStopped()メソッド
    • ButtonGroupに属している各ボタンからハンドラクラス(Handler)のアクションリスナを除去。
  • アダプタが保持しているButtonGroupの操作用メソッド
    • 読み込みだけだと使わないけど。
Handlerクラス

JRadioButtonの選択が変更された時の読み込みだけなので、ActionListenerを継承したクラスを作成します。actionPerformed()メソッドでは、以前に選択されていたラジオボタンと、操作して選択されたラジオボタンを引数にして、BeanAdapterBaseクラスのfirePropertyChange()を発行するだけです。

このアダプタを使ったバインド方法(NetBeansの場合)

  1. バインドターゲットの部品のバインド設定ダイアログを開く
  2. ソースに「ButtonGroup」のインスタンスを選択する
  3. 式に「${selection.actionCommand}」を選択する。
  4. RadioButtonのactionCommandプロパティを設定する。(この例ではAAAという文字列を指定。)