1

DEMO: https://github.com/CorellianAle/so_tableviewexpandercolumn

GIST: https://gist.github.com/CorellianAle/0f6844edc759a766400b98dda690dfbe

I have a JavaFX application that uses TableView to show list of employees that is stored in the database. I also use TableRowExpanderColumn from ControlsFX to provide addition Ui to modify emails and numbers cause I don't know how to display multiple values in a single column/cell.

enter image description here

Problem: When I refresh items bound to the tableview, it refreshed itself and closes all expanded rows. I need a way to store expanded rows to reopen them after the update.

If I want to add multiple emails or numbers, I need to click expand the row after each addition.

My thoughts: I've found that it is impossible to access a collection of rows because TableView uses VirtualFlow which only creates rows for the visible items.

I could add a boolean flag variable (isExpanded) to EmployeeEx but it feels like a dirty hack. I understand that it happens because every time I update employees, I store a completely new instance of collection and it causes TableView to generate a completely new row/cell collection, losing it's settings in the process.

I think that in order to achieve my goal I need to keep my collection static (same memory address) - update each item of the current collection (ListProperty<EmployeeEx> employees) instead of creating new collection on each update.

I need to implement UPDATE IF EXISTS, INSERT IF NEW, DELETE IF MISSING logic to update method.

I am not sure if it is a correct approach (is it retarded?). How to implement it if it is a valid solution?

class Position
{
    protected IntegerProperty id = new SimpleIntegerProperty();
    protected StringProperty name = new SimpleStringProperty();
    protected StringProperty comment = new SimpleStringProperty();
    //...
}

class EmployeeEmail
{
    protected IntegerProperty id = new SimpleIntegerProperty();
    protected StringProperty email= new SimpleStringProperty();
    protected StringProperty comment = new SimpleStringProperty();
    //...
}

class EmployeeNumber
{
    protected IntegerProperty id = new SimpleIntegerProperty();
    protected StringProperty number= new SimpleStringProperty();
    protected StringProperty comment = new SimpleStringProperty();
    //...
}

class Employee
{
    protected StringProperty firstName = new SimpleStringProperty("");
    protected StringProperty middleName = new SimpleStringProperty("");
    protected StringProperty lastName = new SimpleStringProperty("");

    protected BooleanProperty isOffice = new SimpleBooleanProperty();
    protected IntegerProperty position = new SimpleIntegerProperty();
    //...
}

/**
 * Complete object.
 */
class EmployeeEx
{
    protected StringProperty firstName = new SimpleStringProperty();
    protected StringProperty middleName = new SimpleStringProperty();
    protected StringProperty lastName = new SimpleStringProperty();

    protected BooleanProperty isOffice = new SimpleBooleanProperty();
    protected ObjectProperty<Position> position = new SimpleObjectProperty<>();

    protected ListProperty<EmployeeNumber> numbers = new SimpleListProperty<>();
    protected ListProperty<EmployeeEmail> emails = new SimpleListProperty<>();
    //...
}

/**
 * fxml controller class
 */
class Controller
{
    ListProperty<EmployeeEx> employees; //polulates tableview
    ListProperty<Position> positions; //populates comboboxes

    IEmployeesDAO employeesDAO; //these DAOs get ResultSets from JDBC and convert them to ObservableLists.
    IPositionsDAO positionsDAO;
    IEmployeeNumberDAO employeeNumberDAO;
    IEmployeeEmailDAO employeeEmailDAO;'

    /**
     * Gets positions from database.
     */
    void updatePositions()
    {
        //get data
        ObservableList<Position> positions = positionsDAO.getAll();

        //update table
        this.positions.setValue(positions);
    }

    /**
     * Gets employees from database. Uses data from multiple table and constructs a list of complete objects.
     */
    void updateEmployees()
    {
        //get data
        ObservableList<Employee> employees = employeesDAO.getAll();
        ObservableList<EmployeeEx> employeeExs = FXCollections.observableArrayList();

        ObservableList<EmployeeNumber> numbers = employeeNumberDAO.getAll();
        ObservableList<EmployeeEmail> emails = employeeEmailDAO.getAll();

        //contruct complete objects
        for (Employee employee : employees)
        {
            EmployeeEx employeeEx = ExConverters.toEmployeeEx(employee, positions, numbers, emails);
            employeeExs.add(employeeEx);
        }

        //update table
        this.employees.setValue(employeeExs);
    }

