Übungsaufgaben zu Formularen und Dialogen: ein Mitfahrgesuch eintragen

https://bildung.social/@oerinformatik/

https://oer-informatik.de/javafx-formulare-dialoge-uebungsaufgaben

JavaFX, Formular, Dialog, Alert, DatePicker, Spinner, ComboBox, TextFormatter, Validierung, Fehlersuche, Übungsaufgaben, Prüfungsvorbereitung

tl/dr; (Bearbeitungszeit insgesamt 56 min, mehrere Aufgaben): Übungen zu Formularen und Dialogen in der JavaFX-Desktop-App: Du ordnest Steuerelemente und Arten des Alert zu und sagst vorher, was ein Eingabefilter aus einer Tippfolge macht, baust ein geprüftes Formular für Mitfahrgesuche als ViewModel mit Tests und einen eigenen Dialog dazu und findest vier Fehler in einem Formular anhand ihrer Symptome [AI-Assisted]. (Zuletzt geändert am 11.11.2026)

Dieser Artikel ist Teil einer Artikelreihe über eine Desktop-App mit JavaFX: Übersicht / Kanban-Projektstart / Projekt anlegen / FXML, Layouts und CSS / Properties, Bindings und MVVM / Backend anbinden / TableView für Buchungen / Formulare und Dialoge / Anmeldung mit Keycloak / Verwaltung und Diagramme / GUI-Tests mit TestFX / Abnahmetest und Kanban-Metriken / Paketieren mit jpackage

Diese Übungsaufgaben gehören zum Artikel Formulare und Dialoge: Fahrtangebote anlegen, prüfen und absagen. Sie spielen im Projekt Mitfahrlabor und führen den Strang der Mitfahrgesuche weiter: Das Gesuche-Fenster aus Ticket JFX-13 „Offene Gesuche an meinem Haltepunkt anzeigen“ zeigt die Gesuche seit den Übungen zur TableView als Tabelle mit Filter. Jetzt sollen Mitfahrende dort auch selbst ein Gesuch eintragen können.

Überblick: Der Button „Neues Angebot …“ öffnet den AngebotDialog; seine Steuerelemente sind bidirektional an das AngebotFormularViewModel gebunden; das ViewModel liefert je Feld eine Fehlermeldung und das Binding gueltig, das den Button Anlegen sperrt; der ResultConverter macht aus dem Klick auf Anlegen ein AngebotNeu; Absagen fragt per Alert nach (mit KI erstellt)

Hinweise zur Bearbeitung: Bis zur Teil-1-Prüfung der IT-Berufe solltest Du die Basiskompetenz beherrschen, also Aufgabe 1.1. Als FIA, FIDP oder FIDV musst Du zur Teil-2-Prüfung Aufgaben auf Kernkompetenz-Ebene lösen können, das sind Aufgabe 2.1 und Aufgabe 2.2. Von FIA wird zusätzlich erwartet, dass ihnen Aufgaben auf Profilkompetenz-Ebene (Aufgabe 3.1) keine Probleme bereiten. Als Startcode für die Aufgaben 2.1 und 2.2 dient der Code-Stand src/06-tableview-buchungen/uebung-mitfahrgesuche mit Deinem GesucheViewModel aus den letzten Übungen; die Klasse Eingabefilter übernimmst Du aus dem Code-Stand des Artikels. Eine vollständige Beispiellösung mit allen Tests liegt unter src/07-formulare-dialoge/uebung-mitfahrgesuche. Code-Aufgaben löst Du am besten am Rechner und prüfst sie mit gradlew test (bzw. mvnw test). Die Operatoren (nennen, beschreiben, erläutern, beurteilen …) sind im Artikel Wie sind die Aufgaben aufgebaut? erklärt. Schreib Deine Lösung zuerst vollständig auf, bevor Du die Beispiellösung aufklappst.

Du möchtest eigene Aufgaben zum Üben von einem KI-Sprachmodell generieren lassen?

Kopiere den Text in ein KI-Sprachmodell Deiner Wahl und passe Kompetenzstufe, Anzahl, Bearbeitungszeit und Szenario bei Bedarf an. Wie die Aufgaben aufgebaut sind, beschreibt der Artikel Wie sind die Aufgaben aufgebaut?. Prüfe die Ergebnisse mit dem Lehrartikel: KI-Sprachmodelle machen Fehler.

Erstelle neue Übungsaufgaben zum Thema „Formulare und Dialoge: ein
Mitfahrgesuch eintragen“.

Bauplan: Halte Dich an Aufbau, Kompetenzstufen, Operatoren,
Anforderungsbereiche, Punkte und Zeitschätzung, die im Artikel
https://oer-informatik.de/aufgaben-aufbau beschrieben sind.

