JavaFX 和 RxJava-TableView 无限调用 setCellValueFactory()

标签 javafx java-8 reactive-programming rx-java reactfx

我遇到了 TableView 和使用来自 ReactFX 的响应式(Reactive)绑定(bind)的问题。对于setCellValueFactory。驱动绑定(bind)的 EventStream 源自 RxJava Observable 但是,在下面的代码中,您将找到一个将其转换为 EventStream 的方法。

但是,TableView 除了列的初始绑定(bind)值之外从不显示任何内容。当我将 System.out.println 添加到 setCellValueFactory() 主体时,我发现 setCellValueFactory() 被无限循环调用并且发出的值从未进入绑定(bind)。

我对此感到非常困惑。如何停止这种行为并让 Observable 成功将单个值发送到 EventStream,然后发送到 Binding?

这是我的 SSCCE。

public class ReactiveTableViewTest extends Application {

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

        Group root = new Group();
        Scene scene = new Scene(root);

        root.getChildren().add(new ReactiveTable(buildSampleData()));

        stage.setScene(scene);
        stage.show();
    }

    private ObservableList<ReactivePoint> buildSampleData() { 
        ObservableList<ReactivePoint> points = FXCollections.observableArrayList();
        points.add(new ReactivePoint(Observable.just(1), Observable.just(2)));
        return points;
    }

    private static final class ReactivePoint {
        private final Observable<Integer> x;
        private final Observable<Integer> y;

        ReactivePoint(Observable<Integer> x, Observable<Integer> y) { 
            this.x = x;
            this.y = y;
        }
        public Observable<Integer> getX() {
            return x;
        }
        public Observable<Integer> getY() { 
            return y;
        }
    }

    private static final class ReactiveTable extends TableView<ReactivePoint> {

        @SuppressWarnings("unchecked")
        private ReactiveTable(ObservableList<ReactivePoint> reactivePoints) { 
            this.setItems(reactivePoints);

            TableColumn<ReactivePoint,Number> xCol = new TableColumn<>("X");
            xCol.setCellValueFactory(cb -> {
                System.out.println("Calling cell value factory for x col");
                return toReactFX(cb.getValue().getX().map(x -> (Number) x)).toBinding(-1); //causes infinite call loop
                //return new SimpleObjectProperty<Number>(1); //works fine
            });

            TableColumn<ReactivePoint,Number> yCol = new TableColumn<>("Y");
            yCol.setCellValueFactory(cb -> {
                System.out.println("Calling cell value factory for y col");
                return toReactFX(cb.getValue().getY().map(y -> (Number) y)).toBinding(-1); //causes infinite call loop
                //return new SimpleObjectProperty<Number>(1); //works fine
            });

            this.getColumns().addAll(xCol, yCol);
        }
    }
    private static <T> EventStream<T> toReactFX(Observable<T> obs) {
        EventSource<T> es = new EventSource<>();
        obs.subscribe(foo -> Platform.runLater(() -> es.push(foo)), e -> e.printStackTrace());
        return es;
    }
    public static void main(String[] args) { 
        launch(args);
    }
}

更新

我认为我发现下面提出的解决方案存在问题。如果在平台线程之外的其他线程上发出任何 Observable,则不会向该属性填充任何值。

我尝试通过在将调用 rxToProperty 的线程放入平台线程之前检查它是否是平台线程来解决此问题,但这不起作用并再次导致无限循环。我不知道 Property 的线程安全性是否会使事情脱轨。

但是如何才能在多个线程上发出 Observable 来安全地填充 Property 呢?这是我更新的 SSCCE,显示了这种行为。 “X”列永远不会填充,因为它是多线程的,但“Y”列会填充,因为它保留在平台线程上。

public class ReactiveTableViewTest extends Application {

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

        Group root = new Group();
        Scene scene = new Scene(root);

        root.getChildren().add(new ReactiveTable(buildSampleData()));

