Adam
Adam

Reputation: 36703

JavaFX table update threading architecture mismatch

I have a JavaFX table which is ultimately with data received on a network thread.

Using Platform.runLater() to update the view-model is simple, but it does not fit into our architecture.

The current architecture separates applications into "view" and "network/comms" parts.

So I'm in a dilemma.

I've attempted to illustrate this in code

The simple approach

Just call Platform.runLater() from network reader

public class SimpleUpdate extends Application {
    private int clock;

    public class Item {
        private IntegerProperty x = new SimpleIntegerProperty(0);

        public final IntegerProperty xProperty() {
            return this.x;
        }

        public final int getX() {
            return this.xProperty().get();
        }

        public final void setX(final int x) {
            this.xProperty().set(x);
        }
    }

    @Override
    public void start(Stage primaryStage) throws Exception {

        ObservableList<Item> viewModel = FXCollections.observableArrayList();
        TableView<Item> table = new TableView<Item>(viewModel);
        TableColumn<Item, Integer> colX = new TableColumn<>();
        colX.setCellValueFactory(new PropertyValueFactory<Item, Integer>("x"));
        table.getColumns().add(colX);

        primaryStage.setScene(new Scene(table));
        primaryStage.show();

        new Thread(() -> {
            while (true) {
                Platform.runLater(() -> { // update on JavaFX thread
                    if (clock % 2 == 0) {
                        viewModel.add(new Item());
                        viewModel.add(new Item());
                    } else {
                        viewModel.remove(1);
                    }
                    for (Item each : viewModel) {
                        each.setX(each.getX() + 1);
                    }
                    clock++;
                });
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Network update").start();
    }

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

}

Pure approach

Code

public class PureUpdate extends Application {
    private int clock;

    public class Item {
        private IntegerProperty x = new SimpleIntegerProperty(0);

        public final IntegerProperty xProperty() {
            return this.x;
        }

        public final int getX() {
            return this.xProperty().get();
        }

        public final void setX(final int x) {
            this.xProperty().set(x);
        }
    }

    public class ViewItem extends Item {
        private Item original;

        public ViewItem(Item original) {
            super();
            this.original = original;
            sync();
        }

        public void sync() {
            setX(original.getX());
        }

        public Item getOriginal() {
            return original;
        }
    }

    @Override
    public void start(Stage primaryStage) throws Exception {

        ObservableList<ViewItem> viewModel = FXCollections.observableArrayList();
        TableView<ViewItem> table = new TableView<ViewItem>(viewModel);
        TableColumn<ViewItem, Integer> colX = new TableColumn<>();
        colX.setCellValueFactory(new PropertyValueFactory<ViewItem, Integer>("x"));
        table.getColumns().add(colX);

        primaryStage.setScene(new Scene(table));
        primaryStage.show();

        ObservableList<Item> networkModel = FXCollections
                .synchronizedObservableList(FXCollections.observableArrayList());

        networkModel.addListener((Observable obs) -> {
            Platform.runLater(() -> {
                List<Item> alreadyKnown = new ArrayList<>();
                for (Iterator<ViewItem> it = viewModel.iterator(); it.hasNext();) {
                    ViewItem each = it.next();
                    alreadyKnown.add(each.getOriginal());
                    if (networkModel.contains(each.getOriginal())) {
                        each.sync();
                    } else {
                        it.remove();
                    }
                }
                for (Item each : networkModel.toArray(new Item[0])) {
                    if (!alreadyKnown.contains(each)) {
                        viewModel.add(new ViewItem(each));
                    }
                }
            });

        });

        new Thread(() -> {
            while (true) {
                if (clock % 2 == 0) {
                    networkModel.add(new Item());
                    networkModel.add(new Item());
                } else {
                    networkModel.remove(1);
                }
                for (Item each : networkModel) {
                    each.setX(each.getX() + 1);
                }
                clock++;
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Network update").start();
    }

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

}

Question. Can I achieve the pure approach without writing additional code?

Upvotes: 1

Views: 66

Answers (1)

M. le Rutte
M. le Rutte

Reputation: 3563

Have the processor post notifications to a Runnable. or a Consumer<T>, or define a custom @FunctionalInterface.

This will make testing easier to as you design out threading and the dependency on JavaFX or any other framework needing synchronization on a specific thread.

Example using consumer:

public class NetworkReader {
   private final Consumer<? super Data> consumer;

   public NetworkReader(Consumer<? super Data> consumer) {
       this.consumer = Objects.requireNonNull(consumer);
   }

   public void readStuff() {
       while (...) {
           Data data = ...;
           consumer.accept(data);
       }
   }
}

The NetworkReader would be constructed with e.g. new NetworkReader(d -> Platform.runLater(() -> updateModel(d)));

When you want to test you could pass do as follows:

NetworkReader reader = new NetworkReader(d -> this.actuals = d);
reader.readStuff();
assertEquals(expecteds, actuals);

A smart consumer could coelesc updates until it has actually been processed.

Upvotes: 4

Related Questions