在JavaFx中应用MVC

kar*_*rim 26 java model-view-controller user-interface javafx

我是GUI世界/ OO设计模式的新手,我想在我的GUI应用程序中使用MVC模式,我已经阅读了一些关于MVC模式的教程,模型将包含数据,View将包含可视元素和控制器将在视图和模型之间建立联系.

我有一个包含ListView节点的View,ListView将使用Person Class(Model)填充名称.但我对一件事情有点困惑.

我想知道的是,如果从文件加载数据是由Controller或模型负责的?名称的ObservableList:它应该存储在Controller还是Model中?

Jam*_*s_D 68

这种模式有许多不同的变化.特别地,在胖客户端(例如桌面)应用程序的上下文中,Web应用程序的上下文中的"MVC"与"MVC"的解释稍有不同(因为Web应用程序必须位于请求 - 响应周期的顶部).这只是使用JavaFX在胖客户端应用程序的上下文中实现MVC的一种方法.

你的Person类不是真正的模型,除非你有一个非常简单的应用程序:这通常是我们所谓的域对象,模型将包含对它的引用以及其他数据.在一个狭窄的环境中,例如当你只是想到它时ListView,你可以将其Person视为你的数据模型(它模拟了每个元素中的数据ListView),但是在更广泛的应用程序环境中,有更多的数据和国家考虑.

如果您要显示ListView<Person>所需的数据,至少应该是ObservableList<Person>.您可能还需要一个属性,例如currentPerson,可能代表列表中的选定项.

如果您拥有的唯一视图是ListView,那么创建一个单独的类来存储它将是过度的,但任何真正的应用程序通常会以多个视图结束.此时,在模型中共享数据成为不同控制器相互通信的非常有用的方法.

所以,例如,你可能会有这样的事情:

public class DataModel {

    private final ObservableList<Person> personList = FXCollections.observableArrayList();

    private final ObjectProperty<Person> currentPerson = new SimpleObjectPropery<>(null);

    public ObjectProperty<Person> currentPersonProperty() {
        return currentPerson ;
    }

    public final Person getCurrentPerson() {
        return currentPerson().get();
    }

    public final void setCurrentPerson(Person person) {
        currentPerson().set(person);
    }

    public ObservableList<Person> getPersonList() {
        return personList ;
    }
}
Run Code Online (Sandbox Code Playgroud)

现在您可能有一个ListView显示控制器,如下所示:

public class ListController {

    @FXML
    private ListView<Person> listView ;

    private DataModel model ;

    public void initModel(DataModel model) {
        // ensure model is only set once:
        if (this.model != null) {
            throw new IllegalStateException("Model can only be initialized once");
        }

        this.model = model ;
        listView.setItems(model.getPersonList());

        listView.getSelectionModel().selectedItemProperty().addListener((obs, oldSelection, newSelection) -> 
            model.setCurrentPerson(newSelection));

        model.currentPersonProperty().addListener((obs, oldPerson, newPerson) -> {
            if (newPerson == null) {
                listView.getSelectionModel().clearSelection();
            } else {
                listView.getSelectionModel().select(newPerson);
            }
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

该控制器基本上只是将列表中显示的数据绑定到模型中的数据,并确保模型currentPerson始终是列表视图中的选定项.

现在,你可能有另一种观点,说一个编辑器,用了三个文本字段firstName,lastName以及email一个人的性质.它的控制器可能看起来像:

public class EditorController {

    @FXML
    private TextField firstNameField ;
    @FXML
    private TextField lastNameField ;
    @FXML
    private TextField emailField ;

    private DataModel model ;

    public void initModel(DataModel model) {
        if (this.model != null) {
            throw new IllegalStateException("Model can only be initialized once");
        }
        this.model = model ;
        model.currentPersonProperty().addListener((obs, oldPerson, newPerson) -> {
            if (oldPerson != null) {
                firstNameField.textProperty().unbindBidirectional(oldPerson.firstNameProperty());
                lastNameField.textProperty().unbindBidirectional(oldPerson.lastNameProperty());
                emailField.textProperty().unbindBidirectional(oldPerson.emailProperty());
            }
            if (newPerson == null) {
                firstNameField.setText("");
                lastNameField.setText("");
                emailField.setText("");
            } else {
                firstNameField.textProperty().bindBidirectional(newPerson.firstNameProperty());
                lastNameField.textProperty().bindBidirectional(newPerson.lastNameProperty());
                emailField.textProperty().bindBidirectional(newPerson.emailProperty());
            }
        });
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果您进行设置,以便这两个控制器共享相同的模型,编辑器将编辑列表中当前选定的项目.

应通过模型完成加载和保存数据.有时您甚至会将其分解为模型具有引用的单独类(允许您在基于文件的数据加载器和数据库数据加载器之间轻松切换,或者访问Web服务的实现).在简单的情况下,你可能会这样做

public class DataModel {

    // other code as before...

    public void loadData(File file) throws IOException {

        // load data from file and store in personList...

    }

    public void saveData(File file) throws IOException {

        // save contents of personList to file ...
    }
}
Run Code Online (Sandbox Code Playgroud)

然后,您可能有一个控制器,可以访问此功能:

public class MenuController {

    private DataModel model ;

    @FXML
    private MenuBar menuBar ;

    public void initModel(DataModel model) {
        if (this.model != null) {
            throw new IllegalStateException("Model can only be initialized once");
        }
        this.model = model ;
    }

    @FXML
    public void load() {
        FileChooser chooser = new FileChooser();
        File file = chooser.showOpenDialog(menuBar.getScene().getWindow());
        if (file != null) {
            try {
                model.loadData(file);
            } catch (IOException exc) {
                // handle exception...
            }
        }
    }

    @FXML
    public void save() {

        // similar to load...

    }
}
Run Code Online (Sandbox Code Playgroud)

现在您可以轻松组装应用程序:

public class ContactApp extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception {

        BorderPane root = new BorderPane();
        FXMLLoader listLoader = new FXMLLoader(getClass().getResource("list.fxml"));
        root.setCenter(listLoader.load());
        ListController listController = listLoader.getController();

        FXMLLoader editorLoader = new FXMLLoader(getClass().getResource("editor.fxml"));
        root.setRight(editorLoader.load());
        EditorController editorController = editorLoader.getController();

        FXMLLoader menuLoader = new FXMLLoader(getClass().getResource("menu.fxml"));
        root.setTop(menuLoader.load());
        MenuController menuController = menuLoader.getController();

        DataModel model = new DataModel();
        listController.initModel(model);
        editorController.initModel(model);
        menuController.initModel(model);

        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.show();
    }
}
Run Code Online (Sandbox Code Playgroud)

正如我所说,这种模式有很多变化(这可能更像是模型 - 视图 - 演示者,或"被动视图"变体),但这是一种方法(我基本上赞成).通过构造函数向控制器提供模型更为自然,但是使用fx:controller属性定义控制器类要困难得多.这种模式也非常适合依赖注入框架.

更新:此示例的完整代码在此处.

  • 设计模式的很好的总结,但是当您设置模型时,您的代码不会总是抛出异常,因为if语句引用(希望非空)模型参数而不是模型实例变量?您应该使用if(this.model!= null). (3认同)
  • 哦,好地方:谢谢。这就是直接在此处键入代码的结果,而不是首先实际运行它。更新修复。 (2认同)
  • 非常感谢您提供的示例以及[不带 FXML 的版本](/sf/answers/2581163791/)。 (2认同)