I'm trying to create editable cells using Oracle poor tutorial. I figured out that their EditCell
class only updates when I click on the same row I currently edit or outside any row. In case when I click on another row, edit is cancelled. Here is the link to this tutorial and on the end of it you can find EditCell
class, but it's not the point of this question:
https://docs.oracle.com/javase/8/javafx/user-interface-tutorial/table-view.htm
This class creates TextField
for edit purposes. Clicking on another row launches cancel()
method. And there is this code line:
setText((String( getItem());
that blocks edit. I replaced it with:
setText((String) textField.getText());
and edit works now. But after editing this cell again old value is loaded to TextField
. I guess that ObservableList
is not updated after first edit.
Here is FXML code:
<GridPane fx:controller="sample.Controller"
xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10">
<TableView GridPane.columnIndex="0" GridPane.rowIndex="1" items="${controller.data}" editable="true">
<columns>
<TableColumn fx:id="colName" text="name">
<cellValueFactory>
<PropertyValueFactory property="Name"/>
</cellValueFactory>
</TableColumn>
<TableColumn fx:id="colSurname" text="surname">
<cellValueFactory>
<PropertyValueFactory property="Surname"/>
</cellValueFactory>
</TableColumn>
</columns>
</TableView>
</GridPane>
In controller I declare ObservableList
:
public class Controller {
@FXML
private TableColumn<Person, String> colName;
@FXML
private TableColumn<Person, String> colSurname;
@FXML
private ObservableList<Person> data;
public Controller(){
data = FXCollections.observableArrayList(
new Person("John", "S."),
new Person("Jane", "S.")
);
}
public TableColumn<Person, String> getColName() {
return colName;
}
public void setColName(TableColumn<Person, String> colName) {
this.colName = colName;
}
public TableColumn<Person, String> getColSurname() {
return colSurname;
}
public void setColSurname(TableColumn<Person, String> colSurname) {
this.colSurname = colSurname;
}
public ObservableList<Person> getData() {
return data;
}
public void setData(ObservableList<Person> data) {
this.data = data;
}
}
Person.java code:
public class Person {
private final SimpleStringProperty name;
private final SimpleStringProperty surname;
public Person(String name, String surname){
this.name = new SimpleStringProperty(name);
this.surname = new SimpleStringProperty(surname);
}
public String getName() {
return name.get();
}
public SimpleStringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getSurname() {
return surname.get();
}
public SimpleStringProperty surnameProperty() {
return surname;
}
public void setSurname(String surname) {
this.surname.set(surname);
}
}
In Main
I declare controller and editable column:
public class Main extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
FXMLLoader loader = new FXMLLoader(getClass().getResource("sample.fxml"));
Parent root = (Parent) loader.load();
primaryStage.setScene(new Scene(root, 300, 275));
Controller controller = loader.getController();
TableColumn<Person, String> colName = controller.getColName();
Callback<TableColumn<Person, String>, TableCell<Person, String>> cellFactory =
(TableColumn<Person, String> p) -> new sample.EditCell();
colName.setCellFactory(cellFactory);
colName.setOnEditCommit(
(TableColumn.CellEditEvent<Person, String> t) -> {
((Person) t.getTableView().getItems().get(
t.getTablePosition().getRow())
).setName(t.getNewValue());
});
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Do I need bind cell with ObservableList
? Or refresh it? How to update data
to have TextField always filled with actual value?
Here is whole EditCell
class:
class EditCell extends TableCell<Person, String> {
private TextField textField;
public EditCell() {
}
@Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.selectAll();
}
}
@Override
public void cancelEdit() {
super.cancelEdit();
setText((String) getItem());
//setText((String) textField.getText());
//This line updates cell, but textField keeps old value after next edit.
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(
(ObservableValue<? extends Boolean> arg0,
Boolean arg1, Boolean arg2) -> {
if (!arg2) {
commitEdit(textField.getText());
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
When editing, the onEditCommit
handler is notified when an edit has been committed (unsurprisingly). This handler is responsible for writing the new value to the model (in your case, Person
). When this happens the TableView
will automatically update to display the new value.
Your solution to set the text of the Cell
to the value of the TextField
when the edit is cancelled won't work. Eventually, once an update is triggered somehow, the Cell
will refresh to display the real data provided by the model (obtained by the cellValueFactory
). Besides that, you haven't actually updated the model and so the supposed edit is just a visual thing.
The tutorial you link to has issues. The biggest of which is that it assumes when the TextField
loses focus you can successfully commit the new value. As you are experiencing, this is not the case. You can see many others have experienced this problem by looking the this question: TableView doesn't commit values on focus lost event. The answers to that question provide many ways to hack around the problem. Some also point to bug reports, indicating the no-commit-on-lost-focus behavior is actually unintended; however, those bugs have not been fixed as of JavaFX 11.0.2.
What this means is that:
textField.focusedProperty().addListener(
(ObservableValue<? extends Boolean> arg0,
Boolean arg1, Boolean arg2) -> {
if (!arg2) {
commitEdit(textField.getText());
}
});
Won't ever commit the edit. You (but really the tutorial) provide no working means to commit the new value because the edit is cancelled by the time if (!arg2) { commitEdit(...); }
is invoked. Since the edit is cancelled there is no commit edit event fired and your TableColumn
can't write the new value to the model item. What you can do, though this won't fix the no-commit-on-lost-focus problem, is add an onAction
handler to your TextField
that commits the edit. You'll probably want to provide a means to cancel the edit via the keyboard as well. This would look something like:
textField.setOnAction(event -> {
commitEdit(textField.getText());
event.consume();
}
textField.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ESCAPE) {
cancelEdit();
event.consume();
}
}
This will commit the edit when the Enter key is pressed and cancel the edit when the Esc key is pressed.
Note that TextFieldTableCell
already provides this behavior out of the box, no need to roll your own EditCell
implementation. However, if you want to commit the edit when the focus is lost then you'll have to look at the answers to TableView doesn't commit values on focus lost event (or its linked/related questions) and attempt to use one of the given solutions (hacks).
Also, as noted in the below documentation, you don't have to provide your own onEditCommit
handler in order to write the new value to the model—TableColumn
does that by default (assuming cellValueFactory
returns a WritableValue
).
Perhaps reading the documentation of TableView
will be more beneficial than, or at least complimentary to, the tutorial you're reading:
Editing
This control supports inline editing of values, and this section attempts to give an overview of the available APIs and how you should use them.
Firstly, cell editing most commonly requires a different user interface than when a cell is not being edited. This is the responsibility of the
Cell
implementation being used. ForTableView
, it is highly recommended that editing be per-TableColumn
, rather than per row, as more often than not you want users to edit each column value differently, and this approach allows for editors specific to each column. It is your choice whether the cell is permanently in an editing state (e.g. this is common forCheckBox
cells), or to switch to a different UI when editing begins (e.g. when a double-click is received on a cell).To know when editing has been requested on a cell, simply override the
Cell.startEdit()
method, and update the cell text and graphic properties as appropriate (e.g. set the text to null and set the graphic to be aTextField
). Additionally, you should also overrideCell.cancelEdit()
to reset the UI back to its original visual state when the editing concludes. In both cases it is important that you also ensure that you call thesuper
method to have the cell perform all duties it must do to enter or exit its editing mode.Once your cell is in an editing state, the next thing you are most probably interested in is how to commit or cancel the editing that is taking place. This is your responsibility as the cell factory provider. Your cell implementation will know when the editing is over, based on the user input (e.g. when the user presses the Enter or ESC keys on their keyboard). When this happens, it is your responsibility to call
Cell.commitEdit(Object)
orCell.cancelEdit()
, as appropriate.When you call
Cell.commitEdit(Object)
an event is fired to theTableView
, which you can observe by adding anEventHandler
viaTableColumn.setOnEditCommit(javafx.event.EventHandler)
. Similarly, you can also observe edit events for edit start and edit cancel.By default the
TableColumn
edit commit handler is non-null, with a default handler that attempts to overwrite the property value for the item in the currently-being-edited row. It is able to do this as theCell.commitEdit(Object)
method is passed in the new value, and this is passed along to the edit commit handler via theCellEditEvent
that is fired. It is simply a matter of callingTableColumn.CellEditEvent.getNewValue()
to retrieve this value.It is very important to note that if you call
TableColumn.setOnEditCommit(javafx.event.EventHandler)
with your ownEventHandler
, then you will be removing the default handler. Unless you then handle the writeback to the property (or the relevant data source), nothing will happen. You can work around this by using theTableColumnBase.addEventHandler(javafx.event.EventType, javafx.event.EventHandler)
method to add aTableColumn.editCommitEvent()
EventType
with your desiredEventHandler
as the second argument. Using this method, you will not replace the default implementation, but you will be notified when an edit commit has occurred.