3

It is hard to explain so I'll use an example:

@Override
public void start(Stage primaryStage) throws Exception
{
    final VBox vbox = new VBox();
    final Scene sc = new Scene(vbox);
    primaryStage.setScene(sc);

    final TableView<Person> table = new TableView<>();
    final TableColumn<Person, String> columnName = new TableColumn<Person, String>("Name");
    table.getColumns().add(columnName);

    final ObservableList<Person> list = FXCollections.observableArrayList();
    list.add(new Person("Hello"));
    list.add(new Person("World"));
    Bindings.bindContent(table.getItems(), list);

    columnName.setCellValueFactory(new PropertyValueFactory<>("name"));

    vbox.getChildren().add(table);

    final Button button = new Button("test");
    button.setOnAction(event ->
    {
        final Person removed = list.remove(0);
        removed.setName("Bye");
        list.add(0, removed);
    });
    vbox.getChildren().add(button);


    primaryStage.show();
}

public static class Person
{
    private String name = "";

    public Person(String n)
    {
        name = n;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String n)
    {
        name = n;
    }
}

In this example, I show a TableView with a single column named "Name". Running this sample code, you will get two rows: first row with "Hello" in "Name" column; and second row with "World" in "Name" column.

Additionally, there is a button, this button removes the first Person object from the list, then makes some changes to the object, then adds it back in at the same index. Doing so would cause any ListChangeListener added to the ObservableList to be triggered, and I have tested this to be true.

I would expect the row with "Hello" to be replaced with "Bye", but it seems like the TableView continues to show "Hello". If I used a TimeLine to add delay before I add the removed Person object back to the list, it would change to "Bye".

final Timeline tl = new Timeline(new KeyFrame(Duration.millis(30), ae -> list.add(0, removed)));
tl.play();

Is there something weird with the API? Is there any way to do this without this problem?

Jai
  • 8,165
  • 2
  • 21
  • 52

1 Answers1

3

This is essentially expected behavior.

Note that (and I'm guessing you are trying to work around this issue), if you simply called

list.get(0).setName("Bye");

which has the same effect in terms of the underlying data, the table would not update as it has no way of being notified that the String field name in the element of the list has changed.

The code

Person removed = list.remove(0);
removed.setName("Bye");
list.add(0, removed);

is really equivalent to list.get(0).setName("Bye");: you just temporarily remove the item from the list before changing it, and then add it back. As far as the list is concerned, the net result is the same. I guess you are doing this in the hope that removing and replacing the item from the list will persuade the table to notice the state of the item has changed. There's no guarantee this will be the case. Here is what's happening:

The binding between your two lists:

Bindings.bindContent(table.getItems(), list);

works like any other binding: it defines how to get the value of the binding (the elements of list), and marks the data as invalid if list is invalidated at any time. The latter happens when you add and remove elements from list.

The TableView will not perform layout every time the binding to the list changes; instead, when then binding is invalidated (add or remove an element), then the table view marks itself as potentially needing to be redrawn. Then, on the next rendering pulse, the table will check the data and see if it really needs to be redrawn, and re-render if needed. There are obvious performance-saving features of this implementation.

So what happens with your code is that an item is removed from the list, causing the binding to be marked as invalid. The item is then changed (by calling setName(...)), and the same item is then added back into the list at the same position. This also causes the binding to be marked as invalid, which has no effect (it is already invalid).

No rendering pulse can occur between the removal and re-addition of this element. Consequently, the first time the table actually looks at the changes that were made to the list has to be after the entire remove-change-add process. At that point, the table will see that the list still contains the exact same elements in the exact same order that it previously contained. (The internal state of one of the elements has changed, but since this is not an observable value - not a JavaFX property - the table is unaware of this.) Consequently, the table sees no changes (or sees that all the changes have cancelled each other out), and doesn't re-render.

In the case where you add the pause, then a rendering frame (or two) occurs between the removal of the item and its re-addition. Consequently, the table actually renders one or two frames without the item, and when it is added back in, it adds it back and renders the current value. (You might, possibly, be able to make the behavior unpredictable, by pausing for 16 or 17 milliseconds, which is right on the cusp of the time for one rendering frame.)

It's not clear what you really intend to do. If you are trying to persuade the table to update without using JavaFX properties, you can do

list.get(0).setName("Bye");
table.refresh();

though this is not a very satisfactory solution.

Note too that

list.remove(0);
list.add(0, new Person("Bye"));

will also work (since now the added element is not the same as the removed element).

The better approach is to implement your model class with JavaFX properties:

public static class Person
{
    private final StringProperty name = new SimpleStringProperty("");

    public Person(String n)
    {
        setName(n);
    }

    public StringProperty nameProperty() {
        return name ;
    }

    public final String getName()
    {
        return nameProperty().get();
    }

    public final void setName(String n)
    {
        nameProperty().set(n);
    }
}

and then simply calling

list.get(0).setName("Bye");

will update the table (because the cell will be observing the property).

James_D
  • 201,275
  • 16
  • 291
  • 322
  • You were absolutely right that I was trying to force an update of table without using FX properties, because my model class comes from server-side API as DTO classes, and it is weird for me to simply rewrite those classes from scratch. Your answer is truly well explained. I still have a question though: does an `ObservableList` automatically subscribes for changes within its elements' FX properties? If this is so, what is [this overload](https://docs.oracle.com/javase/8/javafx/api/javafx/collections/FXCollections.html#observableArrayList-javafx.util.Callback-) for? – Jai Mar 29 '17 at 00:57
  • @Jai, No, an observable list does not automatically listen to properties belong to the list elements (unless you provide an "extractor" that supplies those properties). But in this case, if your model class used JavaFX properties, the `PropertyValueFactory` would supply the property belonging to the model to the cell, and the cell would observe that property for changes. – James_D Mar 29 '17 at 01:01
  • @Jai Note that, depending on the nature of your server application, you might still want to use JavaFX properties. See [this](http://asipofjava.blogspot.com/2013/05/javafx-properties-in-jpa-entity-classes.html?m=0) or [this](http://www.marshall.edu/genomicjava/2014/05/09/one-bean-to-bind-them-all/). You might also consider [this pattern](http://stackoverflow.com/questions/23522130/javabean-wrapping-with-javafx-properties), which uses adapters to expose traditional JavaBean properties as observable JavaFX properties. – James_D Mar 29 '17 at 01:04
  • Thanks for clarifying and providing all these readups. Unfortunately for me, I'm not part of the server team. I doubt the server guys would want to import JavaFX API, then change all the DTO classes to use JavaFX properties. Also, their DTO classes are pure POJO bean classes, so there aren't any `PropertyChangeListener`s. And again, I doubt they want to change it. So I'm pretty stuck rewriting the classes. :( – Jai Mar 29 '17 at 01:59
  • @Jai `PropertyChangeListener`s are part of the (POJO) JavaBean specification, so you might have some chance with a request for those. Or, as you say, create a JavaFX-specific model for your client code. – James_D Mar 29 '17 at 02:02
  • Thanks, I will talk to them to see what I can do. – Jai Mar 29 '17 at 02:45