Editable ListView Cells in JavaFX

This post is part of the 100 Days of JavaFX Series.

Following up on the previous article on ListView and Observable Collections, we want to use it in our JSON Schema Editor project with the ability to edit the name of the schema.

Let’s explore how we can use TextFieldListCell for this purpose.

Using the built-in TextFieldListCell

JJavaFX provides an out-of-the-box mechanism to have ListView cells with are editable by using a TextFieldListCell.

public class Demo2 extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        ObservableList<String> items = FXCollections.observableArrayList("a", "b", "c");

        StringConverter<String> converter = new DefaultStringConverter();
        ListView<String> listView = new ListView<>(items);
        listView.setEditable(true);
        listView.setCellFactory(param -> new TextFieldListCell<>(converter));

        Scene scene = new Scene(listView, 640, 480);
        stage.setScene(scene);
        stage.show();
    }
}
Simple Editable Cells

TextFieldListCell works great for scalar values or simple value objects (such as Currency). However, it will not play well with our previous example where we listed Schema objects composed of a name (that we want to edit) and a JSON Schema as a String.

A TextFieldListCell needs a StringConverter object in order to transform the string the user typed into the object type we desire to use.

public static class Converter extends StringConverter<Schema> {
    @Override
    public String toString(Schema schema) {
        return schema.getName();
    }

    @Override
    public Schema fromString(String string) {
        return new Schema(string, "");
    }
}

When converting a string to a Schema, we can only fill the name of the Schema and TextFieldListCell will replace the existing Schema in the list with the one it created, effectively some data along the way.

Extend TextFieldListCell and Rebuild the Converter

The main problem with the earlier approach is: The StringConverter always create a new Schema instance and only fill its name, losing existing data in the process.

We could have the converter try to reusing the current schema object. However, since we created the converter beforehand it.

The trick is to have the converter re-created whenever a new schema instance is assigned to the cell by overriding the updateItem method.

package eu.leward.jschema;

import javafx.scene.control.cell.TextFieldListCell;
import javafx.util.StringConverter;

public class SchemaListCell extends TextFieldListCell<Schema> {

    public SchemaListCell() {
        super();
        refreshConverter();
    }

    private void refreshConverter() {
        StringConverter<Schema> converter = new StringConverter<>() {
            @Override
            public String toString(Schema schema) {
                return schema.getName();
            }

            @Override
            public Schema fromString(String string) {
                if (isEmpty()) {
                    return new Schema(string, "{}");
                }
                Schema schema = getItem();
                schema.setName(string);
                return schema;
            }
        };
        setConverter(converter);
    }

    @Override
    public void updateItem(Schema item, boolean empty) {
        super.updateItem(item, empty);
        refreshConverter();
    }
}
Custom ListCell in Action

With this approach we can leverage any class of our choosing with TextFieldListCell.