Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
657 views
in Technique[技术] by (71.8m points)

javafx - Cancel the modification of a TableView cell

I'd like to have an example which explains me how I can cancel an edit and reset the old value of a specific cell in a TableView that was edited but failed to pass through the validation. See the code below for more info.

tcAantalDagen.setOnEditCommit(cell -> {
        int dagen = Integer.parseInt(cell.getNewValue());
        if (Integer.parseInt(cell.getNewValue()) < 1 || Integer.parseInt(cell.getNewValue()) > 31) {
            // This shows an Alert Dialog
            Main.toonFoutbericht("Het item kan maar tussen 1 en 31 dagen uitgeleend worden");
            // The "return;" is successful in canceling the passing through of the new value of the cell to the backend code, 
            // but in the TableView the new value is still set in the cell, which can confuse the user
            return;
        }
}

The cell value is set like this:

// getAantalDagen() returns an Integer
tcAantalDagen.setCellValueFactory(cell -> {
            return new ReadOnlyObjectWrapper<String>(Integer.toString(cell.getValue().getAantalDagen()));
        });
// Make the cells of the tcAantalDagen column editable
tcAantalDagen.setCellFactory(TextFieldTableCell.forTableColumn());
// The table needs to be set to editable too for the above column to work
tblUitleningen.setEditable(true);
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

I played with this a while. The bottom line is that the default commitEdit method in TableCell, which is invoked when you commit the change in the text field in the TextFieldTableCell, calls updateItem(...) on the table cell with the new value. That's why the value in the cell is changing even though you don't change it in the model.

To prevent this, you need to implement the table cell yourself, which isn't too hard. The easiest implementation should would probably just use a text field table cell and override updateItem(...) to check the validity of the value. Something like

tcAantalDagen.setCellFactory(col -> new TextFieldTableCell<T, Integer>(new IntegerStringConverter()) {
    @Override
    public void updateItem(Integer item, boolean empty) {
        if (empty) {
            super.updateItem(item, empty) ;
        } else {
            // if out of range, revert to previous value:
            if (item.intValue() < 1 || item.intValue() > 31) {
                item = getItem();
            }
            super.updateItem(item, empty);
        }
    }
});

should work (though I haven't tested it). Obviously replace T with whatever the type of the items in the table are. Note that I used Integer as the column type here, which you can do if you provide an appropriate converter to the TextFieldTableCell. You would modify the cell value factory as

tcAantalDagen.setCellValueFactory(cellData -> cellData.getValue().aantalDagenProperty().asObject());

However... once you're going to all the trouble of implementing a table cell, you may as well provide one that simply doesn't allow the user to enter an invalid value, which is a much nicer user experience imho. You can do this by creating a text field for the cell that uses a TextFormatter that just vetoes invalid values. You have to be a little careful with these, as you want to allow values that would be partially-edited (so in general, it's not enough to allow only values that are valid, as they are checked every time the text changes, not just on commits). In this case, the only thing this means is that you should allow empty strings in the text field (else the user won't be able to delete the current value while editing, which would be awkward). You can use the converter to return the current value if the user tries to commit with an empty string.

So an implementation of this might look like

public class IntegerEditingCell extends TableCell<Item, Integer> {

    private TextField textField ;
    private TextFormatter<Integer> textFormatter ;

    public IntegerEditingCell(int min, int max) {
        textField = new TextField();
        UnaryOperator<TextFormatter.Change> filter = c -> {
            String newText = c.getControlNewText() ;

            // always allow deleting all characters:
            if (newText.isEmpty()) {
                return c ;
            }

            // otherwise, must have all digits:
            if (! newText.matches("\d+")) {
                return null ;
            }

            // check range:
            int value = Integer.parseInt(newText) ;
            if (value < min || value > max) {
                return null ;
            } else {
                return c ;
            }
        };
        StringConverter<Integer> converter = new StringConverter<Integer>() {

            @Override
            public String toString(Integer value) {
                return value == null ? "" : value.toString() ;
            }

            @Override
            public Integer fromString(String string) {

                // if it's an int, return the parsed value

                if (string.matches("\d+")) {
                    return Integer.valueOf(string);
                } else {

                    // otherwise, just return the current value of the cell:
                    return getItem() ;
                }
            }

        };
        textFormatter = new TextFormatter<Integer>(converter, 0, filter) ;
        textField.setTextFormatter(textFormatter);

        textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
            if (e.getCode() == KeyCode.ESCAPE) {
                cancelEdit();
            }
        });

        textField.setOnAction(e -> commitEdit(converter.fromString(textField.getText())));

        textProperty().bind(Bindings
                .when(emptyProperty())
                .then((String)null)
                .otherwise(itemProperty().asString()));

        setGraphic(textField);
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }

    @Override
    protected void updateItem(Integer value, boolean empty) {
        super.updateItem(value, empty);
        if (isEditing()) {
            textField.requestFocus();
            textField.selectAll();
            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        } else {
            setContentDisplay(ContentDisplay.TEXT_ONLY);
        }
    }

    @Override
    public void startEdit() {
        super.startEdit();
        textFormatter.setValue(getItem());
        setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
        textField.requestFocus();
        textField.selectAll();
    }

    @Override
    public void commitEdit(Integer newValue) {
        super.commitEdit(newValue);
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }

    @Override
    public void cancelEdit() {
        super.cancelEdit();
        setContentDisplay(ContentDisplay.TEXT_ONLY);
    }

}

