## Editing Null Data Values in a Cell with JavaFX 2

In an earlier article I gave a complete break down of how to write a generic editable table cell. I've found versions of this code posted around the place so I thought it was only right that I point out a small flaw and a fix that I believe is safe.

The typical example, and the one I have used, for showing off editable table cells is to use a person object with various properties such as first name and last name. Since it's an example, like good programmers, we assign a value to every property. The trouble is that doesn't reflect real life. In the real world we always have to deal with some missing data which often gets expressed as a null value. The problem, I discovered, is that the editable cells don't deal very well with null values, in fact you can't change a null value to a non-null value.

Since I wrote the earlier article on generic editable table cells JavaFX 2.2 has been released which comes complete with a range of editable cells for tables, trees and lists such as text area and combo box cells. Thinking that I had fluffed my implementation I switched one of the columns over to using a TextFieldTableCell and to my surprise I got the same behaviour.

Before I continue the discussion an example of the behaviour is shown below. Sorry for the length this is about as small as I could get it.

NullCellEditingExample.java

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.setEditable(true);

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) {
t.getRowValue().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) {
System.out.println( "Commiting last name change. Previous: " + t.getOldValue() + "   New: " + t.getNewValue() );
t.getRowValue().setLastName(t.getNewValue());
}
});

return lastNameCol;
}
}

EditingCell.java

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);
//		super.updateItem(item, false);
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);
@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.java

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; }
}

If you fire up that code you'll be presented with a table displaying the details of five people. The first person has a null first name and the second person has a null last name. Column one uses a TextFieldTableCell, column two uses an EditingCell from listing two.

With the example running click in the empty first name cell of the first person. The cell switches to editing mode and allows you to enter a value but, and here's the problem, you can't then commit the edit. The only way to leave editing mode is to press escape and cancel the edit. Exactly the same behaviour is seen with the EditingCell in column two - notice there's a System.out for this cell in the onEditCommit handler.

## Is this a bug or is it by design?

That's a tough call but I'm going to say it's a bug. I've dug through some of the source for JavaFX and it's pretty adamant that a null value should be treated as empty but to quote from the documentation for Cell.updateItem: Because null is a perfectly valid value in the application domain, Cell needs some way to distinguish whether or not the cell actually holds a value. The empty flag indicates this. It is an error to supply a non-null item but a true value for empty. So it seems the intention was to allow nulls in the domain model.

# Why doesn't the edit commit?

That's easy, the code calls TableCell.commitEdit the first line of which is

if (! isEditing()) return;

If you examine isEditing in your KeyHandler (created in the createTextField method) you'll find that it's false. The cell never really entered editing mode despite the fact that it passed through the startEdit method and drew a text field. If you drill down through the super calls in startEdit you'll end up at Cell.startEdit which checks if the cell is editable, currently editing and whether it's empty. If it passes all three checks it sets editing to true. The problem is that the empty returns true which stops the editing flag from being set to true.

Ok, so why does empty return true? This is bit more complex, Cell.isEmpty is final and returns the value of a read only boolean property. The only way to change the value of the empty flag is via a private method called, you guessed it, setEmpty. This private method is only called from Cell.updateItem which uses the value of empty that is passed in to it.

The solution therefore is to pass in a value of false for empty when calling super.updateItem from EditingCell which is what the commented out line of code does (line 40). As far as I can tell this is safe since updateItem only gets called when a row has a value so you don't end up with empty rows suddenly becoming editable.

## Can this fix be applied to the supplied editable cells?

Yes, all you need to do is override the updateItem method of the provided implementation calling super with an empty value of false. Considering that the commit handlers are less than ideal (even the author of the official tutorials admits this) for the provided text field implementation though I'm not sure this is worth it. Change createSimpleFirstNameColumn to this if you want to use the provided text field.

private TableColumn createSimpleFirstNameColumn() {
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
//firstNameCol.setCellFactory(TextFieldTableCell.forTableColumn());
firstNameCol.setCellFactory(new Callback<TableColumn<Person, String>, TableCell<Person, String>>() {
@Override
public TableCell<Person, String> call(TableColumn<Person, String> p) {
TextFieldTableCell<Person, String> cellFactory = new TextFieldTableCell<Person, String>() {
@Override
public void updateItem(String t, boolean bln) {
super.updateItem(t, false);
}
};
return cellFactory;
}
});
firstNameCol.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Person, String>>() {
@Override
public void handle(TableColumn.CellEditEvent<Person, String> t) {
t.getRowValue().setFirstName(t.getNewValue());
}
});

return firstNameCol;
} 

As you can see there's not much to overriding the method but you don't get a loss of focus commit or any other niceness so you probably will want to stick with your own implementation for now. If you have any issues leave a comment below.

0 #5 Alistair 2013-02-01 09:17
Thanks for this. I had started to dig into the code myself trying to find out why my edit wasn't working so you have saved me a lot of head scratching.

0 #4 Zekir je Car 2013-01-04 22:11
The proposed solution was successfully tested by inserting array of Persons with JDBC into Oracle DB. It worked without a hitch. Why ? In Oracle inserting empty string equals null =>

Actually editing empty cells in TableView with JavaFX is something Oracle should look into. Oracle's tutorial on how to edit TableView having few columns empty was not given.

Cheers.

0 #3 Graham Smith 2013-01-04 15:48
Checking in the constructor for nulls is not an ideal solution. If Person is an automatically persisted object (e.g. you're using JPA) then you must have a no-arg constructor. The check could be performed in the set method but what if null is a valid value for a given field?

0 #2 Zekir je Car 2013-01-04 15:31
There is a much more elegant solution without making any of the above mentioned changes.

Inside the constructor of the Person's class verify if the input parameter is null. In case it is null assign empty string to its SimpleStringPro perty.

E.G.

public class Person{
private SimpleStringPro perty fname;

public Person(String name){

if(name == null){
this.fname = "";
}else{
this.fname = name;
}

}

}

Greetings from Slovenia, the land of the best coders on the planet
Sarcasm off

0 #1 Antoine Mischler 2012-10-31 11:25
Thanks for your article, really useful. It took me quiet a long time to understand why my commit handler was not called!

Refresh

• #### How to Fix an Electric Drill - Part 1

I was about to toss my electric drill out but I decided not to. After reading up what you did i.e.

• #### Clinical Guard CMS 50EW

Amazing write up and reverse engineering. Thanks so much for sharing!

• #### JavaFX 2 Generic Editable Table Cells

thinks a lot.I find a lot time for this and I always don't know how to achieve.

• #### MikroTik RB751G Configuration

Thanks, it helped me. I was on the middle of something then this simple configuration make it happen ...