javafxdatepickertableviewtablecell

problem when Filtering Table with custom DatePicker TableCell JavaFX


i'm trying to make custom table cell with a DatePicker. and having Filtered list in the table that filters the contents depending on radio button selection. when i select a date for a cell everything works fine.

Screenshot 1

screenshot

but when trying to filter and fire the predicate, the date looks going down to the bottom of the table. and when re-selecting the old radio button, it appears in the correct row.

Screenshot 2

screenshot 2

any Ideas?

my controller :

import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.RadioButton;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.ComboBoxTableCell;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.util.Callback;
import javafx.util.converter.IntegerStringConverter;

import java.net.URL;
import java.time.LocalDate;
import java.util.ResourceBundle;

public class Controller implements Initializable {

    @FXML
    private TableColumn<SetterGetter, String> colStatus;
    @FXML
    private TableColumn<SetterGetter, Integer> colCode;

    @FXML
    private TableColumn<SetterGetter, LocalDate> colDueDate;

    @FXML
    private TableColumn<SetterGetter, String> colName;

    @FXML
    private RadioButton rdAll;

    @FXML
    private RadioButton rdDelayed;

    @FXML
    private RadioButton rdDone;

    @FXML
    private TableView<SetterGetter> tableTasks;

    private RadioButton selectedRadioButton;

    ObservableList<SetterGetter> mainTaskList = FXCollections.observableArrayList();
    FilteredList<SetterGetter> tableTaskList = new FilteredList<>(mainTaskList, p -> true);

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {

        selectedRadioButton = rdAll;

        colCode.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, Integer>>() {
            @Override
            public void handle(TableColumn.CellEditEvent<SetterGetter, Integer> event) {
                SetterGetter row = event.getRowValue();
                row.setId(event.getNewValue());
            }
        });

        colName.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, String>>() {
            @Override
            public void handle(TableColumn.CellEditEvent<SetterGetter, String> event) {
                SetterGetter row = event.getRowValue();
                row.setName(event.getNewValue());
            }
        });

        colStatus.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, String>>() {
            @Override
            public void handle(TableColumn.CellEditEvent<SetterGetter, String> event) {
                SetterGetter row = event.getRowValue();
                row.setStatus(event.getNewValue().equals("منجز") ? 0 : 1);
            }
        });
        colCode.setCellValueFactory(b -> new SimpleIntegerProperty(b.getValue().getId()).asObject());
        colDueDate.setCellValueFactory(b -> b.getValue().getDate());
        colName.setCellValueFactory(b -> new SimpleStringProperty(b.getValue().getName()));
        colStatus.setCellValueFactory(b -> new SimpleStringProperty(
                b.getValue().getStatus() == 0 ? "منجز"
                        : "لم ينجز بعد")
        );

        colCode.setCellFactory(TextFieldTableCell.forTableColumn(new IntegerStringConverter()));
        colName.setCellFactory(TextFieldTableCell.forTableColumn());
        colStatus.setCellFactory(ComboBoxTableCell.forTableColumn(FXCollections.observableArrayList(
                "منجز",
                "لم ينجز بعد")
                )
        );
        colDueDate.setCellFactory(new Callback<TableColumn<SetterGetter, LocalDate>, TableCell<SetterGetter, LocalDate>>() {
            @Override
            public TableCell<SetterGetter, LocalDate> call(TableColumn<SetterGetter, LocalDate> setterGetterStringTableColumn) {
                return new DatePickerTableCell();
            }
        });

        mainTaskList.addAll(
                new SetterGetter(0, null, null, 1),
                new SetterGetter(1, null, null, 1)
        );

        tableTasks.setItems(tableTaskList);
    }

    @FXML
    void addCheck(ActionEvent event) {
        mainTaskList.add(new SetterGetter(0,
                null,
                null,
                0)
        );
    }

    @FXML
    void selectRadioButton(ActionEvent event) {
        if (event.getSource() != selectedRadioButton) {
            RadioButton newRadio = (RadioButton) event.getSource();
            newRadio.setStyle("-fx-font-family:arial;-fx-font-size:14;-fx-font-weight:bold;-fx-border-color:red;-fx-border-radius:20;");
            selectedRadioButton.setStyle("-fx-font-family:arial;-fx-font-size:14;-fx-font-weight:bold;");
            selectedRadioButton = newRadio;
        }
        firePredicate();
    }

    private void firePredicate() {
        tableTaskList.setPredicate(p -> {
            if (selectedRadioButton.equals(rdDone) && p.getStatus() != 0)
                return false;
            else if (selectedRadioButton.equals(rdDelayed) && p.getStatus() != 1)
                return false;
            else return true;
        });
    }

}

DatePickerTableCell class:

import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.DatePicker;
import javafx.scene.control.TableCell;

import java.time.LocalDate;

public class DatePickerTableCell extends TableCell<SetterGetter, LocalDate> {
    private final DatePicker datePicker = new DatePicker();

    public DatePickerTableCell() {

        super();
    }

    @Override
    public void startEdit() {
        super.startEdit();
        setGraphic(datePicker);
        setText(null);
        datePicker.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent actionEvent) {
                commitEdit(datePicker.getValue());
            }
        });
    }

     @Override
      public void commitEdit(LocalDate s) {
          super.commitEdit(s);
          setText(s.toString());
          setGraphic(null);
          setItem(s);
     }


    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setText(datePicker.getValue() == null ? null : datePicker.getValue().toString());
        setGraphic(null);
    }
}

