0

I've been searching for a while, but all I found seems very old and can't get it to work and I'm very confused.

I have a tableview with a checkbox in a column header (select all) and another checkbox for each row (select row). What I am trying to achieve is to get all the rows whose checkboxes are checked to perform an action.

Here's what it looks like:

enter image description here

And here's the code in my controller:

package com.comparador.controller;

import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.comparador.ComparadorPreciosApplication;
import com.comparador.entity.Commerce;
import com.comparador.entity.Items;
import com.comparador.entity.ShoppingListPrices;
import com.comparador.repository.CommerceRepository;
import com.comparador.repository.ProductRepository;
import com.comparador.service.ShoppingService;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Scene;
import javafx.scene.SceneAntialiasing;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.converter.IntegerStringConverter;

@Component
public class ShoppingController implements Initializable {
//    @Autowired
//    @Qualifier("lblTitulo")
    private String titulo = "Productos";

    @Autowired
    private ProductRepository productRepository;
    @Autowired
    private CommerceRepository commerceRepository;
    @Autowired
    private ShoppingService shoppingService;

    @FXML
    private Label lblTitulo;
    @FXML
    private Button btBack;
    @FXML
    private TableView<Items> tvProducts;
    @FXML
    private TableColumn<Items, CheckBox> colSelected;       //THE CHECKBOX COLUMN
    @FXML
    private TableColumn<Items, String> colName;
    @FXML
    private TableColumn<Items, Integer> colAmount;
    @FXML
    private TableView<ShoppingListPrices> tvTotalPrices;
    @FXML
    private TableColumn<ShoppingListPrices, String> colCommerce;
    @FXML
    private TableColumn<ShoppingListPrices, Double> colTotal;

    private CheckBox selectAll;
    List<ShoppingListPrices> shoppingList = new ArrayList<>();
    
    
    
    @Override
    public void initialize(URL location, ResourceBundle resources) {
        colName.setCellValueFactory(new PropertyValueFactory<>("name"));
        colAmount.setCellValueFactory(new PropertyValueFactory<>("amount"));
        colAmount.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
//      colSelected.setCellFactory(CheckBoxTableCell.forTableColumn(colSelected));
//      colSelected.setCellValueFactory(cellData -> new ReadOnlyBooleanWrapper(cellData.getValue().getChecked()));
        colSelected.setCellValueFactory(new PropertyValueFactory<>("selected"));
        colCommerce.setCellValueFactory(new PropertyValueFactory<>("commerceName"));
        colTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
        lblTitulo.setText(titulo);
        tvProducts.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        reloadTableViewProducts();
        
        selectAll = new CheckBox();
        selectAll.setOnAction(event -> {
            event.consume();
            tvProducts.getItems().forEach(item -> {
                item.getSelected().setSelected(selectAll.isSelected());
            });
            
        });
        
        setShoppingList();
        
        colSelected.setGraphic(selectAll);
    }
    
    @FXML
    public void editAmount(CellEditEvent<Items, Integer> event) {
        Items item = event.getRowValue();
        if(event.getTableColumn().getText().equals("Cantidad")) {
            item.setAmount(event.getNewValue());
        }
        setShoppingList();
    }
    
