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