使用JavaFx实现MVC模式

53

我对GUI/OO设计模式很陌生,想在我的GUI应用中使用MVC模式。我已经阅读了一些有关MVC模式的教程,其中Model将包含数据,View将包含可视化元素,而Controller将连接View和Model。

我有一个View,其中包含一个ListView节点,该ListView将填充来自Person类(Model)的名称。但我有点困惑。

我想知道的是,从文件加载数据是控制器(Controller)的责任还是模型(Model)的责任?名称的ObservableList应该存储在控制器(Controller)还是模型(Model)中?


1
这个PAQ很有见地 https://dev59.com/KmAg5IYBdhLWcg3wx9fG?rq=1 - Cobusve
你可能想要看一下MVVM模式,它是MVC模式的一个变体,在我看来非常适用于JavaFX。 - findusl
2个回答

117

这种模式有许多不同的变体。特别是,在 web 应用程序的上下文中,“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 ;
    }
}

现在,您可能会拥有一个用于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教程感兴趣,请参见:


4
设计模式的总结很棒,但是当你设置模型时,你的代码不会一直抛出异常吗?因为if语句是引用(希望非空)的model参数而不是模型实例变量。应该使用if (this.model != null)代替。 - mipa
2
哦,好眼力:谢谢。这就是在这里直接输入代码而不是先运行它的结果。已更新并修复。 - James_D
1
@findusl SelectionModel.selectedItemProperty() 是一个 ReadOnlyProperty(因此它没有任何 bind 方法,也不能传递给 bindBidirectional(...) 方法)。 - James_D
2
非常感谢您提供的示例,以及没有FXML版本 - Piovezan
1
@James_D 你的回答非常详尽,非常感谢! - Kon
显示剩余2条评论

3
我想知道的是,从文件中加载数据的责任是控制器还是模型?
对我来说,模型只负责提供代表应用程序业务逻辑的所需数据结构。
从任何来源加载数据的操作应该由控制器层执行。您也可以使用 repository pattern,它可以帮助您在从视图访问数据时抽象出源类型。实现了这一点后,您不需要关心存储库实现是从文件、SQL、NoSQL、Web服务等加载数据。
姓名的ObservableList将存储在控制器或模型中?
对我而言,ObservableList 是视图的一部分。它是一种可以绑定到 JavaFX 控件的数据结构。因此,例如 ObservableList<String> 可以从模型中填充字符串,但是 ObservableList 引用应该是某个视图类的属性。
在 JavaFX 中,使用由模型支持的可观察属性绑定 JavaFX 控件非常方便。
你也可以查看 viewmodel concept。对我而言,由 POJO 支持的 JavaFX bean 可以被认为是视图模型,你可以将其视为准备在视图中呈现的模型对象。因此,例如,如果您的视图需要显示从 2 个模型属性计算出的某些总值,则此总值可以是视图模型的属性。此属性不会持久化,每次显示视图时都会重新计算。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接