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.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) {
				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);
		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.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.

Posted in JavaFX and tagged , .

Leave a Reply

Your email address will not be published. Required fields are marked *