        stage.setScene(scene);
        stage.show();
    }

    private ObservableList<ReactivePoint> buildSampleData() { 
        ObservableList<ReactivePoint> points = FXCollections.observableArrayList();
        points.add(new ReactivePoint(
                Observable.just(1, 5, 6, 8,2,3,5,2).observeOn(Schedulers.computation()), 
                Observable.just(2,6,8,2,14)
                )
            );
        return points;
    }

    private static final class ReactivePoint {
        private final Observable<Integer> x;
        private final Observable<Integer> y;

        ReactivePoint(Observable<Integer> x, Observable<Integer> y) { 
            this.x = x;
            this.y = y;
        }
        public Observable<Integer> getX() {
            return x;
        }
        public Observable<Integer> getY() { 
            return y;
        }
    }

    private static final class ReactiveTable extends TableView<ReactivePoint> {

        @SuppressWarnings("unchecked")
        private ReactiveTable(ObservableList<ReactivePoint> reactivePoints) { 
            this.setItems(reactivePoints);


            System.out.println("Constructor is happening on FX THREAD: " + Platform.isFxApplicationThread());

            TableColumn<ReactivePoint,Number> xCol = new TableColumn<>("X");
            xCol.setCellValueFactory(cb -> { 
                System.out.println("CellValueFactory for X called on FX THREAD: " + Platform.isFxApplicationThread());
                return rxToProperty(cb.getValue().getX().map(x -> (Number) x));
                }
            );

            TableColumn<ReactivePoint,Number> yCol = new TableColumn<>("Y");
            yCol.setCellValueFactory(cb -> {
                System.out.println("CellValueFactory for Y called on FX THREAD: " + Platform.isFxApplicationThread());
                return rxToProperty(cb.getValue().getY().map(y -> (Number) y));
            }
        );

            this.getColumns().addAll(xCol, yCol);
        }
    }
    private static <T> ObjectProperty<T> rxToProperty(Observable<T> obs) { 
        ObjectProperty<T> property = new SimpleObjectProperty<>();

        obs.subscribe(v -> { 
            if (Platform.isFxApplicationThread()) {
                System.out.println("Emitting " + v + " on FX Thread");
                property.set(v);
            }
            else { 
                System.out.println("Emitting " + v + " on Non-FX Thread");
                Platform.runLater(() -> property.set(v));
            }
        });

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

最佳答案

我找到了一个解决方案,尽管我还没有完全弄清楚OP代码中问题的确切根本原因(如果有人能解释为什么,我会将其标记为答案)。我最初认为 Platform.runLater() 可能会导致最初设置的无限循环(如下所示)。

private static <T> EventStream<T> toReactFX(Observable<T> obs) {
    EventSource<T> es = new EventSource<>();
    obs.subscribe(foo -> Platform.runLater(() -> es.push(foo)), e -> e.printStackTrace());
    return es;
}

这个理论被证明是正确的。删除 Platform.runLater() 导致无限循环消失。也许发出的值不断地把自己扔到 GUI 线程的后面,因此永远不会进入 Binding?但仍然没有发出任何东西,并且表值保持在 -1,即初始绑定(bind)值。

private static <T> EventStream<T> toReactFX(Observable<T> obs) {
    EventSource<T> es = new EventSource<>();
    obs.subscribe(foo -> es.push(foo), e -> e.printStackTrace());
    return es;
}

我确实找到了有用的东西。我创建了一个名为 rxToProperty() 的新转换方法,并用它替换了对 rxToReactFX() 的调用。在那之后,一切似乎都很顺利。

private static <T> ObjectProperty<T> rxToProperty(Observable<T> obs) { 
    ObjectProperty<T> property = new SimpleObjectProperty<>();
    obs.subscribe(v -> property.set(v));
    return property;
}

这是新的 TableColumn 设置。

TableColumn<ReactivePoint,Number> xCol = new TableColumn<>("X");
xCol.setCellValueFactory(cb -> rxToProperty(cb.getValue().getX().map(x -> (Number) x)));

TableColumn<ReactivePoint,Number> yCol = new TableColumn<>("Y");
yCol.setCellValueFactory(cb -> rxToProperty(cb.getValue().getY().map(y -> (Number) y)));

如果有人有更好的解决方案,或者可以解释为什么 Platform.runLater()EventStream 不起作用,我会将其标记为已接受的答案。

更新

在非 FX 线程上发出的 Observables 存在一些问题,并且值从未填充到属性。我发现可以通过使用 cache() 保存最后一个值来解决这个问题,并且它将在订阅的调用线程(即 FX 线程)上重新发出。我还做了一些同步并只读返回的 Property 的包装器。

private static <T> ReadOnlyObjectProperty<T> rxToProperty(Observable<T> obs) { 
        ReadOnlyObjectWrapper<T> property = new ReadOnlyObjectWrapper<>();

        obs.cache(1).subscribe(v -> { 
            synchronized(property) { 
                if (Platform.isFxApplicationThread()) {
                    System.out.println("Emitting val " + v + " on FX Thread");
                }
                else { 
                    System.out.println("Emitting val " + v + " on Non-FX Thread");
                }
                property.set(v);
            }
        });

        return property.getReadOnlyProperty();
    }

最终更新 Tomas Mikula 在 ReactFX GitHub 项目上对此行为以及解决方案提供了一些非常有用的见解。

https://github.com/TomasMikula/ReactFX/issues/22

关于JavaFX 和 RxJava-TableView 无限调用 setCellValueFactory(),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30804401/

相关文章:

java - 从 GridPane 上的单元格返回节点。 (JavaFX)

java - 如何在 JavaFX 中将可见性绑定(bind)到 Controller

java - 将 MathContext 设置为 BinaryOperator 引用方法

android - RxJava2 类似于 Android 中的 AsyncTask

javascript - 每次流结束时都会延迟随机时间的 Observable

java - Android Volley RxJava - 多个请求

来自 Controller 的 JavaFX 转换场景

java - 如何使 RadioButton 看起来像 JavaFX 中的常规 Button

java - 如何使用 java-8 lambda 和流将 List<Map<String,String>> 转换为 Map<String,String>

Java 8 LocalDateTime 在使用 00 秒(如 "2018-07-06 00:00:00")解析日期字符串值时丢弃 00 秒值