SetterGetter class:

import javafx.beans.property.ObjectProperty;

import java.time.LocalDate;

public class SetterGetter {
    int id;
    String name;
    ObjectProperty<LocalDate> date;
    int status;

    public SetterGetter(int id, String name, ObjectProperty<LocalDate> date, int status) {
        this.id = id;
        this.name = name;
        this.date = date;
        this.status = status;
    }

    public int getStatus() {
        return status;
    }

    public void setStatus(int status) {
        this.status = status;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public ObjectProperty<LocalDate> getDate() {
        return date;
    }

    public void setDate(ObjectProperty<LocalDate> date) {
        this.date = date;
    }
}

Main class :

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setScene(new Scene(root, 565, 551));
        primaryStage.show();
    }


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

fxml file:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>

<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="551.0" prefWidth="565.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <children>
      <TableView fx:id="tableTasks" editable="true" layoutX="16.0" layoutY="163.0" nodeOrientation="LEFT_TO_RIGHT" prefHeight="400.0" prefWidth="554.0" style="-fx-font-family: arial; -fx-font-size: 15;" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0" AnchorPane.rightAnchor="10.0">
         <columns>
            <TableColumn fx:id="colDueDate" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="التاريخ" />
            <TableColumn fx:id="colStatus" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="الحالة" />
            <TableColumn fx:id="colName" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="الأسم" />
            <TableColumn fx:id="colCode" prefWidth="112" style="-fx-font-family: arial; -fx-font-size: 15;" text="الكود" />
         </columns>
         <columnResizePolicy>
            <TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
         </columnResizePolicy>
      </TableView>
      <HBox alignment="CENTER" layoutX="160.0" layoutY="27.0" spacing="15.0">
         <children>
            <RadioButton fx:id="rdDelayed" mnemonicParsing="false" onAction="#selectRadioButton" style="-fx-font-family: arial; -fx-font-size: 14; -fx-font-weight: bold;" text="لم ينجز بعد">
               <padding>
                  <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
               </padding>
               <toggleGroup>
                  <ToggleGroup fx:id="radioGroup" />
               </toggleGroup>
            </RadioButton>
            <RadioButton fx:id="rdDone" mnemonicParsing="false" onAction="#selectRadioButton" style="-fx-font-family: arial; -fx-font-size: 14; -fx-font-weight: bold;" text="منجز" toggleGroup="$radioGroup">
               <padding>
                  <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
               </padding>
            </RadioButton>
            <RadioButton fx:id="rdAll" mnemonicParsing="false" onAction="#selectRadioButton" selected="true" style="-fx-font-family: arial; -fx-font-size: 14; -fx-font-weight: bold; -fx-border-color: red; -fx-border-radius: 20 20 20 20;" text="الكل" toggleGroup="$radioGroup">
               <padding>
                  <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
               </padding>
            </RadioButton>
         </children>
      </HBox>
      <Button layoutX="473.0" layoutY="31.0" mnemonicParsing="false" onAction="#addCheck" prefHeight="34.0" prefWidth="77.0" style="-fx-font-family: arial; -fx-font-size: 18;" text="إضافة">
         <font>
            <Font name="Arial" size="17.0" />
         </font>
      </Button>
   </children>
</AnchorPane>

Solution

  • There are multiple things missing in your demo. Among that, one is already mentioned by @jewelsea in his comment.

    Issue#1:

    I noticed that you have not added a commit event for the date column. You need to persist the value(date) into the row object to get correct results when filtering.

    Your code in the initialize method will be something like this:

    colDueDate.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<SetterGetter, LocalDate>>() {
        @Override
        public void handle(TableColumn.CellEditEvent<SetterGetter, LocalDate> event) {
            SetterGetter row = event.getRowValue();
            if(row.date==null){
                row.setDate(new SimpleObjectProperty<>());
            }
            row.date.set(event.getNewValue());
        }
    });
    

    Issue#2:

    As mentioned by @jewelsea, you need to override the updateItem() method of the TableCell when defining a custom cell. The code in the DatePickerTableCell will be like:

    @Override
    protected void updateItem(LocalDate item, boolean empty) {
        super.updateItem(item, empty);
        if (isEditing()) {
            setText(null);
            setGraphic(datePicker);
        } else {
            setGraphic(null);
            if (item != null) {
                setText(item.toString());
            } else {
                setText(null);
            }
        }
    }
    

    The missing if-else condition in the updateItem is the main cause for your actual issue of the cell value to be in inappropriate place.

    A VirtualFlow reuses the cells and can place it any where. It is our duty to ensure that it renders correctly when the updateItem is called. If the if-else condition is missing, you can see cell being used with wrong value.

    Issue#3: Not exactly an issue ;)

    Use proper naming conventions for the setter/getter of observable properties in the SetterGetter class.

    public LocalDate getDate() {
        return date.get();
    }
    
    public void setDate(LocalDate date) {
        this.date.set(date);
    }
    
    public ObjectProperty<LocalDate> dateProperty() {
        return date;
    }