That looks like a lot of code, but most of it is just setting up the text field and the formatter, then there's some pretty standard cell implementation that just makes sure the text field is displayed in editing mode and the plain text is displayed in non-editing mode.

Now you simply don't need to worry about checking the validity of the input, because the user cannot enter an invalid value.

Here's a simple usage example:

import java.util.Random;
import java.util.function.UnaryOperator;

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;

public class ValidatingTableColumn extends Application {

    @Override
    public void start(Stage primaryStage) {
        TableView<Item> table = new TableView<>();
        table.setEditable(true);

        TableColumn<Item, String> nameColumn = new TableColumn<>("Item");
        nameColumn.setCellValueFactory(cellData -> cellData.getValue().nameProperty());

        TableColumn<Item, Integer> valueColumn = new TableColumn<>("Value");
        valueColumn.setCellValueFactory(cellData -> cellData.getValue().valueProperty().asObject());

        table.getColumns().add(nameColumn);
        table.getColumns().add(valueColumn);

        valueColumn.setCellFactory(col -> new IntegerEditingCell(1, 31));

        valueColumn.setOnEditCommit(e -> {
            table.getItems().get(e.getTablePosition().getRow()).setValue(e.getNewValue());
        });

        Random rng = new Random();
        for (int i = 1; i <= 20; i++) {
            table.getItems().add(new Item("Item "+i, rng.nextInt(31)+1));
        }

        Button debug = new Button("Show all values");
        debug.setOnAction(e -> table.getItems().forEach(item -> System.out.println(item.getName()+" ("+item.getValue()+")")));
        BorderPane.setAlignment(debug, Pos.CENTER);
        BorderPane.setMargin(debug, new Insets(5));

        BorderPane root = new BorderPane(table, null, null, debug, null);
        primaryStage.setScene(new Scene(root, 600, 600));
        primaryStage.show();
    }

    public static class Item {
        private final IntegerProperty value = new SimpleIntegerProperty() ;
        private final StringProperty name = new SimpleStringProperty();

        public Item(String name, int value) {
            setName(name);
            setValue(value);
        }

        public final IntegerProperty valueProperty() {
            return this.value;
        }

        public final int getValue() {
            return this.valueProperty().get();
        }

        public final void setValue(final int value) {
            this.valueProperty().set(value);
        }

        public final StringProperty nameProperty() {
            return this.name;
        }

        public final String getName() {
            return this.nameProperty().get();
        }

        public final void setName(final String name) {
            this.nameProperty().set(name);
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...