Применение MVC с JavaFx
Я новичок в мире проектирования GUI / шаблона OO и хочу использовать шаблон MVC для своего приложения GUI. Я прочитал небольшое руководство по шаблону MVC, модель будет содержать данные, представление будет содержать визуальный элемент и Контроллер будет связывать вид и модель.
У меня есть View, который содержит узел ListView, и ListView будет заполнен именами из класса Person (Model). Но я немного запутался в одном.
То, что я хочу знать, является ли ответственность за загрузку данных из файла контроллером или моделью? И ObservableList имен: должен ли он храниться в контроллере или модели?
2 ответа
Есть много разных вариаций этого паттерна. В частности, "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
приписывать. Эта модель также сильно подходит для структур внедрения зависимостей.
Обновление: полный код для этого примера здесь.
Что я хочу знать, так это то, что если загрузка данных из файла является обязанностью контроллера или модели?
Для меня модель отвечает только за привлечение необходимых структур данных, которые представляют бизнес-логику приложения.
Действие загрузки этих данных из любого источника должно быть выполнено на уровне контроллера. Вы также можете использовать шаблон репозитория, который может помочь вам абстрагироваться от типа источника, когда вы обрабатываете данные из представления. При этом вы не должны заботиться, загружает ли репозиторий данные из файла, sql, nosql, webservice...
А ObservableList из имен будет храниться в контроллере или модели?
Для меня ObservableList является частью View. Это структура данных, которую вы можете привязать к элементам управления javafx. Так, например, ObservableList может быть заполнен строками из модели, но ссылка ObservableList должна быть атрибутом некоторого класса View. В Javafx очень удобно связывать элементы управления javafx с помощью Observable Properties, поддерживаемых объектами домена из модели.
Вы также можете взглянуть на концепцию viewmodel. Для меня bean-компонент JavaFx, поддерживаемый POJO, можно рассматривать как модель представления, вы можете видеть его как объект модели, готовый для представления в представлении. Так, например, если вашему представлению необходимо показать какое-то общее значение, рассчитанное по 2 атрибутам модели, это общее значение может быть атрибутом модели представления. Этот атрибут не будет сохранен и будет рассчитываться каждый раз, когда вы показываете представление.