Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFx Bindings : is there any way to bind value to observable List?

i have table view it's contents is observable list that contains numbers and i have a text field that should display the sum of these values in the table is there any way to bind this text fields to sum of the number properties . note : the user may edit the values in this list , may add more elements , may delete some element how can i bind the sum of these numbers correctly using javafx binding instead of doing this by the old fashion way iterate over the list and sum the numbers manually and every change reiterate over it again .

like image 890
Hassan Kbbewar Avatar asked Apr 22 '15 01:04

Hassan Kbbewar


1 Answers

An ObservableList will fire update events if (and only if) you create the list with an extractor. The extractor is a function that maps each element of the list to an array of Observables; if any of those Observables change their value, the list fires the appropriate update events and becomes invalid.

So the two steps here are:

  1. Create the list with an extractor
  2. Create a binding that computes the total whenever the list is invalidated.

So if you have a model class for your table such as:

public class Item {

    private final IntegerProperty value = new SimpleIntegerProperty();

    public IntegerProperty valueProperty() {
        return value ;
    }

    public final int getValue() {
        return valueProperty().get();
    }

    public final void setValue(int value) {
        valueProperty().set(value);
    }

    // other properties, etc...
}

Then you create the table with:

TableView<Item> table = new TableView<>();
table.setItems(FXCollections.observableArrayList(item -> 
    new Observable[] { item.valueProperty() }));

Now you can create the binding with

IntegerBinding total = Bindings.createIntegerBinding(() ->
    table.getItems().stream().collect(Collectors.summingInt(Item::getValue)),
    table.getItems());

Implementation note: the two arguments to createIntegerBinding above are a function that computes the int value, and any values to observe. If any of the observed values (here there is just one, table.getItems()) is invalidated, then the value is recomputed. Remember we created table.getItems() so it would be invalidated if any of the item's valueProperty()s changed. The function that is the first argument uses a lambda expression and the Java 8 Streams API, it is roughly equivalent to

() -> {
    int totalValue = 0 ;
    for (Item item : table.getItems()) {
        totalValue = totalValue + item.getValue();
    }
    return totalValue ;
}

Finally, if you want a label to display the total, you can do something like

Label totalLabel = new Label();
totalLabel.textProperty().bind(Bindings.format("Total: %d", total));

Here is an SSCCE:

import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.geometry.Pos;
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.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
import javafx.util.converter.NumberStringConverter;

public class TotallingTableView extends Application {

    @Override
    public void start(Stage primaryStage) {

        TableView<Item> table = new TableView<>();
        table.setEditable(true);

        table.getColumns().add(
                column("Item", Item::nameProperty, new DefaultStringConverter()));

        table.getColumns().add(
                column("Value", Item::valueProperty, new NumberStringConverter()));

        table.setItems(FXCollections.observableArrayList(
                item -> new Observable[] {item.valueProperty() }));

        IntStream.rangeClosed(1, 20)
            .mapToObj(i -> new Item("Item "+i, i))
            .forEach(table.getItems()::add);

        IntegerBinding total = Bindings.createIntegerBinding(() -> 
            table.getItems().stream().collect(Collectors.summingInt(Item::getValue)),
            table.getItems());

        Label totalLabel = new Label();
        totalLabel.textProperty().bind(Bindings.format("Total: %d", total));

        Button add = new Button("Add item");
        add.setOnAction(e -> 
            table.getItems().add(new Item("New Item", table.getItems().size() + 1)));

        Button remove = new Button("Remove");
        remove.disableProperty().bind(
                Bindings.isEmpty(table.getSelectionModel().getSelectedItems()));

        remove.setOnAction(e -> 
            table.getItems().remove(table.getSelectionModel().getSelectedItem()));

        HBox buttons = new HBox(5, add, remove);
        buttons.setAlignment(Pos.CENTER);
        VBox controls = new VBox(5, totalLabel, buttons);
        VBox.setVgrow(totalLabel, Priority.ALWAYS);
        totalLabel.setMaxWidth(Double.MAX_VALUE);
        totalLabel.setAlignment(Pos.CENTER_RIGHT);

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

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

        col.setCellFactory(TextFieldTableCell.forTableColumn(converter));

        return col ;
    }

    public static class Item {
        private final StringProperty name = new SimpleStringProperty();
        private final IntegerProperty value = new SimpleIntegerProperty();

        public Item(String name, int value) {
            setName(name);
            setValue(value);
        }

        public final StringProperty nameProperty() {
            return this.name;
        }

        public final java.lang.String getName() {
            return this.nameProperty().get();
        }

        public final void setName(final java.lang.String name) {
            this.nameProperty().set(name);
        }

        public final IntegerProperty valueProperty() {
            return this.value;
        }

        public final int getValue() {
            return this.valueProperty().get();
        }

        public final void setValue(final int value) {
            this.valueProperty().set(value);
        }


    }

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

Note that this is not the most efficient possible implementation, but the one that (IMHO) keeps the code the cleanest. If you have a very large number of items in the table, recomputing the total from scratch by iterating through and summing them all might be prohibitively expensive. The alternative approach is to listen for add/remove changes to the list. When an item is added, add its value to the total, and register a listener with the value property that updates the total if the value changes. When an item is removed from the list, remove the listener from the value property and subtract the value from the total. This avoids continually recomputing from scratch, but the code is harder to decipher.

like image 94
James_D Avatar answered Nov 05 '22 01:11

James_D