TableView with Cell Editing


Question

I need a TableView with editing.

So I have tried a bit and put together the following code that seemed to work fine for me - until I started testing with a few more data rows.

Problem: TableView does not initialise all persons/rows at once, but only those that come into view; and moving rows out and into view again re-initialises their content.

Can a TableView be forced to load and keep all available data at once? I will not have more than 200-300 rows so there is no memory issue. Or is there a better way to do this?

I have tried the standard Oracle EditingCell example and modified that a bit, but I cannot imagine handing out an application where the user needs to quadruple-click a field before he can enter text (and quadruple-click again for the next field)

Here is the code in one single class:

package config_V1;

import java.util.Vector;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
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.control.Button;
import javafx.scene.control.CheckBox;
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.cell.PropertyValueFactory;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.scene.Scene;

/**
 * 
 * Standard person table in a self running application <br>
 * - some configurable textField derivatives within<br>
 * - Person class within
 * 
 * - unfortunately not working as expected: 
 *      first row gets initialised always twice
 *      fields not in view are not initialised
 *      the same row gets initialised again whenever coming in view during scrolling
 *      issues with sorting (after sorting changing age updates the wrong row)
 *       this can be circumvented with setSortable(false) on each column;
 *      
 */
public class TestCustomCellFactories extends Application  {

