Панель пользовательских основных параметров JavaFX

#tableview #javafx-8 #master-detail #setfocus

#tableview #javafx-8 #мастер-деталь #setfocus

Вопрос:

Я создал пользовательскую панель Master-Detail для своего проекта, где я использую разделенную панель, в каждой из которых у меня есть две панели привязки. В одном из них есть TableView, заполненный пользователями (ObservableList). В каждой строке (User) я внедрил список table.getSelectionModel().selectedItemProperty().addListener(listElementChangeListener()); изменений, когда выбрана строка, я передаю UserObject для моей панели DetailPane и визуализирую пользовательские данные в текстовых полях как detail. Я реализовал элементы управления, чтобы понять, находится ли пользователь в процессе детальной модификации, и если да, я хотел бы предотвратить изменение строки в моем TableView. Я попытался удалить список изменений из TableView при изменении пользователя, но он не работает хорошо. Я думаю о таком решении, как установка фокуса и удерживание его в строке до тех пор, пока я не отменю или не сохраню измененное пользователем.

Есть ли какие-нибудь хорошие решения?

Спасибо за вашу помощь.

Комментарии:

1. Как насчет отключения таблицы, чтобы предотвратить любое изменение выбора при изменении данных? После, например, сброса или сохранения (например, с помощью кнопок) изменений вы можете снова включить таблицу. С моей точки зрения, это лучший подход для вашей цели и тот, который обычно используется в таком сценарии.

2. предложение @SSchuette является хорошим. Второй вариант, который дает несколько иной пользовательский интерфейс, заключается в использовании двунаправленной привязки между вашими текстовыми полями и свойствами вашего пользовательского объекта, чтобы изменения немедленно отражались в объекте (и в таблице). Затем вы можете добавить кнопку «Отмена» в подробном представлении, которая вернет исходные значения. Любое из этих решений довольно легко реализовать.

3. @James_D я полностью с тобой! К сожалению, мой опыт в контексте JavaFX заключается в том, что большинство людей переходят / переходят с Swing на JavaFX, поэтому такие концепции, как, например, MVVM (Я из области .NET / WPF ;-)) неизвестны и / или недостаточно понятны.

4. @SSchuette Интересно. Я пришел в JavaFX из Swing, но я изучил Swing по сути как реализацию MVC (оригинальная интерпретация MVC для толстого клиента, а не ее искажение, которое находится поверх цикла HTTP-запроса-ответа). MVVM, MVP и т. Д. Кажутся мне естественными эволюциями и настройками MVC, поэтому все они пришли ко мне совершенно естественно.

5. Хорошо, но, увидев несколько ваших ответов, вы являетесь человеком, интересующимся разработкой программного обеспечения / небольшими, элегантными и простыми решениями («любой может создавать сложные решения»), поэтому вам, вероятно, довольно легко использовать преимущества MVVM как «эволюцию MVC». Я испытываю, что мои коллеги по Swing не видят преимуществ, например, привязки элементов пользовательского интерфейса к значениям viewmodel. Они устанавливают значения напрямую:-/.

Ответ №1:

Я бы, вероятно, подошел к этому немного по-другому. Я бы привязал элементы управления в «подробном представлении» двунаправленно к свойствам User объекта. Таким образом, они будут обновляться в объекте (и таблице) по мере их редактирования пользователем. Если хотите, вы также можете указать кнопку «Отмена», чтобы вернуться к предыдущим значениям.

Вот полное решение, использующее этот подход:

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>
  

И, наконец, 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);
    }
}
  

Если вы предпочитаете описанный вами пользовательский интерфейс, вы можете (как описывает @SSchuette в комментариях) просто привязать свойство отключения таблицы к свойству изменения. Это не позволит пользователю изменять выделение во время редактирования данных (т. Е. Не согласуется с данными в таблице). Для этого вам просто нужно свойство modifying в модели:

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

Единственное, где нужно немного поработать, это убедиться, что для свойства modify установлено значение true каждый раз, когда данные не синхронизированы (хотя, похоже, вы это уже сделали):

 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())
                        amp;amp; model.getCurrentUser().getLastName().equals(lastNameField.getText())
                        amp;amp; 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 amp;amp; model.getCurrentUser() != null) {
            model.getCurrentUser().setFirstName(firstNameField.getText());
            model.getCurrentUser().setLastName(lastNameField.getText());
            model.getCurrentUser().setAdmin(adminCheckBox.isSelected());

        }
    }


}
  

Для этого решения требуется дополнительная кнопка для принудительного обновления данных (и таблицы):

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

Комментарии:

1. На данный момент я реализовал решение @SSchuette, поскольку это было действительно простое и «простое» решение для реализации. James_D спасибо за ваш длинный и подробный ответ, сегодня я попробую ваше решение! Наконец-то я понял, как использовать привязку JavaFX! (я действительно новичок в JavaFX и пытаюсь понять, какой лучший способ ускорить процесс!) Спасибо, ребята!

2. Добро пожаловать! Это правильный подход (если у вас достаточно времени для «экспериментов») для реализации различных способов увидеть и понять различия / преимущества. Когда вы начинаете с «концепции привязки» (например, MVVM [ en.wikipedia.org/wiki/Model–view–viewmodel ] ) поначалу это кажется бесполезными накладными расходами — но однажды понято…