architecture-mvc-mvvm-javafx.html 23.9 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
<h4>MVC in JavaFX</h4>

<p>Die Architekturmuster bleiben ohne konkrete Umsetzung in einem Framework sehr abstrakt. Zur Veranschaulichung von MVC und MVVM sollen diese beiden Muster folgend in JavaFX umgesetzt werden. JavaFX wird hier als populäres UI-Framework für Desktop-Anwendungen in Java exemplarisch ausgewählt. Auf Grundlagen von JavaFX wird an dieser Stelle nur kurz eingegangen. Eine ausführlichere Einführung in JavaFX erfolgt im Kapitel <a href="ui-javafx" class="navigate">Desktop-App mit JavaFX</a>.</p>

<p>Als Beispiel soll das einfache Spiel <a href="https://de.wikipedia.org/wiki/Tic-Tac-Toe">Tic-Tac-Toe</a> implementiert werden. Das UI wird in der folgenden Abbildung dargestellt. Die <i>View</i> enthält folgende Attribute, um ihren Zustand zu erfassen, und Methoden zur Steuerung der Anwenderinteraktion:
<ul>
<li>Attribut <code>board</code>: Spielbrett bestehend 9 Zellen, die jeweils leer oder mit einem X oder einem O belegt sind</li>
<li>Attribut <code>winner</code>: Label, dass ggf. einen Gewinner ausweist</li>
<li>Methode <code>selectCell</code>: Auswählen einer Zelle, um diese im Wechsel mit einem X oder einem O zu markieren</li>
<li>Methode <code>reset</code>: Zurücksetzen des bisherigen Spiels, um ein neues Spiel zu starten</li>
</ul>
</p>

<img src="media/architecture_tictactoe.png" style="width:200px">
<label>UI der Beispielanwendung Tic-Tac-Toe</label>

<p>Es folgt der Code einer Tic-Tac-Toe-Implementierung, die dem gemäß Dokumentation vorgeschlagen MVC-Muster in JavaFX folgt. Der Code wird anschließend erläutert.</p>

<ul class="nav nav-tabs" id="mvc-tabs" role="tablist">
  <li class="nav-item"><a href="#mvc-tabs-view" class="nav-link active" data-toggle="tab" role="tab">View</a></li>
  <li class="nav-item"><a href="#mvc-tabs-controller" class="nav-link" data-toggle="tab" role="tab">Controller</a></li>
  <li class="nav-item"><a href="#mvc-tabs-model" class="nav-link" data-toggle="tab" role="tab">Model</a></li>
  <li class="nav-item"><a href="#mvc-tabs-app" class="nav-link" data-toggle="tab" role="tab">Application</a></li>
</ul>
<div class="tab-content" id="mvc-tabs-content">
  <div class="tab-pane show active" id="mvc-tabs-view" role="tabpanel">
	<pre><code class="language-xml line-numbers"><!-- <StackPane xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="controller.TicTacToeController">
    <stylesheets><URL value="@tictactoe.css" /></stylesheets>
    <children>
        <VBox>
            <GridPane fx:id="board" hgap="10" vgap="10"/>
            <Label fx:id="winner" styleClass="winnerMessage"/>
            <HBox alignment="CENTER_RIGHT">
                <Button onAction="#reset" text="Reset"/>
            </HBox>
        </VBox>
    </children>
</StackPane> --></code></pre> 
  </div>
  <div class="tab-pane" id="mvc-tabs-controller" role="tabpanel">
	<pre><code class="language-java line-numbers">import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
// ...
	
public class TicTacToeController {

    private Board model; // controller is connected to model here

    @FXML
    private GridPane board;

    @FXML
    private Label winner;

    @FXML
    protected void initialize() {
        model = new Board();
        for (int row = 0; row &lt; 3; row++) {
            for (int col = 0; col &lt; 3; col++) {
                Button cell = new Button();
                cell.getStyleClass().add("cell");
                cell.setOnAction(event -> selectCell(event));
                board.add(cell, col, row);
            }
        }
    }

    public void selectCell(ActionEvent event) {
        Button cell = (Button) event.getSource();
        int row = GridPane.getRowIndex(cell);
        int col = GridPane.getColumnIndex(cell);

        Player player = model.markCell(row, col);
        if (player != null) {
            cell.setText(player.toString());
            if (model.getWinner() != null) {
                winner.setText("Winner is " + player.toString());
            }
        }
    }

