Javabean Wrapping with Javafx Properties

JavaBean wrapping with JavaFX Properties

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

JavaFX Wrap an Existing Object with Simple Properties

In general, as long as you don't change the public API of the class - in other words you don't remove any public methods, modify their parameter types or return types, or change their functionality - you should not break any code that uses them.

So, e.g. a change from

public class Foo {

private String bar ;

public String getBar() {
return bar ;
}

public void setBar(String bar) {
this.bar = bar ;
}
}

to

public class Foo {
private final StringProperty bar = new SimpleStringProperty();

public StringProperty barProperty() {
return bar ;
}

public String getBar() {
return barProperty().get();
}

public void setBar(String bar) {
barProperty().set(bar);
}
}

should not break any clients of the class Foo. The only possible problem is that classes that have subclassed Foo and overridden getBar() and/or setBar(...) might get unexpected behavior if their superclass is replaced with the new implementation (specifically, if getBar() and setBar(...) are not final, you have no way to enforce that getBar()==barProperty().get(), which is desirable).

For enums (and other objects) you can use an ObjectProperty<>:

Given

public enum Option { FIRST_CHOICE, SECOND_CHOICE, THIRD_CHOICE }

Then you can do

public class Foo {

private final ObjectProperty<Option> option = new SimpleObjectProperty<>();

public ObjectProperty<Option> optionProperty() {
return option ;
}

public Option getOption() {
return optionProperty().get();
}

public void setOption(Option choice) {
optionProperty().set(choice);
}
}

One caveat to all this is that you do introduce a dependency on the JavaFX API that wasn't previously present in these classes. JavaFX ships with the Oracle JDK, but it is not a full part of the JSE (e.g. it is not included in OpenJDK by default, and not included in some other JSE implementations). So in practice, you're highly unlikely to be able to persuade the developers of the open source library to accept your changes to the classes in the library. Since it's open source, you can of course maintain your own fork of the library with JavaFX properties, but then it will get tricky if you want to incorporate new versions of that library (you will need to merge two different sets of changes, essentially).

Another option is to use bound properties in the classes, and wrap them using a Java Bean Property Adapter. This is described in this question.

JavaBeanProperties in JavaFX without pulling in java.desktop (Swing, AWT)

You could use a generic solution that doesn’t use Reflection at all:

public class DelegatingProperty<B,T> extends ObjectPropertyBase<T>
implements JavaBeanProperty<T> {
/**
* Create a property without PropertyChangeEvent based notification.
*/
public static <O, V> JavaBeanProperty<V> get(O bean, String name,
Function<? super O, ? extends V> getter,
BiConsumer<? super O, ? super V> setter) {
return new DelegatingProperty<>(bean, name, getter, setter, null, null);
}
/**
* Create a property with PropertyChangeEvent based notification.
*/
public static <O, V> JavaBeanProperty<V> get(O bean, String name,
Function<? super O, ? extends V> getter, BiConsumer<? super O, ? super V> setter,
BiConsumer<? super O, ? super PropertyChangeListener> register,
BiConsumer<? super O, ? super PropertyChangeListener> unregister) {
return new DelegatingProperty<>(bean, name, getter, setter, register, unregister);
}
B bean;
String name;
Function<? super B, ? extends T> getter;
BiConsumer<? super B, ? super T> setter;
BiConsumer<? super B, ? super PropertyChangeListener> unregister;
PropertyChangeListener listener;

private DelegatingProperty(B bean, String name,
Function<? super B, ? extends T> getter,
BiConsumer<? super B, ? super T> setter,
BiConsumer<? super B, ? super PropertyChangeListener> register,
BiConsumer<? super B, ? super PropertyChangeListener> unregister) {
this.bean = Objects.requireNonNull(bean);
this.name = name;
this.getter = Objects.requireNonNull(getter);
this.setter = Objects.requireNonNull(setter);
if(register != null || unregister != null) {
Objects.requireNonNull(register);
this.unregister = Objects.requireNonNull(unregister);
register.accept(bean, listener = event -> fireValueChangedEvent());
}
}

@Override
public Object getBean() {
return bean;
}

@Override
public String getName() {
return name;
}

@Override
public T get() {
return getter.apply(bean);
}

@Override
public void set(T value) {
if(isBound()) throw new IllegalStateException("bound property");
T old = getter.apply(bean);
setter.accept(bean, value);
T now = getter.apply(bean);
if(!Objects.equals(old, now)) fireValueChangedEvent();
}

@Override
protected void invalidated() {
if(isBound()) {
setter.accept(bean, super.get());
}
}

@Override
public void fireValueChangedEvent() {
super.fireValueChangedEvent();
}

@Override
public void dispose() {
if(unregister != null && listener != null) {
unregister.accept(bean, listener);
listener = null;
}
}
}

