In general, a JavaFX property may be regarded as a specialization of a Java Beans
. What it ad
Using JavaFX properties implies a certain API style (similar to Java Beans), as it is expected to provide a getter and setter to access the property value, as well as an accessor for the property itself. We may consider
javafx.scene.Node as an example, which amongst various others provides a boolean
pickOnBounds property that (is lazily created and) controls whether picking is computed by intersecting with the rectangular bounds of the node or not:
public abstract class Node implements EventTarget, Stylable {
...
private BooleanProperty pickOnBounds;
public final void setPickOnBounds(boolean value) {
pickOnBoundsProperty().set(value);
}
public final boolean isPickOnBounds() {
return pickOnBounds == null ? false : pickOnBounds.get();
}
public final BooleanProperty pickOnBoundsProperty() {
if (pickOnBounds == null) {
pickOnBounds = new SimpleBooleanProperty(this, "pickOnBounds");
}
return pickOnBounds;
}
}
In addition to the notification support already mentioned, values of properties may be bound to values of others, or even to values that are computed by more complex expressions, via so called
bindings. This is a quite powerful mechanism that reduces the need for custom listener implementations significantly. If the
pickOnBounds value of one node should for instance be kept equal to the
pickOnBounds value of another, the following binding is all that is required:
node1.pickOnBoundsProperty().bind(node2.pickOnBoundsProperty());
Binding it to a more complex boolean expression is pretty easy as well:
node1.pickOnBoundsProperty().bind(node2.pickOnBoundsProperty().or(node3.visibleProperty()));
One can even define own bindings that compute the property value (lazily) based on values of arbitrary other properties:
node1.pickOnBoundsProperty().bind(new BooleanBinding() {
{
// specify dependencies to other properties, whose changes
// will trigger the re-computation of our value
super.bind(node2.pickOnBoundsProperty());
super.bind(node3.layoutBoundsProperty());
}
@Override
protected boolean computeValue() {
// some arbitrary expression based on the values of our dependencies
return node2.pickOnBoundsProperty().get() &&
node3.layoutBoundsProperty().get().isEmpty();
}
});
JavaFX provides property implementations for all Java primitives (
BooleanProperty,
DoubleProperty,
FloatProperty,
IntegerProperty,
LongProperty,
StringProperty, as well as a generic
ObjectProperty, which can be used to wrap arbitrary object values. It is important to point out that an
ObjectProperty will of course only notify invalidation and change listeners in case the property value is changed, i.e. it is altered to refer to a different object identity, not when changes are applied to the contained property value. Accordingly, an
ObjectProperty that wraps a collection only notifies about changes in case a different collection is set as property value, not when the currently observed collection is changed by adding elements to or removing elements from it:
ObjectProperty<List<Integer>> observableListObjectProperty = new SimpleObjectProperty<>();
observableListObjectProperty.addListener(new ChangeListener<List<Integer>>() {
@Override
public void changed(ObservableValue<? extends List<Integer>> observable,
List<Integer> oldValue, List<Integer> newValue)
System.out.println("Change from " + oldValue + " to " + newValue);
}
});
// change listener will be notified about identity change from 'null' to '[]'
observableListObjectProperty.set(new ArrayList<Integer>());
// change listener will not be notified
observableListObjectProperty.get().addAll(Arrays.asList(1, 2, 3));
This is where JavaFX observable collections come into play. As Java does not provide notification support in its standard collections, JavaFX delivers dedicated observable variants:
ObservableList,
ObservableMap and
ObservableSet. They all support invalidation listener notification (as properties do) and in addition define their own respective change listeners (
ListChangeListener,
MapChangeListener, and
SetChangeListener).
ObservableList also extends
List by adding
setAll(E... elements) and
setAll(Collection<? extends E> c), which combines a
clear() with an
addAll(Collection< ? extends E> c) into a single atomic replace operation, as well as a
remove(int from, int to) that supports removal within an index interval. This allows to 'reduce noise', which is quite important to a graphical framework like GEF, where complex computations might be triggered by changes.
List changes are iterable, i.e. they comprise several sub-changes, so that even a complex operation like
setAll(Collection<? extends E> c) results in a single change notification:
ObservableList<Integer> observableList = FXCollections.observableArrayList();
observableList.addListener(new ListChangeListener<Integer>() {
@Override
public void onChanged(Change<? extends Integer> change) {
while (change.next()) {
int from = change.getFrom();
int to = change.getTo();
// iterate through the sub-changes
if (change.wasReplaced()) {
// replacement (simultaneous removal and addition in a continuous range)
System.out.println("Replaced " + change.getRemoved()
+ " with " + change.getAddedSubList() + ".");
} else if (change.wasAdded()) {
// addition (added sublist within from-to range)
System.out.println("Added " + change.getAddedSubList()
+ " within [" + from + ", " + to + ").");
} else if (change.wasRemoved()) {
// removal (change provides removed sublist and from index)
System.out.println("Removed " + change.getRemoved() + " at " + from + ".");
} else if (change.wasPermutated()) {
// permutation (change provides mapping of old indexes to new indexes)
System.out.print("Permutated within [" + change.getFrom() + ", " + to + "):");
for (int i = from; i < to; i++) {
System.out.print((i == from ? " " : ", ")
+ i + " -> " + change.getPermutation(i)
+ (i == to - 1 ? ".\n" : ""));
}
}
}
}
});
// one comprised change: 'Added [3, 1, 2] within [0, 3).'
observableList.setAll(Arrays.asList(3, 1, 2));
// one comprised change: 'Permutated within [0, 3): 0 -> 2, 1 -> 0, 2 -> 1.'
Collections.sort(observableList);
// one comprised change: 'Replaced [1, 2, 3] with [4, 5, 6].'
observableList.setAll(Arrays.asList(4, 5, 6));
// two comprised changes: 'Removed [4] at index 0.', 'Removed [6] at index 1.'
observableList.removeAll(Arrays.asList(4, 6));
Similar to properties, observable collections may even be used to establish bindings using so called content bindings:
// ensure that elements of list are synchronized with that of observableList
List<Integer> list = new ArrayList<>();
Bindings.bindContent(list, observableList);
As such, observable collections are quite usable, even if not being wrapped into a property. As long as the identity of an observable collection is not to be changed, it may directly be exposed without being wrapped into a property. And that's exactly how JavaFX uses them in its own API. As an example consider
javafx.scene.Parent, which exposes its children via an
ObservableList:
public abstract class Parent extends Node {
...
protected ObservableList<Node> getChildren() {
return children;
}
@ReturnsUnmodifiableCollection
public ObservableList<Node> getChildrenUnmodifiable() {
return unmodifiableChildren;
}
}
Wrapping it into a property however is required, if a collection's identity is to be changed (in a way transparent to listeners) or properties are to be bound to it. In principle an observable collection could be wrapped directly into an
ObjectProperty but this has the disadvantage that two listeners are required if collection changes are to be properly tracked.
Consider an
ObservableList being wrapped into a
SimpleObjectProperty as an example. While changes to the list can be observed by registering a
ListChangeListener a
ChangeListener is required in addition to keep track of changes to the property's value itself (and to transfer the list change listener from an old property value to a new one):
ObjectProperty<ObservableList<Integer>> observableListObjectProperty =
new SimpleObjectProperty<>();
final ListChangeListener<Integer> listChangeListener = new ListChangeListener<Integer>(){
@Override
public void onChanged(ListChangeListener.Change<? extends Integer> c) {
// react to list changes
}
};
// register list change listener at (current) property value
observableListObjectProperty.get().addListener(listChangeListener);
// register change listener to transfer list change listener
observableListObjectProperty.addListener(new ChangeListener<ObservableList<Integer>>() {
@Override
public void changed(ObservableValue<? extends ObservableList<Integer>> observable,
ObservableList<Integer> oldValue,
ObservableList<Integer> newValue) {
// transfer list change listener from old value to new one
if(oldValue != null && oldValue != newValue){
oldValue.removeListener(listChangeListener);
}
if(newValue != null && oldValue != newValue){
newValue.addListener(listChangeListener);
}
}
});
}
As this is quite cumbersome, JavaFX offers respective collection properties that can be used as an alternative:
ListProperty,
SetProperty, and
MapProperty. They support invalidation and change listeners as well as the respective collection specific listeners and will even synthesize a collection change when the observed property value is changed:
ListProperty<Integer> listProperty = new SimpleListProperty<>(
FXCollections.<Integer> observableArrayList());
final ListChangeListener<Integer> listChangeListener = new ListChangeListener<Integer>() {
@Override
public void onChanged(ListChangeListener.Change<? extends Integer> change) {
// handle list changes
}
};
listProperty.addListener(listChangeListener);
// forwarded list change: 'Added [1, 2, 3] within [0, 3).'
listProperty.addAll(Arrays.asList(1, 2, 3));
// synthesized list change: 'Replaced [1, 2, 3] with [4, 5, 6].'
listProperty.set(FXCollections.observableArrayList(4, 5, 6));
In addition, collection properties define their own (read-only) properties for emptiness, equality, size, etc., so that advanced bindings can also be created:
// bind boolean property to 'isEmpty'
BooleanProperty someBooleanProperty = new SimpleBooleanProperty();
someBooleanProperty.bind(listProperty.emptyProperty());
// bind integer property to 'size'
IntegerProperty someIntegerProperty = new SimpleIntegerProperty();
someIntegerProperty.bind(listProperty.sizeProperty());
While observable properties are thus not required to notify about collection changes (which is already possible using observable collections alone), they add quite some comfort when having to deal with situations where collections may be replaced or where bindings have to rely on certain properties (emptiness, size, etc.) of a collection.
GEF4 Common Collections
As already mentioned, GEF4 MVC uses some of Google Guava's collection classes, while JavaFX only offers observable variants of Set, Map, and List. In order to facilitate a unique style for property change notifications in our complete code base, observable variants had to be created up front. That actually involved more design decisions than I had expected. To my own surprise, I also ended up with a replacement class for ObservableList and a utility class to augment the original API, because that seemed quite necessary in a couple of places.
Obtaining atomic changes
As this might not be directly obvious from what was said before, let me point out that only ObservableList is indeed capable of notifying about changes atomically in the way laid out before. ObservableSet and ObservableMap notify their listeners for each elementary change individually. That is, an ObservableMap notifies change listeners independently for each affected key change, while ObservableSet will do likewise for each affected element. Calling clear() on an ObservableMap or can thus lead to various change notifications.
I have no idea why the observable collections API was designed in such an inhomogeneous way (it's discussed at JDK-8092534 without providing much more insight
), but I think that an observable collection should rather behave like ObservableList, i.e. fire only a single change notification for each method call. If all required operations can be performed atomically via dedicated methods, a client can fully control which notifications are produced. As already laid out, ObservableList follows this to some extend with the additionally provided setAll() methods that combine clear() and addAll() into a single atomic operation, which would otherwise yield two notifications. However, an atomic move() operation is still lacking for
ObservableList, so that movement of elements currently cannot be performed atomically.
When creating ObservableSetMultimap and ObservableMultiset, I tried to follow the contract of ObservableList for the above mentioned reasons. Both notify their listeners through a single atomic change for each method call, which provides details about elementary sub-changes (related to a single element or key), very similar to ListChangeListener#Change. In accordance to the addAll() of ObservableList, I added a replaceAll() operation to both to offer an atomic operation via which the contents of the collections can be replaced. Change notifications are iterable, as for ObservableList:
ObservableSetMultimap<Integer, String> observableSetMultimap = CollectionUtils.<Integer, String> observableHashMultimap();
observableSetMultimap.addListener(new SetMultimapChangeListener<Integer, String>() {
@Override
public void onChanged(SetMultimapChangeListener.Change<? extends Integer,
? extends String> change) {
while (change.next()) {
if (change.wasAdded()) {
// values added for key
System.out.println("Added " + change.getValuesAdded()
+ " for key " + change.getKey() + ".");
} else if (change.wasRemoved()) {
// values removed for key
System.out.println("Removed " + change.getValuesRemoved() + " for key "
+ change.getKey() + ".");
}
}
}
});
// one comprised change: 'Added [1] for key 1.'
observableSetMultimap.put(1, "1");
// one comprised change: 'Added [2] for key 2.'
observableSetMultimap.put(2, "2");
// two comprised changes: 'Removed [1] for key 1.', 'Removed [2] for key 2.'
observableSetMultimap.clear();
I also though about providing replacement classes for ObservableMap and ObservableSet that rule out the inhomogeneity of the JavaFX collections API, but this would have required to extend their respective listener interfaces, and I thus abstained.
Retrieving the "previous" contents of an observable collection