使用 JavaFx 应用 MVC

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

我有一个包含 ListView 节点的视图,ListView 将填充来自人员类(模型)的名称。但是我对一件事有点困惑。

我想知道的是,从文件加载数据是控制器还是模型的责任?以及名称的可观察列表:它应该存储在控制器还是模型中?


答案 1

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

你的类并不是真正的模型,除非你有一个非常简单的应用程序:这通常是我们所说的域对象,模型将包含对它的引用以及其他数据。在狭窄的上下文中,例如当您只是考虑时,您可以将 它视为您的数据模型(它对每个元素中的数据进行建模),但是在更广泛的应用程序上下文中,需要考虑更多的数据和状态。PersonListViewPersonListView

如果要显示所需的数据,则至少是 .您可能还需要一个属性,如 ,它可能表示列表中的选定项。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 ;
    }
}

现在,您可能有一个如下所示的显示器控制器: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);
            }
        });
    }
}

此控制器实质上只是将列表中显示的数据绑定到模型中的数据,并确保模型始终是列表视图中的选定项。currentPerson

现在,您可能有另一个视图,例如编辑器,其中包含一个人的 、和 属性的三个文本字段。它的控制器可能看起来像这样:firstNamelastNameemail

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

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

加载和保存数据应通过模型完成。有时,您甚至会将其分解为模型具有引用的单独类(例如,允许您轻松地在基于文件的数据加载程序和数据库数据加载程序或访问 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 ...
    }
}

然后,您可能有一个提供对此功能的访问的控制器:

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

    }
}

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

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

正如我所说,这种模式有很多变体(这可能更像是一个模型 - 视图 - 呈现器,或“被动视图”变体),但这是一种方法(我基本上喜欢的方法)。通过构造函数向控制器提供模型会更自然一些,但使用属性定义控制器类要困难得多。这种模式也非常适合依赖关系注入框架。fx:controller

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

如果您对 JavaFX 中的 MVC 教程感兴趣,请参阅:


答案 2

我想知道的是,如果从文件加载数据是控制器还是模型的责任?

对我来说,模型只负责引入表示应用程序业务逻辑的所需数据结构。

从任何源加载该数据的操作应由控制器层完成。您还可以使用存储库模式,当您从视图中访问数据时,它可以帮助您从源类型中抽象出来。实现后,您不应该关心存储库实现是否正在从文件,SQL,NoSQL,Web服务加载数据...

名称的可观察列表将存储在控制器或模型中?

对我来说,这是风景的一部分。它是可以绑定到 JavaFX 控件的数据结构。例如,可以使用模型中的字符串填充 a,但引用应该是某个 View 类的属性。在 JavaFX 中,将 JavaFX 控件与模型中的域对象支持的可观察属性绑定是非常愉快的。ObservableListObservableList<String>ObservableList

您还可以查看视图模型概念。对我来说,由POJO支持的JavaFX bean可以被视为视图模型,您可以将其视为准备在视图中呈现的模型对象。因此,例如,如果您的视图需要显示根据 2 个模型属性计算得出的某个总值,则此总值可能是视图模型的属性。此属性不会保留,并且会在您显示视图时随时计算。


推荐