JavaFX Programmatic POJO Expression Bindings (Part I)
June 6, 2012 12 Comments
UPDATE: SEE PART II
The Ties That Bind
A common feature found in most UI frameworks is the ability to bind an application’s domain model to a UI control. JavaFX uses the JavaBeans component architecture as a basis to handle bindings. JavaFX bindings provide an abstraction layer that allows properties to be bidirectionally bound. When either one of the property’s value changes the other property will reflect the change. Like many presentation layer implementations, JavaFX provides a means in FXML to bind your own properties to a JavaFX control properties to ensure the property values are the same (similar in syntax to the Expression Language used in JSP).
The Problem
While the JavaBeans API/design pattern provides a clean separation from other business specific data it does however inherit some additional coding requirements in order to provide a relationship between it and an application’s POJOs. Say, for instance, we had a Person POJO that contained multiple fields for contact information.
public class Person {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
In order to use an instance of a Person and follow the design patterns set forth by JavaFX properties we would need to create another class that would translate each of the Person fields into JavaFX recognizable Properties.
public class PersonProperties {
private final Person person;
private StringProperty firstName = new SimpleStringProperty() {
@Override
public void set(String v) {
super.set(v);
person.setFirstName(v);
}
};
private StringProperty lastName = new SimpleStringProperty() {
@Override
public void set(String v) {
super.set(v);
person.setLastName(v);
}
};
public PersonProperties(Person person) {
this.person = person;
}
public final String getFirstName() {
return firstName.get();
}
public final void setFirstName(String value) {
firstName.set(value);
}
public StringProperty firstNameProperty() {
return firstName;
}
public final String getLastName() {
return lastName.get();
}
public final void setLastName(String value) {
lastName.set(value);
}
public StringProperty lastNameProperty() {
return lastName;
}
}
The Solution
As you can see from the prior Person example the amount of extra code that would need to be generated could easily become very large and difficult to maintain. As mentioned before FXML allows for expression bindings between different properties. For example, we could take a personProperties.firstName property and bind it to a JavaFX control…
<label></label>
If we take a similar approach that FXML uses for expression bindings and apply it in a different manner we could potentially abstract the Person properties into a generic pattern that would walk an expression. Unfortunately, the plumbing FXML uses to accomplish the expression evaluation isn’t completely exposed publicly. However, we can accomplish the same thing using Java 7′s new java.lang.invoke.MethodHandle (we could use the java.lang.reflect API, but this could possibly be much slower).
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Arrays;
import javafx.beans.property.ObjectPropertyBase;
public class PathProperty extends ObjectPropertyBase {
private final String fieldPath;
private PropertyMethodHandles propMethHandles;
private final B bean;
public PathProperty(final B bean, final String fieldPath, final Class type) {
super();
this.bean = bean;
this.fieldPath = fieldPath;
try {
this.propMethHandles = PropertyMethodHandles.build(getBean(), getName());
} catch (final Throwable t) {
throw new RuntimeException(String.format(
"Unable to instantiate expression %1$s on %2$s",
getBean(), getName()), t);
}
}
@Override
public void set(T v) {
try {
getPropMethHandles().getSetter().invoke(v);
super.set(v);
} catch (final Throwable t) {
throw new RuntimeException("Unable to set value: " + v, t);
}
};
@Override
public T get() {
try {
// TODO : here we are lazily loading the property which will prevent any property listeners
// from receiving notice of a direct model field change until the next time the get method
// is called on the PathProperty
final T prop = (T) getPropMethHandles().getAccessor().invoke();
if (!super.get().equals(prop)) {
super.set(prop);
}
return super.get();
} catch (final Throwable t) {
throw new RuntimeException("Unable to get value", t);
}
}
@Override
public B getBean() {
return bean;
}
@Override
public String getName() {
return fieldPath;
}
public PropertyMethodHandles getPropMethHandles() {
return propMethHandles;
}
public static class PropertyMethodHandles {
private final String fieldName;
private final MethodHandle accessor;
private final MethodHandle setter;
private Object setterArgument;
protected PropertyMethodHandles(final Object target, final String fieldName,
final boolean insertSetterArgument) throws NoSuchMethodException {
this.fieldName = fieldName;
this.accessor = buildGetter(target, fieldName);
this.setter = buildSetter(getAccessor(), target, fieldName, insertSetterArgument);
}
public static PropertyMethodHandles build(final Object initialTarget,
final String expString) throws NoSuchMethodException, IllegalStateException {
final String[] expStr = expString.split("\\.");
Object target = initialTarget;
PropertyMethodHandles pmh = null;
for (int i = 0; i < expStr.length; i++) {
pmh = new PropertyMethodHandles(target, expStr[i], i < (expStr.length - 1));
target = pmh.getSetterArgument();
}
return pmh;
}
protected MethodHandle buildGetter(final Object target, final String fieldName)
throws NoSuchMethodException {
final MethodHandle mh = buildAccessor(target, fieldName, "get", "is", "has");
if (mh == null) {
throw new NoSuchMethodException(fieldName);
}
return mh;
}
protected MethodHandle buildSetter(final MethodHandle accessor,
final Object target, final String fieldName,
final boolean insertSetterArgument) {
if (insertSetterArgument) {
try {
this.setterArgument = accessor.invoke();
} catch (final Throwable t) {
this.setterArgument = null;
}
if (getSetterArgument() == null) {
try {
this.setterArgument = accessor.type().returnType().newInstance();
} catch (final Exception e) {
throw new IllegalArgumentException(
String.format("Unable to build setter expression for %1$s using %2$s.",
fieldName, accessor.type().returnType()));
}
}
}
try {
final MethodHandle mh1 = MethodHandles.lookup().findVirtual(target.getClass(),
buildMethodName("set", fieldName),
MethodType.methodType(void.class,
accessor.type().returnType())).bindTo(target);
if (getSetterArgument() != null) {
mh1.invoke(getSetterArgument());
}
return mh1;
} catch (final Throwable t) {
throw new IllegalArgumentException("Unable to resolve setter "
+ fieldName, t);
}
}
protected static MethodHandle buildAccessor(final Object target,
final String fieldName, final String... fieldNamePrefix) {
final String accessorName = buildMethodName(fieldNamePrefix[0], fieldName);
try {
return MethodHandles.lookup().findVirtual(target.getClass(), accessorName,
MethodType.methodType(
target.getClass().getMethod(
accessorName).getReturnType())).bindTo(target);
} catch (final NoSuchMethodException e) {
return fieldNamePrefix.length buildAccessor(target, fieldName,
Arrays.copyOfRange(fieldNamePrefix, 1,
fieldNamePrefix.length));
} catch (final Throwable t) {
throw new IllegalArgumentException(
"Unable to resolve accessor " + accessorName, t);
}
}
public static String buildMethodName(final String prefix,
final String fieldName) {
return (fieldName.startsWith(prefix) ? fieldName : prefix +
fieldName.substring(0, 1).toUpperCase() +
fieldName.substring(1));
}
public String getFieldName() {
return fieldName;
}
public MethodHandle getAccessor() {
return accessor;
}
public MethodHandle getSetter() {
return setter;
}
public Object getSetterArgument() {
return setterArgument;
}
}
}
Now with the above PathProperty we create bindings on the fly by passing a path expression string into it’s constructor. The path will be traversed until the last field is reached. It supports the instantiation of intermediate objects found while walking the path expression (as long as the class type of that object contains a no-argument constructor). So, revisiting the Person class, if we add another field called address that takes an Address class with an accessible getStreetName method we can do the following:
Person person = new Person();
PathProperty prop = new PathProperty(
person, "address.streetName", String.class);
// bind it to a JavaFX control
Bindings.bindBidirectional(prop, myTextField.textProperty());
// apply value to the JavaFX property and verify the same value exists in the model
prop.set("123 1st Street");
System.out.println("Address Property: " + prop.get());
System.out.println("Address POJO: " + person.getAddress().getStreetName());
// apply value to the POJO field and verify the same value exists in the JavaFX property
person.getAddress().setStreetName("456 2nd Street");
System.out.println("Address Property: " + prop.get());
System.out.println("Address POJO: " + person.getAddress().getStreetName());
In A Bind
As you can see the values are virtually bound bidirectionally between the JavaFX property and the POJO. Any calls to PathProperty#set(T v) will dispatch the required events that allow JavaFX to reflect those changes in the bound property of the control. However, there are some drawbacks to this approach. You may have noticed the TODO in the PathProperty#get() method shown above. Due to Java’s lack of native dependency injection instrumentation without a Java agent or byte-code manipulation any listeners that are listening to a PathProperty will not be notified of changes made directly to the POJO until the next time the PathProperty#get() method is called. Lazy loading of property values is really less than adequate for most applications where domain POJOs are being altered outside the scope of JavaFX.
Pingback: JavaFX Programmatic POJO Expression Bindings (Part II) « UGate
Looks very good! In order to prevent not compiler checked path-strings all over the place, a good step would be to put those string in one place, but that could be expanded to a wrapped class that takes a person and sets up a bunch of those PathProperty methods, e.g. “streetNameProperty()”, which would mimick JavaFX more closely. Or should one go for final public variables, e.g. “streetNameProperty” because it involves less scaffolding?
What I ended up doing is creating an enum for each top level class so the path strings would at least be in one central location. I actually went a step further and abstracted the PathProperty portion in part 2.
public enum ActorType implements IModelType<Actor> { USERNAME("login"), PASSWORD("pwd"), USE_METRIC("host.useMetric"), HOST_COM_ADDY("host.comAddress"), HOST_COM_PORT("host.comPort"), HOST_BAUD_RATE("host.comBaud"), MAIL_RECIPIENTS("host.mailRecipients"), MAIL_SMTP_HOST("host.mailSmtpHost"), MAIL_SMTP_PORT("host.mailSmtpPort"), MAIL_IMAP_HOST("host.mailImapHost"), MAIL_IMAP_PORT("host.mailImapPort"), MAIL_INBOX_NAME("host.mailInboxName"), MAIL_USERNAME("host.mailUserName"), MAIL_PASSWORD("host.mailPassword"), WEB_HOST("host.webHost"), WEB_PORT("host.webPort"); public final String key; private ActorType(final String key) { this.key = key; } public String getKey() { return this.key; } }Hi, how would you go about binding a Collection (List) of POJOs?
Part 2 has a more robust version of POJO binding that may answer your question? If you have a list of POJOs you can create as many BeanPathAdapter instances for each of the POJOs you need to display at one time. You can also reuse a BeanPathAdapter if all you need is to display fields of one POJO at a time, but would like to switch between a list of POJOs (see example in part 2).
Sorry for not being concise enough. I was thinking more on binding the TableView’s or ListView’s items property to a List. For example, if a Person can have zero or more Phones (0:n cardinality, represented by an List in the Person POJO), and I want to bind JavaFX ListView to those Phones bidirectionally? Would it be possible for me to do this:
personPA.bindBidirectional("phones", phonesListView.itemsProperty());Currently, there isn’t a way to bind collection properties to POJO fields. I’m working on a solution that will allow for
BeanPathAdapter#bindBidirectional(String, Property<ObservableList>, Class),BeanPathAdapter#bindBidirectional(String, Property<ObservableSet>, Class), andBeanPathAdapter#bindBidirectional(String, Property<ObservableMap>, Class, Class). I’ll post the results once completed.There’s now a way to bind collections/maps… see Part III
Thanks, I was just checking it out!
I’ll make sure to give some feedback when I’m done.
OK, thank you very much, what you’ve done so far is excellent. BTW, are you aware of the javafx.beans.property.adapter package in JavaFX 2.1?
I chose not to use
javafx.beans.property.adapterso I could take advantage of Java 7′s MethodHandles which are much faster and less memory intensive than the reflection API that is used injavafx.beans.property.adapter.Pingback: JavaFX Programmatic POJO Expression Bindings (Part III) « UGate