31. August 2015
Manuel Mauky
0

mvvmFX: Model-View-ViewModel mit JavaFX (Teil 2)

Im ersten Teil dieser Artikelserie habe ich die Idee hinter dem Architekturmuster „Model-View-ViewModel“ vorgestellt und gezeigt, wie das Pattern auch mit JavaFX umsetzbar ist. In diesem Teil möchte ich nun das OpenSource Applicationframework mvvmFX vorstellen, an dem ich zusammen mit Alexander Casall arbeite. Neben Interfaces und Basisklassen für die saubere Strukturierung der Anwendung stellt es zahlreiche Hilfsmittel zur Verfügung, die die Entwicklung nach MVVM-Muster vereinfachen. Das Framework ist im Maven Central enthalten und kann daher einfach in Maven oder Gradle-Projekte eingebunden werden.

Wie im vorherigen Teil beschrieben, ist eines der Hauptargumente für das Pattern die bessere Testbarkeit. Daher werde ich das Framework anhand eines Beispiels vorstellen, welches wir testgetrieben entwickeln wollen. Anwendungsfall sei ein Formular zum Registrieren eines Nutzers. Dieses enthält neben dem Eingabefeld für den Nutzernamen zwei weitere Eingabefelder für die Eingabe und die Bestätigung des Passworts. Ausserdem enthält das Formular einen „Registrieren“-Button, der nur aktiv sein soll, wenn folgende Regeln erfüllt sind:

  • das Nutzernamen-Eingabefeld ist ausgefüllt
  • beide Passwort-Felder sind ausgefüllt
  • das Passwort und die Bestätigung stimmen überein

Diese Regeln sind Präsentations-Logik und sollten daher im ViewModel implementiert werden. Zunächst überlegen wir uns daher die Schnittstelle unseres ViewModels, d.h. welche Properties und welche Methoden wird es nach aussen hin anbieten? Folgender Vorschlag:

class RegisterViewModel {
    public StringProperty usernameProperty() {...}
    public StringProperty passwordProperty() {...}
    public StringProperty confirmPasswordProperty() {...}
    public ReadOnlyBooleanProperty registerButtonEnabledProperty() {...}
}

Die GUI wird 3 Textfelder enthalten, daher besitzt auch das ViewModel für diese 3 Felder jeweils eine StringProperty. Um auszudrücken, ob der Button aktiv ist oder nicht wird eine ReadOnlyBooleanProperty benutzt. Wie der Name schon andeutet, ist diese Property von aussen nur lesend verfügbar und kann nicht von aussen geändert werden. Über den Wert, den diese Property annimmt entscheidet also ausschließlich das ViewModel selbst, nach den oben genannten Regeln. Für den Benutzer des ViewModels, nämlich die View, sind die Interna verborgen.

Der JUnit-Test dazu könnte so aussehen:

public class RegisterViewModelTest {
  
  RegisterViewModel viewModel;
  
  @Before
  public void setup() {
    viewModel = new RegisterViewModel();
  }
  
  @Test
  public void test() {
    // initial values
    assertThat(viewModel.usernameProperty()).hasValue("");
    assertThat(viewModel.passwordProperty()).hasValue("");
    assertThat(viewModel.confirmPasswordProperty()).hasValue("");
    assertThat(viewModel.registerButtonEnabledProperty()).isFalse();
    
    
    // entering username
    viewModel.usernameProperty().setValue("manuel");
    
    // still disabled
    assertThat(viewModel.registerButtonEnabledProperty()).isFalse();
    
    // enter password
    viewModel.passwordProperty().set("geheim"); // no, that's not really my password
    assertThat(viewModel.registerButtonEnabledProperty()).isFalse();
    
    // confirm password
    viewModel.confirmPasswordProperty().set("geheim");
    
    // now register button is enabled
    assertThat(viewModel.registerButtonEnabledProperty()).isTrue();
    
    // change confirm password
    viewModel.confirmPasswordProperty().set("secret");
    
    // disabled again
    assertThat(viewModel.registerButtonEnabledProperty()).isFalse();
    
    viewModel.passwordProperty().set("secret");
    
    // now both passwords are equal again
    assertThat(viewModel.registerButtonEnabledProperty()).isTrue();
    
    
    // when username is removed ...
    viewModel.usernameProperty().setValue("");
    
    // ... the button is disabled again
    assertThat(viewModel.registerButtonEnabledProperty()).isFalse();
  }
}

Der Testfall zeigt den möglichen Ablauf bei der Registrierung. Das ViewModel ist reaktiv, d.h. es verändert seinen Zustand selbstständig basierend auf den Wertänderungen seiner Property-Felder. Wer möchte, kann natürlich auch die einzelnen Testschritte und Anforderungen in einzelne Testmethoden aufteilen, anstatt alles in einer großen Testmethode zu prüfen. Das kommt auf den persönlichem Geschmack an. Aktuell ist dieser Testfall natürlich noch rot, da ja noch garkein Code dazu existiert. Der nächste Schritt ist also die Implementierung des ViewModels:

public class RegisterViewModel {

  private StringProperty username = new SimpleStringProperty();
  private StringProperty password = new SimpleStringProperty();
  private StringProperty confirmPassword = new SimpleStringProperty();
  private ReadOnlyBooleanWrapper registerButtonEnabled = new ReadOnlyBooleanWrapper();

  public StringProperty usernameProperty() {
    return username;
  }

  public StringProperty passwordProperty() {
    return password;
  }

  public StringProperty confirmPasswordProperty() {
    return confirmPassword;
  }

  public ReadOnlyBooleanProperty registerButtonEnabledProperty() {
    return registerButtonEnabled.getReadOnlyProperty();
  }
}

Im ersten Schritt legen wir die Felder mit unseren Properties an und implementieren die entsprechenden Accessor-Methoden. Der JavaFX-Beans-Konvention folgend sollte man auch reguläre Getter und Setter anlegen, worauf ich aus Platzgründen hier aber verzichtet habe.

Damit ist der Testfall zwar immer noch rot, jedoch zumindest nicht mehr wegen einer NullPointerException. Für die richtige Implementierung der Logik sind mehre Varianten denkbar. Eine elegante Variante ist die Benutzung von Databinding:

public class RegisterViewModel {
  ...
  public RegisterViewModel() {

    final BooleanBinding passwordsEqual = Bindings.createBooleanBinding(() -> {
      final String pw = password.get() == null ? "" : password.get();
      final String confirm = confirmPassword.get() == null ? "" : confirmPassword.get();

      return pw.equals(confirm);
    }, password, confirmPassword);


    registerButtonEnabled.bind(
        LogicBindings.and(
            username.isNotEmpty(),
            password.isNotEmpty(),
            confirmPassword.isNotEmpty(),
            passwordsEqual));
  }
...
}

Im Konstruktor wird zunächst ein Custom-Binding angelegt, welches ausdrückt, ob beide Passwörter gleich sind, wobei ein leerer String behandelt wird wie null.

Anschließend benutzen wir die Klasse LogicBindings aus der Bibliothek Advanced-Bindings, die eine Logische UND-Verknüpfung für beliebig viele Parameter (vom Typ ObservableBooleanValue) zur Verfügung stellt. Die Methode isNotEmpty() von StringProperty gibt ein BooleanBinding zurück und kann damit für die UND-Verknüpfung verwendet werden. Erst wenn alle diese Bedingungen erfüllt sind, soll auch das Property für den Registrieren-Button auf true springen. Mit dieser Implementierung wird nun auch der Testfall grün.

Bisher haben wir noch keine mvvmFX-Spezifischen Klassen verwendet. Dies wird sich aber in Kürze ändern, wenn wir zu unserem ViewModel auch eine entsprechende View bauen und das Ganze zusammenstecken wollen. Die View selbst kann mit dem SceneBuilder (der mittlerweile von GluonHQ angeboten und weiterentwickelt wird) zusammengeklickt werden. Heraus kommt dabei eine FXML-Datei, die z.B. folgende Oberfläche beschreiben könnte:

mvvmfx_screenshot1

Zu jeder FXML-Datei gehört eine Java-Klasse, die den Zugriff auf die Elemente der FXML-Datei ermöglicht. JavaFX nennt diese Klasse „Controller“ und verwendet folglich das FXML-Attribut fx:controller. Im Kontext von mvvmFX sprechen wir aber von der „View-Klasse“ oder „CodeBehind“, da sie konzeptionell mit zur View gehört. Die „View“ in unserem Sinne besteht also aus der FXML-Datei und der dazugehörigen Java-Klasse. Diese sieht so aus:

public class RegisterView implements FxmlView<RegisterViewModel> {

  @FXML
  public TextField usernameInput;
  @FXML
  public PasswordField passwordInput;
  @FXML
  public PasswordField confirmInput;
  @FXML
  public Button registerButton;

  @InjectViewModel
  private RegisterViewModel viewModel;

  public void initialize() {
    usernameInput.textProperty().bindBidirectional(viewModel.usernameProperty());
    passwordInput.textProperty().bindBidirectional(viewModel.passwordProperty());
    confirmInput.textProperty().bindBidirectional(viewModel.confirmPasswordProperty());

    registerButton.disableProperty().bind(viewModel.registerButtonEnabledProperty().not());
  }
}

Zunächst fällt auf, dass wir das Interface de.saxsys.mvvmfx.FxmlView implementieren. Dies ist ein Marker-Interface von mvvmFX. Es besitzt einen generischen Typ-Parameter, nämlich den des ViewModels, welches zu dieser View gehört. Damit das funktioniert muss auch das ViewModel ein Marker-Interface (de.saxsys.mvvmfx.ViewModel) implementieren.

Anschließend werden Referenzen auf die Controls der FXML-Datei angelegt und mit der Standard-JavaFX-Annotation @FXML versehen. Vorausgesetzt, dass die fx:id-Attribute der entsprechenden Controls in der FXML-Datei mit den Feld-Namen in der View-Klasse übereinstimmen, kann JavaFX selbstständig die richtigen Referenzen zuweisen. Wenn also in der FXML-Datei das Textfeld die fx:id="usernameInput" bekommen hat, muss das Feld in der View-Klasse auch usernameInput heißen.

mvvmFX-Spezifisch ist wiederum die nächste Zeile: Mit der Annotation @InjectViewModel wird mvvmFX angewiesen, die ViewModel-Instanz, die zu dieser View gehört, bereitzustellen. Anschließend muss nur noch die Verbindung zwischen den Controls und den Properties des ViewModels hergestellt werden. Der richtige Platz dafür ist die initialize-Methode. Nach JavaFX-Konvention wird diese Methode automatisch ausgeführt, nachdem alle Referenzen vergeben wurden. Die Logik innerhalb der initialize-Methode ist trivial. Wir stellen lediglich das Databinding zum ViewModel her.

Diese View können wir nun in unserer JavaFX-Anwendung laden. Um mit JavaFX-Bordmitteln eine FXML-Datei zu laden muss man mit dem genauen Pfad zur FXML-Datei hantieren, was weniger robust gegen Refactorings ist. mvvmFX verfolgt stattdessen einen Convention-over-Configuration-Ansatz. Konvention ist, dass die FXML-Datei genauso heißt (mit Ausnahme der Dateiendung) und im gleichen Package liegt wie die View-Klasse. In unserem Fall heißt die Fxml-Datei beispielsweise „RegisterView.fxml“ und die View-Klasse „RegisterView.java“.

Die View kann dann mit dem FluentViewLoader von mvvmFX geladen werden, wie im folgenden Code zu sehen ist:

public class App extends Application {

	public static void main(String... args) {
		launch(args);
	}

	@Override
	public void start(Stage stage) throws Exception {
		final Parent root = FluentViewLoader.fxmlView(RegisterView.class).load().getView();

		stage.setScene(new Scene(root));
		stage.show();
	}
}

Damit funktioniert unsere Anwendung bereits. Der gesamte Code des Beispiels kann in diesem Gist angeschaut und ausprobiert werden. Als Abhängigkeiten sind nur mvvmfx und die Advanced-Bindings-Bibliothek notwendig. Um den Test auszuführen wird JUnit benötigt, sowie die Assertion-Bibliotheken AssertJ und AssertJ-JavaFX.

Fazit

Wir haben gesehen, wie mit MVVM testgetrieben Benutzeroberflächen mit JavaFX entwickelt werden können. Die Präsentationslogik ist im ViewModel gekapselt und kann mit einfachen JUnit-Tests überprüft werden. mvvmFX stellt Hilfsmittel bereit um das Verknüpfen der Komponenten und das Laden der Ansichten zu vereinfachen. Darüber hinaus bietet es aber noch weitere Features, die in diesem Blogpost nicht gezeigt wurden. An oberster Stelle ist sicherlich die Unterstützung für Dependency-Injection-Frameworks (kurz DI) zu nennen. Bei DI geht es um die Frage, wie eine Klasse zu ihren Abhängigkeiten, z.B. Services oder andere Komponenten gelangt, ohne diese selbst anzulegen. Dieses Muster ist wichtig um größere Anwendungen zu verdrahten und gleichzeitig eine gute Testbarkeit zu ermöglichen. mvvmFX bietet Unterstützung für die beiden DI-Frameworks CDI/Weld und Google Guice, kann aber auch mit beliebigen anderen DI-Frameworks kombiniert werden. Alle Instanzen der View-Klassen und ViewModelle werden dann von dem entsprechenden DI-Framework verwaltet. Andere interessante Features, die mvvmFX bietet sind Unterstützung bei der Internationalisierung mittels ResourceBundles, Validierung und Notifications zwischen Komponenten.

Zu finden ist das Framework auf Github, wo neben dem Quellcode auch zahlreiche Beispiele und die Dokumentation im Wiki zur Verfügung gestellt werden. Ausserdem existiert eine deutsche und eine englische Projektseite. Und natürlich freuen wir uns über Bugreports und Verbesserungsvorschläge, die im Issue-Tracker auf Github eingereicht werden können.

Manuel arbeitet seit 2010 als Softwareentwickler bei der Saxonia Systems AG in Görlitz. Er beschäftigt sich dort vor allem mit der Frontend-Entwicklung mittels JavaFX. Daneben interessiert er sich für Softwarearchitekturen und funktionale Programmierung. Aktuell arbeitet er an der Open-Source-Bibliothek mvvmFX zur Umsetzung von Model-View-ViewModel mit JavaFX.

Twitter Google+ 

TeilenTweet about this on TwitterShare on Facebook0Share on Google+0Share on LinkedIn0