JavaFX TableView: fast change of list of items not reflected

It's 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 am showing TableView

with one column named "Name". Running this code example will give you two lines: the first line with "Hello" in the "Name" column; and the second row with "World" in the "Name" column.

Also there is a button, this button removes the first object Person

from the list, then makes some changes to the object, and then adds it back to the same index. This will cause anyone ListChangeListener

added to ObservableList

it to run, and I checked that as true.

I would expect the string with "Hello" to be replaced with "Bye", but it looks like it TableView

keeps showing "Hello". If I used TimeLine

to add a delay before adding the deleted object Person

back to the list, it changes to "Bye".

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

      

Is there something weird about the API? Is there a way to do this without this problem?

+3


source to share


1 answer


This is essentially the expected behavior.

Note that (and I'm assuming you are trying to work around this issue) if you just called

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

      

which has the same effect in terms of the underlying data, the table will not update as there is no way to get notified that a field String

name

in the list item has changed.

Code

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

      

is really equivalent list.get(0).setName("Bye");

: you just temporarily remove the item from the list before replacing it, and then add it back. As far as the list is concerned, the net result remains the same. I think you are doing this in the hope that removing and replacing an item from the list will cause the table to notice that the item's state has changed. There is no guarantee that this will be the case. Here's what's going on:

Linking between your two lists:

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

      

works like any other binding: it defines how to get the value of the binding (elements list

) and marks the data as invalid if list

invalid at any time. The latter happens when you add and remove items from list

.

TableView

will not do the layout every time the binding to the list changes; instead, when the binding is invalid (adding or removing an element), then the table view marks itself as potentially in need of redrawing. Then, on the next render pulse, the table will check the data and see if it really needs to be redrawn, and re-render if necessary. There are obvious possibilities for saving the performance of this implementation.

So what happens with your code is that the item is removed from the list, which will mark the binding as invalid. The item is then modified (by calling setName(...)

), and the same item is then added back to the list at the same position. This also causes the binding to be marked invalid, which has no effect (it is no longer valid).

No pulsating impulse can occur between removing and re-adding this element. Hence, the first time the table actually looks at the changes made to the list must be after the entire delete-modify-add process. At this point, the table will see that the list still contains the same elements in the same order as it was previously contained. (The internal state of one of the elements has changed, but since this is not an observable value, and not a JavaFX property, the table is not aware of this.) Therefore, the table does not see the changes (or sees that all changes have been reversed to each other) and is not re-rendered.



In the case where you add a pause, there is a render box (or two) between removing the element and re-adding it. Hence the table actually displays one or two frames with no item, and when it is added back it adds it back in and displays the current value. (You might perhaps be able to make the behavior unpredictable by pausing for 16 or 17 milliseconds, which is right at the threshold of time for a single render frame.)

It is not clear what you are actually going to do. If you are trying to convince the table to update without using JavaFX properties, you can do

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

      

although this is not a very satisfying solution.

note that

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

      

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

The best approach is to implement the model class using 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 just calling

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

      

will update the table (because the cell will observe the property).

+3


source







All Articles