JavaFX: MVVM

JavaFX Serie

Wir haben uns im letzten Beitrag etwas mit der Aufteilung in Model / View / Controller beschäftigt und mussten in der View für das Binding teilweise auf Code ausweichen. Dies haben wir in Form von kleinen JavaScript Elementen eingefügt, was natürlich nicht unbedingt optimal ist.

Besonders schwerwiegend war, dass wir uns vom SceneBuilder verabschiedet haben, der hier mit gewissen Teilen Probleme bekommen hat.

Um diese Problematik aufzulösen, kommen wir automatisch zu einem neuen Pattern: Aus dem Model View Controller (MVC) Pattern wird ein Model – View – View Model (MVVM) Pattern.

Die erste wichtige Veränderung: Unsere View wird aus zwei Bestandteilen bestehen: Wie gewohnt das fxml aber es kommt auch eine Java Datei hinzu. Dies macht Sinn, denn wir haben bereits festgestellt, dass wir Code in der View benötigen und daher können wir dies in eine separate java Datei packen.

Desweiteren bekommen wir ein View Model. Für unser Binding brauchten wir in den vorhergehenden Beiträgen ein Model, welches mit JavaFX spezifischen Properties arbeitete. Wenn wir aber Software Entwickeln, dann hatten unsere Models (hoffentlich) statt z.B. (Simple)StringProperties immer Strings. Wir hatten hier also immer einen massiven Mehraufwand und den können wir jetzt gezielt abbilden: Wir haben ein dediziertes View Model. In diesem verwenden wir dann unser Model, das im Rahmen der „normalen Java Entwicklung“ entstanden ist.

Library für MVVM

Rund um MVVM gibt es sehr viel Dokumentation. Microsoft hat sehr stark auf MVVM zugegriffen z.B. in seinem Windows Presentation Framework (WPF). Ein Projekt hat dies auf JavaFX adaptiert: mvvmFX. Im Wiki finden sich zu MVVM auch gute Links, die das Thema sehr ausführlich beschreiben und ich kann nur raten, dies im Detail zu lesen um ein tiefes Verständnis zu bekommen.

Anpassen unseres Projekts

Solltest Du der Serie nicht gefolgt sein: Der Code der ganzen JavaFX Serie findet sich unter https://github.com/kneitzel/blog-javafx-series. Der MVC Code in „04 helloworld-mvc“ und die mvvm Version in „05 helloworld-mvvm“

Abhängigkeit

Wir wollen die mvvmFX Library nutzen, daher tragen wir diese als Abhängigkeit in unsere build.gradle Datei ein, indem wir in dem dependency Block einfügen:

compile group: 'de.saxsys', name: 'mvvmfx', version: '1.8.0'

Neue Klasse: helloworld.entity.Greeting

Im Namespace helloworld legen wir eine neue Klasse an: entity.Greeting, in der wir unsere Business Logik / Daten für eine Begrüßung hinterlegen:

package helloworld.entity;

public class Greeting {
private String greeting;

public Greeting() {
greeting = "Hallo Welt!";
}

public Greeting(final String name) {
greetName(name);
}

public String getGreeting() { return greeting; }

public void greetName(final String name) {
if (name == null || name.isEmpty()) {
greeting = "Hallo Welt!";
} else {
greeting = "Hallo " + name + "!";
}
}
}

Dies ist eine einfache Klasse mit Daten und Business Logik, ganz ohne irgend welche JavaFX Spezialitäten wie StringProperties und Co. Die Klasse wirkt von Design etwas gekünstelt, aber wir nehmen dies an dieser Stelle einfach so hin.

Umbenennen von Dateien

Nun wollen wir einige Dateien umbenennen:

a) View

Zu der View gehören ab jetzt das fxml File und das bisherige Controller File. Daher benennen wir diese um in HelloWorldView(.java|.fxml)

b) ViewModel

Das bisherige Model wird zum ViewModel, daher benennen wir die Datei um in HelloWorldViewModel.java

Das Umbenennen machen wir mit dem Refactoring der IDE, damit es immer gleich an allen Stellen geändert ist.

Anpassungen der View

Als erstes passen wir die HelloWorldView.fxml an:

  • Wir löschen die Referenz auf JavaScript <?language javascript?>
  • Das Label mit dem Greeting bekommt ein fx:id=“greetingLabel“ und das Binding für text wird gelöscht (text=“${controller.model.greeting}“)
  • Das fx:script wird komplett entfernt.

Dann passen wir die HelloWorldView.java an:

package helloworld;

import de.saxsys.mvvmfx.FxmlView;
import de.saxsys.mvvmfx.InjectViewModel;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;

import java.net.URL;
import java.util.ResourceBundle;

public class HelloWorldView implements FxmlView<HelloWorldViewModel>, Initializable {

/**
* Model with our data.
*/
@InjectViewModel
private HelloWorldViewModel model;


/**
* TextField for name.
*/
@FXML private TextField nameTextField;

/**
* Label for greeting
*/
@FXML private Label greetingLabel;

/**
* Close application.
* @param e ActionEvent (unused).
*/
public void closeApplicationAction(final ActionEvent e) {
model.closeApplication();
}

/**
* Updates Greeting in model.
* @param e ActionEvent (unused).
*/
public void updateGreeting(final ActionEvent e) { model.updateGreeting();}

@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
nameTextField.textProperty().bindBidirectional(model.nameProperty);
greetingLabel.textProperty().bind(model.greetingProperty);
}
}

Die folgenden wichtigen Änderungen sind sichtbar:

  • Es wird nun FxmlView<HelloWorldViewModel>, Initializable implementiert.
  • Das Model wird nicht mehr selbst erzeugt sondern kommt per Injection.
  • Die Controls sind nun in der View bekannt (greetingLabel und nameTextField).
  • Die Business-Logik (Beenden der Applikation) ist in das ViewModel gewandert.
  • Das Binding ist nun in der initialize Methode.

Die ViewModel Klasse wird auch angepasst:

package helloworld;

import de.saxsys.mvvmfx.ViewModel;
import helloworld.entity.Greeting;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
* Our Model for Hello World.
*/
public class HelloWorldViewModel implements ViewModel {

/**
* Our Model to use.
*/
private Greeting greeting;

/**
* Greeting to show.
*/
public StringProperty greetingProperty = new SimpleStringProperty();
public StringProperty nameProperty = new SimpleStringProperty("Bitte Namen eingeben");

public HelloWorldViewModel() {
greeting = new Greeting("Welt");
updateViewModel();
}

/**
* Updates the greeting to use the name given.
*/
public void updateGreeting() {
if (nameProperty.get().length() > 0) {
greeting.greetName(nameProperty.get());
nameProperty.setValue("Bitte Namen eingeben");
} else {
greeting.greetName("Welt");
}

updateViewModel();
}

/**
* Updates the ViewModel when Model was changed.
*/
protected void updateViewModel() {
greetingProperty.setValue(greeting.getGreeting());
}

/**
* Close application.
*/
public void closeApplication() {
System.exit(0);
}
}

Die wichtigen Änderungen sind nun:

  • die Klasse implementiert ViewModel
  • closeApplication als Business Logik ist dazu gekommen.
  • Die Greeting Entity wird verwendet. Wir haben jedoch unseren eigenen Code geschrieben, um Änderungen an den Properties auszulesen / zu setzen.

Unsere Applikation muss auch geändert werden:

package helloworld;

import de.saxsys.mvvmfx.FluentViewLoader;
import de.saxsys.mvvmfx.ViewTuple;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class HelloWorld extends Application {

public static void main(final String[] args) {
launch(args);
}

@Override
public void start(final Stage primaryStage) throws IOException {
primaryStage.setTitle("Hello World Application");
ViewTuple<HelloWorldView, HelloWorldViewModel> viewTuple = FluentViewLoader.fxmlView(HelloWorldView.class).load();
Parent root = viewTuple.getView();
primaryStage.setScene(new Scene(root));
primaryStage.show();
}

}

Der Aufruf einer MVVM Applikation sieht nun leicht verändert aus. So müssen wir ein ViewTupel erzeugen und nutzen dazu einen neuen, speziellen FluentViewLoader. 

Damit wäre die Applikation erst einmal umgestellt auf das MVVM Pattern mit Hilfe der mvvmFX Library. 

Links

Edit

  • 2020-07-03 Wechsel zu einem Repository für die ganze Serie.