    private Vector<Person> persons;
    private TableView <Object>tableView = new TableView<Object>();
    private ObservableList<Object> tableData = FXCollections.observableArrayList(); 
    private static Vector<TextField> textFields=new Vector<TextField>();
    private CheckBox focusFirstNameCB;
    Button addButton = new Button("Add Rows");
    Button removeButton= new Button("Remove Rows");
    Button defaultButton= new Button("set defaults");


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

    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void start(Stage stage) {



        //***********   CREATE COLUMNS *****************************************
        tableView.setEditable(true);

        TableColumn firstNameCol = new TableColumn<Object,String>("First Name");
        firstNameCol.setCellFactory(makeEditableStringCellFactory(0,0));
        firstNameCol.setCellValueFactory(
                new PropertyValueFactory<Person, String>("firstName"));

        TableColumn lastNameCol = new TableColumn<Object,String>("Last Name");
        lastNameCol.setCellFactory(makeEditableStringCellFactory(3,1));
        lastNameCol.setCellValueFactory(
                new PropertyValueFactory<Person, String>("lastName"));

        TableColumn emailCol = new TableColumn<Object,String>("Email");
        emailCol.setCellFactory(makeEditableStringCellFactory(1,2));
        emailCol.setCellValueFactory(
                new PropertyValueFactory<Person, String>("email"));
        emailCol.setMinWidth(200);


        TableColumn age = new TableColumn<Object,String>("age");
        age.setCellFactory(makeEditableStringCellFactory(2,3));
        age.setCellValueFactory(
                new PropertyValueFactory<Person, String>("age"));

        TableColumn ageMinus5 = new TableColumn<Object,String>("real age");
        ageMinus5.setCellFactory(makeEditableStringCellFactory(2,4));
        ageMinus5.setCellValueFactory(
                new PropertyValueFactory<Person, String>("realAge"));





        //************* ADD EVENT HANDLER TO BUTTONS ****************************

        focusFirstNameCB= new CheckBox("focus column 1");
        focusFirstNameCB.setSelected(true);

        addButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                //prt(Platform.isFxApplicationThread());
                if(persons==null){
                    persons = new Vector<Person>();
                    persons.add(new Person("Jacob", "Smith", "jacob.smith@example.com"));
                    persons.add(new Person("Isabella", "Johnson", "isabella.johnson@example.com"));
                    persons.add(new Person("Ethan", "Williams", "ethan.williams@example.com"));
                    persons.add(new Person("Jacob2", "Smith2", "jacob.smith@example.com"));
                    persons.add(new Person("Isabella2", "Johnson2", "isabella.johnson@example.com"));
                    persons.add(new Person("Ethan2", "Williams2", "ethan.williams@example.com"));
                    persons.add(new Person("Jacob3", "Smith3", "jacob.smith@example.com"));
                    persons.add(new Person("Isabella3", "Johnson3", "isabella.johnson@example.com"));
                    persons.add(new Person("Ethan3", "Williams3", "ethan.williams@example.com"));
                    persons.add(new Person("Jacob4", "Smith4", "jacob.smith@example.com"));
                    persons.add(new Person("Isabella4", "Johnson4", "isabella.johnson@example.com"));
                    persons.add(new Person("Ethan4", "Williams4", "ethan.williams@example.com"));
                    persons.add(new Person("Jacob5", "Smith5", "jacob.smith@example.com"));
                    persons.add(new Person("Isabella5", "Johnson5", "isabella.johnson@example.com"));
                    persons.add(new Person("Ethan5", "Williams5", "ethan.williams@example.com"));
                    persons.add(new Person("Jacob6", "Smith6", "jacob.smith@example.com"));
                    persons.add(new Person("Isabella6", "Johnson6", "isabella.johnson@example.com"));
                    persons.add(new Person("Ethan6", "Williams6", "ethan.williams@example.com"));

                }
                tableView.setItems(tableData);
                for(Person p:persons){
                    if(!p.isOnTable) tableData.add(p);
                    p.isOnTable=true;
                }

                updateFocus();//does not work here
            }
        });
        removeButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                for(Person person: persons){
                    person.updatePerson();
                    prt(person.getFirstName()+";"+person.getLastName()+";" + person.getEmail() + ";" + person.getAge() + ";" + person.getRealAge());
                    tableData.remove(person);   //or tableView.getItems().remove(p);
                    person.isOnTable=false;
                }
            }
        });

        defaultButton.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {

                for(Person person: persons){
                    prt("default for " + person.getEmail() );
                    person.setFirstName("John");
                    person.setLastName("Doe");
                    //  person.setEmail("John.Doe@xy.com");
                }
            }
        });

        focusFirstNameCB.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                for(TextField tf:textFields){
                    if(tf.getId().equals("0"))tf.setFocusTraversable(focusFirstNameCB.isSelected());
                }
            }
        });




        //***********   ASSEMBLE GUI *****************************************  

        Scene scene = new Scene(new StackPane());
        stage.setTitle("Table View Sample");
        stage.setWidth(550);
        stage.setHeight(500);
        tableView.getColumns().addAll(firstNameCol, lastNameCol, emailCol, age, ageMinus5);
        StackPane sp= new StackPane();
        sp.getChildren().add(tableView);

        final VBox vbox = new VBox();
        vbox.setSpacing(5);
        vbox.setPadding(new Insets(5, 5, 5, 5));
        vbox.getChildren().addAll( addButton,removeButton,defaultButton,focusFirstNameCB,sp);
        VBox.setVgrow(sp, Priority.ALWAYS);

        ((StackPane) scene.getRoot()).getChildren().add(vbox);
        stage.setScene(scene);
        stage.show();
    }

    private void updateFocus(){
        prt(textFields.size());
        for(TextField tf:textFields){
            switch(Integer.parseInt(tf.getId())){
            case 0: tf.setFocusTraversable(focusFirstNameCB.isSelected());
            }
        }
    }




    //***********   TABLE CELL CREATION  *****************************************  

    public static Callback<TableColumn<Object, String>, TableCell<Object, String>> makeEditableStringCellFactory(final int type, final int id){

        return new Callback<TableColumn<Object,String>, TableCell<Object,String>>() {
            @Override
            public TableCell<Object, String> call(final TableColumn<Object, String> tableColumn) {
                TableCell <Object, String> cell = new TableCell<Object, String>(){
                    private boolean ini=true;
                    private TextField tf=null;
                    private Person person;

                    @Override
                    public void updateItem(String s, boolean empty)
                    {
                        super.updateItem(s, empty);
                        if (empty) {
                            if(tf!=null) setText(getString());//or once removed the textFields cannot be added again
                            else 
                            {
                                setText(null);
                                setGraphic(null);
                            }
                        }
                        else 
                            if(ini){ 
                                ini=false;
                                tableColumn.getTableView().getSelectionModel().select(getIndex());
                                person = (Person)tableColumn.getTableView().getSelectionModel().getSelectedItem();

                                switch (type){
                                case 0: {
                                    tf= new TextField();break;
                                }
                                case 1: {
                                    tf= new RightAlignedUnEditableTextField();break;
                                }
                                case 2: {
                                    tf= new IntegerTextField();break;
                                }
                                case 3: {
                                    tf= new UpperCaseTextField();
                                }
                                }
                                tf.setId(""+id);
                                person.registerTF(tf);
                                tf.setEditable(true);
                                setGraphic(tf);
                                setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
                                tf.setText(s);
                                this.textProperty().bindBidirectional(tf.textProperty())  ;

                                textFields.add(tf);

                            }else
                                if(tf!=null){
                                    setText(getString());
                                }
                    }
                    private String getString() {
                        return getItem() == null ? "" : getItem().toString();
                    }
                };

                return cell;
            }
        };
    }







    //************************* PERSON CLASS *****************************************************

    public static class Person {
        private final SimpleStringProperty firstName;
        private final SimpleStringProperty lastName;
        private final SimpleStringProperty email;
        private final SimpleStringProperty age=new SimpleStringProperty();
        private final SimpleStringProperty realAge=new SimpleStringProperty();
        private boolean isOnTable = false;
        private Vector<TextField> vtf = new Vector<TextField>();

        private Person(String fName, String lName, String email) {
            this.firstName = new SimpleStringProperty(fName);
            this.lastName = new SimpleStringProperty(lName);
            this.email = new SimpleStringProperty(email);
        }

        public void setFirstName(final String s) {
            firstName.set("");
            firstName.set(s);
        }
        public void setLastName(final String s) {
            lastName.set("");
            lastName.set(s);
        }
        public void setEmail(final String s) {
            email.set("");
            email.set(s);
        }
        public void setAge(final String s) {
            age.set("");
            age.set(s);
        }
        public void setRealAge(final String s) {
            realAge.set("");
            realAge.set(s);
        }

        public String getLastName() {
            return lastName.get();
        }
        public String getFirstName() {
            return firstName.get();
        }
        public String getEmail() {
            return email.get();
        }
        public String getAge() {
            return age.get();
        }
        public String getRealAge() {
            return realAge.get();
        }


        public SimpleStringProperty lastNameProperty(){
            return lastName;
        }
        public SimpleStringProperty firstNameProperty(){
            return firstName;
        }
        public SimpleStringProperty emailProperty(){
            return email;
        }
        public SimpleStringProperty ageProperty(){
            return age;
        }
        public SimpleStringProperty realAgeProperty(){
            return realAge;
        }


        public void registerTF(TextField tf){
            prt("registered a TextField for "+ this.getFirstName());
            vtf.add(tf);
            int id=Integer.parseInt(tf.getId());

            if(id==3){
                //
                tf.textProperty().addListener(new ChangeListener<String>() {
                    @Override
                    public void changed(ObservableValue<? extends String> observable,
                            String oldValue, String newValue) {
                        if(newValue !=null && newValue.length()>1 && getFirstName().startsWith("Isabella")) {
                            setRealAge((Integer.parseInt(newValue)+5)+"");
                        }else setRealAge(newValue);

                    }
                });
            }
        }

        public void updatePerson(){  //starts with id 0!!!
            for(TextField tf:vtf){
                switch(Integer.parseInt(tf.getId())){
                case 0: setFirstName(tf.getText());
                case 1: setLastName(tf.getText());
                case 2: setEmail(tf.getText());
                case 3: setAge(tf.getText());
                case 4: setRealAge(tf.getText());
                }
            }
        }
    }


    //************************* TEXT FIELD CLASSES *****************************************************

    private static class IntegerTextField extends TextField {

        public IntegerTextField() {
            super();
            addEventFilter(KeyEvent.KEY_TYPED, new EventHandler<KeyEvent>() {
                @Override
                public void handle(KeyEvent event) {
                    if(!event.getCharacter().matches("[0123456789]")){
                        event.consume();
                    }
                }
            });
        }
    }

    private static class UpperCaseTextField extends TextField {

        public UpperCaseTextField() {
            super();
            focusedProperty().addListener(new ChangeListener<Boolean>() {
                @Override
                public void changed(ObservableValue<? extends Boolean> arg0, Boolean oldPropertyValue, Boolean newPropertyValue)
                {
                    if (newPropertyValue)    {//Textfield on focus
                    }   
                    else
                        if(oldPropertyValue)    {   //Textfield out focus
                            setText(getText().trim().toUpperCase());
                            if(getText().length()>10) setText( getText().substring(0, 10));
                        }
                }
            });
        }
    }


    private static class RightAlignedUnEditableTextField extends TextField {

        public RightAlignedUnEditableTextField() {
            super();
            this.setAlignment(Pos.CENTER_RIGHT);
            this.setEditable(false);
            this.setFocusTraversable(false);
        }
    }

    private static void prt(Object o) {System.out.println(o);}
}

enter image description here

1
1
2/10/2014 8:01:41 AM

After some more searching I think I´ll try out a VBox & GridPane combination. TableView documentation says "The TableView control is designed to VISUALIZE (not enter) an unlimited number of rows of data" and "If you want to lay your user interface out in a grid-like fashion, consider the GridPane layout."

0
2/10/2014 12:57:51 AM

Licensed under: CC-BY-SA with attribution
Not affiliated with: Stack Overflow
Icon