JavaFX custom MasterDetail pane

徘徊边缘 提交于 2019-12-06 13:37:29

问题


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 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 dosent 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?

Thanks for your help.


回答1:


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>


来源:https://stackoverflow.com/questions/40106017/javafx-custom-masterdetail-pane

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!