Ü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
Alertzu 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.
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.
- 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)
| 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 |
Das Feld „Abfahrt“ des Angebotsdialogs hat den
TextFormatterausEingabefilter.uhrzeit()(Muster[0-9]{0,2}(:[0-9]{0,2})?aufgetControlNewText()). Ben tippt in das leere Feld nacheinander1,4,3,:,3,0,5und drückt danach einmal die Rücktaste. Gib den Feldinhalt nach der letzten Taste an und erläutere, was bei den Tasten3(dritte Taste) und5passiert. Nenne außerdem, was unter dem Feld steht, sobald Ben in das nächste Feld wechselt. (4 Punkte)Ordne jeder Situation einen
AlertTypezu. (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.
- 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)
- Je Zeile 1 Punkt (Steuerelement und passende Property):
| 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>.
Feldinhalt am Ende:
14:3(1 Punkt). Die dritte Taste3ergäbe143; das passt nicht auf das Muster (nach zwei Ziffern ist nur ein Doppelpunkt erlaubt), der Filter gibtnullzurück und die Änderung wird verworfen, das Feld zeigt weiter14(1 Punkt). Ebenso wird5verworfen, weil14:305mehr als zwei Ziffern nach dem Doppelpunkt hätte; das Feld bleibt14:30(1 Punkt). Die Rücktaste löscht die0, denn14:3passt auf das Muster. Beim Wechsel ins nächste Feld steht darunter „Uhrzeit als HH:MM eingeben, z. B. 07:05.“, weil das ViewModel14:3als unvollständige Uhrzeit erkennt (1 Punkt).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).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.
Lege im Paket
…desktop.viewmodeldie KlasseGesuchFormularViewModelan. Der Konstruktor bekommt die Liste der Haltepunkte (als Text wie imGesucheViewModel) und das heutige Datum. Lege für alle Eingaben Properties mit Gettern an; „zur Schule“ ist vorausgewählt. (3 Punkte)Ergänze je Regel eine Fehlermeldung als
StringBinding(leer = gültig) für Haltepunkt, Datum, Zeitfenster und Notiz sowie einBooleanBinding gueltig. Achte auf vollständige Abhängigkeiten. (6 Punkte)Schreibe die Methode
gesuch(), die aus gültigen Eingaben einGesuchmit derid0 baut und bei ungültigen Eingaben eineIllegalStateExceptionwirft. (2 Punkte)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)
- 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()- Bindings im Konstruktor (Haltepunkt und Datum 1 Punkt, Zeitfenster mit beiden Abhängigkeiten 2 Punkte, Regeln des Zeitfensters 1 Punkt, Notiz 1 Punkt,
gueltig1 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).
- 2 Punkte (Prüfung auf
gueltig1 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());
}- 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.
Lege
gesuch-dialog.fxmlan: einGridPaneohnefx:controllermitComboBoxfür den Haltepunkt, zweiRadioButtons in einerToggleGroup,DatePicker, zwei Textfeldern für „frühestens“ und „spätestens“ in einer Zeile und einem Textfeld für die Notiz. Jedes Feld hat ein beschriftendesLabelmit Tastenkürzel, unter den prüfbaren Feldern steht einLabelfür die Fehlermeldung. (4 Punkte)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 FormatTT.MM.JJJJund 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)Ergänze den
ResultConverter, den Button imGesucheControllerund eine Methodehinzufuegen(Gesuch)imGesucheViewModel, die dem Gesuch die nächste freieidgibt. (3 Punkte)Nenne zwei Prüfungen, die ein Test des Dialogs machen sollte, ohne den Dialog anzuzeigen. (1 Punkt)
- FXML (Felder und Labels mit
labelFor2 Punkte,ToggleGroupinfx:define1 Punkt, Fehler-Labels 1 Punkt). Jedes Feld steht vor seinem Label, weillabelFor="$…"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>- Dialog (Laden mit
setController1 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.
ResultConvertermit Prüfung des Buttons (letzte Zeile oben) 1 Punkt; Button mitshowAndWait().ifPresent(…)1 Punkt;hinzufuegen1 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 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 erwarteteGesuch,call(ButtonType.CANCEL)liefertnull; der Notizfilter verwirft das 81. Zeichen und lässt Löschen zu. Die Beispiellösung enthält dazuGesuchDialogTest.
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.“
Ordne jede Meldung der fehlerhaften Stelle (1) bis (4) zu und erläutere jeweils, wie der Fehler das beschriebene Verhalten verursacht. (8 Punkte)
Gib für jede Stelle eine korrigierte Fassung an. (4 Punkte)
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)
- Je Meldung 2 Punkte (Zuordnung 1, Erklärung 1):
| 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. |
- 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.
- Je Fehler 0,5 Punkte für die Einordnung, 2 Punkte für die Testskizze:
- findet ein Komponententest des ViewModels: nur
spaetestensändern und die Meldung prüfen (genau das tutspaetestensVorFruehestensIstUngueltigaus Aufgabe 2.1, in umgekehrter Richtung).
- findet ein Komponententest des ViewModels: nur
- (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,deleteTextundgetResultConverter().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.
Links und weitere Informationen
zum Artikel: Formulare und Dialoge: Fahrtangebote anlegen, prüfen und absagen
zum Board: Kanban-Projektstart für die Desktop-App
als Nachschlagewerk: JavaFX-API: Klasse Dialog
als Nachschlagewerk: JavaFX-API: Klasse TextFormatter
für den Einstieg: Szenario Mitfahrlabor mit Personas und Regeln
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]
