JavaFX - deleting row in tableview with custom textfield tablecell
Asked Answered
R

1

1

I am implementing a custom editable text cell to get the TAB feature in tableview. All is working fine but when i delete a row, the editable cells below the deleted row all change the values at the same time. So typing in a cell below the deleted row will change the item for multiple cells in that column.

Screenshot 1 - Tableview populated with initial values & row to be deleted is selected
Screenshot 2 - Tableview after deleting selected row
Screenshot 3 - The editable text cells in the rows below the deleted rows are updated simultaneously

Incase I reduce the window size so that only 3 rows are visible at a time and hit the delete button, then the values of editable cell outside the current view get the behavior mentioned above.

Below is my code.

Main:

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("IndexIssueSample.fxml"));
    primaryStage.setTitle("Hello World");
    primaryStage.setScene(new Scene(root, 600, 575));
    primaryStage.show();
}


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

Controller:

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import java.net.URL;
import java.util.ResourceBundle;

public class IndexIssueController implements Initializable {
@FXML
private TableView<Person> personTableView;

@FXML
private TableColumn<Person, String> someDataColumn;

@FXML
private TableColumn<Person, String> someMoreDataColumn;

@FXML
private TableColumn<Person, String> additionalDataColumn;

Person person = new Person();

@Override
public void initialize(URL location, ResourceBundle resources) {
    someDataColumn.setCellValueFactory(new PropertyValueFactory<>("someData"));
    someMoreDataColumn.setCellValueFactory(new PropertyValueFactory<>("someMoreData"));
    ObservableList<Person> personObservableList = FXCollections.observableArrayList();
    additionalDataColumn.setCellValueFactory(new PropertyValueFactory<>("additionalData"));
    someDataColumn.setCellFactory(e -> new EditableTextCell(0));
    someMoreDataColumn.setCellFactory(e -> new EditableTextCell(1));
    Person initPerson = setUpPersonData();
    Person personData1 = null, personData2 = null, personData3 = null, personData4 = null, personData5 = null, personData6 = null;
    try {
        personData1 = (Person) initPerson.clone();
        personData2 = (Person) initPerson.clone();
        personData3 = (Person) initPerson.clone();
        personData4 = (Person) initPerson.clone();
        personData5 = (Person) initPerson.clone();
        personData6 = (Person) initPerson.clone();
        personData1.setSomeData("1");
        personData1.setSomeMoreData("a");
        personData2.setSomeData("2");
        personData2.setSomeMoreData("b");
        personData3.setSomeData("3");
        personData3.setSomeMoreData("c");
        personData4.setSomeData("4");
        personData4.setSomeMoreData("d");
        personData5.setSomeData("5");
        personData5.setSomeMoreData("e");
        personData6.setSomeData("6");
        personData6.setSomeMoreData("f");
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
    personObservableList.addAll(personData1, personData2, personData3, personData4, personData5, personData6);
    personTableView.setItems(personObservableList);
    personTableView.getSelectionModel().setCellSelectionEnabled(true);
}

private Person setUpPersonData() {
    try {
        person.setSomeData("This is SomeData");
        person.setSomeMoreData("This is SomeMoreDate");
        person.setAdditionalData("This is AdditionalData");
    } catch (Exception e1) {
        e1.printStackTrace();
    }
    return person;
}

@FXML
public void deleteRowButtonPushed(){
    personTableView.getItems().remove(personTableView.getSelectionModel().getSelectedItem());
}
}

Custom Editable text cell:

import javafx.collections.ObservableList;
import javafx.scene.control.*;

class EditableTextCell extends TableCell<Person, String> {
private final TextField textField;
    EditableTextCell(int columnIndex) {

        textField = new TextField();

        this.indexProperty().addListener((obs, oldValue, newValue) -> {
                ObservableList<Person> tableData = getTableView().getItems();
                int oldIndex = oldValue.intValue();
                if (oldIndex >= 0 && oldIndex < tableData.size()) {
                    if (columnIndex == 0)
                        textField.textProperty().unbindBidirectional(tableData.get(oldIndex).someDataProperty());
                    if (columnIndex == 1)
                        textField.textProperty().unbindBidirectional(tableData.get(oldIndex).someMoreDataProperty());
                }
                int newIndex = newValue.intValue();
                if (newIndex >= 0 && newIndex < tableData.size()) {
                    if(columnIndex == 0){
                        textField.textProperty().bindBidirectional(tableData.get(newIndex).someDataProperty());
                        //System.out.println("Printing value for newIndex " + newIndex + " someData textField " + textField.getText() + " someDataProperty " + tableData.get(newIndex).someDataProperty().getValue());
                    }
                    if(columnIndex == 1){
                        textField.textProperty().bindBidirectional(tableData.get(newIndex).someMoreDataProperty());
                        //System.out.println("Printing value for newIndex " + newIndex + " someMoreData textField " + textField.getText()+ " someMoreDataProperty " + tableData.get(newIndex).someMoreDataProperty().getValue());
                    }
                    setGraphic(textField);
                } else {
                    setGraphic(null);
                }
        });
    }
}

Person Class:

import javafx.beans.property.SimpleStringProperty;
public class Person {
private SimpleStringProperty someData, someMoreData, additionalData;

public Person() {
    this.someData = new SimpleStringProperty("");
    this.someMoreData = new SimpleStringProperty("");
    this.additionalData = new SimpleStringProperty("");
}

public Person(String someData, String someMoreData, String additionalData) {
    this.someData = new SimpleStringProperty(someData);
    this.someMoreData = new SimpleStringProperty(someMoreData);
    this.additionalData = new SimpleStringProperty(additionalData);
}

@Override
public Object clone()throws CloneNotSupportedException{
    String someDataCloned = this.someData.getValue();
    String someMoreDataCloned = this.someMoreData.getValue();
    String additionalDataCloned = this.additionalData.getValue();
    Person personCloned = new Person(someDataCloned, someMoreDataCloned, additionalDataCloned);
    return personCloned;
}

public String getSomeData() {
    return someData.get();
}

public SimpleStringProperty someDataProperty() {
    return someData;
}

public void setSomeData(String someData) {
    this.someData.set(someData);
}

public String getSomeMoreData() {
    return someMoreData.get();
}

public SimpleStringProperty someMoreDataProperty() {
    return someMoreData;
}

public void setSomeMoreData(String someMoreData) {
    this.someMoreData.set(someMoreData);
}

public String getAdditionalData() {
    return additionalData.get();
}

public SimpleStringProperty additionalDataProperty() {
    return additionalData;
}

public void setAdditionalData(String additionalData) {
    this.additionalData.set(additionalData);
}
}

FXML:

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

<?import com.jfoenix.controls.JFXButton?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>

<AnchorPane prefHeight="400.0" prefWidth="1250.0" xmlns="http://javafx.com/javafx/8.0.141" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.IndexIssueController">
<children>
    <TableView fx:id="personTableView" layoutY="50.0" AnchorPane.bottomAnchor="50.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
        <columns>
            <TableColumn fx:id="someDataColumn" prefWidth="84.0" text="Some Data" />
            <TableColumn fx:id="someMoreDataColumn" minWidth="100.0" prefWidth="100.0" text="Some More Data" />
            <TableColumn fx:id="additionalDataColumn" minWidth="150.0" prefWidth="150.0" text="Additional Data" />
        </columns>
    </TableView>
  <JFXButton fx:id="deleteRowButton" layoutX="14.0" layoutY="363.0" onAction="#deleteRowButtonPushed" style="-fx-background-color: #0097db;" text="Delete Row" textFill="WHITE" AnchorPane.bottomAnchor="10.0" AnchorPane.leftAnchor="10.0">
     <font>
        <Font name="Arial" size="12.0" />
     </font>
  </JFXButton>
</children>
</AnchorPane>
Rousseau answered 18/5, 2018 at 0:31 Comment(0)
Q
1

Your EditableTextCell isn't implemented well. In most cases the index listener won't be notified of removed items. The indices of the items displayed remain the same after all (except for those that may be become empty, if there are not enough items to fill all cells). Furthermore a listener to a ObservableList is notified when the item is already gone from the list. You cannot properly unbind from the old item this way.

Since the cellValueFactorys return properties that can be writen to, I recommend using them to do the changes to the items.

public class EditableTextCell<E> extends TableCell<E, String> {

    private final TextField textField;
    private boolean updating = false;

    public EditableTextCell() {
        textField = new TextField();
        textField.textProperty().addListener((o, oldValue, newValue) -> {
            if (!updating) {
                ((WritableValue<String>) getTableColumn().getCellObservableValue((E) getTableRow().getItem())).setValue(newValue);
            }
        });
    }

    @Override
    protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (empty) {
            setGraphic(null);
        } else {
            setGraphic(textField);
            if (!Objects.equals(textField.getText(), item)) { // prevent own updates from moving the cursor
                updating = true;
                textField.setText(item);
                updating = false;
            }
        }
    }
}
Quennie answered 18/5, 2018 at 10:10 Comment(2)
Thanks @fabian. This fixes the issue.Rousseau
beware: it changes the data under the feet of the edit mechanism, which is an absolute no-go area (with exceptions ;)Sungsungari

© 2022 - 2024 — McMap. All rights reserved.