    public void reset(ActionEvent event) {
        model.restart();
        winner.setText("");
        board.getChildren().forEach(node -> ((Button) node).setText(""));
    }
}</code></pre> 
  </div>
  <div class="tab-pane" id="mvc-tabs-model" role="tabpanel">
	<pre><code class="language-java line-numbers">enum Player {X, O}
	
class Cell {
	Player value;
}
	
class Board {
    Cell[][] cells = new Cell[3][3];
    Player currentTurn;	
    Player winner;

    public Board() { restart(); }

    /**
     * Start a new game, i.e. clear the board and the game status.
     */
    public void restart() {
        clearCells();
        winner = null;
        currentTurn = Player.X;
    }

    /**
     * Mark a cell for the player in turn.
     * Nothing is done, if row or col are out of range, or the cell is already marked, or the game is finished.
     *
     * @param row 0..2
     * @param col 0..2
     * @return player who made the turn
     */
    public Player markCell(int row, int col) {
        if (isValid(row, col)) {
            Player player = currentTurn;
            cells[row][col].value = currentTurn;
            if (isWinningMoveByPlayer(currentTurn, row, col)) { // check if game is won
                winner = currentTurn;
            } else {
                currentTurn = currentTurn == Player.X ? Player.O : Player.X; // flip current turn
            }
            return player;
        }
        return null;
    }

    public Player getWinner() { return winner; }

    void clearCells() {
        for (int i = 0; i &lt; 3; i++) {
            for (int j = 0; j &lt; 3; j++) {
                cells[i][j] = new Cell();
            }
        }
    }

    boolean isValid(int row, int col) {
        return !(winner != null || isOutOfBounds(row) || isOutOfBounds(col) || isCellValueAlreadySet(row, col));
    }

    boolean isOutOfBounds(int i) { return i &lt; 0 || i > 2; }

    boolean isCellValueAlreadySet(int row, int col) { return cells[row][col].value != null; }

    boolean isWinningMoveByPlayer(Player player, int row, int col) {
        return cells[row][0].value == player && cells[row][1].value == player && cells[row][2].value == player // 3 in a row
                || cells[0][col].value == player && cells[1][col].value == player && cells[2][col].value == player // 3 in a col
                || cells[0][0].value == player && cells[1][1].value == player && cells[2][2].value == player // 3 in a diagonal
158
                || cells[0][2].value == player && cells[1][1].value == player && cells[2][0].value == player; // other diagonal
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
    }
}</code></pre> 
  </div>
  <div class="tab-pane" id="mvc-tabs-app" role="tabpanel">
	<pre><code class="language-java line-numbers">import javafx.application.Application;
// ...
	
public class TicTacToeApplicationMVC extends Application {

	@Override
	public void start(Stage stage) throws Exception {
		Parent view = FXMLLoader.load(getClass().getResource("view/tictactoe.fxml"));
		Scene scene = new Scene(view);
		stage.setScene(scene);
		stage.show();
	}

	public static void main(String[] args) { launch(args); }
}</code></pre> 
  </div>
</div>

