Bindings are an effective way to keep two properties in sync. Unfortunately Java does not provide a mechanism nor an API to make bindings, but Griffon does.As shown in section 6.2 Binding, Griffon relies on PropertyChangeEvent
and PropertyChangeListener
to keep track of property changes and notify observers. Swing components are already observable by default. You can build your own observable classes by following a convention, or implement the Observable interface (there's a handy partial implementation in AbstractObservable that you can subclass).Bindings can be created by using BindUtils.binding(), like the following example showspackage sample;import java.util.Map;
import groovy.util.FactoryBuilderSupport;
import griffon.swing.BindUtils;
import org.codehaus.griffon.runtime.core.AbstractGriffonView;public class SampleView extends AbstractGriffonView {
private SampleController controller;
private SampleModel model; public void setController(SampleController controller) {
this.controller = controller;
} public void setModel(SampleModel model) {
this.model = model;
} public void mvcGroupInit(Map<String, Object> args) {
buildViewFromXml(args); FactoryBuilderSupport builder = getBuilder(); /*
* Equivalent Groovy code
* bind(source: input, sourceProperty: 'text',
* target: model, targetProperty: 'value')
*/
BindUtils.binding()
.withSource(builder.getVariable("input"))
.withSourceProperty("text")
.withTarget(model))
.withTargetProperty("value")
.make(builder); /*
* Equivalent Groovy code
* bind(source: model, sourceProperty: 'value',
* target: input, targetProperty: 'text')
*/
BindUtils.binding()
.withSource(model)
.withSourceProperty("value")
.withTarget(builder.getVariable("output"))
.withTargetProperty("text")
.make(builder);
}
}
The following rules apply:
- both
source
and target
values must be specified. An IllegalArgumentException
will be thrown if that's not the case.
- both
source
and target
instances must be observable. This does not imply that both must implement Observable per se, as Swing components do not.
- either
sourceProperty
or targetProperty
can be omitted but not both. The missing value will be taken from the other property.
- the
builder
instance must be able to resolve the bind()
node. This is typically the case for the default builder supplied to Views (because Swingbuilder is included).
Bindings created in this way also support the following attributes: mutual
, converter
and validator
. The next snippet improves on the previous example by setting a converter and a validator, only numeric values will be accepted.package sample;import java.util.Map;
import groovy.util.FactoryBuilderSupport;
import griffon.swing.BindUtils;
import griffon.util.CallableWithArgs;
import org.codehaus.griffon.runtime.core.AbstractGriffonView;public class SampleView extends AbstractGriffonView {
private SampleController controller;
private SampleModel model; public void setController(SampleController controller) {
this.controller = controller;
} public void setModel(SampleModel model) {
this.model = model;
} public void mvcGroupInit(Map<String, Object> args) {
buildViewFromXml(args); FactoryBuilderSupport builder = getBuilder(); /*
* Equivalent Groovy code
* bind(source: input, sourceProperty: 'text',
* target: model, targetProperty: 'value',
* converter: {v -> v? "FOO $v" : 'BAR'},
* validator: {v ->
* if(v == null) true
* try { Integer.parseInt(String.valueOf(v)); true }
* catch(NumberFormatException e) { false }
* })
*/
BindUtils.binding()
.withSource(builder.getVariable("input"))
.withSourceProperty("text")
.withTarget(model)
.withTargetProperty("value")
.withConverter(new CallableWithArgs<String>() {
public String call(Object[] args) {
return args.length > 0 ? "FOO "+ args[0] : "BAR";
}
})
.withValidator(new CallableWithArgs<Boolean>() {
public Boolean call(Object[] args) {
if(args.length == 0) return Boolean.TRUE;
try {
Integer.parseInt(String.valueOf(args[0]));
return Boolean.TRUE;
} catch(NumberFormatException e) {
return Boolean.FALSE;
}
}
})
.make(builder); /*
* Equivalent Groovy code
* bind(source: model, sourceProperty: 'value',
* target: input, targetProperty: 'text')
*/
BindUtils.binding()
.withSource(model)
.withSourceProperty("value")
.withTarget(builder.getVariable("output"))
.withTargetProperty("text")
.make(builder);
}
}
The View for these examples is defined in XML format (as described in the previous section)<application title="app.config.application.title"
pack="true">
<actions>
<action id="'clickAction'"
name="'Click'"
closure="{controller.click(it)}"/>
</actions>
<gridLayout cols="1" rows="3"/>
<textField id="'input'" columns="20"/>
<textField id="'output'" columns="20" editable="false"/>
<button action="clickAction"
</application>
However you can build the View in any way, bindings do not require an specific View construction mechanism in order to work.