5

I am attempting to enable a JavaFX Button depending on the aggregate of a property value in the selected rows in a TableView. The following is an example application that demonstrates the problem:

package test;

import java.util.Random;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

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

    private static class Row {
        private final BooleanProperty myProp;
        public Row(final boolean value) {
            myProp = new SimpleBooleanProperty(value);
        }
        public BooleanProperty propProperty() { return myProp; }
    }

    @Override
    public void start(final Stage window) throws Exception {
        // Create a VBox to hold the table and button
        final VBox root = new VBox();
        root.setMinSize(200, 200);

        // Create the table, and enable multi-select
        final TableView<Row> table = new TableView<>();
        final MultipleSelectionModel<Row> selectionModel = table.getSelectionModel();
        selectionModel.setSelectionMode(SelectionMode.MULTIPLE);
        root.getChildren().add(table);

        // Create a column based on the value of Row.propProperty()
        final TableColumn<Row, Boolean> column = new TableColumn<>("Value");
        column.setCellValueFactory(p -> p.getValue().propProperty());
        table.getColumns().add(column);

        // Add a button below the table
        final Button button = new Button("Button");
        root.getChildren().add(button);

        // Populate the table with true/false values
        final ObservableList<Row> rows = table.getItems();
        rows.addAll(new Row(false), new Row(false), new Row(false));

        // Start a thread to randomly modify the row values
        final Random rng = new Random();
        final Thread thread = new Thread(() -> {
            // Flip the value in a randomly selected row every 10 seconds
            try {
                do {
                    final int i = rng.nextInt(rows.size());
                    System.out.println("Flipping row " + i);
                    Thread.sleep(10000);
                    final BooleanProperty prop = rows.get(i).propProperty();
                    prop.set(!prop.get());
                } while (true);
            } catch (final InterruptedException e) {
                System.out.println("Exiting Thread");
            }
        }, "Row Flipper Thread");
        thread.setDaemon(true);
        thread.start();

        // Bind the button's disable property such that the button
        //     is only enabled if one of the selected rows is true
        final ObservableList<Row> selectedRows = selectionModel.getSelectedItems();
        button.disableProperty().bind(Bindings.createBooleanBinding(() -> {
            for (int i = 0; i < selectedRows.size(); ++i) {
                if (selectedRows.get(i).propProperty().get()) {
                    return false;
                }
            }
            return true;
        }, selectedRows));

        // Show the JavaFX window
        final Scene scene = new Scene(root);
        window.setScene(scene);
        window.show();
    }
}

To test, start the above application, and select the row indicated by the text "Flipping row N", where N is in [0, 2]. When the value of the selected row changes to true...

Observed Behavior button remains disabled.
Desired Behavior button becomes enabled.

Does anyone know how to create a BooleanBinding that exhibits the desired behavior?

Jeff G
  • 4,470
  • 2
  • 41
  • 76
  • 2
    FYI, you need to execute JavaFX code on the JavaFX thread. `final BooleanProperty prop = rows.get(i).propProperty(); prop.set(!prop.get());` should be wrapped with `Platform.runLater()`. – John Kugelman Sep 21 '16 at 19:34

1 Answers1

6

Your binding needs to be invalidated if any of the propPropertys of the selected rows change. Currently the binding is only observing the selected items list, which will fire events when the list contents change (i.e. items become selected or unselected) but not when properties belonging to items in that list change value.

To do this, create a list with an extractor:

final ObservableList<Row> selectedRows = 
    FXCollections.observableArrayList(r -> new Observable[]{r.propProperty()});

This list will fire events when items are added or removed, or when the propProperty() of any item in the list changes. (If you need to observe multiple values, you can do so by including them in the array of Observables.)

Of course, you still need this list to contain the selected items in the table. You can ensure this by binding the content of the list to the selectedItems of the selection model:

Bindings.bindContent(selectedRows, selectionModel.getSelectedItems());

Here is a version of your MCVE using this:

import java.util.Random;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.MultipleSelectionModel;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Main extends Application {

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

    private static class Row {
        private final BooleanProperty myProp;
        public Row(final boolean value) {
            myProp = new SimpleBooleanProperty(value);
        }
        public BooleanProperty propProperty() { return myProp; }
    }

    @Override
    public void start(final Stage window) throws Exception {
        // Create a VBox to hold the table and button
        final VBox root = new VBox();
        root.setMinSize(200, 200);

        // Create the table, and enable multi-select
        final TableView<Row> table = new TableView<>();
        final MultipleSelectionModel<Row> selectionModel = table.getSelectionModel();
        selectionModel.setSelectionMode(SelectionMode.MULTIPLE);
        root.getChildren().add(table);

        // Create a column based on the value of Row.propProperty()
        final TableColumn<Row, Boolean> column = new TableColumn<>("Value");
        column.setCellValueFactory(p -> p.getValue().propProperty());
        table.getColumns().add(column);

        // Add a button below the table
        final Button button = new Button("Button");
        root.getChildren().add(button);

        // Populate the table with true/false values
        final ObservableList<Row> rows = table.getItems();
        rows.addAll(new Row(false), new Row(false), new Row(false));

        // Start a thread to randomly modify the row values
        final Random rng = new Random();
        final Thread thread = new Thread(() -> {
            // Flip the value in a randomly selected row every 10 seconds
            try {
                do {
                    final int i = rng.nextInt(rows.size());
                    System.out.println("Flipping row " + i);
                    Thread.sleep(10000);
                    final BooleanProperty prop = rows.get(i).propProperty();
                    Platform.runLater(() -> prop.set(!prop.get()));
                } while (true);
            } catch (final InterruptedException e) {
                System.out.println("Exiting Thread");
            }
        }, "Row Flipper Thread");
        thread.setDaemon(true);
        thread.start();


        // Bind the button's disable property such that the button
        //     is only enabled if one of the selected rows is true
        final ObservableList<Row> selectedRows = 
                FXCollections.observableArrayList(r -> new Observable[]{r.propProperty()});
        Bindings.bindContent(selectedRows, selectionModel.getSelectedItems());
        button.disableProperty().bind(Bindings.createBooleanBinding(() -> {
            for (int i = 0; i < selectedRows.size(); ++i) {
                if (selectedRows.get(i).propProperty().get()) {
                    return false;
                }
            }
            return true;
        }, selectedRows));

        // Show the JavaFX window
        final Scene scene = new Scene(root);
        window.setScene(scene);
        window.show();
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Note that the changes are the addition of `Platform.runLater(...)` to the thread setting the property value, `FXCollections.observableArrayList(...)` to create a new `ObservableList` with an extractor, and `Bindings.bindContent(...)` to bind the contents of the new list to the selected rows in the `TableView`. – Jeff G Sep 21 '16 at 20:30
  • 1
    I ran into a problem with this approach that I wanted to document in case anyone else has the same issue. When I select one or more rows in the table, then call `table.getItems().clear()` on the main UI thread, I get an `IndexOutOfBoundsException`. To fix it, I changed the call to `clear` to be `final List> items = table.getItems(); while (!items.isEmpty()) items.remove(items.size() - 1);`, then changed the for-loop to be `for (final Row selectedRow : selectionModel.getSelectedItems())`, instead of iterating over the `selectedRows` list. – Jeff G Sep 28 '16 at 18:42