0

In this question I was shown how to deal with the problem of having a property change by changing it's wrapping object and thus not sending updates that it changed. A solution was using 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));
}

So now if the shape property itself changes it triggers a change in size too (unless the new shape has the same size). But now I want to bind to the property with custom bindings and I have an problem explained below.

My data classes are these:

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
}

And i want to create a GUI representation for them by binding their properties to GUI properties. I do it this way:

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
}

When I run the code below the first option for binding will create a transparent cylinder. The second option will create a white (default color) cylinder. I don't know why this happens.

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();
    }
}

I am also open to design suggestions if this is not a good way to create representations for the data classes using bindings.

Community
  • 1
  • 1
Mark
  • 2,167
  • 4
  • 32
  • 64
  • What is `size` in your last code block? Don't you really need something like `if (sizeVar.getValue().intValue() == 1) { ...}` etc? – James_D Jan 11 '17 at 00:57
  • @James_D `size` is the property of `Shape`. I did try before with the line you wrote (taking the value from `sizeVar`) but there was no difference. I'll create an MCVE. i thought was just wrong usage. – Mark Jan 11 '17 at 03:08
  • Yeah, I'm confused. Surely `size` is a member of `Shape`, but the binding is outside the `Shape` class. And that wouldn't compile anyway, because `size` is an `IntegerProperty` and cannot be compared to `1` by `==`. If that version is not working, the binding is probably getting garbage collected. Does my answer not work? – James_D Jan 11 '17 at 03:13
  • @James_D it's a no-MCVE confusion which i will solve. your solution doesn't work in my case and actually produces "worse" results than the one i posted so at least on some level they are not identical. hang on please... – Mark Jan 11 '17 at 03:16
  • @James_D ok, it was a bit of a convoluted problem in my code but I fixed it. So now i cheated and asked a slightly different question based on your answer but still same topic. I hope no one will mind... – Mark Jan 12 '17 at 02:34
  • Yeah, it's a bit annoying when you change a question which is answered, because then the answer has to be changed. See update. – James_D Jan 12 '17 at 03:06

1 Answers1

2

Val and Var are "observable monadics" (think observable Optionals). They are either empty or hold a value. The map method works just like Optional.map: if the Val is empty, map results in an empty Val; otherwise it results in a Val containing the result of applying the function to the original Val's value. So if sizeVar evaluates to null, the mapping results in an empty Val (so your material is set to null) without even evaluating your lambda expression.

To handle null (i.e. empty Vals), you should use orElse or similar methods:

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)));

    }
}

Updated example for testing:

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();
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Where is `sizeVar` used here? – Mark Jan 12 '17 at 03:40
  • @Mark it's not, but the bindings you posted didn't rely on it either (they just needed to know if the shape was null/empty). I assumed you still needed it somewhere else in your code. – James_D Jan 12 '17 at 03:43
  • sorry, i thought that using the `sizeVar` inside the binding will work but it doesn't. I tired `s -> { if(sizeVar.getValue().intValue() == 1) return new PhongMaterial(Color.RED); else return new PhongMaterial(Color.BLUE);` i need to tell the binding to listen to `sizeVar`. – Mark Jan 12 '17 at 04:00
  • @Mark What is the actual logic you are trying to implement? (Including the case where the shape and/or size are null.) – James_D Jan 12 '17 at 10:23
  • If the shape or size are null then no shape is displayed in the cell. i do it by making it transparent. if not then the color varies by the size the algorithm doesn't matter. something like: for size == 1 red, for size ==2 blue... otherwise white. – Mark Jan 12 '17 at 12:33
  • @Mark Well the same thing works just fine for me. See update. – James_D Jan 12 '17 at 13:18
  • Yes. it's because when shape is null then size is also null. what i'm trying to say (very badly until now) is that the solution with `map` allows me to listen to changes only in that variable. The custom `createXxxBinding` allowed me to specify a list of observables as the 2nd argument. The map allows me only 1 as I see it. so how do i listen to multiple variables this way? – Mark Jan 14 '17 at 03:53