Because this is a question about design I'll start by saying what i have and what i want.
I have a design that uses composition. A Cell
object holds a Shape
and a Background
objects (custom made ones for this example). Each of these 2 have their own data that defines them. here is the example in code:
class Cell {
Shape shape;
Background background;
class Shape {
int size;
Color color;
Point location;
//...
}
class Background {
Color color;
String name;
CoverType type;
//...
}
}
I also have a GUI that needs to represent many cells and i have written how to do it (how to use color, size etc. to create what i want on the screen). It includes classes such as CellRepresentation, ShapeRepresentation and BackgroundRepresentation that have their display properties bound to the the data properties (i think this is called Model and View).
I want to be able to represent changes in the GUI by changing the above data:
- a user can (for example) right-click on a shape and set its color. So the data above changes and the change needs to be reflected in the GUI.
- a user can also change the whole shape (for example copy-paste it from another cell). Or even the whole cell. These changes also need to reflect in the GUI.
My question is which of the class members need to be JavaFX properties that I bind to.
Here is what I am thinking: the "leaf" properties (size, color, location...) must be properties so I can bind to them the GUI property. But do I need to make the shape and background objects properties too? Only their properties have "Actual" representation on the screen. Ideally i would have liked it that if Shape changes then all of its properties tell their bindings that they could have changed (maybe the color didn't but size did). But it doesn't work this way - even though the Color of a Shape can change when the Shape changes the Color property won't tell whatever is bound to it that it changed.
The same goes for making Cell a property in the lager picture where there are many cells and so on: properties of properties delegating changes.
So I thought of making the Shape and Background also properties and registering an InvalidationListener
to them updates their properties. This just doesn't seem right because i would think that with all the support for properties there would be a way to do what i want.
Can someone suggest a way to do this?
Using just the standard JavaFX API you can leverage the Bindings.selectXXX
methods to observe a "property of a property".
So for example:
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.paint.Color;
public class Cell {
private final ObjectProperty<Shape> shape = new SimpleObjectProperty<>(new Shape());
public final ObjectProperty<Shape> shapeProperty() {
return this.shape;
}
public final Cell.Shape getShape() {
return this.shapeProperty().get();
}
public final void setShape(final Cell.Shape shape) {
this.shapeProperty().set(shape);
}
public static class Shape {
private final IntegerProperty size = new SimpleIntegerProperty(0);
private final ObjectProperty<Color> color = new SimpleObjectProperty<>(Color.BLACK);
public final IntegerProperty sizeProperty() {
return this.size;
}
public final int getSize() {
return this.sizeProperty().get();
}
public final void setSize(final int size) {
this.sizeProperty().set(size);
}
public final ObjectProperty<Color> colorProperty() {
return this.color;
}
public final javafx.scene.paint.Color getColor() {
return this.colorProperty().get();
}
public final void setColor(final javafx.scene.paint.Color color) {
this.colorProperty().set(color);
}
}
public static void main(String[] args) {
Cell cell = new Cell();
Bindings.selectInteger(cell.shapeProperty(), "size").addListener(
(obs, oldSize, newSize) -> System.out.println("Size changed from "+oldSize+" to "+newSize));
cell.getShape().setSize(10);
cell.setShape(new Shape());
Shape s = new Shape();
s.setSize(20);
cell.setShape(s);
}
}
Will produce the (desired) output
Size changed from 0 to 10
Size changed from 10 to 0
Size changed from 0 to 20
This API has a bit of a legacy feel to it, in that it relies on passing the property name as a string, and consequently is not typesafe and cannot be checked at compile time. Additionally, if any of the intermediate properties are null (e.g. if cel.getShape()
returns null in this example), the bindings generate annoying and verbose warning messages (even though this is supposed to be a supported use case).
Tomas Mikula has a more modern implementation in his ReactFX library, see this post for a description. Using ReactFX, you would do:
public static void main(String[] args) {
Cell cell = new Cell();
Var<Number> size = Val.selectVar(cell.shapeProperty(), Shape::sizeProperty);
size.addListener(
(obs, oldSize, newSize) -> System.out.println("Size changed from "+oldSize+" to "+newSize));
cell.getShape().setSize(10);
cell.setShape(new Shape());
Shape s = new Shape();
s.setSize(20);
cell.setShape(s);
}
Finally, if you are creating a list of cells, you can create an ObservableList
specifying an extractor
. The extractor is a function mapping each element in the list (each Cell
) to an array of Observable
s. If any of those Observable
s changes, the list fires an update event. So you could do
ObservableList<Cell> cellList =
FXCollections.observableArrayList(cell -> new Observable[] {Bindings.selectInteger(cell.shapeProperty(), "size")});
using the standard API, or
ObservableList<Cell> cellList =
FXCollections.observableArrayList(cell -> new Observable[] {Val.selectVar(cell.shapeProperty(), Shape::sizeProperty)});
using ReactFX. Then just add a ListChangeListener
to the list, and it will be notified if the size changes (or if the shape changes to a new shape with a different size). You can add as many observables that are properties (or properties of properties) of the cell in the returned array as you need.