JavaBean wrapping with JavaFX Properties

warakawa picture warakawa · May 7, 2014 · Viewed 9.4k times · Source

I want to use JavaFX properties for UI binding, but I don't want them in my model classes (see Using javafx.beans properties in model classes). My model classes have getters and setters, and I want to create properties based on those. For example, assuming an instance bean with methods String getName() and setName(String name), I would write

 SimpleStringProperty property = new SimpleStringProperty(bean, "name")

expecting that property.set("Foobar") would trigger a call to bean.setName. But this doesn't seem to work. What am I missing?

Answer

James_D picture James_D · May 7, 2014

The Simple*Property classes are full, standalone implementations of their corresponding Property abstract classes, and do not rely on any other object. So, for example, SimpleStringProperty contains a (private) String field itself which holds the current value of the property.

The parameters to the constructor you showed:

new SimpleStringProperty(bean, "name")

are:

  • bean: the bean to which the property belongs, if any
  • name: the name of the property

The bean can be useful in a ChangeListener's changed(...) method as you can retrieve the "owning bean" of the property that changed from the property itself. The name can be used similarly (if you have the same listener registered with multiple properties, you can figure out which property changed: though I never use this pattern).

So a typical use of a SimpleStringProperty as an observable property of an object looks like:

public class Person {
    private final StringProperty firstName 
        = new SimpleStringProperty(this, "firstName");

    public final String getFirstName() {
        return firstName.get();
    }

    public final void setFirstName(String firstName) {
        this.firstName.set(firstName);
    }

    public StringProperty firstNameProperty() {
        return firstName ;
    }

    // ... other properties, etc
}

The functionality you are looking for: to wrap an existing Java Bean style property in a JavaFX observable property is implemented by classes in the javafx.beans.property.adapter package. So, for example, you could do

StringProperty nameProperty = new JavaBeanStringPropertyBuilder()
        .bean(bean)
        .name("name")
        .build();

Calling

nameProperty.set("James");

with this setup will effectively cause a call to

bean.setName("James");

If the bean supports PropertyChangeListeners, the JavaBeanStringProperty will register a PropertyChangeListener with the bean. Any changes to the name property of the Java Bean will be translated by the JavaBeanStringProperty into JavaFX property changes. Consequently, if the underlying JavaBean supports PropertyChangeListeners, then changes to the bean via

bean.setName(...);

will result in any ChangeListeners (or InvalidationListeners) registered with the JavaBeanStringProperty being notified of the change.

So, for example, if the Bean class is

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class Bean {

    private String name ;
    private final PropertyChangeSupport propertySupport ;

    public Bean(String name) {
        this.name = name ;
        this.propertySupport = new PropertyChangeSupport(this);
    }

    public Bean() {
        this("");
    }

    public String getName() {
        return name ;
    }

    public String setName(String name) {
        String oldName = this.name ;
        this.name = name ;
        propertySupport.firePropertyChange("name", oldName, name);
    }

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        propertySupport.addPropertyChangeListener(listener);
    }
}

Then the following code:

Bean bean = new Bean();
StringProperty nameProperty() = new JavaBeanStringPropertyBuilder()
        .bean(bean)
        .name("name")
        .build();
nameProperty().addListener((obs, oldName, newName) -> System.out.println("name changed from "+oldName+" to "+newName));
bean.setName("James");
System.out.println(nameProperty().get());

will produce the output:

name changed from to James 
James

If the JavaBean does not support PropertyChangeListeners, then changes to the bean via bean.setName(...) will not propagate to ChangeListeners or InvalidationListeners registered with the JavaBeanStringProperty.

So if the bean is simply

public class Bean {

    public Bean() {
        this("");
    }

    public Bean(String name) {
        this.name = name ;
    }

    private String name ;

    public String getName() {
        return name ;
    }

    public void setName(String name) {
        this.name = name ;
    }
}

The JavaBeanStringProperty would have no way to observe the change, so the change listener would never be invoked by a call to bean.setName(). So the test code above would simply output

James