JavaFX - TableView applying styles on a row (after filtering the data)


Question

Note: this is about a very specific bug and not a duplicate of Programmatically change the TableView row appearance, but it is quite related to that question.

I have a table with a CheckBox and when the user select that row (i.e click on the CheckBox) I change the style of that row to make it clear that one is selected.

But, when the user apply a filter to change the displayed data on the TableView, I cannot style each row depending on the select property. And when I debugged, I saw that it was actually going where it should set the style, but no style change at all.

Illustration of the current behavior

Below is the minimal code that I could make to reproduce this error:

DataView.java

package table;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;

public class DataView extends Application{
    TableView<DataRow> table = new TableView<DataRow>();
    ComboBox<Integer> cb = new ComboBox<Integer>();
    ObservableList<DataRow> masterData = FXCollections.observableArrayList();
    ObservableList<DataRow> filteredData = FXCollections.observableArrayList();
    @Override
    public void start(Stage stage) throws Exception {
        VBox root = new VBox();
        initializeGUI();
        addDummyData();
        applyFilters();
        root.getChildren().addAll(cb,table);
        Scene scene = new Scene(root);
       // scene.getStylesheets().add("/res/styleSummary.css"); 
        scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
        stage.setScene(scene);
        stage.show();
    }
    private void applyFilters() {
        filteredData = FXCollections.observableArrayList(new Callback<DataRow, Observable[]>() {
            @Override
            public Observable[] call(DataRow tr) {
                return new Observable[] { tr.selectedProperty() };
            }
        });
        filteredData.addListener(new ListChangeListener<DataRow>() {
            @Override
            public void onChanged(javafx.collections.ListChangeListener.Change<? extends DataRow> change) {
                try{
                    while (change.next()) {
                        if (change.wasUpdated()) {
                            int i = 0;
                            for (Node n: table.lookupAll("TableRow")){
                                if(i==change.getFrom()){
                                    @SuppressWarnings("unchecked")
                                    TableRow<DataRow> row = (TableRow<DataRow>) n;
                                    if (table.getItems().get(i).getSelected())
                                        row.getStyleClass().add("selected");
                                    else
                                        row.getStyleClass().remove("selected");
                                }
                                i++;
                            }
                            updateMaster(filteredData.get(change.getFrom()));
                        }
                    }
                }catch(Exception e){
                }
            }

            private void updateMaster(table.DataRow changed) {
                for (DataRow tr : masterData) {
                    if (tr.getNumSlices()==changed.getNumSlices()) {
                        tr.selectedProperty().set(changed.getSelected());
                        break;
                    }
                }
            }
        });
        filteredData.addAll(masterData);
        for(DataRow dr: masterData){
            if(dr.getNumSlices()==cb.getValue()){
                filteredData.remove(dr);
            }
        }
        table.setItems(filteredData);
        //This fails:
        int i = 0;
        for (Node n: table.lookupAll("TableRow")){
            TableRow<DataRow> row = (TableRow<DataRow>) n;
            if (table.getItems().get(i).getSelected())
                row.getStyleClass().add("selected");
            else
                row.getStyleClass().remove("selected");
            i++;
            if (i == table.getItems().size())
                break;
        }
    }
    @SuppressWarnings("unchecked")
    public void initializeGUI(){
        //combobox
        cb.getItems().addAll(null,2,4,5);
        cb.valueProperty().addListener(new ChangeListener<Number>(){
            @Override
            public void changed(ObservableValue<? extends Number> arg0, Number arg1, Number arg2) {
                applyFilters();
            }
        });
        //table
        TableColumn<DataRow, Integer> numColumn = new TableColumn<DataRow, Integer>();
        numColumn.setCellValueFactory(new PropertyValueFactory<DataRow, Integer>("numSlices"));
        TableColumn<DataRow, Boolean> selectionColumn = new TableColumn<DataRow, Boolean>();
        selectionColumn.setCellValueFactory(new PropertyValueFactory<DataRow, Boolean>("selected"));
        final Callback<TableColumn<DataRow, Boolean>, TableCell<DataRow, Boolean>> cellFactory = CheckBoxTableCell
                .forTableColumn(selectionColumn);
        selectionColumn.setCellFactory(new Callback<TableColumn<DataRow, Boolean>, TableCell<DataRow, Boolean>>() {
            @Override
            public TableCell<DataRow, Boolean> call(TableColumn<DataRow, Boolean> column) {
                TableCell<DataRow, Boolean> cell = cellFactory.call(column);
                cell.setAlignment(Pos.CENTER);
                return cell;
            }
        });
        selectionColumn.setCellFactory(cellFactory);
        selectionColumn.setEditable(true);
        table.getColumns().addAll(numColumn,selectionColumn);
        table.setEditable(true);
    }
    public void addDummyData(){
        for(int i=0;i<13;i++)
            masterData.add(new DataRow(i,false));
    }
    public static void main(String[] args) throws Exception { launch(args); }
}

The, the DataRow which is essentially a Integer and Boolean type:

DataRow.java

package table;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;

public class DataRow {
    private Integer numSlices;
    private BooleanProperty selected;
    public DataRow(int numSlices, boolean selected) {
        this.selected = new SimpleBooleanProperty(selected);
        this.numSlices =  numSlices;
    }
    public Integer getNumSlices() {return numSlices;}
    public void setNumSlices(int num){this.numSlices = num;}
    public void setSelected(boolean value) {selected.set(value);}
    public boolean getSelected() {return selected.get();}
    public BooleanProperty selectedProperty(){return selected;}
}

style.css

.selected { 
    -fx-font-weight: bold;
    -fx-font-size: 13;  
}

And just to make it more specific, the error is probably here :

    //(DataView.java) -- line 89
    //This fails:
    int i = 0;
    for (Node n: table.lookupAll("TableRow")){
        TableRow<DataRow> row = (TableRow<DataRow>) n;
        if (table.getItems().get(i).getSelected())
            row.getStyleClass().add("selected");
        else
            row.getStyleClass().clear();
        i++;
        if (i == table.getItems().size())
            break;
    }
1
1
5/23/2017 12:09:14 PM

Accepted Answer

Why not just use the method in your linked answer. You never know which cell is which so you have to add everything you need to the cell (or row) when it's created.

I just added this in your initializeGUI() and removed the complicated listener. Seems to work fine. If you really want a style class then use a listener like in the linked answer by james_D. Or even better, make a psuedoClass.

    table.setRowFactory(new Callback<TableView<DataRow>, TableRow<DataRow>>() {
        @Override
        public TableRow<DataRow> call(TableView<DataRow> tableView) {
            final TableRow<DataRow> row = new TableRow<DataRow>() {
                @Override
                protected void updateItem(DataRow row, boolean empty) {
                    super.updateItem(row, empty);
                    if (!empty)
                        styleProperty().bind(Bindings.when(row.selectedProperty())
                            .then("-fx-font-weight: bold; -fx-font-size: 16;")
                            .otherwise(""));
                }
            };
            return row;
        }
    });
2
7/3/2014 3:35:18 AM

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon