Javafx How to make a Binding to a list of Properties

2.9k Views Asked by At

I want to order a random number of Nodes according to their width. But I fail to calculate the sum of the width (using their properties), I have the following example code - I fail to get informed about the change of one of the properties:

@Override
public void start(Stage arg0) throws Exception {
    List<SimpleIntegerProperty> l = IntStream.range(0, 10)
            .mapToObj(SimpleIntegerProperty::new)
            .collect(Collectors.toList());
    ObservableList<IntegerProperty> widthAr = FXCollections.observableArrayList();
    widthAr.addAll(l);

    IntegerBinding nextheight = Bindings.createIntegerBinding(() -> widthAr.stream()
            .mapToInt(IntegerProperty::get)
            .sum(), widthAr);

    nextheight.addListener((v, o, n) -> System.out.println("New value: " + v.getValue()));

    //Now change randomly one of the IntegerProperties 
    ScheduledExecutorService tfsqueryScheduler = Executors.newScheduledThreadPool(1);

    tfsqueryScheduler.scheduleAtFixedRate(() -> {
        System.out.println("Changing");
        int i = (int) Math.round(Math.random() * 9.4);
        SimpleIntegerProperty v = l.get(i);
        v.set(0);
    }, 0, 3, TimeUnit.SECONDS);

    System.out.println("Start...");
}

The nextheight.addListener is never called :( ... any ideas? Thanks!

3

There are 3 best solutions below

1
On BEST ANSWER

By default, ObservableLists only fire updates if the structure of the list changes (e.g. items are added to or removed from the list), not if the state of individual elements in the list changes. To create a list which fires notifications if properties belonging to any of its elements change, you need to create the list with an extractor.

In this case the property you are interested in is just the list element itself, so you need to replace

ObservableList<IntegerProperty> widthAr = FXCollections.observableArrayList();

with

ObservableList<IntegerProperty> widthAr = 
    FXCollections.observableArrayList(w -> new Observable[] {w});

Note also that, depending on your real use case, you may need to ensure your binding is not prematurely garbage collected, by making it a field instead of a local variable.

0
On

When creating the IntegerProperty here:

Bindings.createIntegerBinding(() -> widthAr.stream()
        .mapToInt(IntegerProperty::get)
        .sum(), widthAr);

you also need to add all the elements in widthAr as dependencies, because the list will not notify should it's element change, only if an element gets added or removed.

NOTE: This would not work if removing or adding elements from the List, but you don't do that.

0
On

Here's a example i used in a project. The model is about a Group with a list of recipes, and each Recipe has a list of Materials with name and amount. This only holds the structure and doesn't have any prices loaded.

The prices are pulled from a external list of reagents. So first i had to map all reagents to each material.

    grupos.stream().map(Grupo::getRecetas).flatMap(List::stream).map(Receta::getObsMateriales).flatMap(List::stream)
            .forEach(material -> material.setReagent(buscarReagent(material.getNombre())));

Next, after all prices are loaded to the material's reagentProperty. It's time create a binding that sums the cost from all material*amount. I also setted the recipe's reagentProperty while streaming but it has nothing to do with the sum. Since everything is done with bindings if the source reagent updates it's price this sum should update dynamically.

grupos.stream().map(Grupo::getRecetas).flatMap(List::stream).forEach(receta -> {
    receta.setReagent(buscarReagent(receta.getNombre()));//doesn't affect the sum binding
    receta.costoProduccionProperty()
            .bind(Bindings.createLongBinding(
                    () -> receta.getObsMateriales().stream().mapToLong(m -> m.getReagent().getValue()*m.getCantidad()).sum(),
                    receta.getObsMateriales()));
});