I'm having serious problems getting a JavaFX 2 TableView to deal with null values in my data. So much trouble in fact I've put together a simple demo of the problem which is shown below.
Essentially the problem is that some of my data may be null and coercing the null to a value, such as an empty string, is not valid, it must be null. In the real code I'm working on I have a null date value, to keep the example simple I've shown a null string below.
Rows 1 and 2 of the table have null values. The two columns are different, the first shows the behaviour of the TextFieldTableCell the second my implementation of an editable cell. Both show the same incorrect behaviour.
The current behaviour is this:
- Click the cell to enter editing mode
- Enter a value in the cell
- Press enter to commit the edit
- Nothing happens
At step 4 I would expect the onEditCommit handler for the column to get called but it isn't. Having a look at the source for javax.scene.control.TableCell the commit isn't happening because of the first line of commitEdit is:
if (! isEditing()) return;
It seems that because the cell is null the editing property never gets set to true although I admit I've not yet traced through all the code to see why it never get's switched to true.
Thanks as always for any help.
Example
Main Application
package example;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.stage.Stage;
import javafx.util.Callback;
public class NullCellEditingExample extends Application {
private TableView table = new TableView();
private final ObservableList<Person> data =
FXCollections.observableArrayList( new Person(null, "Smith"), new Person("Isabella", null),
new Person("Ethan", "Williams"), new Person("Emma", "Jones"), new Person("Michael", "Brown"));
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
TableColumn firstNameCol = createSimpleFirstNameColumn();
TableColumn lastNameCol = createLastNameColumn();
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol);
table.setEditable(true);
((Group) scene.getRoot()).getChildren().addAll(table);
stage.setScene(scene);
stage.show();
}
private TableColumn createSimpleFirstNameColumn() {
TableColumn firstNameCol = new TableColumn("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
firstNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
firstNameCol.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Person, String>>() {
@Override
public void handle(TableColumn.CellEditEvent<Person, String> t) {
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setFirstName(t.getNewValue());
}
});
return firstNameCol;
}
private TableColumn createLastNameColumn() {
Callback<TableColumn, TableCell> editableFactory = new Callback<TableColumn, TableCell>() {
@Override
public TableCell call(TableColumn p) {
return new EditingCell();
}
};
TableColumn lastNameCol = new TableColumn("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
lastNameCol.setCellFactory(editableFactory);
lastNameCol.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Person, String>>() {
@Override
public void handle(TableColumn.CellEditEvent<Person, String> t) {
t.getRowValue().setLastName(t.getNewValue());
}
});
return lastNameCol;
}
}
Editing Cell
package example;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.scene.control.TableCell;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public class EditingCell extends TableCell<Person, String> {
private TextField textField;
public EditingCell() {
}
@Override
public void startEdit() {
super.startEdit();
if( textField == null ) {
createTextField();
}
setText(null);
setGraphic(textField);
textField.selectAll();
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText((String) getItem());
setGraphic(null);
}
@Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) {
if (!arg2) { commitEdit(textField.getText()); }
}
});
textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
String value = textField.getText();
if (value != null) { commitEdit(value); } else { commitEdit(null); }
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
Person
package example;
import javafx.beans.property.SimpleStringProperty;
public class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
public Person(String firstName, String lastName) {
this.firstName = new SimpleStringProperty(firstName);
this.lastName = new SimpleStringProperty(lastName);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public SimpleStringProperty firstNameProperty() {
return firstName;
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
public SimpleStringProperty lastNameProperty() {
return lastName;
}
}
Since posting I found another question that is basically identical. They took the approach of always just avoiding null values which is fine for strings (e.g. use an empty string) but not acceptable for dates or other data types where the is no obvious "empty" value.
The solution is to pass a value of false into the super.updateItem call in the EditingCell.updateItem method. I've put together a full write up of this if anyone is interested in the complete analysis.