<ul>
<li><b>View</b>: In JavaFX kann das UI sowohl deklarativ beschrieben als auch imperativ aufgebaut werden. Beide Ansätze können auch kombiniert werden. Es ist zu empfehlen, die statische Grundstruktur einer <i>View</i>, z.B. den grundlegenden Aufbau eines Formulars, deklarativ zu beschreiben. Die Auszeichnungssprache von JavaFX basiert auf XML und heißt <a href="https://docs.oracle.com/javase/8/javafx/get-started-tutorial/get_start_apps.htm">FXML</a>. In obigem Code ist die Grundstruktur der <i>View</i> in einer Datei <code>view/tictactoe.fxml</code> mittels FXML deklariert. 
<ul>
<li>In Zeile 1 wird die <i>View</i> mit einer <i>Controller</i>-Klasse verknüpft: <code>fx:controller="controller.TicTacToeController"</code>. In JavaFX wird die <i>View</i> auch im MVC-Muster nicht direkt auf das <i>Model</i> zugreifen, sondern indirekt über den <i>Controller</i> kommunizieren.</li>
<li>In JavaFX kann das Layout einer <i>View</i> wie aus der Web-Entwicklung bekannt mittels <i>Cascading Stylesheets (CSS)</i> gestaltet werden. In Zeile 2 wird auf ein Stylesheet verwiesen, in dem die verwendeten Style-Klassen definiert sind.</li>
<li>Das GridPane (Zeile 5), das später die 9 Zellen des Spielbretts enthalten soll, ist hier leer. Die Zellen werden dynamisch bei der Initialisierung des <i>Controllers</i> als Kindelemente hinzugefügt.</li>
</ul>
</li>
<li><b>Controller</b>: JavaFX erzeugt beim Laden der <i>View</i> automatisch ein Objekt der verknüpften <i>Controller</i>-Klasse. Diese Klasse hat diverse Abhängigkeiten zu spezifischen UI-Elementen von JavaFX, z.B. <code>javafx.fxml.FXML</code> und <code>javafx.scene.control.Button</code> (Zeilen 1-4).
<ul>
<li>Die Attribute, die mit einer <code>FXML</code>-Annotation versehen sind, werden an ein Element in der deklarativen <i>View</i> bidirektional gebunden. Die Konvention ist, dass das Id-Attribut <code>fx:id</code> des zu verbindenden Elements im FXML-Dokument der Bezeichnung des Attributs in der <i>Controller</i>-Klasse entspricht und die jeweiligen Typen übereinstimmen. Das Label <code>&lt;Label fx:id="winner"></code> (Zeile 6 in <i>View</i>) ist z.B. über seine Id an das Attribut <code>Label winner</code> (Zeile 15 in <i>Controller</i>) gebunden. Die Attribute müssen nicht instantiiert werden, die entsprechenden Objekte werden von JavaFX injiziert (Dependency Injection).</li>
<li>JavaFX führt die mit einer <code>FXML</code>-Annotation versehene Methode <code>initialize</code> aus, während das <i>Controller</i>-Objekt erzeugt wird. In dieser Methode wird das <i>Model</i> instantiiert (Zeile 19), und es werden 9 Buttons dem Spielbrett als Zellen hinzugefügt (Zeilen 20-27). Es bietet sich an, die Grundstruktur einer <i>View</i> immer dann dynamisch zu erweitern, wenn (wie hier) eine Menge ähnlicher Elemente hinzugefügt werden soll und dazu mittels einer oder mehrerer Schleifen über die Menge iteriert werden kann.</li>
<li>Die Methode <code>reset</code> wird über ihre Bezeichnung in der View referenziert (Zeile 8: <code>&lt;Button onAction="#reset"></code>) und durch JavaFX aufgerufen, wenn der Anwender den Button anklickt.</li>
</ul>
</li>
<li><b>Model</b>: Im <i>Model</i> ist die einfache Logik des Spiels Tic-Tac-Toe implementiert – und das unabhängig davon, mit welchem UI-Framework das Spiel dargestellt wird. In diesem Fall ist das <i>Model</i> einem JavaFX-<i>Controller</i> (<code>TicTacToeController</code> für JavaFX) bekannt, der es nach Eingaben des Anwenders manipuliert. Es ist vorstellbar, dass das Spiel in einem weiteren UI-Framework für andere Zielumgebungen (z.B. Android für Smartphones) implementiert wird. Mehrere <i>Controller</i> könnten sich dann ein gemeinsames <i>Model</i> teilen.</li>
<li><b>Application</b>: Die Klasse <code>TicTacToeApplicationMVC</code> erweitert die generische Klasse <code>javafx.application.Application</code> und startet die Anwendung. In der Methode <code>start</code> wird die <i>View</i> <code>view/tictactoe.fxml</code> initial geladen und angezeigt.</li>
</ul>

<h4>MVVM in JavaFX</h4>

<p>Die Beispielanwendung soll jetzt dahingehend umgebaut (oder auch "refaktorisiert") werden, dass sie dem MVVM-Muster entspricht. Die Grundidee von MVVM ist es, den gesamten Zustand des UI im <i>ViewModel</i> abzubilden. Das <i>ViewModel</i> enthält also Attribute für die aktuellen Werte in Textfeldern, Tabellen und anderen UI-Elementen. Auch unscheinbare Aspekte, wie z.B. ob ein Button aktiv oder inaktiv ist, werden im <i>ViewModel</i> erfasst. Die <i>View</i> präsentiert nur den Zustand des <i>ViewModel</i>. Wenn der Zustand durch den Anwender verändert wird, z.B. indem in einem Textfeld getippt wird, delegiert die <i>View</i> diese Interaktion umgehend an das <i>ViewModel</i>. Entgegengesetzt muss auch eine Änderung, die vom <i>ViewModel</i> ausgeht, umgehend in der <i>View</i> angezeigt werden. <i>View</i> und <i>ViewModel</i> müssen also stets synchron gehalten werden.</p>

