JavaFX: Model / View / Controller

JavaFX Serie

Wir haben bisher den Controller und die View gesehen, konnten Aktionen durchführen, aber wir haben bisher noch nicht mit Daten gearbeitet.

HelloWorldModel

Die Daten liegen in einem Model vor. Dazu erstellen wir uns nun erst einmal eine eigene Klasse: HelloWorldModel

package helloworld;<br></br><br></br>/**<br></br> * Our Model for Hello World.<br></br> */<br></br>public class HelloWorldModel {<br></br><br></br>}

Nun können wir ein erstes Element hinzu fügen: Unser Greeting. Ein Binding setzt voraus, dass man informiert werden kann, wenn sich Werte ändern. Dazu existieren in JavaFX diverse Property Klassen.

Für String Werte gibt es StringProperty und SimpleStringProperty, welche wir nutzen können.

So erstellen wir uns eine Variable greeting:

private StringProperty greeting = new SimpleStringProperty();

Wenn der Controller Werte lesen können soll, dann wird ein Getter benötigt:

public String getGreeting() { return greeting.get(); }

Für Schreibzugriffe wird der Setter benötigt:

public void setGreeting(final String greeting) { this.greeting.setValue(greeting); }

Und falls die Oberfläche Änderungen mitbekommen soll, so muss das Property selbst bekannt gemacht werden:

public StringProperty greetingProperty() { return greeting; }

Nach dem gleichen Schema können wir nun auch noch den Namen hinzu fügen sowie eine Methode, die das Greeting ändert, wenn der Name geändert wurde:

package helloworld;<br></br><br></br>import javafx.beans.property.SimpleStringProperty;<br></br>import javafx.beans.property.StringProperty;<br></br><br></br>/**<br></br> * Our Model for Hello World.<br></br> */<br></br>public class HelloWorldModel {<br></br><br></br>    private StringProperty name = new SimpleStringProperty();<br></br><br></br>    public void setName(final String name) {<br></br>        this.name.setValue(name);<br></br>    }<br></br><br></br>    public String getName() { return name.get(); }<br></br><br></br>    public StringProperty nameProperty() { return name; }<br></br><br></br>    private StringProperty greeting = new SimpleStringProperty();<br></br><br></br>    public void setGreeting(final String greeting) {<br></br>        this.greeting.setValue(greeting);<br></br>    }<br></br><br></br>    public String getGreeting() { return greeting.get(); }<br></br><br></br>    public StringProperty greetingProperty() { return greeting; }<br></br><br></br>    public HelloWorldModel() {<br></br>        setGreeting("Hallo Welt!");<br></br>    }<br></br><br></br>    public void updateGreeting() {<br></br>        if (name.get().length() > 0) {<br></br>            greeting.setValue("Hallo " + name.get() + "!");<br></br>            name.setValue("Bitte Namen eingeben");<br></br>        } else {<br></br>            greeting.setValue("Hallo Welt!");<br></br>        }<br></br>    }<br></br>}

Damit hätten wir ein fertiges Model, welches wir nutzen können.

HelloWorldController

Den Controller können wir nun noch entsprechend aufbauen:

  • Wir benötigen eine Instanz vom Model sowie ein Getter, damit das Model gelesen werden kann.
  • Wir benötigen die Behandlung von 2 Actions: Einmal zum Beenden der Applikation und zum Anderen um das Greeting anpassen zu können.
package helloworld;<br></br><br></br>import javafx.event.ActionEvent;<br></br><br></br>public class HelloWorldController {<br></br><br></br>    /**<br></br>     * Model with our data.<br></br>     */<br></br>    private HelloWorldModel model = new HelloWorldModel();<br></br><br></br>    /**<br></br>     * Gets our model.<br></br>     * @return Our model.<br></br>     */<br></br>    public HelloWorldModel getModel() { return model; }<br></br><br></br>    /**<br></br>     * Close application.<br></br>     * @param e ActionEvent (unused).<br></br>     */<br></br>    public void closeApplicationAction(final ActionEvent e) {<br></br>        System.exit(0);<br></br>    }<br></br><br></br>    /**<br></br>     * Updates Greeting in model.<br></br>     * @param e ActionEvent (unused).<br></br>     */<br></br>    public void updateGreeting(final ActionEvent e) { model.updateGreeting();}<br></br>}

HelloWorld.fxml

Nun fehlt zuletzt nur noch die View.

In der View benötigen wir ein Label für unser Greeting, ein Eingabefeld für den Namen und zwei Buttons: Einmal zum Aktualisieren und zum Anderen um die Applikation zu beenden.

Bei mir sieht dies auf die Schnelle so aus:

Achtung: Bezüglich Bindings unterstützt der SceneBuilder leider nicht, was wir hier benötigen. Daher ist es nach der rein optischen Erstellung dieser Maske leider Zeit, uns vom SceneBuilder wieder zu verabschieden (Für diesen Blog-Beitrag – wir benötigen diesen ab dem nächsten Beitrag wieder). Wir werden später auch weitere Möglichkeiten lernen, wie wir Fenster schnell und sauber ohne GUI Editor erstellen können, die dann auch in der Größe veränderbar sind und gut aussehen. Oder wie wir schnell und einfach Formulare erstellen können.

Somit schließen wir den SceneBuilder und editieren nur noch das Textfile direkt z.B. in IntelliJ.

Als erstes binden wir das Label an unsere greeting Property in unserem Model:

<Label ... text="${controller.model.greeting}" ... >

Über die ${} können wir ein Binding erstellen. Wichtig ist, dass der Controller gesetzt ist so dass wir auf das model zugreifen können (Getter muss da sein!) und dann auf die Property greeting zugreifen können (Getter und greetingProperty müssen da sein!).

Die Buttons binden wir an unsere Methoden im Controller:

<Button ... onAction="#closeApplicationAction" text="Beenden" /><br></br><Button ... onAction="#updateGreeting" text="Aktualisiere" />

Nun fehlt nur noch das Binding des TextFields. Hier laufen wir in ein kleines Problem, wenn wir benötigen ein Bidirectionales Binding, denn wir wollen den Text ja verändern können. Das ist leider etwas, das derzeit nicht im fxml möglich ist. In Zukunft ist dies vorgesehen über die # statt dem $.

Nun haben wir Code, den wir nicht in den Controller packen wollen, also müssen wir etwas in die Trickkiste greifen:

Wir können Scripts mit in die fxml Datei packen. Dazu müssen wir als erstes im Kopf der Datei die Sprache angeben, die wir verwenden wollen:

<?language javascript?>

Nun können wir das TextField definieren ohne jedes Binding aber mit einer fx🆔

<TextField fx:id="nameTextField" ... text="Bitte Namen eingeben." />

Nun benötigen wir Code, der ein bidirectional Binding aufbaut. Dazu müssen wir in der Klasse Bindings die Methode bindBidirectional aufrufen und dabei die Property und das Control angeben:

<fx:script><br></br>   javafx.beans.binding.Bindings.bindBidirectional(<br></br>         controller.model.nameProperty(),<br></br>         nameTextField.textProperty()<br></br>   );<br></br></fx:script>

Mit diesem Trick haben wir nun auch das Binding in beide Richtungen erstellt und es spricht nichts dagegen, die Applikation einmal zu starten und dann einen Namen anzugeben und zu staunen, wie das Greeting sich ändert, wenn man den Knopf drückt und erneut der Text erscheint: „Bitte Name eingeben“.

Der Code des ganzen Projekts findet sich unter https://github.com/kneitzel/blog-javafx-series im Verzeichnis „04 helloworld-mvc“

Edit

  • 2020-07-03: Wechsel zu einem Repository für die ganze Serie.
  • 2020-06-29: Den Part bezüglich SceneEditor etwas überarbeitet – wir werden diesen in späteren Blog Beiträgen wieder nutzen (können).