Search code examples
javajavafxtableviewjavafx-8listener

JavaFx: How to properly fire updateItem in a TableCell


I have to implement a lot of custom TableCell which behavior relies on the model's change. I could manage to get somehow the expected result, but I think in many cases it was a workaround rather a really good solution. I have used bindings/listeners to achieve the expected result, but the problem I face is that I may add the listeners/bind the properties multiple times and it can create memory leaks.

Here is an example what I mean.

Controller:

public class Controller implements Initializable {

    @FXML private TableView<Model> table;
    @FXML private TableColumn<Model, String> column;
    @FXML private Button change;

    @Override
    public void initialize(URL location, ResourceBundle resources) {

        column.setCellValueFactory(data -> data.getValue().text);
        column.setCellFactory(cell -> new ColoredTextCell());

        Model apple = new Model("Apple", "#8db600");

        table.getItems().add(apple);
        table.getItems().add(new Model("Banana", "#ffe135"));

        change.setOnAction(event -> apple.color.setValue("#ff0800"));

    }

    @Getter
    private class Model {
        StringProperty text;
        StringProperty color;

        private Model(String text, String color) {
            this.text = new SimpleStringProperty(text);
            this.color = new SimpleStringProperty(color);
        }
    }

    private class ColoredTextCell extends TableCell<Model, String> {

        @Override
        protected void updateItem(String item, boolean empty) {
            super.updateItem(item, empty);
            if (empty || getTableRow() == null || getTableRow().getItem() == null) {
                setGraphic(null);
                return;
            }
            Model model = (Model) getTableRow().getItem();
            Text text = new Text(item);
            text.setFill(Color.web(model.getColor().getValue()));

            // This way I add the listener evey item updateItem is called.
            model.getColor().addListener((observable, oldValue, newValue) -> {
                if (newValue != null) {
                    text.setFill(Color.web(newValue));
                } else {
                    text.setFill(Color.BLACK);
                }
            });
            setGraphic(text);
        }
    }

}

FXML:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.control.Button?>
<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="stackoverflow.tabpane.Controller">
    <VBox>
        <Button fx:id="change" text="Change color"/>
        <TableView fx:id="table">
            <columns>
                <TableColumn fx:id="column" prefWidth="200"/>
            </columns>
        </TableView>
    </VBox>
</AnchorPane>

Since the color property isn't directly observed by the cell the updateItem is not called if it changes, so I have to listen to somehow. I would need that the updateItem to be triggered after the color is changed. This would result a single call to the content of the listener.

Is there any way to listen to another change of the model in the same cell, or call the update item somehow, so the change gets rendered.


Solution

  • I guess you could do it the other way around.

    I would create a color property like this:

        ObjectBinding<Paint> colorProperty = Bindings.createObjectBinding(()->{
            String color = model.getColor().get();
            return Paint.valueOf(color==null?"BLACK":color);
        } , model.getColor());
    

    Then I would bind the property like that:

    text.fillProperty().bind(model.colorProperty);
    

    It would be even simpler if you just had:

        SimpleObjectProperty<Paint> textColor = new SimpleObjectProperty<Paint>(Paint.valueOf("BLACK"));
    

    and then in getter and setter of your model update such property.