<p>In JavaFX ermöglichen Implementierungen des Interface <a href="https://openjfx.io/javadoc/11/javafx.base/javafx/beans/property/Property.html">Property</a> sowohl unidirektionales als auch bidirektionales <i><a href="https://en.wikipedia.org/wiki/Data_binding">Data Binding</a></i>. Bidirektionales Binding (auch <i>Two-Way-Binding</i> genannt) basiert in JavaFX darauf, dass sich zwei <code>Property</code>-Objekte gegenseitig beobachten. Der folgende Code verdeutlicht das Prinzip anhand von zwei <a href="https://openjfx.io/javadoc/11/javafx.base/javafx/beans/property/StringProperty.html"><code>StringProperty</code></a>-Objekten.</p>

<pre><code class="language-java line-numbers">StringProperty a = new SimpleStringProperty();
StringProperty b = new SimpleStringProperty();

a.bindBidirectional(b); // two-way-binding

a.set("Hello");
System.out.println(b.get()); // prints "Hello"

b.set("World!");
System.out.println(a.get()); // prints "World!"</code></pre>

<span>Die Aufteilung in <i>View</i> und <i>ViewModel</i> hat folgende Vorteile:</span>
<ul>
<li>Designer können das UI hinsichtlich einer einfachen Bedienbarkeit gestalten, ohne Aspekte der Präsentationslogik im <i>ViewModel</i> beachten zu müssen. Gleichzeitig können Entwickler die Präsentationslogik implementieren, ohne die konkrete Darstellung der <i>View</i> zu kennen.</li>
<li>Die Testbarkeit wird stark verbessert. Die <i>View</i> enthält nahezu keinen Kontrollfluss mehr, der im Unit Testing	 zu berücksichtigen ist. Ein wichtiger Vorteil ist es, dass das <i>ViewModel</i> frei von Abhängigkeiten zu den UI-Elementen von JavaFX ist. Daher müssen auch die <code>FXML</code>-Annotationen aus dem <i>ViewModel</i> ausgelagert werden, damit das <i>ViewModel</i> losgelöst von JavaFX getestet werden kann. Es kann z.B. einfach mittels <a href="https://junit.org/">JUnit</a> getestet werden, wo vorher noch ein aufwändiger Integrationstest erforderlich war, in dem zunächst die gesamte Anwendung gestartet und Anwenderverhalten simuliert werden musste.</li>
</ul>

<p>Im folgenden Code wird zur Implementierung des MMVM-Musters das Framework <a href="http://sialcasa.github.io/mvvmFX/">MvvmFX</a> eingesetzt, das als Zusatz auf dem JavaFX-Standard aufsetzt. Es folgt zunächst wieder der Code, der anschließend erläutert wird.</p>

<ul class="nav nav-tabs" id="mvvm-tabs" role="tablist">
  <li class="nav-item"><a href="#mvvm-tabs-view" class="nav-link active" data-toggle="tab" role="tab">View</a></li>
  <li class="nav-item"><a href="#mvvm-tabs-viewmodel" class="nav-link" data-toggle="tab" role="tab">ViewModel</a></li>
  <li class="nav-item"><a href="#mvvm-tabs-model" class="nav-link" data-toggle="tab" role="tab">Model</a></li>
  <li class="nav-item"><a href="#mvvm-tabs-app" class="nav-link" data-toggle="tab" role="tab">Application</a></li>
</ul>
<div class="tab-content" id="mvvm-tabs-content">
  <div class="tab-pane show active" id="mvvm-tabs-view" role="tabpanel">
	<pre>
<code class="language-java line-numbers"> // TicTacToeView.fxml</code>
<code class="language-xml line-numbers"><!-- <StackPane xmlns="http://javafx.com/javafx/8.0.121" xmlns:fx="http://javafx.com/fxml/1" fx:controller="view.TicTacToeView">
    <stylesheets><URL value="@../tictactoe.css"/></stylesheets>
    <children>
        <VBox>
            <GridPane fx:id="board" hgap="10" vgap="10"/>
            <Label fx:id="winner" styleClass="winnerMessage"/>
            <HBox alignment="CENTER_RIGHT">
                <Button onAction="#reset" text="Reset"/>
            </HBox>
        </VBox>
    </children>
</StackPane> --></code>
<code class="language-java line-numbers">
// TicTacToeView.java
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.GridPane;
import de.saxsys.mvvmfx.FxmlView;
import de.saxsys.mvvmfx.InjectViewModel;
// ...

public class TicTacToeView implements FxmlView&lt;TicTacToeViewModel>, Initializable {

