Search code examples
javajavafxjavafx-tableview

What is right way to work with tableview?


For some time i have been trying to get my tableview work as kind of spreadsheet that is updated by background thread and when cell get updated, it for few seconds higlights ( changes style ) and then goes back to original style. I already know, that i can't store and set styles directly in table cell and i need some kind of backing class, that will hold this data. But tableview with its "reusing" of cells (using same cells for different data) acts really weird. When all cells fits on screen it works flawlessly for me, but once i place around 100 cells and it becomes scrollable it starts being buggy, sometimes styles ( or setted graphic) disappears and after scrolling appears, if i disable some top cells of view, some other cells after scrolling get disabled as well and so on. Is there any right way to do this?

What i need basically is

Background data thread ---updates--> tableview
Another thread --after few seconds removes style--> tableview

As i have it now, i have model class that holds data, style and reference to table cell where it should be ( i disabled ordering, so it should be ok ) and background thread updates data in model class, and that model class changes style on referenced cell and register itself in "style remover" thread, that after while removes style.

I think posting my actual code won't be useful, because once i've discovered that cells are being reused my code has become too complicated and a little bit unreadable so i want to completely redo it right way.

Peformance is not that important for me, there wont be more than 100 cells, but this highlighting and having buttons in tableview must work flawlessly.

This is how my app looks like now - for idea of what i need. enter image description here

EDIT: here is link to my another question related to this.


Solution

  • The collaborators:

    • on the data side, a (view) model which has a recentlyChanged property, that's updated whenever the value is changed
    • on the view side, a custom cell that listens to that recentlyChanged property and updates its style as appropriate

    The tricky part is to clean up cell state when re-used or not-used: the method that's always (hopefully!) called is cell.updateIndex(int newIndex), so that's the place to un-/register the listener.

    Below a runnable (though crude ;) example

    import java.util.logging.Logger;
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    import de.swingempire.fx.util.FXUtils;
    import javafx.animation.KeyFrame;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import javafx.beans.property.ReadOnlyBooleanProperty;
    import javafx.beans.property.ReadOnlyBooleanWrapper;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    import javafx.beans.value.ChangeListener;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    import javafx.scene.Parent;
    import javafx.scene.Scene;
    import javafx.scene.control.Button;
    import javafx.scene.control.TableCell;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.layout.BorderPane;
    import javafx.scene.layout.HBox;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    public class TableCoreRecentlyChanged extends Application {
    
        public static class RecentChanged extends TableCell<Dummy, String> {
    
            private ChangeListener<Boolean> recentListener = (src, ov, nv) -> updateRecentStyle(nv);
            private Dummy lastDummy;
    
            /*
             * Just to see any effect.
             */
            protected void updateRecentStyle(boolean highlight) {
                if (highlight) {
                    setStyle("-fx-background-color: #99ff99");
                } else {
                    setStyle("-fx-background-color: #009900");
                }
            }
    
            @Override
            public void updateIndex(int index) {
                if (lastDummy != null) {
                    lastDummy.recentlyChangedProperty().removeListener(recentListener);
                    lastDummy = null;
                }
                updateRecentStyle(false);
                super.updateIndex(index);
                if (getTableRow() != null && getTableRow().getItem() != null) {
                    lastDummy = getTableRow().getItem();
                    updateRecentStyle(lastDummy.recentlyChangedProperty().get());
                    lastDummy.recentlyChangedProperty().addListener(recentListener);
                } 
            }
    
            @Override 
            protected void updateItem(String item, boolean empty) {
                if (item == getItem()) return;
    
                super.updateItem(item, empty);
    
                if (item == null) {
                    super.setText(null);
                    super.setGraphic(null);
                } else {
                    super.setText(item);
                    super.setGraphic(null);
                }
            }
    
        }
    
        private Parent getContent() {
            TableView<Dummy> table = new TableView<>(createData(50));
            table.setEditable(true);
    
            TableColumn<Dummy, String> column = new TableColumn<>("Value");
            column.setCellValueFactory(c -> c.getValue().valueProperty());
            column.setCellFactory(e -> new RecentChanged());
            column.setMinWidth(200);
            table.getColumns().addAll(column);
    
            int editIndex = 20; 
    
            Button changeValue = new Button("Edit");
            changeValue.setOnAction(e -> {
                Dummy dummy = table.getItems().get(editIndex);
                dummy.setValue(dummy.getValue()+"x");
            });
            HBox buttons = new HBox(10, changeValue);
            BorderPane content = new BorderPane(table);
            content.setBottom(buttons);
            return content;
        }
    
        private ObservableList<Dummy> createData(int size) {
            return FXCollections.observableArrayList(
                    Stream.generate(Dummy::new)
                    .limit(size)
                    .collect(Collectors.toList()));
        }
    
        private static class Dummy {
            private static int count;
    
            ReadOnlyBooleanWrapper recentlyChanged = new ReadOnlyBooleanWrapper() {
    
                Timeline recentTimer;
                @Override
                protected void invalidated() {
                    if (get()) {
                        if (recentTimer == null) {
                            recentTimer = new Timeline(new KeyFrame(
                                    Duration.millis(2500),
                                    ae -> set(false)));
                        }
                        recentTimer.playFromStart();
                    } else {
                        if (recentTimer != null) recentTimer.stop();
                    }
                }
    
            };
            StringProperty value = new SimpleStringProperty(this, "value", "initial " + count++) {
    
                @Override
                protected void invalidated() {
                    recentlyChanged.set(true);
                }
    
            };
    
            public StringProperty valueProperty() {return value;}
            public String getValue() {return valueProperty().get(); }
            public void setValue(String text) {valueProperty().set(text); }
            public ReadOnlyBooleanProperty recentlyChangedProperty() { return recentlyChanged.getReadOnlyProperty(); }
            public String toString() {return "[dummy: " + getValue() + "]";}
        }
    
        @Override
        public void start(Stage primaryStage) throws Exception {
            primaryStage.setScene(new Scene(getContent()));
         //   primaryStage.setTitle(FXUtils.version());
            primaryStage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    
        @SuppressWarnings("unused")
        private static final Logger LOG = Logger
                .getLogger(TableCoreRecentlyChanged.class.getName());
    }