    /**
     *
     */
    void generateTable()
    {
        //emails and numbers ui
        expanderColumn = new TableRowExpanderColumn<EmployeeEx>(this::createEditor);
        expanderColumn.setMinWidth(15);
        tableView.getColumns().add(expanderColumn);

        //first name
        String tableColumn_title_firstName = Utility.getResourceString("employees", "tableColumn_firstName", CM.getLocale());
        TableColumn<EmployeeEx, String> tableColumn_firstName = new TableColumn<>(tableColumn_title_firstName);
        tableColumn_firstName.setMinWidth(150);
        tableColumn_firstName.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
        tableColumn_firstName.setCellFactory(TextFieldTableCell.forTableColumn());
        tableColumn_firstName.setOnEditCommit((TableColumn.CellEditEvent<EmployeeEx, String> event) ->
        {
            TablePosition<EmployeeEx, String> position = event.getTablePosition();
            String str = event.getNewValue();
            EmployeeEx employeeEx = event.getTableView().getItems().get(position.getRow());
            employeeEx.setFirstName(str);
        });
        tableView.getColumns().add(tableColumn_firstName);

        //middle name
        String tableColumn_title_middleName = Utility.getResourceString("employees", "tableColumn_middleName", CM.getLocale());
        TableColumn<EmployeeEx, String> tableColumn_middleName = new TableColumn<>(tableColumn_title_middleName);
        tableColumn_middleName.setMinWidth(150);
        tableColumn_middleName.setCellValueFactory(cellData -> cellData.getValue().middleNameProperty());
        tableColumn_middleName.setCellFactory(TextFieldTableCell.forTableColumn());
        tableColumn_middleName.setOnEditCommit((TableColumn.CellEditEvent<EmployeeEx, String> event) ->
        {
            TablePosition<EmployeeEx, String> position = event.getTablePosition();
            String firstName = event.getNewValue();
            EmployeeEx employeeEx = event.getTableView().getItems().get(position.getRow());
            employeeEx.setFirstName(firstName);
        });
        tableView.getColumns().add(tableColumn_middleName);

        //last name
        String tableColumn_title_lastName = Utility.getResourceString("employees", "tableColumn_lastName", CM.getLocale());
        TableColumn<EmployeeEx, String> tableColumn_lastName = new TableColumn<>(tableColumn_title_lastName);
        tableColumn_lastName.setMinWidth(150);
        tableColumn_lastName.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
        tableColumn_lastName.setCellFactory(TextFieldTableCell.forTableColumn());
        tableColumn_lastName.setOnEditCommit((TableColumn.CellEditEvent<EmployeeEx, String> event) ->
        {
            TablePosition<EmployeeEx, String> position = event.getTablePosition();
            String str = event.getNewValue();
            EmployeeEx employeeEx = event.getTableView().getItems().get(position.getRow());
            employeeEx.setLastName(str);
        });
        tableView.getColumns().add(tableColumn_lastName);

        //is office
        String tableColumn_title_isOffice = Utility.getResourceString("employees", "tableColumn_isOffice", CM.getLocale());
        TableColumn<EmployeeEx, Boolean> tableColumn_isOffice = new TableColumn<>(tableColumn_title_isOffice);
        tableColumn_isOffice.setMinWidth(150);
        tableColumn_isOffice.setCellValueFactory(param ->
        {
            EmployeeEx employeeEx = param.getValue();
            return employeeEx.isOfficeProperty();
        });
        tableColumn_isOffice.setCellFactory(p ->
        {
            CheckBoxTableCell<EmployeeEx, Boolean> cell = new CheckBoxTableCell<EmployeeEx, Boolean>();
            cell.setPadding(new Insets(3, 0, 0, 0));
            cell.setAlignment(Pos.TOP_CENTER);
            return cell;
        });

        tableView.getColumns().add(tableColumn_isOffice);

        //position
        String tableColumn_title_company = Utility.getResourceString("employees", "tableColumn_position", CM.getLocale());
        TableColumn<EmployeeEx, Position> tableColumn_position = new TableColumn<>(tableColumn_title_company);
        tableColumn_position.setMinWidth(150);
        tableColumn_position.setCellValueFactory(param ->
        {
            EmployeeEx userEx = param.getValue();
            return userEx.positionProperty();
        });
        tableColumn_position.setCellFactory(ComboBoxTableCell.forTableColumn(positions));
        tableColumn_position.setOnEditCommit((TableColumn.CellEditEvent<EmployeeEx, Position> event) -> {
            TablePosition<EmployeeEx, Position> pos = event.getTablePosition();
            Position posiiton = event.getNewValue();
            EmployeeEx employeeEx = event.getTableView().getItems().get(pos.getRow());
            employeeEx.setPosition(posiiton);
        });

        tableView.getColumns().add(tableColumn_position);

        //comment
        String tableColumn_title_comment = Utility.getResourceString("employees", "tableColumn_comment", CM.getLocale());
        TableColumn<EmployeeEx, String> tableColumn_comment = new TableColumn<>(tableColumn_title_comment);
        tableColumn_comment.setMinWidth(150);
        tableColumn_comment.setCellValueFactory(cellData -> cellData.getValue().commentProperty());
        tableColumn_comment.setCellFactory(TextFieldTableCell.forTableColumn());
        tableColumn_comment.setOnEditCommit((TableColumn.CellEditEvent<EmployeeEx, String> event) ->
        {
            TablePosition<EmployeeEx, String> position = event.getTablePosition();
            String str = event.getNewValue();
            EmployeeEx employeeEx = event.getTableView().getItems().get(position.getRow());
            employeeEx.setLastName(str);
        });
        tableView.getColumns().add(tableColumn_comment);

        //this is where binding happens
        tableView.setItems(employees);
    }
}

