mynameisJEFF mynameisJEFF - 2 months ago 21
Java Question

javafx: Bindings not working as expected

I am trying to have a property

total
which is obtained by multiplying two properties together, namely
currentPrice
and
volumeHeld
, where
currentPrice
is actually obtained by downloading google finance stock price every 10 secs. And it automatically updates every 10 seconds.

Now the
getCurrentPrice()
is initialized at
0
, as shown in the code. 10 seconds later, it picks up a new value and this all works fine.

But in the binding method below, the
total
is not automatically updated when the
currentPrice
property has changed.

totalBinding = Bindings.createDoubleBinding(() -> {
System.out.println("current price: " + getCurrentPrice() + "vol held: " + getVolumeHeld());
return getCurrentPrice() * getVolumeHeld();
});

total.bind(totalBinding);


Question: I discovered that within the
createDoubleBinding
statement above, the
getCurrentPrice()
has a value of 0 (as mentioned above) and when its value is changed, the change is NOT propagated in the
total
property. By that I mean the
total
property is not able to pick up the new value from
getCurrentPrice()
even when the current price has changed.

So the problem is two-fold but I am guessing the solutions for both of my questions below will be similar if not exactly the same:


  1. How can I fix the problem mentioned above?

  2. Later on, I will be binding this
    total
    property to another property to work out the total of the
    total
    property for all
    Trade
    objects). This fails miserably and it is always equal to 0. This method is written in a different class, i.e. not in the Trade class.



UPDATE:

Code shown below:

class SummaryofTrade{
...
sumOfTotals = new ReadOnlyDoubleWrapper();
sumOfTotalsBinding = Bindings.createDoubleBinding(() -> {
double sum = 0;
for(Trade t : this.observableListOfTrades){
sum += t.getTotal();
}
return sum;
}, total); // I cannot put "total" as a second parameter, as it is a property that resides in the Trade class , not this class.
sumOfTotals.bind(sumOfTotalsBinding);
...
}


The error log message:

Caused by: java.lang.Error: Unresolved compilation problem:
total cannot be resolved to a variable


Please note that the
sumOfTotalsBinding
and
sumOfTotals
live in another class.

Code for Trade object below:

class Trade{
...
private final ReadOnlyDoubleWrapper total;
private final ReadOnlyDoubleWrapper currentPrice;
private DoubleProperty volumeHeld;
public DoubleBinding totalBinding;



private final ScheduledService<Number> priceService = new ScheduledService<Number>() {
@Override
public Task<Number> createTask(){
return new Task<Number>() {
@Override
public Number call() throws InterruptedException, IOException {
return getCurrentPriceFromGoogle();
}
};
}
};

public Trade(){
...
priceService.setPeriod(Duration.seconds(10));
priceService.setOnFailed(e -> priceService.getException().printStackTrace());
this.currentPrice = new ReadOnlyDoubleWrapper(0);
this.currentPrice.bind(priceService.lastValueProperty());
startMonitoring();
this.total = new ReadOnlyDoubleWrapper();
DoubleBinding totalBinding = Bindings.createDoubleBinding(() ->
getCurrentPrice() * getVolumeHeld(),
currentPriceProperty(), volumeHeldProperty());
total.bind(totalBinding);
}


// volume held
public double getVolumeHeld(){
return this.volumeHeld.get();
}

public DoubleProperty volumeHeldProperty(){
return this.volumeHeld;
}

public void setVolumeHeld(double volumeHeld){
this.volumeHeld.set(volumeHeld);
}

// multi-threading
public final void startMonitoring() {
priceService.restart();
}

public final void stopMonitoring() {
priceService.cancel();
}

public ReadOnlyDoubleProperty currentPriceProperty(){
return this.currentPrice.getReadOnlyProperty();
}

public final double getCurrentPrice(){
return currentPriceProperty().get();
}

// total
public final Double getTotal(){
return totalProperty().getValue();
}

public ReadOnlyDoubleProperty totalProperty(){
return this.total;
}
}


UPDATE 9/15/2015:

