Anwenden von MVC mit JavaFx

Ich bin neu in der GUI Welt / OO Design Pattern und ich möchte MVC-Muster für meine GUI-Anwendung verwenden, habe ich ein kleines Tutorial über MVC Muster gelesen, das Modell wird die Daten enthalten, die Ansicht wird das visuelle Element enthalten und die Der Controller verbindet die Ansicht mit dem Modell.

Ich habe eine View, die einen ListView-Knoten enthält, und die ListView wird mit Namen aus einer Personenklasse (Model) gefüllt. Aber ich bin ein bisschen verwirrt wegen einer Sache.

Was ich wissen möchte ist, ob das Laden der Daten aus einer Datei die Verantwortung des Controllers oder des Modells ist? Und die ObservableList der Namen: Soll es im Controller oder im Model gespeichert werden?

   

Es gibt viele verschiedene Variationen dieses Musters. Insbesondere wird “MVC” im Kontext einer Webanwendung etwas anders als “MVC” im Kontext einer Thick-Client- (z. B. Desktop-) Anwendung interpretiert, da eine Webanwendung auf dem Anfrage-Antwort-Zyklus sitzen muss. Dies ist nur ein Ansatz zur Implementierung von MVC im Kontext einer Thick-Client-Anwendung mit JavaFX.

Ihre Person class ist nicht wirklich das Modell, es sei denn, Sie haben eine sehr einfache Anwendung: Dies nennen wir normalerweise ein Domänenobjekt, und das Modell enthält Verweise darauf sowie andere Daten. In einem engen Kontext, beispielsweise wenn Sie nur an ListView denken, können Sie sich die Person als Ihr Datenmodell ListView (sie modelliert die Daten in jedem Element der ListView ), aber im weiteren Kontext der Anwendung mehr Daten und Status zu berücksichtigen.

Wenn Sie eine ListView anzeigen, benötigen Sie mindestens eine ObservableList . Möglicherweise möchten Sie auch eine Eigenschaft wie currentPerson , die das ausgewählte Element in der Liste darstellen könnte.

Wenn die einzige Ansicht, die Sie haben, die ListView , dann wäre das Erstellen einer separaten class, um diese zu speichern, übertrieben, aber jede reale Anwendung wird normalerweise mit mehreren Ansichten enden. An diesem Punkt wird die gemeinsame Nutzung der Daten in einem Modell eine sehr nützliche Möglichkeit für verschiedene Controller, miteinander zu kommunizieren.

So könnten Sie beispielsweise Folgendes haben:

 public class DataModel { private final ObservableList personList = FXCollections.observableArrayList(); private final ObjectProperty currentPerson = new SimpleObjectPropery<>(null); public ObjectProperty currentPersonProperty() { return currentPerson ; } public final Person getCurrentPerson() { return currentPerson().get(); } public final void setCurrentPerson(Person person) { currentPerson().set(person); } public ObservableList getPersonList() { return personList ; } } 

Jetzt haben Sie vielleicht einen Controller für die ListView Anzeige, der folgendermaßen aussieht:

 public class ListController { @FXML private ListView 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); } }); } } 

Dieser Controller bindet im Wesentlichen nur die in der Liste angezeigten Daten an die Daten im Modell und stellt sicher, dass das currentPerson des Modells immer das ausgewählte Element in der currentPerson ist.

Jetzt haben Sie vielleicht eine andere Ansicht, sagen wir einen Editor, mit drei Textfeldern für die Eigenschaften firstName , lastName und email einer Person. Der Controller könnte folgendermaßen aussehen:

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

Wenn Sie nun die Einstellungen so einrichten, dass beide Controller dasselbe Modell verwenden, bearbeitet der Editor das aktuell ausgewählte Element in der Liste.

Das Laden und Speichern von Daten sollte über das Modell erfolgen. Manchmal wird dies sogar in einer separaten class berücksichtigt, auf die das Modell verweist (so können Sie beispielsweise einfach zwischen einem dateibasierten Datenladeprogramm und einem databasedatenladeprogramm oder einer Implementierung, die auf einen Webdienst zugreift, wechseln). Im einfachen Fall könnten Sie das tun

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

Dann haben Sie möglicherweise einen Controller, der Zugriff auf diese functionalität bietet:

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

Jetzt können Sie einfach eine Anwendung zusammenstellen:

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

Wie ich bereits sagte, gibt es viele Variationen dieses Musters (und das ist wahrscheinlich mehr eine Model-View-Presenter oder “passive View” Variation), aber das ist ein Ansatz (den ich grundsätzlich favorisiere). Es ist etwas natürlicher, das Modell den Controllern über ihren Konstruktor bereitzustellen, aber dann ist es viel schwieriger, die Controller-class mit einem fx:controller Attribut zu definieren. Dieses Muster eignet sich auch stark für Frameworks zur Abhängigkeitsinjektion.

Update: Der vollständige Code für dieses Beispiel ist hier .

Was ich wissen möchte ist, dass, wenn das Laden der Daten aus einer Datei die Verantwortung des Controllers oder des Modells ist?

Für mich ist das Modell nur dafür verantwortlich, die benötigten Datenstrukturen zu bringen, die die Geschäftslogik der Anwendung repräsentieren.

Das Laden dieser Daten aus einer beliebigen Quelle sollte von der Controller-Schicht durchgeführt werden. Sie können auch das Repository-Muster verwenden , das Ihnen beim Abstrahieren von der Art der Quelle helfen kann, wenn Sie die Daten aus der Sicht eingeben. Mit dieser Implementierung sollte es Ihnen egal sein, ob die Repository-Implementierung die Daten aus Datei, SQL, Nosql, Webservice … lädt.

Und die ObservableList der Namen wird im Controller oder im Modell gespeichert?

Für mich ist die ObservableList Teil der View. Es ist die Art von Datenstruktur, die Sie an javafx-Steuerelemente binden können. So könnte beispielsweise eine ObservableList mit Strings aus dem Modell befüllt werden, aber die ObservableList-Referenz sollte ein Attribut einer Views-class sein. In Javafx ist es sehr angenehm, javafx-Steuerelemente mit Observable-Eigenschaften zu verbinden, die von Domänenobjekten aus dem Modell unterstützt werden.

Sie können auch sehen, um Modell-Konzept zu sehen . Für mich könnte eine JavaFx-Bean, die von einem POJO unterstützt wird, als ein Ansichtsmodell betrachtet werden. Sie könnten es als ein Modellobjekt sehen, das bereit ist, in der Ansicht dargestellt zu werden. Wenn zum Beispiel Ihre Ansicht einen Gesamtwert anzeigen muss, der aus 2 Modellattributen berechnet wird, könnte dieser Gesamtwert ein Attribut des Ansichtsmodells sein. Dieses Attribut wird nicht beibehalten und wird jedes Mal berechnet, wenn Sie die Ansicht anzeigen.