MVVM mit JavaFX

Die Geschichte der Entwicklung von grafischen Benutzeroberflächen ist begleitet von speziellen UI-Design-Patterns, allen voran natürlich das bekannte Model-View-Controller-Muster (MVC-Muster). Grundidee ist die Aufteilung des Codes in einen allgemeinen, oberflächenunabhängigen Teil (das Model) und auf der anderen Seite den oberflächenspezifischen Code. Dieser kann wiederum in Code zur reinen Anzeige (die View) und Code zur Verarbeitung von Benutzereingaben und Steuerung der Oberfläche (der Controller) geteilt werden.
Über die konkrete Umsetzung dieser Aufteilung, die Aufgaben der einzelnen Komponenten und die Beziehungen und Sichtbarkeiten existiert jedoch kein einheitliches Verständnis. Eine Internetsuche zum Schlagwort "MVC" bringt zahlreiche ähnliche, im Detail aber doch unterschiedliche Variationen zum Vorschein. Dies ist zum einen darin begründet, dass verschiedene Programmiersprachen und Zielumgebungen andere Möglichkeiten und Anforderungen mitbringen. Die Implementierung einer Webanwendung mit PHP unterscheidet sich natürlich stark von der Umsetzung einer Desktopoberfläche mit Java. Zum anderen ist die Idee von MVC (und seinen Varianten) auf verschiedenen Abstraktionsniveaus anwendbar: Ein einzelnes UI-Control, wie beispielsweise eine Textfeld-Komponente eines UI-Toolkits, kann intern nach MVC implementiert sein. Eine Stufe darüber ist die Betrachtung von Gesamt- oder Teil-Ansichten der Anwendung, beispielsweise eines Formulars oder eines Einstellungsdialogs. Auf der anderen Seite kann mit MVC auch die Gesamtarchitektur einer Anwendung beschrieben werden, wobei hier eher andere Architekturmuster wie Schichtenarchitekturen vorherrschen.
Während die Grundidee von MVC weiterhin gültig ist, haben sich im Laufe der Zeit einige Varianten herausgebildet, die zum einen die Struktur der Komponenten konkretisieren und sich in entscheidenden Details vom Original unterscheiden. Zu diesen Varianten zählt "Model-View-ViewModel", kurz MVVM, das von Microsoft Mitte der 2000er Jahre für das Windows-Presentation-Foundation-Framework von .Net entwickelt wurde. In der Zwischenzeit wurde MVVM aber auch in anderen Umgebungen und Programmiersprachen übernommen, beispielsweise mit dem JavaScript-Framework KnockoutJS. Und auch JavaFX ist sehr gut geeignet um MVVM umzusetzen.