I am trying to elaborate my problem in a logical way here. Let me know if this does not make sense. Thanks.

First, in the
Trade class
above (please note the code above has been updated and specified the property dependency), each Trade object contains a
total
property, which is the product of
currentPrice
and
VolumeHeld
. If the user manually edit the values of current price and volume held. The
total
property will be updated automatically.

Now, I have an ObservableList of Trade objects, each of them has a
total
property. My goal is to sum up the the
total
property of each Trade object in the observable list and bind the sum to a variable called
sumOfTotals
. This is done in a class called
SummaryOfTrade
. And whenever the
total
property of any one of the Trades in the Observable list changes, the
sumOfTotals
property should also change automatically.

class SummaryofTrade{
...
// within constructor, we have
sumOfTotals = new ReadOnlyDoubleWrapper();
sumOfTotalsBinding = Bindings.createDoubleBinding(() -> {
double sum = 0;
for(Trade t : this.observableListOfTrades){
sum += t.getTotal();
}
return sum;
}, totalProperty());
sumOfTotals.bind(sumOfTotalsBinding);
...
}


This is where the problem comes in. Eclipse is saying that it does not recognise the Trade object's property,
totalProperty
. Error message shown below.

The error log message:

Caused by: java.lang.Error: Unresolved compilation problem:
The method totalProperty() is undefined for the type SummaryOfTrade


I have specified the property dependency already yet Eclipse is throwing an error. How should I resolve this?

Answer

You have an ObservableList<Trade>, where each Trade object has an observable totalProperty(). Your sumOfTotals need to be updated when either the content of that list change, or when any of the individual totalProperty()s belonging to any of the elements change.

You can do this by hand:

DoubleBinding sumOfTotalsBinding = new DoubleBinding() {

    {
        bind(observableListOfTrades);
        observableListOfTrades.forEach(trade -> bind(trade.totalProperty());
        observableListOfTrades.addListener((Change<? extends Trade> change) -> {
            while (change.next()) {
                if (change.wasAdded()) {
                    change.getAddedSubList().forEach(trade -> bind(trade.totalProperty()));
                }
                if (change.wasRemoved()) {
                    change.getRemoved().forEach(trade -> unbind(trade.totalProperty()));
                }
            }
        });
    }

    @Override
    public double computeValue() {
        return observableListOfTrades.stream().collect(Collectors.summingDouble(Trade::getTotal));
    }
};

Or, you can create your list with an extractor. This will cause the list to fire update notifications (thereby labeling it as invalid) when any of the specified properties belonging to the elements change:

ObservableList<Trade> observableListOfTrades = 
    FXCollections.observableArrayList(trade -> new Observable[] { trade.totalProperty() });

and then you can just do

sumOfTotals.bind(Bindings.createDoubleBinding(() ->
    observableListOfTrades.stream().collect(Collectors.summingDouble(Trade::getTotal)),
    observableListOfTrades);

since now binding just to the observableListOfTrades will cause recomputation when any of the individual totals change.

Here's an SSCCE:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import java.util.stream.Collectors;

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.ReadOnlyDoubleWrapper;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.HPos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.stage.Stage;
import javafx.util.converter.DoubleStringConverter;
import javafx.util.converter.IntegerStringConverter;

public class TradeTableExample extends Application {

    @Override
    public void start(Stage primaryStage) {
        TableView<Trade> table = new TableView<>();
        table.setEditable(true);
        TableColumn<Trade, String> nameCol = column("Name", trade -> new ReadOnlyStringWrapper(trade.getName()));
        TableColumn<Trade, Integer> volumeCol = column("Volume", t -> t.volumeProperty().asObject());
        TableColumn<Trade, Double> priceCol = column("Price", t -> t.priceProperty().asObject());
        TableColumn<Trade, Number> totalCol = column("Total", Trade::totalProperty);

        volumeCol.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
        priceCol.setCellFactory(TextFieldTableCell.forTableColumn(new DoubleStringConverter()));

        table.getColumns().addAll(Arrays.asList(nameCol, volumeCol, priceCol, totalCol));

        ObservableList<Trade> data = FXCollections.observableArrayList(trade -> new Observable[] {trade.totalProperty()});

        DoubleBinding grandTotal = Bindings.createDoubleBinding(() -> 
            data.stream().collect(Collectors.summingDouble(Trade::getTotal)),
            data);

        data.addAll(createData());
        table.setItems(data);

        Label totalLabel = new Label();
        totalLabel.textProperty().bind(grandTotal.asString("Total: %,.2f"));

        TextField nameField = new TextField();
        TextField volumeField = new TextField("0");
        TextField priceField = new TextField("0.00");

        Button add = new Button("Add");
        add.setOnAction(e -> {
            data.add(
                new Trade(nameField.getText(), 
                        Integer.parseInt(volumeField.getText()), 
                        Double.parseDouble(priceField.getText())));
            nameField.setText("");
            volumeField.setText("0");
            priceField.setText("0.00");
        });

        Button delete = new Button("Delete");
        delete.setOnAction(e -> data.remove(table.getSelectionModel().getSelectedIndex()));
        delete.disableProperty().bind(table.getSelectionModel().selectedItemProperty().isNull());

        HBox buttons = new HBox(5, add, delete);

        GridPane controls = new GridPane();
        controls.addRow(0, new Label("Name:"), nameField);
        controls.addRow(1, new Label("Volume:"), volumeField);
        controls.addRow(2, new Label("Price:"), priceField);
        controls.add(buttons, 0, 3, 2, 1);
        controls.add(totalLabel, 0, 4, 2, 1);

        ColumnConstraints leftCol = new ColumnConstraints();
        leftCol.setHalignment(HPos.RIGHT);
        ColumnConstraints rightCol = new ColumnConstraints();
        rightCol.setHgrow(Priority.ALWAYS);

        controls.getColumnConstraints().addAll(leftCol, rightCol);

        GridPane.setHalignment(controls, HPos.LEFT);
        GridPane.setHalignment(totalLabel, HPos.LEFT);

        controls.setHgap(5);
        controls.setVgap(5);

        BorderPane root = new BorderPane(table, null, null, controls, null);
        Scene scene = new Scene(root, 600, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private List<Trade> createData() {
        Random rng = new Random();
        List<Trade> trades = new ArrayList<>();
        for (int i=0; i<10; i++) {
            StringBuilder name = new StringBuilder();
            for (int c = 0; c < 3; c++) {
                name.append(Character.toString((char)(rng.nextInt(26)+'A')));
            }
            double price = rng.nextInt(100000)/100.0 ;
            int volume = rng.nextInt(10000);
            trades.add(new Trade(name.toString(), volume, price));
        }
        return trades ;
    }

    private <S,T> TableColumn<S,T> column(String text, Function<S, ObservableValue<T>> property) {
        TableColumn<S,T> col = new TableColumn<>(text);
        col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
        return col ;
    }

    public static class Trade {
        private final String name ;
        private final IntegerProperty volume = new SimpleIntegerProperty();
        private final DoubleProperty price = new SimpleDoubleProperty();
        private final ReadOnlyDoubleWrapper total = new ReadOnlyDoubleWrapper();

        public Trade(String name, int volume, double price) {
            this.name = name ;
            setPrice(price);
            setVolume(volume);
            total.bind(priceProperty().multiply(volumeProperty()));
        }

        public final String getName() {
            return name ;
        }

        public final IntegerProperty volumeProperty() {
            return this.volume;
        }

        public final int getVolume() {
            return this.volumeProperty().get();
        }

        public final void setVolume(final int volume) {
            this.volumeProperty().set(volume);
        }

        public final DoubleProperty priceProperty() {
            return this.price;
        }

        public final double getPrice() {
            return this.priceProperty().get();
        }

        public final void setPrice(final double price) {
            this.priceProperty().set(price);
        }

        public final ReadOnlyDoubleProperty totalProperty() {
            return this.total.getReadOnlyProperty();
        }

        public final double getTotal() {
            return this.totalProperty().get();
        }


    }

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