Then, to stay at your example, you could get the name property of Entity as

JavaBeanProperty<String> prop = DelegatingProperty.get(bean, "name",
Entity::getName, Entity::setName,
Entity::addPropertyChangeListener, Entity::removePropertyChangeListener);

It’s more verbose, but on the other hand, provides more compile time safety, as the presence of all methods required for the property is checked at compile-time, and will likely have a higher runtime performance.

When you have a lot of properties in one bean class with event support, you may benefit from a dedicated factory method, e.g.

public static <V> JavaBeanProperty<V> property(Entity theBean, String name,
Function<? super Entity, ? extends V> getter,
BiConsumer<? super Entity, ? super V> setter) {
return DelegatingProperty.get(theBean, name, getter, setter,
Entity::addPropertyChangeListener, Entity::removePropertyChangeListener);
}

which you then can use as

JavaBeanProperty<String> nameProp
= property(bean, "name", Entity::getName, Entity::setName);
JavaBeanProperty<String> otherProp
= property(bean, "other", Entity::getOther, Entity::setOther);

Of course, it would also be possible to provide them via instance methods of the bean itself instead of a static factory method, too, perhaps with a lazily populated field holding the property, etc.

There are several roads you can go from this starting point.

Using javafx.beans properties in model classes

I'm going to offer something of a dissenting opinion here.

JavaFX Properties and JPA

As I commented on jewelsea's answer, using a JavaFX property-based bean with JPA is possible as long as you use "property access" rather than "field access". The blog post I linked there goes into more detail on this, but the basic idea is that any annotations should be on the get...() methods and not on the fields. As far as I can see, this does prevent use of any of the read-only JavaFX property patterns in conjunction with JPA, but I've never really felt JPA played well with read only properties (i.e. get methods and no set method) anyway.

Serialization

Contrary to my comment on jewelsea's answer, and with the benefit of a few weeks to work with this (and having been placed in a position where I was facing replicating several entity classes on a JavaFX client side using JavaFX properties), I think the lack of serialization of JavaFX properties can be worked around. The key observation is that you really only need to serialize the wrapped state of the property (not, for example, any listeners). You can do this by implementing java.io.Externalizable. Externalizable is a sub-interface of Serializable that requires you to fill in the readExternal(...) and writeExternal(...) methods. These methods can be implemented to externalize just the state wrapped by the property, rather than the property itself. This means that if your entity is serialized and then deserialized, you will end up with a new property instance, and any listeners will not be preserved (i.e. the listeners effectively become transient), but as far as I can see this would be what was wanted in any reasonable use case.

I experimented with beans defined this way and it all seems to work nicely. Additionally, I ran a small experiment in transferring them between the client and a restful web service, using the Jackson mapper to convert to and from a JSON representation. Since the mapper just relies on using the get and set methods, this works just fine.

Some caveats

A couple of points need to be observed. As with any Serialization, it's important to have a no-argument constructor. And of course, all values wrapped by the JavaFX properties must themselves be serializable - again this is the same rule as for any serializable bean.