    /*
     * CLICKING ON A CHECKBOX SHOULD CALL THIS METHOD AND ADD THE ROW TO "selectedItems"
     */
    @FXML
    public void setShoppingList() {
        
        List<Items> selectedItems = new ArrayList<>();

        //Before trying this I was selecting each row by Ctrl + Clicking on it
//      List<Items> selectedItems = tvProducts.getSelectionModel().getSelectedItems();
        
        //This didn't seem to work
//      List<ShoppingListItems> selectedItems = tvProducts.getItems().filtered(x->x.getSelected() == true);
        
        
        List<Commerce> commerces = commerceRepository.findByNameContaining("");
        
        ShoppingListPrices pricesMixingCommerces = shoppingService.getCheapestShoppingList(commerces, selectedItems);
        List<ShoppingListPrices> pricesByCommerce = shoppingService.getShoppingListsPerCommerce(commerces, selectedItems);
        
        shoppingList = new ArrayList<>();
        shoppingList.add(pricesMixingCommerces); 
        shoppingList.addAll(pricesByCommerce);
        
        ObservableList<ShoppingListPrices> resultOL = FXCollections.observableArrayList();
        resultOL.addAll(shoppingList);
        tvTotalPrices.setItems(resultOL);
    }
    
    
    @FXML
    public void openShoppingList() throws IOException {
        FXMLLoader loader  = new FXMLLoader(getClass().getResource("/shoppingList.fxml"));
        ShoppingListController shoppingListController = new ShoppingListController();
        loader.setControllerFactory(ComparadorPreciosApplication.applicationContext::getBean);
        loader.setController(shoppingListController);
        shoppingListController.setup(tvTotalPrices.getSelectionModel().getSelectedItem());
        try {
            Scene scene = new Scene(loader.load(), 800, 400, true, SceneAntialiasing.BALANCED);
            Stage stage = new Stage();//(Stage) btBack.getScene().getWindow();
            stage.setUserData(tvTotalPrices.getSelectionModel().getSelectedItem());
            stage.setScene(scene);
            stage.show();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    
    @FXML
    public void goBack() {
        FXMLLoader loader  = new FXMLLoader(ComparadorPreciosApplication.class.getResource("/index.fxml"));
        loader.setControllerFactory(ComparadorPreciosApplication.applicationContext::getBean);
        try {
            Scene scene = new Scene(loader.load(), 800, 800, false, SceneAntialiasing.BALANCED);
            Stage stage = (Stage) btBack.getScene().getWindow();
            stage.setScene(scene);
            stage.show();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    
    
    private void reloadTableViewProducts() {
        List<String> productNames = productRepository.findOnProductPerName("");
        List<Items> items = new ArrayList<>();
        for(String name : productNames) {
            //items.add(new Items(new SimpleBooleanProperty(false), name, 1));
            Items item = new Items((CheckBox) new CheckBox(), name, 1);
            item.getSelected().setSelected(false);
            items.add(item);
        }
        ObservableList<Items> itemsOL = FXCollections.observableArrayList();
        itemsOL.addAll(items);
        tvProducts.setItems(itemsOL);
    }
}
Calfa
  • 233
  • 4
  • 11
  • 3
    You may want to consider [Why should I avoid using PropertyValueFactory in JavaFX?](https://stackoverflow.com/q/72437983/6395627). – Slaw Feb 14 '23 at 10:16
  • 2
    you __must not__ have nodes as items (nor properties of items) - that's currently only doc'ed for ComboBox, soon for all virtualized controls. – kleopatra Feb 14 '23 at 10:28

1 Answers1

3

Your Items class should not reference any UI objects, including CheckBox. The model should ideally not even know the view exists. If you plan on having Items track if it's selected itself, then it should expose a BooleanProperty representing this state. With a properly configured table and column, the check box associated with an item and the item's selected property will remain synchronized. And since the items of the table keep track of their own selected state, getting all the selected items is relatively straightforward. Simply iterate/stream the items and grab all the selected ones.

Here's an example using CheckBoxTableCell:

import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        var table = new TableView<Item>();
        table.setEditable(true);
        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        for (int i = 0; i < 50; i++) {
            table.getItems().add(new Item("Item #" + (i + 1)));
        }

        var selectedCol = new TableColumn<Item, Boolean>("Selected");
        // configure cell factory to use a cell implementation that displays a CheckBox
        selectedCol.setCellFactory(CheckBoxTableCell.forTableColumn(selectedCol));
        // link CheckBox and model selected property
        selectedCol.setCellValueFactory(data -> data.getValue().selectedProperty());
        table.getColumns().add(selectedCol);

        var nameCol = new TableColumn<Item, String>("Name");
        nameCol.setCellValueFactory(data -> data.getValue().nameProperty());
        table.getColumns().add(nameCol);

        var button = new Button("Print checked items");
        button.setOnAction(e -> {
            // filter for selected items and collect into a list
            var checkedItems = table.getItems().stream().filter(Item::isSelected).toList();

            // log selected items
            System.out.printf("There are %,d checked items:%n", checkedItems.size());
            for (var item : checkedItems) {
                System.out.println("  " + item);
            }
        });

        var root = new BorderPane();
        root.setTop(button);
        root.setCenter(table);
        root.setPadding(new Insets(10));
        BorderPane.setMargin(button, new Insets(0, 0, 10, 0));
        BorderPane.setAlignment(button, Pos.CENTER_RIGHT);

        primaryStage.setScene(new Scene(root, 600, 400));
        primaryStage.show();
    }

    public static class Item {

        private final StringProperty name = new SimpleStringProperty(this, "name");
        public final void setName(String name) { this.name.set(name); }
        public final String getName() { return name.get(); }
        public final StringProperty nameProperty() { return name; }

        private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
        public final void setSelected(boolean selected) { this.selected.set(selected); }
        public final boolean isSelected() { return selected.get(); }
        public final BooleanProperty selectedProperty() { return selected; }

        public Item() {}

        public Item(String name) {
            setName(name);
        }

        @Override
        public String toString() {
            return String.format("Item(name=%s, selected=%s)", getName(), isSelected());
        }
    }
}

Note that TableView has a selection model. That is not the same thing. It's used for the selection of rows or cells of the table (and thus works best on a per-table basis). You, however, want to be able to "check" items, and that requires keeping track of that state differently--an item's row could be selected while the item is not checked, and vice versa.

And note I recommend that any model class used with TableView expose JavaFX properties (like the Item class in the example above). It makes it much easier to work with TableView. But that could interfere with other parts of your code (e.g., Spring). In that case, you could do one of three things:

  1. Create a simple adapter class that holds a reference to the "real" object and provides a BooleanProperty. This adapter class would only be used for the TableView.

  2. Create a more complex adapter class that mirrors the "real" class in content, but exposes the properties as JavaFX properties (e.g., BooleanProperty, StringProperty, etc.). Map between them as you cross layer boundaries in your application.

  3. In the controller, or wherever you have the TableView, keep the selected state external to the model class. For instance, you could use a Map<Item, BooleanProperty>.

    I probably would only use this approach as a last resort, if ever.

Slaw
  • 37,820
  • 8
  • 53
  • 80
  • thank you so much for your accurate answer. I really appreciate the time and effort to do it. The only thing I have to figure out is how to add a listener or an event, or an action or whatever it is called in order to perform an action (call the setShoppingList method) when the user clicks on a checkbox (Because columns don't seem to have an "onAction" method), rather than using a button. Also, just want to mention that I'm not very familiar with JavaFX, so I didn't really understand the last part where you tell me to do "one ot three things". I'll investigate it, though. – Calfa Feb 14 '23 at 11:34
  • You could add a listener to the `selected` property to each `Item` in the table (based on the example code in my answer). You can add/remove this listener by observing the table's items list. As for the last part of my answer, it's not specifically about JavaFX _per se_, though I do suggest researching how to use JavaFX properties if you're going to create a JavaFX GUI application. But the concepts are pretty general. Look up the "adapter pattern". And by "layer boundaries", that's regarding a typical architecture (e.g., MVC, MVVM, etc.) with clear "separation of concerns". – Slaw Feb 15 '23 at 08:05