    @InjectViewModel
    private TicTacToeViewModel viewModel;

    @FXML
    private GridPane board;

    @FXML
    private Label winner;

    @Override
    public void initialize(URL url, ResourceBundle resourceBundle) {
        winner.textProperty().bindBidirectional(viewModel.winnerMessage); // winner label is bind to view model

        for (int row = 0; row &lt; 3; row++) {
            for (int col = 0; col &lt; 3; col++) {
                Button cell = new Button();
                cell.getStyleClass().add("cell");
                cell.setOnAction(event -> selectCell(event));
                cell.textProperty().bindBidirectional(viewModel.cells[row][col]); // each cell button is bind to view model
                board.add(cell, col, row);
            }
        }
    }

    public void selectCell(ActionEvent event) {
        Button cell = (Button) event.getSource();
        viewModel.selectCell(GridPane.getRowIndex(cell), GridPane.getColumnIndex(cell));
    }

    public void reset(ActionEvent event) {viewModel.reset(); }
}</code></pre> 
  </div>
  <div class="tab-pane" id="mvvm-tabs-viewmodel" role="tabpanel">
	<pre><code class="language-java line-numbers">import javafx.beans.property.StringProperty;
import de.saxsys.mvvmfx.ViewModel;	
// ...
	
public class TicTacToeViewModel implements ViewModel {

    private Board model; // view model is connected to model here

    public StringProperty[][] cells = new StringProperty[3][3];
    public StringProperty winnerMessage = new SimpleStringProperty();

    public TicTacToeViewModel() {
        model = new Board();
        for (StringProperty[] cellsInRow : cells) {
            Arrays.setAll(cellsInRow, i -> new SimpleStringProperty());
        }
    }

    public void selectCell(int row, int col) {
        Player player = model.markCell(row, col);
        if (player != null) {
            cells[row][col].setValue(player.toString());
            if (model.getWinner() != null) {
                winnerMessage.setValue("Winner is " + player.toString());
            }
        }
    }

    public void reset() {
        model.restart();
        winnerMessage.setValue("");
        for (StringProperty[] cellsInRow : cells) {
            Arrays.stream(cellsInRow).forEach(cell -> cell.setValue(""));
        }
    }
}</code></pre> 
  </div>
  <div class="tab-pane" id="mvvm-tabs-model" role="tabpanel">
	<pre><code class="language-java line-numbers">enum Player {X, O}
	
class Cell {
	Player value;
}
	
class Board {
    Cell[][] cells = new Cell[3][3];
    Player currentTurn;	
    Player winner;

    public Board() { restart(); }

    /**
     * Start a new game, i.e. clear the board and the game status.
     */
    public void restart() {
        clearCells();
        winner = null;
        currentTurn = Player.X;
    }

    /**
     * Mark a cell for the player in turn.
     * Nothing is done, if row or col are out of range, or the cell is already marked, or the game is finished.
     *
     * @param row 0..2
     * @param col 0..2
     * @return player who made the turn
     */
    public Player markCell(int row, int col) {
        if (isValid(row, col)) {
            Player player = currentTurn;
            cells[row][col].value = currentTurn;
            if (isWinningMoveByPlayer(currentTurn, row, col)) { // check if game is won
                winner = currentTurn;
            } else {
                currentTurn = currentTurn == Player.X ? Player.O : Player.X; // flip current turn
            }
            return player;
        }
        return null;
    }

    public Player getWinner() { return winner; }

    void clearCells() {
        for (int i = 0; i &lt; 3; i++) {
            for (int j = 0; j &lt; 3; j++) {
                cells[i][j] = new Cell();
            }
        }
    }

    boolean isValid(int row, int col) {
        return !(winner != null || isOutOfBounds(row) || isOutOfBounds(col) || isCellValueAlreadySet(row, col));
    }

    boolean isOutOfBounds(int i) { return i &lt; 0 || i > 2; }

    boolean isCellValueAlreadySet(int row, int col) { return cells[row][col].value != null; }

    boolean isWinningMoveByPlayer(Player player, int row, int col) {
        return cells[row][0].value == player && cells[row][1].value == player && cells[row][2].value == player // 3 in a row
                || cells[0][col].value == player && cells[1][col].value == player && cells[2][col].value == player // 3 in a col
                || cells[0][0].value == player && cells[1][1].value == player && cells[2][2].value == player // 3 in a diagonal
396
                || cells[0][2].value == player && cells[1][1].value == player && cells[2][0].value == player; // other diagonal
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
    }
}</code></pre> 
  </div>
  <div class="tab-pane" id="mvvm-tabs-app" role="tabpanel">
	<pre><code class="language-java line-numbers">import javafx.application.Application;
// ...
	
public class TicTacToeApplicationMVVM extends Application {

	@Override
	public void start(Stage stage) {
		ViewTuple&lt;TicTacToeView, TicTacToeViewModel> viewTuple = FluentViewLoader.fxmlView(TicTacToeView.class).load();
		Scene scene = new Scene(viewTuple.getView());
		stage.setScene(scene);
		stage.show();
	}

	public static void main(String[] args) { launch(args); }
}</code></pre> 
  </div>
</div>

<ul>
<li><b>View</b>: Nach dem Verständnis des MVVM-Musters besteht die <i>View</i> nun aus 2 Dateien: 
<ul>
<li>zum einen aus dem deklarativen FXML-Dokument, das den strukturellen Aufbau der <i>View</i> beschreibt (Zeilen 2-13),</li>
<li>und zum anderen aus der per Two-Way-Binding verknüpften Java-Klasse, die den Zustand und das Verhalten der <i>View</i> steuert und die im obigen MVC-Muster in Teilen dem <i>Controller</i> entsprach (Zeilen 16-56).</li>
</ul>
Eine Konvention des MvvmFX-Framework verlangt es, dass beide Dateien im gleichen Verzeichnis liegen und gleich benannt sind, hier demzufolge <code>view/TicTacToeView.fxml</code> und <code>view/TicTacToeView.java</code>. Das FXML-Dokument bleibt inhaltlich unverändert. Die Java-Klasse enthält die Abhängigkeiten zu den UI-Elementen von JavaFX, z.B. sämtliche <code>FXML</code>-Annotationen (Zeilen 16-20). In der Klasse wird sämtliche Logik entfernt, die den Zustand der <i>View</i> verändert, und stattdessen an das <i>ViewModel</i> delegiert, z.B. in den Methoden <code>selectCell</code> (Zeile 52) und <code>reset</code> (Zeile 55). Der initiale dynamische Aufbau der <i>View</i> (Zeilen 39-47), der alternativ auch in FXML hätte deklariert werden können, gehört weiterhin zur <i>View</i>. Das <i>ViewModel</i> wird per Dependency Injection injiziert (Zeilen 26-27). Die Attribute, die den Zustand der <i>View</i> abbilden, werden im <i>ViewModel</i> gespiegelt, und per Two-Way-Binding verknüpft (Zeilen 37 und 44). Dazu wird jeweils eine beobachtbare JavaFX-<code>StringProperty</code> genutzt.
</li>
<li><b>ViewModel</b>: Das <i>ViewModel</i> hat keine Abhängigkeiten zu UI-Elementen von JavaFX (s. <code>import</code>-Anweisungen in Zeilen 1-2 im <i>ViewModel</i>). Wie der <i>Controller</i> in MVC kennt das <i>ViewModel</i> eine Instanz des <i>Model</i>, um es zu manipulieren (Zeile 7). Der Zustand der <i>View</i> wird über die <code>StringProperty</code>-Attribute in den Zeilen 9-10 gespiegelt. Ansonsten enthält das <i>ViewModel</i> die Präsentationslogik, die die Anwenderinteraktion steuert, d.h. hier die Methoden <code>selectCell</code> und <code>reset</code>. Für diese Methoden können einfache <i>Unit Tests</i> geschrieben werden – und zwar unabhängig von JavaFX.
</li>
<li><b>Model</b>: Das <i>Model</i> ist nicht verändert worden.</li>
430
<li><b>Application</b>: Wie zuvor startet die Klasse <code>TicTacToeApplicationMVVM</code> in gewohnter Weise die JavaFX-Anwendung. Beim initialen Laden der <i>View</i> (Zeile 8) wird die Klasse <code>FluentViewLoader</code> von MvvmFX verwendet, um das <i>ViewModel</i> an die <i>View</i> zu binden. Ab hier verwaltet das Framework beide Objekte in einem sogenannten <code>ViewTuple</code>.</li>
431
432
433
</ul>

<p>Die gezeigten Code-Beispiele zur Realisierung des MVC- und MVVM-Musters in JavaFX finden sich in den Verzeichnissen <a href="/architecture/mvc" class="repo-link">/architecture/mvc</a> und <a href="/architecture/mvvm" class="repo-link">/architecture/mvvm</a> des Modul-Repository.</p>