Die Idee von MVVM ist es, den gesamten UI-Zustand im sogenannten "ViewModel" abzubilden. Dieses ViewModel enthält also beispielsweise Felder für die aktuellen Werte von Textfeldern, Tabellen und anderen Komponenten. Außerdem werden Informationen wie "Button X ist inaktiv" ebenfalls als Boolean-Feld im ViewModel hinterlegt. Die View präsentiert lediglich den UI-Zustand des ViewModels. Wird der UI-Zustand vom Nutzer verändert, z. B. indem in einem Textfeld getippt wird, reicht die View diese Änderungen umgehend an das ViewModel weiter. Andersherum müssen aber auch Änderungen, die vom ViewModel ausgehen, umgehend von der View angezeigt werden. View und ViewModel müssen also stets synchron gehalten werden. Wie diese Synchronisierung konkret umgesetzt wird, kommt dabei auf die Programmiersprache bzw. das Framework an. Die JavaFX-Umsetzung werden wir weiter unten genauer betrachten.
Eine weitere Aufgabe des ViewModels ist die Implementierung der Präsentationslogik: Was passiert, wenn ein Button geklickt wird? Wie setzt sich der Text eines Labels zusammen? Unter welchen Bedingungen ist Button X ausgegraut? Solche und ähnliche Fragen sind für die View generell uninteressant. Sie zeigt lediglich den Zustand des ViewModels an. Wie und warum dieser Zustand entsteht und sich verändert ist allein Sache des ViewModels.
Diese Aufteilung hat im wesentlichen zwei entscheidende Vorteile: Zum einen können Designer sich um die Gestaltung der Oberfläche kümmern, ohne Aspekte der Präsentationslogik beachten zu müssen. Entwickler können derweil die Logik im ViewModel implementieren ohne ihrerseits Designaspekte beachten zu müssen. Auf der anderen Seite wird die Testbarkeit mittels Unittests enorm verbessert. Die View enthält keine nennenswerte Logik mehr, folglich kann der Testaufwand auf ein Minimum reduziert werden. Testbedürftig ist vor allem das ViewModel, da es sämtliche Präsentationslogik enthält. Gleichzeitig ist das ViewModel aber eine ganz normale Klasse ohne Abhängigkeiten auf UI-Controls. Zum Testen sind daher keine komplizierten Integrationstests notwendig, bei denen zunächst die Applikation hochgefahren und anschließend Nutzerinteraktionen simuliert werden müssen. Diese Art von Tests sind sowohl hinsichtlich Ausführungszeit als auch Entwicklungszeit kostspielig. ViewModelle können stattdessen mit einfachen Unit-Tests schnell und unkompliziert getestet werden.
Bleibt die Frage nach der Synchronisation der View mit dem ViewModel. JavaFX bietet mit seiner Properties- und Databinding-API ein Hilfsmittel, welches sehr gut für diesen Zweck genutzt werden kann. Properties in diesem Sinne sind Wrapper-Klassen um normale Java-Datentypen. Beispielsweise existieren die Klassen IntegerProperty, StringProperty, BooleanProperty und auch ein generisches ObjectProperty. Der Wert, den ein solches Property enthält, kann mittels get und set gelesen bzw. gesetzt werden. Darüber hinaus erlauben Properties aber auch einen Observer-Mechanismus. So können Listener registriert werden, die ausgeführt werden, sobald sich der Wert eines Properties ändert.
Auf diesem Observermechanismus aufbauend unterstützen Properties so genanntes Databinding. So kann ausgedrückt werden, dass ein Property "a" stets den Wert von Property "b" enthalten soll. Ändert sich der Wert von "b", aktualisiert sich auch "a" unmittelbar. Der Entwickler kann sowohl uni- als auch bidirektionale Bindings erzeugen. Bei bidirektionalen Bindings werden Wertänderungen von beiden beteiligten Properties jeweils an den anderen weitergereicht. Das Code-Beispiel soll diesen Mechanismus verdeutlichen:
StringProperty a = new SimpleStringProperty(); StringProperty b = new SimpleStringProperty(); a.bindBidirectional(b); a.set("Hallo"); System.out.println(b.get()); // "Hallo" b.set("Welt"); System.out.println(a.get()); // "Welt"
Alle JavaFX-Controls bieten fast alle ihrer Eigenschaften auch als Properties an. So kann beispielsweise der Textinhalt eines Textfelds per Databinding an den Labeltext eines Labels gebunden werden. Tippt der Nutzer etwas in das Textfeld, ändert sich unmittelbar auch das Label.
Für die Umsetzung von MVVM bedeutet dies: Der UI-Zustand wird vom ViewModel per Properties nach außen gegeben. Ob ein Button inaktiv ist oder nicht wird beispielsweise durch ein BooleanProperty repräsentiert. Die View hat nun die Aufgabe, die Eigenschaften der UI-Controls per Databinding mit den Properties des ViewModels zu verbinden. Das folgende Codebeispiel soll das Prinzip verdeutlichen:
public class MyViewModel { private StringProperty labelText = new SimpleStringProperty("default"); private StringProperty inputText = new SimpleStringProperty(); private BooleanProperty buttonDisabled = new SimpleBooleanProperty(); public MyViewModel() { buttonDisabled.bind(inputText.isEmpty()); } public void changeLabel() { labelText.set(inputText.get()); inputText.set(""); } // getter, setter, property-accessors } public class MyView { @FXML Label label; @FXML TextField input; @FXML Button button; MyViewModel viewModel = new MyViewModel(); public void initialize() { label.textProperty().bind(viewModel.labelTextProperty()); input.textProperty() .bindBidirectional(viewModel.inputProperty()); button.disableProperty() .bind(viewModel.buttonDisabledProperty()); } // will be called when button is pressed public void onAction() { viewModel.changeLabel(); } }
Das Beispiel besteht aus einem Label, einem Textfeld und einem Button. Der Button soll nur aktiv sein, wenn das Textfeld nicht leer ist. Wird der Button gedrückt, soll der Wert aus dem Textfeld als neuer Labeltext gesetzt werden. Das Textfeld wird anschließend geleert. Wie zu sehen ist, besitzt die View-Klasse keine besondere Logik, sondern bindet lediglich die Eigenschaften der UI-Controls an das ViewModel und ruft eine Methode im ViewModel auf, sobald der Button gedrückt wird.
Die Logik befindet sich nur im ViewModel. Für dieses ViewModel kann ganz einfach auch ein Unit-Test erstellt werden, welcher die Usecases und eventuelle Sonderfälle abprüft. Tatsächlich kann das ViewModel sogar testgetrieben entwickelt werden, indem zuerst ein Unittest und erst anschließend die Implementierung des ViewModels geschrieben wird.
Der Nutzen von MVVM in Form von einfach testbarem Oberflächencode ist unverkennbar. Allerdings geht dieser Vorteil mit einigen Einschränkungen einher: Neben der höheren Disziplin, die von Entwicklern verlangt wird, werden zunächst einmal auch mehr Klassen benötigt, wobei diese jeweils für sich gut abgesteckte Zuständigkeiten besitzen. Vor allem bei komplexeren Oberflächen kann aber die Koordinierung und Kommunikation zwischen ViewModellen kompliziert werden.
Um diese Fallstricke zu vermeiden, existiert für JavaFX das Open Source-Applikationsframework (mvvmfx) [1], bei dem der Autor dieses Artikels als Entwickler mitwirkt. Dieses Framework bietet Basisklassen und Interfaces um JavaFX-GUIs nach MVVM zu entwickeln. Ein wichtiger Teil dazu ist die Integration von Dependency-Injection-Frameworks, die benutzt werden können um Abhängigkeiten zwischen Klassen wohlstrukturiert abbilden zu können. Daneben werden zahlreiche Hilfsmittel mitgeliefert, die die JavaFX-Entwicklung vereinfachen sollen, wie beispielsweise ein Validierungsmechanismus oder erweiterte Unterstützung für ResourceBundles und Internationalisierung.
Zusammenfassung
Als Fazit lässt sich sagen, dass MVVM einige Vorteile im Vergleich zu herkömmlichen UI-Patterns bietet. Die Möglichkeit, auch Oberflächen schon während der Entwicklung umfangreich mittels Unittests zu überprüfen, bedeutet einen echten Mehrwert für die Entwicklung. Die Umsetzung mit JavaFX und der Properties- und Databinding-API ist naheliegend, angenehm und sorgt für eine saubere Trennung der Zuständigkeiten. Vor allem aber kann damit das ViewModel losgelöst und ohne statische Abhängigkeiten zur View umgesetzt werden. Durch die Unterstützung eines speziellen MVVM-Frameworks wie mvvmFX, kann auch die Entwicklung von komplexeren Oberflächen einfach und sauber nach MVVM durchgeführt werden. Auf diese Weise kommen die Vorteile des Patterns maximal zur Geltung, während die Nachteile minimiert werden können.