Fachliche Grundlage:
- https://oer-informatik.de/javafx-formulare-dialoge
- https://oer-informatik.de/szenario-mitfahrlabor
- https://oer-informatik.de/javafx-kanban-projektstart
- Stilbeispiel: https://oer-informatik.de/javafx-formulare-dialoge-uebungsaufgaben

Die Aufgaben sollen Inhalte zu diesen Schlagwörtern abdecken:
- JavaFX
- Formular
- Dialog
- Alert
- DatePicker
- Spinner
- ComboBox
- TextFormatter
- Validierung
- Fehlersuche

Kompetenzstufe: Kernkompetenz (alternativ: Basiskompetenz, Profilkompetenz, Expertise)
Anzahl: 1 Aufgabe mit Teilaufgaben
Bearbeitungszeit in Minuten ca.: <5 bis 30 min>
Szenario: eines der Szenarien aus dem Bauplan

Vorgehen:
1. Lies zuerst die verlinkten Artikel. Kannst Du keine Webseiten
   abrufen, sag mir das, statt zu raten.
2. Steht bei einer Angabe noch ein Platzhalter in <…>, frag mich
   zuerst danach. Richte Punkte und Zahl der Teilaufgaben nach der
   Bearbeitungszeit (ca. 0,9 min je Punkt).
3. Gib je Aufgabe Titel mit Kompetenzstufe, Ausgangssituation und
   Teilaufgaben mit Operator und Punktzahl aus, dazu die geschätzte
   Bearbeitungszeit.
4. Gib die Beispiellösungen mit Punkteverteilung erst aus, wenn ich
   danach frage.

Aufgabe 1.1: Steuerelemente und Dialoge zuordnen (Basiskompetenz)

(ca. 13 min Bearbeitungszeit)

Bevor das Übungsteam das Formular für Mitfahrgesuche baut, stimmt Oksana Melnyk mit Dir ab, wie die Angaben in die App kommen und wie sie mit Meldungen umgeht.

  1. Das Team bindet jedes Steuerelement über eine Property bidirektional an das ViewModel. Nenne für jede Angabe ein passendes JavaFX-Steuerelement und die Property, über die es an das ViewModel gebunden wird. (5 Punkte)
Angaben für ein Mitfahrgesuch
Nr. Angabe im Formular Steuerelement Property für die Bindung
1 Haltepunkt, einer von sechs
2 Richtung: zur Schule oder nach Hause
3 Tag der Fahrt, vergangene Tage nicht wählbar
4 Anzahl Personen, die zusammen mitfahren wollen (1 bis 4)
5 Notiz, eine Zeile, höchstens 80 Zeichen
  1. Das Feld „Abfahrt“ des Angebotsdialogs hat den TextFormatter aus Eingabefilter.uhrzeit() (Muster [0-9]{0,2}(:[0-9]{0,2})? auf getControlNewText()). Ben tippt in das leere Feld nacheinander 1, 4, 3, :, 3, 0, 5 und drückt danach einmal die Rücktaste. Gib den Feldinhalt nach der letzten Taste an und erläutere, was bei den Tasten 3 (dritte Taste) und 5 passiert. Nenne außerdem, was unter dem Feld steht, sobald Ben in das nächste Feld wechselt. (4 Punkte)

  2. Ordne jeder Situation einen AlertType zu. (3 Punkte)

  • Das Backend ist beim Eintragen eines Gesuchs nicht erreichbar.
  • Eine Mitfahrerin möchte ihr Gesuch zurückziehen; danach sehen es Fahrer*innen nicht mehr.
  • Nach dem Eintragen soll die App kurz melden, dass Fahrer*innen am Haltepunkt das Gesuch ab sofort sehen.
  1. Das Formular prüft schon, dass „spätestens“ nach „frühestens“ liegt. Nenne zwei Gründe, warum das Backend dieselbe Regel trotzdem prüfen muss. (2 Punkte)
  1. Je Zeile 1 Punkt (Steuerelement und passende Property):
Steuerelemente für ein Mitfahrgesuch
Nr. Angabe im Formular Steuerelement Property für die Bindung
1 Haltepunkt ComboBox<String> (Auswahl aus fester Liste) valueProperty()
2 Richtung zwei RadioButtons in einer ToggleGroup selectedProperty() eines Buttons bzw. selectedToggleProperty() der Gruppe mit Listener
3 Tag der Fahrt DatePicker mit DayCellFactory, die vergangene Tage sperrt valueProperty() (LocalDate)
4 Anzahl Personen Spinner<Integer> mit IntegerSpinnerValueFactory(1, 4) getValueFactory().valueProperty()
5 Notiz TextField mit TextFormatter, der mehr als 80 Zeichen verwirft textProperty()

