Применение MVC с помощью JavaFx

Я новичок в шаблоне проектирования GUI world/OO, и я хочу использовать шаблон MVC для моего приложения GUI, я прочитал небольшой учебник о шаблоне MVC, модель будет содержать данные, представление будет содержать визуальный и контроллер будет связываться между представлением и моделью.

У меня есть представление, содержащее ListView node, и ListView будет заполнен именами из класса Person (Model). Но я немного смущен об одном.

Что я хочу знать, является ли загрузка данных из файла ответственностью Контроллера или Модели? И ObservableList имен: следует ли хранить его в контроллере или модели?

Ответы

Ответ 1

Существует много разных вариантов этого шаблона. В частности, "MVC" в контексте веб-приложения интерпретируется несколько иначе, чем "MVC" в контексте приложения с толстым клиентом (например, настольного) (поскольку веб-приложение должно сидеть поверх цикла запроса-ответа). Это всего лишь один подход к реализации MVC в контексте толстого клиентского приложения с использованием JavaFX.

Ваш класс 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 всегда является выбранным элементом в представлении списка.

Теперь у вас может быть другое представление, например, редактор, с тремя текстовыми полями для свойств 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());
            }
        });
    }
}

Теперь, если вы настроите все так, чтобы оба этих контроллера использовали одну и ту же модель, редактор отредактирует текущий выбранный элемент в списке.

Загрузка и сохранение данных должны выполняться с помощью модели. Иногда вы даже учитываете это в отдельном классе, к которому у модели есть ссылка (что позволяет легко переключаться между файловым загрузчиком на основе файлов и загрузчиком данных базы данных или, например, реализацией, которая обращается к веб-службе). В простом случае вы можете сделать

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. Этот шаблон также сильно подкрепляет рамки внедрения инъекций.

Обновление: полный код для этого примера здесь.

Ответ 2

Что я хочу знать, так это то, что если загрузка данных из файла является ответственностью Контролера или модели?

Для меня модель несет ответственность только за создание требуемых структур данных, представляющих бизнес-логику приложения.

Действие загрузки этих данных из любого источника должно выполняться с помощью уровня контроллера. Вы также можете использовать шаблон репозитория, который может помочь вам абстрагироваться от источника, когда вы получаете данные из представления. При этом вы не должны заботиться о том, загружается ли репозиторий данных из файла, sql, nosql, webservice...

И ObservableList имен будет сохранен в контроллере или модели?

Для меня ObservableList является частью представления. Это тип структуры данных, которую вы можете привязать к элементам управления javafx. Так, например, ObservableList может быть заполнен Strings из модели, но ссылка ObservableList должна быть атрибутом некоторого класса View's. В Javafx его очень любезно связать элементы управления javafx с Observable Properties, поддерживаемые объектами домена из модели.

Вы также можете взглянуть на концепцию viewmodel. Для меня JavaFx bean, поддерживаемый POJO, можно рассматривать как модель представления, вы можете увидеть его как модельный объект, готовый к представлению в представлении. Например, если ваше представление должно показать некоторое общее значение, рассчитанное из двух атрибутов модели, это общее значение может быть атрибутом модели представления. Этот атрибут не будет сохраняться, и он будет вычислен в любое время, когда вы покажете представление.