The point about JavaFX properties working via side-effects is well taken, and care needs to be exercised when moving these properties (which are, to some extent, designed with a single-threaded model in mind) to a potentially multi-threaded server. A good rule of thumb is probably that if you use this strategy, listeners should only be registered on the client side (and remember, those listeners are transient with respect to transfer back to the server, whether by serialization or by JSON representation). Of course, that suggests that using these on the server side then might be a bad design; it becomes a trade-off between the convenience of having a single entity that is "all things to all people" (observable properties for the JavaFX client, serializable for persistence and/or remote access, and with persistent mappings for JPA) versus exposing functionality (e.g. observability) where it might not be fully appropriate (on the server).

Finally, if you do use the JPA annotations, those have runtime retention which implies (I think) that your JavaFX client will need the a javax.persistence specification on the classpath).

Here's an example of such a "man for all seasons" entity:

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.time.MonthDay;

import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
* Entity implementation class for Entity: Person
*
*/
@Entity

public class Person implements Externalizable {

private static final long serialVersionUID = 1L;

public Person() {

}

public Person(String name, MonthDay birthday) {
setName(name);
setBirthday(birthday);
}

private final IntegerProperty id = new SimpleIntegerProperty(this, "id");

@Id
@GeneratedValue(strategy=GenerationType.AUTO)
public int getId() {
return id.get();
}

public void setId(int id) {
this.id.set(id);
}

public IntegerProperty idProperty() {
return id ;
}

private final StringProperty name = new SimpleStringProperty(this, "name");

// redundant, but here to indicate that annotations must be on the property accessors:
@Column(name="name")
public final String getName() {
return name.get();
}

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

public StringProperty nameProperty() {
return name ;
}

private final ObjectProperty<MonthDay> birthday = new SimpleObjectProperty<>();

public final MonthDay getBirthday() {
return birthday.get();
}

public final void setBirthday(MonthDay birthday) {
this.birthday.set(birthday);
}

public ObjectProperty<MonthDay> birthdayProperty() {
return birthday ;
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(getId());
out.writeObject(getName());
out.writeObject(getBirthday());
}

@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
setId(in.readInt());
setName((String) in.readObject());
setBirthday((MonthDay) in.readObject());
}

}

Showing wrapped bean property in JavaFX TableView

I solved the problem implementing Callback interface:

public class MultiPropertyValueFactory<B,T> implements Callback<TableColumn.CellDataFeatures<B, T>, ObservableValue<T>> {

private String propriedade;

public MultiPropertyValueFactory(String propriedade) {
this.propriedade = propriedade;
}

@Override
public ObservableValue<T> call(TableColumn.CellDataFeatures<B, T> dados) {
Object elemento=dados.getValue();
String propriedades[]=propriedade.split("\\.");
for(int indice=0;indice<propriedades.length;indice++){
elemento=recuperarElemento(elemento,propriedades[indice]);
}
return new SimpleObjectProperty<T>((T) elemento);
}

private Object recuperarElemento(Object elemento,String propriedade) {
try {

/*using reflection here*/

Method metodo = RecuperadorMetodos.recuperarMetodoGetPeloNomeDaPropriedade(elemento.getClass(),propriedade);
return metodo.invoke(elemento);
} catch (Exception e) {
e.printStackTrace();
}
return new Object();
}
}

and passing objects of this class to setCellValueFactory() method:

private void configurarTabela() {
colunaCodigo.setCellValueFactory(new MultiPropertyValueFactory<ItemBean,String>("produto.id"));
colunaDescricao.setCellValueFactory(new MultiPropertyValueFactory<ItemBean,String>("produto.descricao"));
colunaLinha.setCellValueFactory(new MultiPropertyValueFactory<ItemBean,String>("produto.nomeLinha"));
.....
}


Related Topics



Leave a reply



Submit