Bei Nr. 1 ist auch eine ListView richtig (dann getSelectionModel().selectedItemProperty(), nur lesend), bei Nr. 4 eine ComboBox<Integer>.

  1. Feldinhalt am Ende: 14:3 (1 Punkt). Die dritte Taste 3 ergäbe 143; das passt nicht auf das Muster (nach zwei Ziffern ist nur ein Doppelpunkt erlaubt), der Filter gibt null zurück und die Änderung wird verworfen, das Feld zeigt weiter 14 (1 Punkt). Ebenso wird 5 verworfen, weil 14:305 mehr als zwei Ziffern nach dem Doppelpunkt hätte; das Feld bleibt 14:30 (1 Punkt). Die Rücktaste löscht die 0, denn 14:3 passt auf das Muster. Beim Wechsel ins nächste Feld steht darunter „Uhrzeit als HH:MM eingeben, z. B. 07:05.“, weil das ViewModel 14:3 als unvollständige Uhrzeit erkennt (1 Punkt).

  2. Je Zuordnung 1 Punkt: Backend nicht erreichbar → ERROR; Gesuch zurückziehen → CONFIRMATION (Rückfrage vor einer Aktion mit Folgen, Buttons z. B. „Gesuch zurückziehen“ und „Behalten“); Meldung nach dem Eintragen → INFORMATION (oder eine Meldung in der Statuszeile, die nicht weggeklickt werden muss).

  3. Je Grund 1 Punkt, z. B.: Die Prüfung in der Oberfläche lässt sich umgehen (anderer Client, Web-Oberfläche, direkter Aufruf der REST-API); eine veraltete App-Version kennt eine geänderte Regel nicht; nur das Backend kann sicherstellen, dass keine ungültigen Gesuche gespeichert werden.

Aufgabe 2.1: Das Formular für Mitfahrgesuche prüfen (Kernkompetenz)

(ca. 15 min Bearbeitungszeit)

Im Replenishment hat Aylin Kaya gefragt, ob sie ihr Gesuch auch in der Desktop-App eintragen kann. Das Übungsteam beginnt mit dem ViewModel, damit die Regeln getestet sind, bevor es einen Dialog gibt. Die Akzeptanzkriterien:

  • Haltepunkt, Richtung, Datum sowie „frühestens“ und „spätestens“ (je HH:MM) sind Pflicht, die Notiz ist freiwillig.
  • Das Datum liegt nicht vor heute. „spätestens“ liegt nach „frühestens“, und das Zeitfenster ist höchstens zwei Stunden lang.
  • Die Notiz hat höchstens 80 Zeichen; Leerzeichen am Rand werden entfernt.
  • Jede verletzte Regel erscheint als verständlicher Text; solange einer da ist, kann nichts eingetragen werden.
  1. Lege im Paket …desktop.viewmodel die Klasse GesuchFormularViewModel an. Der Konstruktor bekommt die Liste der Haltepunkte (als Text wie im GesucheViewModel) und das heutige Datum. Lege für alle Eingaben Properties mit Gettern an; „zur Schule“ ist vorausgewählt. (3 Punkte)

  2. Ergänze je Regel eine Fehlermeldung als StringBinding (leer = gültig) für Haltepunkt, Datum, Zeitfenster und Notiz sowie ein BooleanBinding gueltig. Achte auf vollständige Abhängigkeiten. (6 Punkte)

  3. Schreibe die Methode gesuch(), die aus gültigen Eingaben ein Gesuch mit der id 0 baut und bei ungültigen Eingaben eine IllegalStateException wirft. (2 Punkte)

  4. Schreibe einen Komponententest mit mindestens vier Testmethoden: ein gültiges Formular ergibt das erwartete Gesuch; „spätestens“ vor „frühestens“ (nur „spätestens“ wird geändert); die Grenze von genau zwei Stunden; eine unvollständige Uhrzeit oder ein Datum vor heute. (5 Punkte)

  1. Properties (1 Punkt Haltepunkt/Richtung/Datum, 1 Punkt Uhrzeiten als Text, 1 Punkt Notiz und Getter). Die Uhrzeiten sind StringPropertys, damit das Formular bei jeder Eingabe prüft und nicht erst nach Enter:
public class GesuchFormularViewModel {

    static final Duration LAENGSTES_ZEITFENSTER = Duration.ofHours(2);
    static final int NOTIZ_HOECHSTENS = 80;
    private static final Pattern UHRZEIT = Pattern.compile("([01][0-9]|2[0-3]):[0-5][0-9]");

    private final LocalDate heute;
    private final ObservableList<String> haltepunkte;

    private final ObjectProperty<String> haltepunkt = new SimpleObjectProperty<>(this, "haltepunkt");
    private final BooleanProperty zurSchule = new SimpleBooleanProperty(this, "zurSchule", true);
    private final ObjectProperty<LocalDate> datum = new SimpleObjectProperty<>(this, "datum");
    private final StringProperty fruehestens = new SimpleStringProperty(this, "fruehestens", "");
    private final StringProperty spaetestens = new SimpleStringProperty(this, "spaetestens", "");
    private final StringProperty notiz = new SimpleStringProperty(this, "notiz", "");
    // … Getter wie haltepunktProperty(), getHaltepunkte(), getHeute()
  1. Bindings im Konstruktor (Haltepunkt und Datum 1 Punkt, Zeitfenster mit beiden Abhängigkeiten 2 Punkte, Regeln des Zeitfensters 1 Punkt, Notiz 1 Punkt, gueltig 1 Punkt):
public GesuchFormularViewModel(List<String> haltepunkte, LocalDate heute) {
    this.heute = heute;
    this.haltepunkte = FXCollections.observableArrayList(haltepunkte);
    haltepunktFehler = Bindings.createStringBinding(
            () -> haltepunkt.get() == null ? "Bitte einen Haltepunkt wählen." : "", haltepunkt);
    datumFehler = Bindings.createStringBinding(() -> {
        if (datum.get() == null) {
            return "Bitte ein Datum wählen.";
        }
        return datum.get().isBefore(heute) ? "Das Datum liegt in der Vergangenheit." : "";
    }, datum);
    // hängt von beiden Uhrzeiten ab: beide als Abhängigkeit angeben
    zeitraumFehler = Bindings.createStringBinding(this::zeitraumPruefen, fruehestens, spaetestens);
    notizFehler = Bindings.createStringBinding(() -> notiz.get().length() > NOTIZ_HOECHSTENS
            ? "Höchstens " + NOTIZ_HOECHSTENS + " Zeichen." : "", notiz);
    gueltig = haltepunktFehler.isEmpty().and(datumFehler.isEmpty())
            .and(zeitraumFehler.isEmpty()).and(notizFehler.isEmpty());
}

private String zeitraumPruefen() {
    LocalTime von = uhrzeit(fruehestens.get());
    LocalTime bis = uhrzeit(spaetestens.get());
    if (von == null || bis == null) {
        return "Beide Uhrzeiten als HH:MM eingeben.";
    }
    if (!bis.isAfter(von)) {
        return "„spätestens“ muss nach „frühestens“ liegen.";
    }
    if (Duration.between(von, bis).compareTo(LAENGSTES_ZEITFENSTER) > 0) {
        return "Das Zeitfenster darf höchstens zwei Stunden lang sein.";
    }
    return "";
}

private static LocalTime uhrzeit(String text) {
    return text != null && UHRZEIT.matcher(text.strip()).matches() ? LocalTime.parse(text.strip()) : null;
}

Andere Meldungstexte sind in Ordnung, wenn sie die Regel nennen. Fehlt spaetestens in den Abhängigkeiten, gibt es für das Zeitfenster höchstens 1 Punkt (siehe Aufgabe 3.1).

  1. 2 Punkte (Prüfung auf gueltig 1 Punkt, Aufbau mit Richtung als Text und getrimmter Notiz 1 Punkt):
public Gesuch gesuch() {
    if (!gueltig.get()) {
        throw new IllegalStateException("Formular enthält noch Fehler");
    }
    return new Gesuch(0, haltepunkt.get(), zurSchule.get() ? "zur-schule" : "nach-hause", datum.get(),
            uhrzeit(fruehestens.get()), uhrzeit(spaetestens.get()), notiz.get().strip());
}
  1. Je sinnvoller Testmethode 1 Punkt, 1 Punkt für eine Ausgangslage, die alle Tests teilen (@BeforeEach). Der Test braucht keine JavaFX-Plattform:
class GesuchFormularViewModelTest {

    private static final LocalDate HEUTE = LocalDate.of(2026, 11, 16);
    private GesuchFormularViewModel formular;

    @BeforeEach
    void vorbereiten() {
        formular = new GesuchFormularViewModel(List.of(Beispielgesuche.GUETERSLOH, Beispielgesuche.HALLE), HEUTE);
        formular.haltepunktProperty().set(Beispielgesuche.HALLE);
        formular.datumProperty().set(HEUTE.plusDays(1));
        formular.fruehestensProperty().set("07:00");
        formular.spaetestensProperty().set("07:30");
    }

    @Test
    void vollstaendigesFormularErgibtGesuch() {
        formular.notizProperty().set("  mit Cello  ");
        assertTrue(formular.gueltigProperty().get());
        assertEquals(new Gesuch(0, Beispielgesuche.HALLE, "zur-schule", HEUTE.plusDays(1),
                LocalTime.of(7, 0), LocalTime.of(7, 30), "mit Cello"), formular.gesuch());
    }

    @Test
    void spaetestensVorFruehestensIstUngueltig() {
        formular.spaetestensProperty().set("06:50"); // nur das zweite Feld ändert sich
        assertEquals("„spätestens“ muss nach „frühestens“ liegen.", formular.zeitraumFehlerProperty().get());
        assertFalse(formular.gueltigProperty().get());
        assertThrows(IllegalStateException.class, formular::gesuch);
    }

    @Test
    void zeitfensterHoechstensZweiStunden() {
        formular.spaetestensProperty().set("09:00");
        assertEquals("", formular.zeitraumFehlerProperty().get());
        formular.spaetestensProperty().set("09:01");
        assertEquals("Das Zeitfenster darf höchstens zwei Stunden lang sein.", formular.zeitraumFehlerProperty().get());
    }

    @Test
    void unvollstaendigeUhrzeitUndVergangenesDatum() {
        formular.fruehestensProperty().set("7:0");
        assertEquals("Beide Uhrzeiten als HH:MM eingeben.", formular.zeitraumFehlerProperty().get());
        formular.datumProperty().set(HEUTE.minusDays(1));
        assertEquals("Das Datum liegt in der Vergangenheit.", formular.datumFehlerProperty().get());
    }
}

Aufgabe 2.2: Ein Dialog für Mitfahrgesuche (Kernkompetenz)

(ca. 13 min Bearbeitungszeit)

Mit dem getesteten ViewModel aus Aufgabe 2.1 bekommt das Gesuche-Fenster einen Button „Gesuch eintragen …“, der einen eigenen Dialog öffnet. Das Speichern im Backend folgt in einem eigenen Ticket; vorerst nimmt die App das neue Gesuch nur in ihre Liste auf.

  1. Lege gesuch-dialog.fxml an: ein GridPane ohne fx:controller mit ComboBox für den Haltepunkt, zwei RadioButtons in einer ToggleGroup, DatePicker, zwei Textfeldern für „frühestens“ und „spätestens“ in einer Zeile und einem Textfeld für die Notiz. Jedes Feld hat ein beschriftendes Label mit Tastenkürzel, unter den prüfbaren Feldern steht ein Label für die Fehlermeldung. (4 Punkte)

  2. Schreibe die Klasse GesuchDialog extends Dialog<Gesuch>. Sie lädt die FXML-Datei mit sich selbst als Controller, bietet die Buttons „Eintragen“ und „Abbrechen“, bindet alle Felder und Fehlermeldungen an das ViewModel, zeigt das Datum im Format TT.MM.JJJJ und setzt Filter: die Uhrzeitfelder wie im Artikel, die Notiz auf höchstens 80 Zeichen. „Eintragen“ ist gesperrt, solange das Formular ungültig ist. (6 Punkte)

  3. Ergänze den ResultConverter, den Button im GesucheController und eine Methode hinzufuegen(Gesuch) im GesucheViewModel, die dem Gesuch die nächste freie id gibt. (3 Punkte)

  4. Nenne zwei Prüfungen, die ein Test des Dialogs machen sollte, ohne den Dialog anzuzeigen. (1 Punkt)

  1. FXML (Felder und Labels mit labelFor 2 Punkte, ToggleGroup in fx:define 1 Punkt, Fehler-Labels 1 Punkt). Jedes Feld steht vor seinem Label, weil labelFor="$…" nur bereits geladene Elemente findet:
<GridPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml"
          styleClass="formular" hgap="12" vgap="4">
    <fx:define>
        <ToggleGroup fx:id="richtung"/>
    </fx:define>

    <ComboBox fx:id="haltepunktFeld" promptText="Haltepunkt wählen" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
    <Label text="_Haltepunkt" mnemonicParsing="true" labelFor="$haltepunktFeld"
           GridPane.rowIndex="0" GridPane.columnIndex="0"/>
    <Label fx:id="haltepunktFehler" styleClass="fehlertext" GridPane.rowIndex="1" GridPane.columnIndex="1"/>

    <Label text="Richtung" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
    <HBox spacing="16" GridPane.rowIndex="2" GridPane.columnIndex="1">
        <RadioButton fx:id="zurSchuleFeld" text="zur Schule" toggleGroup="$richtung"/>
        <RadioButton fx:id="nachHauseFeld" text="nach Hause" toggleGroup="$richtung"/>
    </HBox>

    <DatePicker fx:id="datumFeld" promptText="TT.MM.JJJJ" GridPane.rowIndex="3" GridPane.columnIndex="1"/>
    <Label text="_Datum" mnemonicParsing="true" labelFor="$datumFeld" GridPane.rowIndex="3" GridPane.columnIndex="0"/>
    <Label fx:id="datumFehler" styleClass="fehlertext" GridPane.rowIndex="4" GridPane.columnIndex="1"/>

    <HBox spacing="8" alignment="CENTER_LEFT" GridPane.rowIndex="5" GridPane.columnIndex="1">
        <TextField fx:id="fruehestensFeld" promptText="HH:MM" prefColumnCount="5"/>
        <Label text="bis spätestens"/>
        <TextField fx:id="spaetestensFeld" promptText="HH:MM" prefColumnCount="5"/>
    </HBox>
    <Label text="_Zeitfenster" mnemonicParsing="true" labelFor="$fruehestensFeld"
           GridPane.rowIndex="5" GridPane.columnIndex="0"/>
    <Label fx:id="zeitraumFehler" styleClass="fehlertext" GridPane.rowIndex="6" GridPane.columnIndex="1"/>

    <TextField fx:id="notizFeld" promptText="z. B. mit Sporttasche" GridPane.rowIndex="7" GridPane.columnIndex="1"/>
    <Label text="_Notiz" mnemonicParsing="true" labelFor="$notizFeld" GridPane.rowIndex="7" GridPane.columnIndex="0"/>
    <Label fx:id="notizFehler" styleClass="fehlertext" GridPane.rowIndex="8" GridPane.columnIndex="1"/>
</GridPane>
  1. Dialog (Laden mit setController 1 Punkt, ButtonTypes 1 Punkt, Bindungen 2 Punkte, Filter 1 Punkt, gesperrter Button 1 Punkt). Der Notizfilter prüft die Länge des neuen Texts, damit Löschen immer geht:
public class GesuchDialog extends Dialog<Gesuch> {

    public static final ButtonType EINTRAGEN = new ButtonType("Eintragen", ButtonBar.ButtonData.OK_DONE);
    private static final DateTimeFormatter DATUM = DateTimeFormatter.ofPattern("dd.MM.yyyy");

    // … @FXML-Felder wie in der FXML-Datei

    public GesuchDialog(GesuchFormularViewModel formular) {
        this.formular = formular;
        setTitle("Mitfahrlabor");
        setHeaderText("Mitfahrgesuch eintragen");
        FXMLLoader loader = new FXMLLoader(GesuchDialog.class.getResource("gesuch-dialog.fxml"));
        loader.setController(this);
        try {
            getDialogPane().setContent(loader.load());
        } catch (IOException fehler) {
            throw new UncheckedIOException(fehler);
        }
        getDialogPane().getStylesheets().add(GesuchDialog.class.getResource("mitfahrlabor.css").toExternalForm());
        getDialogPane().getButtonTypes().addAll(EINTRAGEN, ButtonType.CANCEL);

        haltepunktFeld.setItems(formular.getHaltepunkte());
        haltepunktFeld.valueProperty().bindBidirectional(formular.haltepunktProperty());
        richtung.selectToggle(zurSchuleFeld);
        richtung.selectedToggleProperty().addListener(
                (p, alt, neu) -> formular.zurSchuleProperty().set(neu == zurSchuleFeld));
        datumFeld.setConverter(new LocalDateStringConverter(DATUM, DATUM));
        datumFeld.valueProperty().bindBidirectional(formular.datumProperty());

        fruehestensFeld.setTextFormatter(Eingabefilter.uhrzeit());
        fruehestensFeld.textProperty().bindBidirectional(formular.fruehestensProperty());
        spaetestensFeld.setTextFormatter(Eingabefilter.uhrzeit());
        spaetestensFeld.textProperty().bindBidirectional(formular.spaetestensProperty());
        // Notiz: Filter begrenzt die Länge, Löschen bleibt immer möglich
        notizFeld.setTextFormatter(new TextFormatter<String>(aenderung ->
                aenderung.getControlNewText().length() <= 80 ? aenderung : null));
        notizFeld.textProperty().bindBidirectional(formular.notizProperty());

        haltepunktFehler.textProperty().bind(formular.haltepunktFehlerProperty());
        datumFehler.textProperty().bind(formular.datumFehlerProperty());
        zeitraumFehler.textProperty().bind(formular.zeitraumFehlerProperty());
        notizFehler.textProperty().bind(formular.notizFehlerProperty());

        getDialogPane().lookupButton(EINTRAGEN).disableProperty().bind(formular.gueltigProperty().not());
        setResultConverter(button -> button == EINTRAGEN ? formular.gesuch() : null);
    }
}

Die Beispiellösung zeigt die Meldungen sofort; wer sie wie im Artikel erst nach dem Verlassen des Felds zeigt, bekommt die volle Punktzahl ebenso.

  1. ResultConverter mit Prüfung des Buttons (letzte Zeile oben) 1 Punkt; Button mit showAndWait().ifPresent(…) 1 Punkt; hinzufuegen 1 Punkt:
// GesucheController; in gesuche.fxml: <Button text="Gesuch _eintragen …" onAction="#gesuchEintragen" …/>
@FXML
private void gesuchEintragen() {
    GesuchDialog dialog = new GesuchDialog(viewModel.neuesFormular(LocalDate.now()));
    dialog.initOwner(gesucheTabelle.getScene().getWindow());
    dialog.showAndWait().ifPresent(viewModel::hinzufuegen);
}

// GesucheViewModel
public GesuchFormularViewModel neuesFormular(LocalDate heute) {
    return new GesuchFormularViewModel(haltepunkte, heute);
}

public void hinzufuegen(Gesuch neu) {
    long id = gesuche.stream().mapToLong(Gesuch::id).max().orElse(0) + 1;
    gesuche.add(new Gesuch(id, neu.haltepunkt(), neu.richtung(), neu.datum(),
            neu.fruehestens(), neu.spaetestens(), neu.notiz()));
}

Weil gesuche die Quelle der FilteredList und SortedList ist, erscheint das neue Gesuch sofort in der Tabelle, sofern es zum Filter passt.

  1. 1 Punkt für zwei passende Prüfungen, z. B.: „Eintragen“ ist bei leerem Formular gesperrt und nach gültigen Eingaben frei (lookupButton(EINTRAGEN).isDisabled()); getResultConverter().call(EINTRAGEN) liefert das erwartete Gesuch, call(ButtonType.CANCEL) liefert null; der Notizfilter verwirft das 81. Zeichen und lässt Löschen zu. Die Beispiellösung enthält dazu GesuchDialogTest.

Aufgabe 3.1: Validierungsfehler anhand von Symptomen finden (Profilkompetenz)

(ca. 15 min Bearbeitungszeit)

Ein Teammitglied hat den Gesuch-Dialog zuerst anders gebaut. Oksana Melnyk prüft das Ticket in der Spalte „Test“ und meldet vier Auffälligkeiten. Der folgende Code enthält absichtlich vier Fehler; die übrigen Teile entsprechen Aufgabe 2.1 und 2.2. Im ViewModel ist „frühestens“ hier eine ObjectProperty<LocalTime> fruehestensZeit, „spätestens“ ein Text.

// GesuchFormularViewModel, Konstruktor (Ausschnitt)
zeitraumFehler = Bindings.createStringBinding(this::zeitraumPruefen, fruehestensZeit);     // (1)

// GesuchDialog, Konstruktor (Ausschnitt)
TextFormatter<LocalTime> von = new TextFormatter<>(
        new LocalTimeStringConverter(HH_MM, HH_MM), null);                                 // (2)
fruehestensFeld.setTextFormatter(von);
von.valueProperty().bindBidirectional(formular.fruehestensZeitProperty());

spaetestensFeld.setTextFormatter(new TextFormatter<String>(
        aenderung -> aenderung.getText().matches("[0-9:]+") ? aenderung : null));        // (3)
spaetestensFeld.textProperty().bindBidirectional(formular.spaetestensProperty());

getDialogPane().lookupButton(EINTRAGEN).disableProperty().bind(formular.gueltigProperty().not());
setResultConverter(button -> formular.gesuch());                                         // (4)

Die Meldungen aus dem Test:

  • A: „Ich habe aus Versehen 07:35 bei ‚spätestens‘ getippt. Mit der Rücktaste lässt sich nichts löschen; wenn ich die Ziffern markiere und überschreibe, geht es.“
  • B: „Ich habe 07:00 bis 06:30 eingetragen und dann ‚spätestens‘ auf 07:30 korrigiert. Die rote Meldung bleibt stehen und ‚Eintragen‘ bleibt gesperrt. Ändere ich danach ‚frühestens‘, ist alles in Ordnung.“
  • C: „Ich habe ein vollständiges Gesuch eingegeben und dann doch ‚Abbrechen‘ geklickt. Das Gesuch steht trotzdem in der Tabelle. Bei einem halb ausgefüllten Formular schließt ‚Abbrechen‘ gar nicht, und in der Konsole steht eine IllegalStateException.“
  • D: „Tippe ich bei ‚frühestens‘ eine Zeit, verschwindet die Meldung erst, wenn ich Enter drücke oder in ein anderes Feld wechsle. Habe ich mich zu 25:00 vertippt, steht nach dem Wechsel kommentarlos wieder die alte Zeit im Feld.“
  1. Ordne jede Meldung der fehlerhaften Stelle (1) bis (4) zu und erläutere jeweils, wie der Fehler das beschriebene Verhalten verursacht. (8 Punkte)

  2. Gib für jede Stelle eine korrigierte Fassung an. (4 Punkte)

  3. Beurteile für jeden der vier Fehler, ob ein Komponententest des ViewModels ihn gefunden hätte oder ob ein Test mit den Steuerelementen des Dialogs nötig ist. Skizziere für einen der Fehler einen Test (Ausgangslage, Aktion, Erwartung). (4 Punkte)

  1. Je Meldung 2 Punkte (Zuordnung 1, Erklärung 1):
Symptome und Ursachen
Meldung Stelle Ursache
A (3) Der Filter prüft getText(), also nur den eingefügten Text. Beim Löschen ist er leer, [0-9:]+ verlangt mindestens ein Zeichen, der Filter gibt null zurück und verwirft das Löschen. Beim Überschreiben ist der eingefügte Text eine Ziffer, deshalb geht das.
B (1) Das Binding nennt nur fruehestensZeit als Abhängigkeit. Eine Änderung von spaetestens löst keine Neuberechnung aus, die alte Meldung bleibt, und damit bleibt gueltig falsch. Erst eine Änderung von „frühestens“ rechnet neu.
C (4) Der ResultConverter prüft den Button nicht. Er wird auch für „Abbrechen“ aufgerufen und liefert bei gültigem Formular ein Gesuch, das der Controller über ifPresent einträgt. Bei ungültigem Formular wirft gesuch() die Ausnahme im Klick-Handler, der Dialog wird nicht geschlossen.
D (2) Ein TextFormatter mit Konverter übernimmt den Wert in seine valueProperty erst bei Enter oder Fokusverlust. Bis dahin sieht das ViewModel den alten Wert. Lässt sich der Text nicht umwandeln (25:00), setzt der Formatter den Text auf den letzten gültigen Wert zurück, ohne Meldung.
  1. Je Stelle 1 Punkt:
// (1) beide Uhrzeiten als Abhängigkeit
zeitraumFehler = Bindings.createStringBinding(this::zeitraumPruefen, fruehestensZeit, spaetestens);

// (2) Text binden und nur filtern; das ViewModel prüft den Text (wie Aufgabe 2.1)
fruehestensFeld.setTextFormatter(Eingabefilter.uhrzeit());
fruehestensFeld.textProperty().bindBidirectional(formular.fruehestensProperty());

// (3) den ganzen neuen Text prüfen; der leere Text ist erlaubt
spaetestensFeld.setTextFormatter(Eingabefilter.uhrzeit());

// (4) nur bei „Eintragen“ ein Ergebnis
setResultConverter(button -> button == EINTRAGEN ? formular.gesuch() : null);

Für (2) ist auch eine Lösung mit ObjectProperty<LocalTime> richtig, wenn ein Listener auf den Text den Wert bei jeder Eingabe setzt (oder null bei unvollständiger Eingabe) und das ViewModel dann eine Meldung zeigt.

  1. Je Fehler 0,5 Punkte für die Einordnung, 2 Punkte für die Testskizze:
    1. findet ein Komponententest des ViewModels: nur spaetestens ändern und die Meldung prüfen (genau das tut spaetestensVorFruehestensIstUngueltig aus Aufgabe 2.1, in umgekehrter Richtung).
  • (2), (3) und (4) liegen in der View und brauchen einen Test mit den Steuerelementen. Dafür muss der Dialog nicht angezeigt werden: Er wird auf dem JavaFX Application Thread gebaut, dann werden replaceText, deleteText und getResultConverter().call(…) aufgerufen.

Beispielskizze für (3): Ausgangslage: Dialog gebaut, im Feld „spätestens“ steht nach replaceText(0, 0, "07:35") der Text 07:35. Aktion: deleteText(4, 5) (wie die Rücktaste). Erwartung: Das Feld enthält 07:3. Mit dem fehlerhaften Filter steht dort weiter 07:35, der Test schlägt fehl.

Beispielskizze für (4): Ausgangslage: Dialog gebaut und gültig ausgefüllt. Aktion: getResultConverter().call(ButtonType.CANCEL). Erwartung: null.


Hinweis zur Nachnutzung als Open Educational Resource (OER)

Dieser Artikel und seine Texte, Bilder, Grafiken, Code und sonstiger Inhalt sind - sofern nicht anders angegeben - lizenziert unter CC BY 4.0. Nennung gemäß TULLU-Regel bitte wie folgt: “Übungsaufgaben zu Formularen und Dialogen: ein Mitfahrgesuch eintragen” von oer-informatik.de (H. Stein), Lizenz: CC BY 4.0. Der Artikel wurde unter https://oer-informatik.de/javafx-formulare-dialoge-uebungsaufgaben veröffentlicht, die Quelltexte sind in weiterverarbeitbarer Form verfügbar im Repository unter https://gitlab.com/oer-informatik/javafx/javafx-einstieg. Erstellt mit KI-Assistenz; von mir umfassend redaktionell überarbeitet, geprüft und freigegeben. Stand: 11.11.2026.

[Kommentare zum Artikel lesen, schreiben] / [Artikel teilen] / [gitlab-Issue zum Artikel schreiben]

Kommentare gerne per Mastodon, Verbesserungsvorschläge per gitlab issue (siehe oben). Beitrag teilen: