1

I am attempting to modify the solution outlined in UITableView - Better Editing through Binding? (Thank you, jKaufmann, for that excellent example) to allow adding rows to the table.

I have introduced a button that, when clicked, invokes code to add an additional TableData row to the ObservableList backing the table.

The new row shows up without incident. However, after a few rows are added and you scroll through the table using the Up/Down keys a couple of times, random rows in the table start getting replaced with a blank row.

It usually takes adding a couple of rows, selecting a row, traversing down the list (using the keyboard) to the new row, adding a couple more rows, traversing down to the new row and all the way to the top again to reproduce the issue. To select a row in the table, I click on the edge of a row, so that the row is selected rather than an individual cell.

The source code (essentially jkaufmann's example modified to include an 'Add Row' button) is here.

package tablevieweditingwithbinding;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;

public class TableViewEditingExample2 extends Application {

    public static class TableData {

        private SimpleStringProperty firstName, lastName, phone, email;
        private ObjectProperty<SimpleStringProperty> firstNameObject;

        public TableData(String firstName, String lastName, String phone, String email) {
            this.firstName = new SimpleStringProperty(firstName);
            this.firstNameObject = new SimpleObjectProperty(firstNameObject);
            this.lastName = new SimpleStringProperty(lastName);
            this.phone = new SimpleStringProperty(phone);
            this.email = new SimpleStringProperty(email);
        }

        public String getEmail() {
            return email.get();
        }

        public void setEmail(String email) {
            this.email.set(email);
        }

        public SimpleStringProperty emailProperty() {
            return email;
        }

        public String getFirstName() {
            return firstName.get();
        }

        public SimpleStringProperty getFirstNameObject() {
            return firstNameObject.get();
        }

        public void setFirstNameObject(SimpleStringProperty firstNameObject) {
            this.firstNameObject.set(firstNameObject);
        }

        public ObjectProperty<SimpleStringProperty> firstNameObjectProperty() {
            return firstNameObject;
        }

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

        public String getPhone() {
            return phone.get();
        }

        public void setPhone(String phone) {
            this.phone.set(phone);
        }

        public SimpleStringProperty phoneProperty() {
            return phone;
        }
    }

    public static class TextFieldCellFactory
            implements Callback<TableColumn<TableData, String>, TableCell<TableData, String>> {

        @Override
        public TableCell<TableData, String> call(TableColumn<TableData, String> param) {
            TextFieldCell textFieldCell = new TextFieldCell();
            return textFieldCell;
        }

        public static class TextFieldCell extends TableCell<TableData, String> {

            private TextField textField;
            private StringProperty boundToCurrently = null;

            public TextFieldCell() {
                String strCss;
                // Padding in Text field cell is not wanted - we want the Textfield itself to "be"
                // The cell.  Though, this is aesthetic only.  to each his own.  comment out
                // to revert back.  
                strCss = "-fx-padding: 0;";
                textField = new TextField();

                // 
                // Default style pulled from caspian.css. Used to play around with the inset background colors
                // ---trying to produce a text box without borders
                strCss = ""
                        + //"-fx-background-color: -fx-shadow-highlight-color, -fx-text-box-border, -fx-control-inner-background;" +
                        "-fx-background-color: transparent;"
                        + //"-fx-background-insets: 0, 1, 2;" +
                        //"-fx-background-insets: 0;" +
                        //"-fx-background-radius: 3, 2, 2;" +
                        //"-fx-background-radius: 0;" +
                        //"-fx-padding: 3 5 3 5;" +   /*Play with this value to center the text depending on cell height??*/
                        //"-fx-padding: 0 0 0 0;" +
                        //"-fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);" +
                        //"-fx-accent: derive(-fx-control-inner-background, -40%);" +
                        "-fx-cell-hover-color: derive(-fx-control-inner-background, -20%);"
                        + "-fx-cursor: text;"
                        + "";

                // 
                textField.focusedProperty().addListener(new ChangeListener<Boolean>() {
                    public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                        TextField tf = (TextField) getGraphic();
                        String strStyleGotFocus = "-fx-background-color: purple, -fx-text-box-border, -fx-control-inner-background;"
                                + "-fx-background-insets: -0.4, 1, 2;"
                                + "-fx-background-radius: 3.4, 2, 2;";
                        String strStyleLostFocus = //"-fx-background-color: -fx-shadow-highlight-color, -fx-text-box-border, -fx-control-inner-background;" +
                                "-fx-background-color: transparent;"
                                + //"-fx-background-insets: 0, 1, 2;" +
                                "-fx-background-insets: 0;"
                                + //"-fx-background-radius: 3, 2, 2;" +
                                "-fx-background-radius: 0;"
                                + "-fx-padding: 3 5 3 5;"
                                + /**/ //"-fx-background-fill: green;" +   /**/
                                //"-fx-background-color: green;" +
                                "-fx-background-opacity: 0;"
                                + //"-fx-opacity: 0;" +
                                //"-fx-padding: 0 0 0 0;" +
                                "-fx-prompt-text-fill: derive(-fx-control-inner-background,-30%);"
                                + "-fx-cursor: text;"
                                + "";

                        if (newValue.booleanValue()) {
                            tf.setStyle(strStyleGotFocus);
                        } else {
                            tf.setStyle(strStyleLostFocus);
                        }

                    }
                });
                textField.setStyle(strCss);
                this.setGraphic(textField);
            }

            @Override
            protected void updateItem(String item, boolean empty) {
                super.updateItem(item, empty);
                if (!empty) {
                    // Show the Text Field
                    this.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);

                    // Retrieve the actual String Property that should be bound to the TextField
                    // If the TextField is currently bound to a different StringProperty
                    // Unbind the old property and rebind to the new one
                    ObservableValue<String> ov = getTableColumn().getCellObservableValue(getIndex());
                    SimpleStringProperty sp = (SimpleStringProperty) ov;

                    if (this.boundToCurrently == null) {
                        this.boundToCurrently = sp;
                        this.textField.textProperty().bindBidirectional(sp);
                    } else {
                        if (this.boundToCurrently != sp) {
                            this.textField.textProperty().unbindBidirectional(this.boundToCurrently);
                            this.boundToCurrently = sp;
                            this.textField.textProperty().bindBidirectional(this.boundToCurrently);
                        }
                    }
                    System.out.println("item=" + item + " ObservableValue<String>=" + ov.getValue());
                } else {
                    this.setContentDisplay(ContentDisplay.TEXT_ONLY);
                }
            }
        }
    }
    private final TableView<TableData> table = new TableView<TableData>();
    final ObservableList<TableData> ol =
            FXCollections.observableArrayList(
            new TableData("Wilma", "Flintstone", "555-123-4567", "WFlintstone@gmail.com"),
            new TableData("Fred", "Flintstone", "555-123-4567", "FFlintstone@gmail.com"),
            new TableData("Barney", "Flintstone", "555-123-4567", "Barney@gmail.com"),
            new TableData("Bugs", "Bunny", "555-123-4567", "BugsB@gmail.com"),
            new TableData("Yo", "Sam", "555-123-4567", "ysam@gmail.com"),
            new TableData("Tom", "", "555-123-4567", "tom@gmail.com"),
            new TableData("Jerry", "", "555-123-4567", "Jerry@gmail.com"),
            new TableData("Peter", "Pan", "555-123-4567", "Ppan@gmail.com"),
            new TableData("Daffy", "Duck", "555-123-4567", "dduck@gmail.com"),
            new TableData("Tazmanian", "Devil", "555-123-4567", "tdevil@gmail.com"),
            new TableData("Mickey", "Mouse", "555-123-4567", "mmouse@gmail.com"),
            new TableData("Mighty", "Mouse", "555-123-4567", "mimouse@gmail.com"));

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        Application.launch(args);
    }
    static int counter = 1;

    @Override
    public void start(Stage Stage) {
        Stage.setTitle("Editable Table");
        BorderPane borderPane = new BorderPane();
        Scene scene = new Scene(borderPane, 800, 600);

        // top of border pane
        Button b1 = new Button("Change value in table list");
        Button b2 = new Button("Add row");
        HBox hbox = new HBox(10);
        hbox.setStyle("-fx-background-color: #336699");
        hbox.setAlignment(Pos.BOTTOM_CENTER);
        HBox.setMargin(b2, new Insets(10, 0, 10, 0));
        HBox.setMargin(b1, new Insets(10, 0, 10, 0));
        hbox.getChildren().addAll(b1, b2);
        borderPane.setTop(hbox);
        BorderPane.setAlignment(hbox, Pos.CENTER);

        // Button Events
        b1.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                String curFirstName = ol.get(0).getFirstName();
                if (curFirstName.contentEquals("Jason")) {
                    ol.get(0).setFirstName("Paul");
                } else {
                    ol.get(0).setFirstName("Jason");
                }
            }
        });

        b2.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                Platform.runLater(new Runnable() {
                    @Override
                    public void run() {
                        int dataListSize = 0;
                        dataListSize = ol.size();
                        System.out.println("Table size = " + dataListSize);
                        ol.add(new TableData("firstName" + counter,
                                "lastName" + counter,
                                "phone" + counter,
                                "email" + counter++));
                        dataListSize = ol.size();
                        System.out.println("Table size = " + dataListSize);
                        table.getColumns().get(0).setVisible(false);
                        table.getColumns().get(0).setVisible(true);
                    }
                });
            }
        });

        table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
        table.setItems(ol);
        borderPane.setCenter(table);
        BorderPane.setAlignment(table, Pos.CENTER);
        BorderPane.setMargin(table, new Insets(25));

        // Add columns
        TableColumn<TableData, String> c1 = new TableColumn<TableData, String>("FirstName");
        c1.setCellValueFactory(new PropertyValueFactory<TableData, String>("firstName"));
        c1.setCellFactory(new TextFieldCellFactory());

        TableColumn<TableData, String> c2 = new TableColumn<TableData, String>("LastName");
        c2.setCellValueFactory(new PropertyValueFactory<TableData, String>("lastName"));
        c2.setCellFactory(new TextFieldCellFactory());

        TableColumn<TableData, String> c3 = new TableColumn<TableData, String>("Phone");
        c3.setCellValueFactory(new PropertyValueFactory<TableData, String>("phone"));
        c3.setCellFactory(new TextFieldCellFactory());

        TableColumn<TableData, String> c4 = new TableColumn<TableData, String>("Email");
        c4.setCellValueFactory(new PropertyValueFactory<TableData, String>("email"));
        c4.setCellFactory(new TextFieldCellFactory());

        table.getColumns().addAll(c1, c2, c3, c4);

        scene.getStylesheets().add(TableViewEditingWithBinding.class.getResource("styles.css").toExternalForm());
        Stage.setScene(scene);
        Stage.show();
    }
}

I tried adding the following code

table.getColumns().get(0).setVisible(false);
table.getColumns().get(0).setVisible(true);
in the handler for the addRow button after the row was added, but that didn't help any.

I also tried clearing the backing observable list and resetting the value to the list + the new row after each row was added; that did not solve the issue either.
Any help would be most appreciated.

Community
  • 1
  • 1
Lambda II
  • 173
  • 1
  • 7

1 Answers1

0

I had the same problem. After deleting one row some other rows are blanked out. If I scrolled the blank rows changed.

My solution was to move the line

setContentDisplay(ContentDisplay.GRAPHIC_ONLY);

to the constructor of the cell and to remove the line

setContentDisplay(ContentDisplay.TEXT_ONLY);

completely.

The API doc of Cell.updateItem states

empty - whether or not this cell represents data from the list. If it is empty, then it does not represent any domain data, but is a cell being used to render an "empty" row.

For me this looks like a bug in JavaFX because all my rows are backed with domain data but still some of them (changing when scrolling) receive an updateItem() with empty=true.

Martin Prikryl
  • 188,800
  • 56
  • 490
  • 992
Golan
  • 11
  • 2