/**
 * Converts {@link Employee} to {@link EmployeeEx}.
 * It is not optimal sure =(
 */
public static EmployeeEx toEmployeeEx(Employee employee, 
                                      ObservableList<Position> positions, 
                                      ObservableList<EmployeeNumber> numbers, 
                                      ObservableList<EmployeeEmail> emails)
{
    EmployeeEx employeeEx = new EmployeeEx();

    employeeEx.setId(employee.getId());
    employeeEx.setFirstName(employee.getFirstName());
    employeeEx.setMiddleName(employee.getMiddleName());
    employeeEx.setLastName(employee.getLastName());
    employeeEx.setIsOffice(employee.isIsOffice());

    //
    Optional<Position> position = positions.stream().filter(c -> c.getId() == employee.getPosition()).findFirst();

    if (position.isPresent())
    {
        employeeEx.setPosition(position.get());
    }

    //
    numbers.stream().filter(n -> n.getEmployee() == employee.getId()).forEach(number -> {
        employeeEx.getNumbers().add(number);
    });

    //
    emails.stream().filter(e -> e.getEmployee() == employee.getId()).forEach(email -> {
        employeeEx.getEmails().add(email);
    });

    employeeEx.setDateTimeLastModified(employee.getDateTimeLastModified());

    return employeeEx;
}

/**
 * From {@link IEmployeesDAO}.
 * Connects to the database and pulls a list of employees.
 */
@Override
public ObservableList<Employee> getAll() throws Exception
{
    Connection connection = null;
    PreparedStatement statement = null;
    ResultSet resultSet = null;

    try
    {
        connection = Database.getInstance().getConnection();
        statement = connection.prepareStatement("SELECT * FROM " + TABLE + " ORDER BY lastname");

        if (statement.execute())
        {
            resultSet = statement.getResultSet();
            ObservableList<Employee> employees = FXCollections.observableArrayList();

            while (resultSet.next())
            {
                Employee employee = new Employee();
                employee.setId(resultSet.getInt("id"));
                employee.setName(resultSet.getString("name"));
                employee.setComment(resultSet.getString("comment"));
                employee.setFirstName(resultSet.getString("firstname"));
                employee.setMiddleName(resultSet.getString("middlename"));
                employee.setLastName(resultSet.getString("lastname"));
                employee.setIsOffice(resultSet.getBoolean("isoffice"));
                employee.setPosition(resultSet.getInt("positionref"));

                Timestamp created_timestamp = resultSet.getTimestamp("lastmodified");
                DateTime created_datetime = DateTime.fromLocalDateTime(created_timestamp.toLocalDateTime());
                employee.setDateTimeLastModified(created_datetime);

                employees.add(employee);
            }

            return employees;
        }

        return null;
    }
    catch (SQLException ex)
    {
        Log.error("Unable to get all employees. " + ex.getMessage());
        throw ex;
    }
    finally
    {
        Database.closeQuietly(resultSet);
        Database.closeQuietly(statement);

    }
}
CorellianAle
  • 645
  • 8
  • 16
  • [mcve] please .. – kleopatra Jun 27 '19 at 02:51
  • @kleopatra I've created a demo as requested. https://gist.github.com/CorellianAle/0f6844edc759a766400b98dda690dfbe – CorellianAle Jun 27 '19 at 09:59
  • no links please (you did read the help page, didn't you :) Strip it down to a handful of lines such that you can show it here - mind the _M_ (no threading code, less properties in your data class, just one data class ... ) – kleopatra Jun 27 '19 at 10:30
  • @kleopatra how can I strip down code this even more if in order to reproduce the issue a working JavaFx application is needed? I need a working `TableView` and a working model (stub), with properties, etc. – CorellianAle Jun 27 '19 at 11:59
  • @kleopatra I've also provided a link to [GitHub](https://github.com/CorellianAle/so_tableviewexpandercolumn) - it is a bare minimum. – CorellianAle Jun 27 '19 at 12:20

0 Answers0