我遇到了 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 项目上对此行为以及解决方案提供了一些非常有用的见解。
关于JavaFX 和 RxJava-TableView 无限调用 setCellValueFactory(),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/30804401/