JavaFX: GridPane, ObservableList and ListChangeListener


Question

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
4
8/26/2014 5:35:39 AM

Accepted Answer

(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 ObservableLists 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 Observables: the list will observe those Observables 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 Labels displayed in the GridPane will be updated autonomously.

So your ListChangeListener can really just ignore changes 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 GridPanes list of child nodes is always the result of calling getSlotContents() on the list of Slots. 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));
6
8/26/2014 7:00:03 PM

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon