Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can I update TableView from a non-UI thread but not ListView?

I have encountered something odd in JavaFX (java version 1.8.0_91). It was my understanding that if one wants to update the UI from a separate thread, one must either use Platform.runLater(taskThatUpdates) or one of the tools in the javafx.concurrent package.

However, if I have a TableView on which I call .setItems(someObservableList), I can update someObservableList from a separate thread and see the corresponding changes to my TableView without the expected Exception in thread "X" java.lang.IllegalStateException: Not on FX application thread; currentThread = X error.

If I replace TableView with ListView, the expected error occurs.

Example code for situation #1: updating a TableView from a different thread, with no call to Platform.runLater()--and no error.

public class Test extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        // Create a table of integers with one column to display
        TableView<Integer> data = new TableView<>();
        TableColumn<Integer, Integer> num = new TableColumn<>("Number");
        num.setCellValueFactory(v -> new ReadOnlyObjectWrapper(v.getValue()));
        data.getColumns().add(num);

        // Create a window & add the table
        VBox root = new VBox();
        Scene scene = new Scene(root);
        root.getChildren().addAll(data);
        stage.setScene(scene);
        stage.show();

        // Create a list of numbers & bind the table to it
        ObservableList<Integer> someNumbers = FXCollections.observableArrayList();
        data.setItems(someNumbers);

        // Add a new number every second from a different thread
        new Thread( () -> {
            for (;;) {
                try {
                    Thread.sleep(1000);
                    someNumbers.add((int) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

Example code for situation #2: updating a ListView from a different thread, with no call to Platform.runLater()--produces an error.

public class Test extends Application {

    public static void main(String[] args) {
        Application.launch(args);
    }

    @Override
    public void start(Stage stage) throws Exception {
        // Create a list of integers (instead of a table)
        ListView<Integer> data = new ListView<>();

        // Create a window & add the table
        VBox root = new VBox();
        Scene scene = new Scene(root);
        root.getChildren().addAll(data);
        stage.setScene(scene);
        stage.show();

        // Create a list of numbers & bind the table to it
        ObservableList<Integer> someNumbers = FXCollections.observableArrayList();
        data.setItems(someNumbers);

        // Add a new number every second from a different thread
        new Thread( () -> {
            for (;;) {
                try {
                    Thread.sleep(1000);
                    someNumbers.add((int) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

Note that the only difference is the instantiation of data as a ListView<Integer> rather than a TableView<Integer>.

So what gives here? Is this happening because of the call to TableColumn::setCellValueFactory() in the first example?--that's my intuition. I would like to know why one does not cause an error and the other does, and more specifically what the rules are for how the .setItems call binds data to the view.

like image 939
John Dorian Avatar asked Jul 13 '16 00:07

John Dorian


People also ask

What does TableView refresh do?

Calling refresh() forces the TableView control to recreate and repopulate the cells necessary to populate the visual bounds of the control. In other words, this forces the TableView to update what it is showing to the user.

What is TableView in JavaFX?

The TableView class provides built-in capabilities to sort data in columns. Users can alter the order of data by clicking column headers. The first click enables the ascending sorting order, the second click enables descending sorting order, and the third click disables sorting. By default, no sorting is applied.

How to select a row in TableView JavaFX?

To select a row with a specific index you can use the select(int) method. Here is an example of selecting a single row with a given index in a JavaFX TableView: selectionModel. select(1);


1 Answers

As @James_D already mentioned, you should not update things that belongs to the FX Application Thread on any other thread. But in your example you've been updating the ObservableList in another thread. No matter why this work for TableView and for ListView not, it is the wrong way of doing it.

Have a look at the Task class if you want to perform intermediate updates on backing ObservableLists.

From the Task-API-Doc

A Task Which Returns An ObservableList

Because the ListView, TableView, and other UI controls and scene graph nodes make use of ObservableList, it is common to want to create and return an ObservableList from a Task. When you do not care to display intermediate values, the easiest way to correctly write such a Task is simply to construct an ObservableList within the call method, and then return it at the conclusion of the Task.

and another hint:

A Task Which Returns Partial Results

Sometimes you want to create a Task which will return partial results. Perhaps you are building a complex scene graph and want to show the scene graph as it is being constructed. Or perhaps you are reading a large amount of data over the network and want to display the entries in a TableView as the data is arriving. In such cases, there is some shared state available both to the FX Application Thread and the background thread. Great care must be taken to never update shared state from any thread other than the FX Application Thread.

The easiest way to do this is to take advantage of the updateValue(Object) method. This method may be called repeatedly from the background thread. Updates are coalesced to prevent saturation of the FX event queue. This means you can call it as frequently as you like from the background thread but only the most recent set is ultimately set.

An example of such a Task class for doing intermediate updates on ObservableLists for TableView and ListView is:

import java.util.ArrayList;
import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class IntermediateTest extends Application {

    @Override
    public void start(Stage primaryStage) {

        TableView<Integer> tv = new TableView<>();
        TableColumn<Integer, Integer> num = new TableColumn<>("Number");
        num.setCellValueFactory(v -> new ReadOnlyObjectWrapper(v.getValue()));
        tv.getColumns().add(num);
        PartialResultsTask prt = new PartialResultsTask();
        tv.setItems(prt.getPartialResults());

        ListView<Integer> lv = new ListView<>();
        PartialResultsTask prt1 = new PartialResultsTask();
        lv.setItems(prt1.getPartialResults());
        new Thread(prt).start();
        new Thread(prt1).start();

        // Create a window & add the table
        VBox root = new VBox();
        root.getChildren().addAll(tv, lv);
        Scene scene = new Scene(root, 300, 450);

        primaryStage.setTitle("Data-Adding");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    public class PartialResultsTask extends Task<ObservableList<Integer>> {

        private ReadOnlyObjectWrapper<ObservableList<Integer>> partialResults
                = new ReadOnlyObjectWrapper<>(this, "partialResults",
                        FXCollections.observableArrayList(new ArrayList()));

        public final ObservableList getPartialResults() {
            return partialResults.get();
        }

        public final ReadOnlyObjectProperty<ObservableList<Integer>> partialResultsProperty() {
            return partialResults.getReadOnlyProperty();
        }

        @Override
        protected ObservableList call() throws Exception {
            updateMessage("Creating Integers...");
            Random rnd = new Random();
            for (int i = 0; i < 10; i++) {
                Thread.sleep(1000);
                if (isCancelled()) {
                    break;
                }
                final Integer r = rnd.ints(100, 10000).findFirst().getAsInt();
                Platform.runLater(() -> {
                    getPartialResults().add(r);
                });
                updateProgress(i, 10);
            }
            return partialResults.get();
        }
    }

}
like image 50
aw-think Avatar answered Oct 20 '22 01:10

aw-think