Jeff G Jeff G - 2 months ago 91
Java Question

JavaFX Boolean Binding Based on Selected Items in a TableView

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?

Answer

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