My question concerns the proper usage of ObservableList and ListChangeListeners in JavaFX (JavaFX 8, though I guess it would be similar in 2.x). As I'm a JavaFX beginner, I'm asking if I've understood correctly how they should be used.
Suppose I have a list of custom objects (let's call them Slots) which I'd want to render in a GridPane: each Slot would know where in the GridPane 'grid' it should go (= the row and the column data).
There will exist an initial (Array)List of Slots, but because the contents of the list are subject to change, I figure it'd make sense to create an ObservableList, and attach to it a ListChangeListener. However, I'm a little puzzled about what do with change.wasUpdated()
etc. methods.
Does the following setup make sense:
public class SlotViewController {
@FXML
private GridPane pane;
private ObservableList<Slot> slots;
@FXML
public void initialize() {
List<Slot> originalSlots = ...
slots = FXCollections.observableList(originalSlots);
slots.addListener(new ListChangeListener<Slot>() {
@Override
public void onChanged(ListChangeListener.Change<? extends Slot> change) {
while (change.next()) {
if (change.wasUpdated()) {
for (Slot slot : change.getList()) {
slot.update(pane);
}
} else {
for (Slot removedSlot : change.getRemoved()) {
removedSlot.removeFrom(pane);
}
for (Slot addedSlot : change.getAddedSubList()) {
addedSlot.addTO(pane);
}
}
}
}
});
}
wherein the Slot would then have methods update(GridPane pane)
, addTO(GridPane pane)
and removeFrom(GridPane pane)` that would look about like this:
(I don't have idea what I should do with update(GridPane pane)
, though.)
public class Slot {
...
public void addTo(GridPane pane) {
// slot contents might be e.g. a couple of String labels in a HBox.
pane.add(this.getSlotContents(), this.getColumn(), this.getRow());
}
public void removeFrom(GridPane pane) {
pane.getChildren().remove(this);
// ?
}
public void update(GridPane pane) {
// ????
}
}
(1) Overall, would this work, or am I missing something? Is this how onChanged(ListChangeListener ...)
is supposed to be used? (2) If yes, then how I should handle the update method?
(1) Overall, would this work, or am I missing something?
Yes, it looks like it should work.
(2) If yes, then how I should handle the update method?
You probably don't need to.
ObservableList
update events and extractors:
First, note that (according to the Javadocs) the update events on the ObservableList
are optional events (not all ObservableList
s will fire them). Update events are intended to indicate that elements of the list have changed their value, while still remaining in the list at the same index. The way to have an ObservableList
generate an update event is to create the list with an "extractor". For example, suppose your Slot
class defines a textProperty()
:
public class Slot {
private final StringProperty text = new SimpleStringProperty(this, "text", "");
public StringProperty textProperty() {
return text ;
}
// ...
}
Then you can create an ObservableList<Slot>
that will fire update events when the text property of any elements change by calling the FXCollections.observableArrayList(...)
method taking a Callback
:
ObservableList<Slot> slots = FXCollections.observableArrayList(
(Slot slot) -> new Observable[] {slot.textProperty()} );
The Callback
here is a function mapping each slot to an array of Observable
s: the list will observe those Observable
s and fire update events if any of them change.
The GridPane
doesn't need to care about Slot
updates:
The reason you are unlikely to need this in your scenario is that the Slot
class encapsulates both any data that could change to create update events, and the view of the Slot
. So it can respond to changes in its data and update its view itself: the GridPane
needs not know anything about these changes.
For a trivial example, suppose your Slot
class just has a textProperty()
as above and getSlotContents()
just returns a simple Label
displaying that text. You would have:
public class Slot {
private final StringProperty text = new SimpleStringProperty(this, "text", "");
public StringProperty textProperty() {
return text ;
}
public final String getText() {
return textProperty().get();
}
public final void setText(String text) {
textProperty().set(text);
}
private final Label label = new Label();
public Slot(String text) {
label.textProperty().bind(text);
setText(text);
}
public Node getSlotContents() {
return label ;
}
}
The GridPane
need not be concerned about when the textProperty()
of any Slot
changes: the Label
s displayed in the GridPane
will be updated autonomously.
So your ListChangeListener
can really just ignore change
s for which wasUpdated()
returns true; just handle the wasAdded()
and wasRemoved()
as you already do.
An Alternative Solution
If you want to consider an alternative solution to this using the EasyBind framework, first notice that you could manage the position of the Slot
in the grid in the same way as shown above:
public class Slot {
private final IntegerProperty column = new SimpleIntegerProperty(this, "column");
public IntegerProperty columnProperty() {
return column ;
}
public final int getColumn() {
return columnProperty().get();
}
public final void setColumn(int column) {
columnProperty().set(column);
}
// similarly for row...
public Slot(int column, int row) {
column.addListener((obs, oldColumn, newColumn) ->
GridPane.setColumnIndex(getSlotContents(), newColumn.intValue()));
// similarly for row
setColumn(column);
setRow(row);
}
// ...
}
Now your ListChangeListener
merely has to make sure that the GridPane
s list of child nodes is always the result of calling getSlotContents()
on the list of Slot
s. You could just simplify your ListChangeListener
implementation:
slots.addListener(new ListChangeListener<Slot>() {
@Override
public void onChanged(ListChangeListener.Change<? extends Slot> change) {
while (change.next()) {
if (change.wasAdded()) {
for (Slot slot : change.getAddedSublist) {
pane.getChildren().add(slot.getSlotContents());
}
} else if (change.wasRemoved()) {
for (Slot slot : change.getRemoved()) {
pane.getChildren().remove(slot.getSlotContents());
}
}
}
}
});
and remove the addTo
and removeFrom
methods from Slot
.
Also note, though, that the Bindings
class defines a method bindContent
that binds the content of one list to an ObservableList
of the same type. The EasyBind framework allows you to create one ObservableList
which is the result of mapping every element in another ObservableList
via an arbitrary function.
So
ObservableList<Node> nodes = EasyBind.map(slots, Slot::getSlotContents);
creates an ObservableList<Node>
that is always equal to the result of calling getSlotContents()
on every element of slots
.
This allows you to replace your ListChangeListener
with a one-liner:
Bindings.bindContent(pane.getChildren(),
EasyBind.map(slots, Slot::getSlotContents));
Collected from the Internet
Please contact [email protected] to delete if infringement.
Comments