Search code examples
javacssjavafxchangelistener

Javafx tableview changelistener changes to often


I want to color the rows in my table depending on the the state. After a search I found this Formatting Rows in a JavaFX TableView Using CSS Pseudo Classes.

I tried to implement it for my purpose. It also works, but if I scroll the color will change back to the default one.
What am i doing wrong?

@FXML
private void initialize(){

    PseudoClass online = PseudoClass.getPseudoClass("online");
    PseudoClass offline = PseudoClass.getPseudoClass("offline");
    PseudoClass unknown = PseudoClass.getPseudoClass("unknown");

    //Set a rowFactory for the table view.
    overviewTable.setRowFactory(tableView -> {
            TableRow<State> row = new TableRow<>();
            ChangeListener<String> changeListener = (obs, oldValue, newValue) -> {
                System.out.println("1: " + obs + " : " + oldValue + " : " + newValue);
                row.pseudoClassStateChanged(online, newValue.equals("online"));
                row.pseudoClassStateChanged(offline, newValue.equals("offline"));
                row.pseudoClassStateChanged(unknown, newValue.equals("unknown"));
            };

            row.itemProperty().addListener((obs, oldValue, newValue) -> {
                System.out.println("2: " + obs + " : " + oldValue + " : " + newValue);
                if (oldValue != null) {
                    oldValue.stateProperty().removeListener(changeListener);
                }
                if (newValue != null) {
                    newValue.stateProperty().addListener(changeListener);
                    row.pseudoClassStateChanged(online, newValue.equals("online"));
                    row.pseudoClassStateChanged(offline, newValue.equals("offline"));
                    row.pseudoClassStateChanged(unknown, newValue.equals("unknown"));
                } else {
                    row.pseudoClassStateChanged(online, false);
                    row.pseudoClassStateChanged(offline, false);
                    row.pseudoClassStateChanged(unknown, false);
                }
            });
            return row;
        });
}

CSS:

.table-row-cell:unknown{
-fx-background-color: blue;
}

.table-row-cell:online{
-fx-background-color: #2EAB15;
}

.table-row-cell:offline{
-fx-background-color: #BB0000;
}

.table-row-cell .text{
-fx-fill: white;
}

Solution

  • The issue is right here in the listener to the item property:

    if (newValue != null) {
        newValue.stateProperty().addListener(changeListener);
        row.pseudoClassStateChanged(online, newValue.equals("online"));
        row.pseudoClassStateChanged(offline, newValue.equals("offline"));
        row.pseudoClassStateChanged(unknown, newValue.equals("unknown"));
    

    Here newValue is a object of type State. It's unlikely that equals is overridden in a way that returns true, if a String is passed (and I'd advise never implementing equals in such a way).

    You should instead pass the new value to the listener to trigger an update as if the value of the state property had changed:

    if (newValue != null) {
        newValue.stateProperty().addListener(changeListener);
        changeListener.changed(null, null, newValue.getState());
    

    or alternatively compare to the state property (which violates the DRY principle though):

    if (newValue != null) {
        newValue.stateProperty().addListener(changeListener);
        row.pseudoClassStateChanged(online, newValue.getState().equals("online"));
        row.pseudoClassStateChanged(offline, newValue.getState().equals("offline"));
        row.pseudoClassStateChanged(unknown, newValue.getState().equals("unknown"));
    

    I assume here that the State class contains a getState() method returning the content of the state property. If this is not the case all calls to that method need to be replaced with stateProperty().getValue().