如何使用 ReactFX 的 Var 进行自定义绑定?

How to do a custom binding with ReactFX's Var?

中,我看到了如何处理 属性 更改的问题,方法是更改​​它的包装对象,从而不发送它已更改的更新。解决方案是使用 ReactFX:

class Cell {

    private final ObjectProperty<Shape> shape = new SimpleObjectProperty<>(new Shape());
    // all getters and setterts

    public static class Shape {

        private final IntegerProperty size = new SimpleIntegerProperty(0);
        // all getters and setterts
    }

    public static void main(String[] args) {

        Var<Number> sizeVar = Val.selectVar(cell.shapeProperty(), Shape::sizeProperty);
        sizeVar.addListener(
            (obs, oldSize, newSize) -> System.out.println("Size changed from "+oldSize+" to "+newSize));
}

所以现在如果 shape 属性 本身发生变化,它也会触发 size 的变化(除非新形状具有相同的大小)。但是现在我想使用自定义绑定绑定到 属性,我遇到了一个问题,解释如下。

我的数据类是这些:

class Cell {

    private final ObjectProperty<Shape> shape = new SimpleObjectProperty<>();
    public final ObjectProperty<Shape> shapeProperty() { return shape; }
    public final Shape getShape()                      { return shapeProperty().get(); }
    public final void setShape(Shape shape)            { shapeProperty().set(shape); }

    // other properties
}

class Shape {

    private final IntegerProperty size = new SimpleIntegerProperty();
    public final IntegerProperty sizeProperty() { return size; }
    public final int getSize()                  { return size.get(); }
    public final void setSize(int size)         { sizeProperty().set(size); }

    // other properties
}

我想通过将它们的属性绑定到 GUI 属性来为它们创建 GUI 表示。我是这样做的:

class CellRepresentation extends Group {

    private final Cell cell;

    CellRepresentation(Cell cell) {

        this.cell = cell;
        getChildren().add(new ShapeRepresentation() /*, other representations of things in the cell*/);
    }

    private class ShapeRepresentation extends Cylinder {

        ObjectProperty<Shape> shape;

        private ShapeRepresentation() {

            super(100, 100);

            shape = new SimpleObjectProperty<Shape>(cell.getShape());
            shape.bind(cell.shapeProperty());

            Var<Number> sizeVar = Val.selectVar(cell.shapeProperty(), Shape::sizeProperty);

            // THIS WILL WORK
            materialProperty().bind(Bindings.createObjectBinding(() -> {
                if (shape.get() == null)
                    return new PhongMaterial(Color.TRANSPARENT);
                return new PhongMaterial(Color.RED);
            }, sizeVar));

            // THIS WILL NOT WORK
            materialProperty().bind(sizeVar.map(n -> {
                if (shape.get() == null)
                    return new PhongMaterial(Color.TRANSPARENT);
                return new PhongMaterial(Color.RED);
            }));
        }
    }

    // the other representations of things in the cell
}

当我运行第一个绑定选项下面的代码将创建一个透明圆柱体。第二个选项将创建一个白色(默认颜色)圆柱体。我不知道为什么会这样。

public class Example extends Application {

    public static void main(String[] args) {

        launch(args);
    }

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

        Cell cell = new Cell();
        CellRepresentation cellRep = new CellRepresentation(cell);

        Group group = new Group(cellRep);
        Scene scene = new Scene(group, 200, 200, Color.AQUA);
        stage.setScene(scene);
        stage.show();
    }
}

如果这不是使用绑定创建数据表示的好方法 类,我也乐于接受设计建议。

ValVar 是 "observable monadics"(想想可观察到的 Optionals)。它们要么是空的,要么有一个值。 map 方法的工作原理与 Optional.map 相同:如果 Val 为空,则 map 会导致空 Val;否则它会导致 Val 包含将函数应用于原始 Val 值的结果。因此,如果 sizeVar 计算为 null,则映射结果为空 Val(因此您的 material 设置为 null),甚至不计算您的 lambda 表达式。

要处理null(即空Vals),你应该使用orElse或类似的方法:

private class ShapeRepresentation extends Cylinder {

    Val<Shape> shape;

    private ShapeRepresentation() {

        super(100, 100);

        shape = Val.wrap(cell.shapeProperty());

        Var<Number> sizeVar = shape.selectVar(Shape::sizeProperty);

        // THIS WILL WORK

        materialProperty().bind(shape
            .map(s -> new PhongMaterial(Color.RED))
            .orElseConst(new PhongMaterial(Color.TRANSPARENT)));

        // SO WILL THIS

        materialProperty().bind(sizeVar
                .map(n -> {
                    if (n.intValue() == 1) return new PhongMaterial(Color.RED) ;
                    if (n.intValue() == 2) return new PhongMaterial(Color.BLUE) ;
                    return new PhongMaterial(Color.WHITE);
                })
                .orElseConst(new PhongMaterial(Color.TRANSPARENT)));

    }
}

更新的测试示例:

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

public class Example extends Application {

    public static void main(String[] args) {

        launch(args);
    }

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

        Cell cell = new Cell();

        CellRepresentation cellRep = new CellRepresentation(cell);

        Group group = new Group(cellRep);

        ComboBox<Integer> sizeCombo = new ComboBox<>();
        sizeCombo.getItems().addAll(0, 1, 2);

        Shape shape = new Shape();
        shape.sizeProperty().bind(sizeCombo.valueProperty());


        CheckBox showShape = new CheckBox("Show shape");
        cell.shapeProperty().bind(Bindings.when(showShape.selectedProperty()).then(shape).otherwise((Shape)null));

        HBox controls = new HBox(5, showShape, sizeCombo);
        controls.setPadding(new Insets(5));

        BorderPane root = new BorderPane(group, controls, null, null, null);
        root.setBackground(null);

        Scene scene = new Scene(root, 400, 400, Color.AQUA);
        stage.setScene(scene);
        stage.show();
    }
}