tableviewjavafx-8master-detailsetfocus

JavaFX custom MasterDetail pane


I have created a custom Master-Detail pane for my project, where i use a split pane, in each i have two Anchor Panes. In one there is a TableView filled with Users (ObservableList). On each row (User) I have implemented a ChangeListener like this:

table.getSelectionModel().selectedItemProperty().addListener(listElementChangeListener());

When the row is selected, I pass the UserObject for my DetailPane, and visualize User data in TextFields as detail. I have implemented controls, to understand if the User is under modification in Detail, and if so I would like to prevent a row change in my TableView.

I tried to remove the ChangeListener from the TableView when I modify the User, but it dosen't work well.

I'm thinking of a solution like setting the focus and holding it on the row until I cancel or save the User modified.

Is there any nice solutions?


Solution

  • I would probably approach this a little differently. I would bind the controls in the "detail view" bidirectionally to the properties in the User object. That way they will be updated in the object (and the table) as the user edits them. If you like, you can also provide a "cancel" button to revert to the previous values.

    Here's a complete solution that uses this approach:

    User.java:

    package usermasterdetail;
    
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.property.SimpleStringProperty;
    import javafx.beans.property.StringProperty;
    
    public class User {
    
        private final StringProperty firstName = new SimpleStringProperty();
        private final StringProperty lastName = new SimpleStringProperty();
        private final BooleanProperty admin = new SimpleBooleanProperty();
    
        public User(String firstName, String lastName, boolean admin) {
            setFirstName(firstName);
            setLastName(lastName);
            setAdmin(admin);
        }
    
        public final StringProperty firstNameProperty() {
            return this.firstName;
        }
    
    
        public final String getFirstName() {
            return this.firstNameProperty().get();
        }
    
    
        public final void setFirstName(final String firstName) {
            this.firstNameProperty().set(firstName);
        }
    
    
        public final StringProperty lastNameProperty() {
            return this.lastName;
        }
    
    
        public final String getLastName() {
            return this.lastNameProperty().get();
        }
    
    
        public final void setLastName(final String lastName) {
            this.lastNameProperty().set(lastName);
        }
    
    
        public final BooleanProperty adminProperty() {
            return this.admin;
        }
    
    
        public final boolean isAdmin() {
            return this.adminProperty().get();
        }
    
    
        public final void setAdmin(final boolean admin) {
            this.adminProperty().set(admin);
        }
    
    }
    

    DataModel.java:

    package usermasterdetail;
    
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    
    public class DataModel {
    
        private final ObservableList<User> userList = FXCollections.observableArrayList(
                new User("Jacob", "Smith", false),
                new User("Isabella", "Johnson", true),
                new User("Ethan", "Williams", false),
                new User("Emma", "Jones", true),
                new User("Michael", "Brown", true)
        );
    
        private final ObjectProperty<User> currentUser = new SimpleObjectProperty<>();
    
        public final ObjectProperty<User> currentUserProperty() {
            return this.currentUser;
        }
    
    
        public final User getCurrentUser() {
            return this.currentUserProperty().get();
        }
    
    
        public final void setCurrentUser(final User currentUser) {
            this.currentUserProperty().set(currentUser);
        }
    
    
        public ObservableList<User> getUserList() {
            return userList;
        }
    
    }
    

    TableController.java:

    package usermasterdetail;
    
    import javafx.fxml.FXML;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.CheckBoxTableCell;
    
    public class TableController {
    
        @FXML
        private TableView<User> table ;
        @FXML
        private TableColumn<User, String> firstNameColumn ;
        @FXML
        private TableColumn<User, String> lastNameColumn ;
        @FXML
        private TableColumn<User, Boolean> adminColumn ;
    
        private DataModel model ;
    
        public void initialize() {
            firstNameColumn.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
            lastNameColumn.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
            adminColumn.setCellValueFactory(cellData -> cellData.getValue().adminProperty());
            adminColumn.setCellFactory(CheckBoxTableCell.forTableColumn(adminColumn));
        }
    
        public void setDataModel(DataModel dataModel) {
            if (model !=  null) {
                model.currentUserProperty().unbind();
            }
            this.model = dataModel ;
            dataModel.currentUserProperty().bind(table.getSelectionModel().selectedItemProperty());
            table.setItems(model.getUserList());
        }
    }
    

    UserEditorController.java:

    package usermasterdetail;
    
    import javafx.beans.value.ChangeListener;
    import javafx.fxml.FXML;
    import javafx.scene.control.CheckBox;
    import javafx.scene.control.TextField;
    
    public class UserEditorController {
    
        @FXML
        private TextField firstNameField ;
        @FXML
        private TextField lastNameField ;
        @FXML
        private CheckBox adminCheckBox ;
    
        private String cachedFirstName ;
        private String cachedLastName ;
        private boolean cachedAdmin ;
    
        private ChangeListener<User> userListener = (obs, oldUser, newUser) -> {
            if (oldUser != null) {
                firstNameField.textProperty().unbindBidirectional(oldUser.firstNameProperty());
                lastNameField.textProperty().unbindBidirectional(oldUser.lastNameProperty());
                adminCheckBox.selectedProperty().unbindBidirectional(oldUser.adminProperty());
            }
    
            if (newUser == null) {
                firstNameField.clear();
                lastNameField.clear();
                adminCheckBox.setSelected(false);
            } else {
                firstNameField.textProperty().bindBidirectional(newUser.firstNameProperty());
                lastNameField.textProperty().bindBidirectional(newUser.lastNameProperty());
                adminCheckBox.selectedProperty().bindBidirectional(newUser.adminProperty());
    
                cachedFirstName = newUser.getFirstName();
                cachedLastName = newUser.getLastName();
                cachedAdmin = newUser.isAdmin();
            }
        };
    
    
        private DataModel model ;
    
        public void setDataModel(DataModel dataModel) {
            if (this.model != null) {
                this.model.currentUserProperty().removeListener(userListener);
            }
            this.model = dataModel ;
            this.model.currentUserProperty().addListener(userListener);
        }
    
        @FXML
        private void cancel() {
            firstNameField.setText(cachedFirstName);
            lastNameField.setText(cachedLastName);
            adminCheckBox.setSelected(cachedAdmin);
        }
    }
    

    Table.fxml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.layout.StackPane?>
    <?import javafx.scene.control.TableView?>
    <?import javafx.scene.control.TableColumn?>
    
    <StackPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="usermasterdetail.TableController">
        <TableView fx:id="table">
            <columns>
                <TableColumn fx:id="firstNameColumn" text="First Name"/>
                <TableColumn fx:id="lastNameColumn" text="Last Name"/>
                <TableColumn fx:id="adminColumn" text="Administrator"/>
            </columns>
        </TableView>
    </StackPane>
    

    UserEditor.fxml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.layout.GridPane?>
    <?import javafx.scene.layout.ColumnConstraints?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.control.TextField?>
    <?import javafx.scene.control.CheckBox?>
    <?import javafx.scene.control.Button?>
    <?import javafx.geometry.Insets?>
    
    <GridPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="usermasterdetail.UserEditorController"
            hgap="5" vgap="5" alignment="CENTER">
    
        <columnConstraints>
            <ColumnConstraints halignment="RIGHT" hgrow="NEVER"/>
            <ColumnConstraints halignment="LEFT" hgrow="SOMETIMES"/>
        </columnConstraints>
    
        <padding>
            <Insets top="5" left="5" bottom="5" right="5"/>
        </padding>
    
        <Label text="First Name:" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
        <Label text="Last Name:" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
        <Label text="Admin Priviliges:" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
    
        <TextField fx:id="firstNameField" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
        <TextField fx:id="lastNameField" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
        <CheckBox fx:id="adminCheckBox" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
        <Button text="Cancel" onAction="#cancel" GridPane.columnIndex="0" GridPane.rowIndex="3" GridPane.columnSpan="2"
            GridPane.halignment="CENTER"/>
    
    </GridPane>
    

    MainController.java:

    package usermasterdetail;
    
    import javafx.fxml.FXML;
    
    public class MainController {
        @FXML
        private TableController tableController ;
        @FXML
        private UserEditorController editorController ;
    
        private final DataModel model = new DataModel();
    
        public void initialize() {
            tableController.setDataModel(model);
            editorController.setDataModel(model);
        }
    }
    

    Main.fxml:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.SplitPane?>
    
    <SplitPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="usermasterdetail.MainController">
        <items>
            <fx:include fx:id="table" source="Table.fxml"/>
            <fx:include fx:id="editor" source="UserEditor.fxml"/>
        </items>
    </SplitPane>
    

    And finally Main.java:

    package usermasterdetail;
    
    import java.io.IOException;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class Main extends Application {
    
        @Override
        public void start(Stage primaryStage) throws IOException {
            primaryStage.setScene(new Scene(FXMLLoader.load(getClass().getResource("Main.fxml")), 800, 600));
            primaryStage.show();
        }
    
        public static void main(String[] args) {
            launch(args);
        }
    }
    

    If you prefer the user experience you described, you can (as @SSchuette describes in the comments), just bind the table's disable property to the modifying property. This will prevent the user from changing the selection while the data is being edited (i.e. is not consistent with the data in the table). For this you just need the modifying property in the model:

    package usermasterdetail;
    
    import javafx.beans.property.BooleanProperty;
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.SimpleBooleanProperty;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.collections.FXCollections;
    import javafx.collections.ObservableList;
    
    public class DataModel {
    
        private final ObservableList<User> userList = FXCollections.observableArrayList(
                new User("Jacob", "Smith", false),
                new User("Isabella", "Johnson", true),
                new User("Ethan", "Williams", false),
                new User("Emma", "Jones", true),
                new User("Michael", "Brown", true)
        );
    
        private final ObjectProperty<User> currentUser = new SimpleObjectProperty<>();
    
        private final BooleanProperty modifying = new SimpleBooleanProperty();
    
        public final ObjectProperty<User> currentUserProperty() {
            return this.currentUser;
        }
    
    
        public final usermasterdetail.User getCurrentUser() {
            return this.currentUserProperty().get();
        }
    
    
        public final void setCurrentUser(final usermasterdetail.User currentUser) {
            this.currentUserProperty().set(currentUser);
        }
    
    
        public ObservableList<User> getUserList() {
            return userList;
        }
    
    
        public final BooleanProperty modifyingProperty() {
            return this.modifying;
        }
    
    
    
        public final boolean isModifying() {
            return this.modifyingProperty().get();
        }
    
    
    
        public final void setModifying(final boolean modifying) {
            this.modifyingProperty().set(modifying);
        }
    
    
    }
    

    then in the table controller you can bind the disable property to it:

    package usermasterdetail;
    
    import javafx.fxml.FXML;
    import javafx.scene.control.TableColumn;
    import javafx.scene.control.TableView;
    import javafx.scene.control.cell.CheckBoxTableCell;
    
    public class TableController {
    
        @FXML
        private TableView<User> table ;
        @FXML
        private TableColumn<User, String> firstNameColumn ;
        @FXML
        private TableColumn<User, String> lastNameColumn ;
        @FXML
        private TableColumn<User, Boolean> adminColumn ;
    
        private DataModel model ;
    
        public void initialize() {
            firstNameColumn.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
            lastNameColumn.setCellValueFactory(cellData -> cellData.getValue().lastNameProperty());
            adminColumn.setCellValueFactory(cellData -> cellData.getValue().adminProperty());
            adminColumn.setCellFactory(CheckBoxTableCell.forTableColumn(adminColumn));
        }
    
        public void setDataModel(DataModel dataModel) {
            if (model !=  null) {
                model.currentUserProperty().unbind();
            }
            this.model = dataModel ;
            dataModel.currentUserProperty().bind(table.getSelectionModel().selectedItemProperty());
            table.setItems(model.getUserList());
            table.disableProperty().bind(model.modifyingProperty());
        }
    }
    

    The only place there is a bit of work to do is to make sure the modifying property is set to true any time the data are not in sync (though it sounds like you have already done this):

    package usermasterdetail;
    
    import javafx.beans.value.ChangeListener;
    import javafx.fxml.FXML;
    import javafx.scene.control.CheckBox;
    import javafx.scene.control.TextField;
    
    public class UserEditorController {
    
        @FXML
        private TextField firstNameField ;
        @FXML
        private TextField lastNameField ;
        @FXML
        private CheckBox adminCheckBox ;
    
        private DataModel model ;
    
        private ChangeListener<Object> modifyingListener = (obs, oldValue, newValue) -> {
            if (model != null) {
                if (model.getCurrentUser() == null) {
                    model.setModifying(false);
                } else {
                    model.setModifying(! (model.getCurrentUser().getFirstName().equals(firstNameField.getText())
                            && model.getCurrentUser().getLastName().equals(lastNameField.getText())
                            && model.getCurrentUser().isAdmin() == adminCheckBox.isSelected()));
                }
            }
    
        };
    
        private ChangeListener<User> userListener = (obs, oldUser, newUser) -> {
            if (oldUser != null) {
                oldUser.firstNameProperty().removeListener(modifyingListener);
                oldUser.lastNameProperty().removeListener(modifyingListener);
                oldUser.adminProperty().removeListener(modifyingListener);
            }
            if (newUser == null) {
                firstNameField.clear();
                lastNameField.clear();
                adminCheckBox.setSelected(false);
            } else {
                firstNameField.setText(newUser.getFirstName());
                lastNameField.setText(newUser.getLastName());
                adminCheckBox.setSelected(newUser.isAdmin());
    
                newUser.firstNameProperty().addListener(modifyingListener);
                newUser.lastNameProperty().addListener(modifyingListener);
                newUser.adminProperty().addListener(modifyingListener);
            }
        };
    
    
        public void setDataModel(DataModel dataModel) {
            if (this.model != null) {
                this.model.currentUserProperty().removeListener(userListener);
            }
            this.model = dataModel ;
            this.model.currentUserProperty().addListener(userListener);
        }
    
        public void initialize() {
            firstNameField.textProperty().addListener(modifyingListener);
            lastNameField.textProperty().addListener(modifyingListener);
            adminCheckBox.selectedProperty().addListener(modifyingListener);
        }
    
    
        @FXML
        private void cancel() {
    
            if (model != null) {
                firstNameField.setText(model.getCurrentUser().getFirstName());
                lastNameField.setText(model.getCurrentUser().getLastName());
                adminCheckBox.setSelected(model.getCurrentUser().isAdmin());
            }
        }
    
        @FXML
        private void update() {
            if (model != null && model.getCurrentUser() != null) {
                model.getCurrentUser().setFirstName(firstNameField.getText());
                model.getCurrentUser().setLastName(lastNameField.getText());
                model.getCurrentUser().setAdmin(adminCheckBox.isSelected());
    
            }
        }
    
    
    }
    

    This solution requires an additional button to force the update in the data (and table):

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.layout.GridPane?>
    <?import javafx.scene.layout.ColumnConstraints?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.control.TextField?>
    <?import javafx.scene.control.CheckBox?>
    <?import javafx.scene.control.Button?>
    <?import javafx.geometry.Insets?>
    <?import javafx.scene.layout.HBox?>
    
    <GridPane xmlns:fx="http://javafx.com/fxml/1" fx:controller="usermasterdetail.UserEditorController"
            hgap="5" vgap="5" alignment="CENTER">
    
        <columnConstraints>
            <ColumnConstraints halignment="RIGHT" hgrow="NEVER"/>
            <ColumnConstraints halignment="LEFT" hgrow="SOMETIMES"/>
        </columnConstraints>
    
        <padding>
            <Insets top="5" left="5" bottom="5" right="5"/>
        </padding>
    
        <Label text="First Name:" GridPane.columnIndex="0" GridPane.rowIndex="0"/>
        <Label text="Last Name:" GridPane.columnIndex="0" GridPane.rowIndex="1"/>
        <Label text="Admin Priviliges:" GridPane.columnIndex="0" GridPane.rowIndex="2"/>
    
        <TextField fx:id="firstNameField" GridPane.columnIndex="1" GridPane.rowIndex="0"/>
        <TextField fx:id="lastNameField" GridPane.columnIndex="1" GridPane.rowIndex="1"/>
        <CheckBox fx:id="adminCheckBox" GridPane.columnIndex="1" GridPane.rowIndex="2"/>
        <HBox spacing="5" alignment="CENTER"  GridPane.columnIndex="0" GridPane.rowIndex="3" GridPane.columnSpan="2">
            <Button text="Update" onAction="#update"/>
            <Button text="Cancel" onAction="#cancel"/>
        </HBox>
    
    </GridPane>