Last active
May 14, 2023 10:58
-
-
Save james-d/9904574 to your computer and use it in GitHub Desktop.
Example of using Bindings (extensively) for validation in JavaFX. Maybe a basis for thinking about a validation framework.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.root { | |
error-color: #ffa0a0 ; | |
} | |
.text-field:validation-error { | |
-fx-background-color: error-color ; | |
} | |
.label.error-instructions { | |
-fx-text-fill: error-color ; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<?import java.lang.*?> | |
<?import javafx.geometry.*?> | |
<?import javafx.scene.control.*?> | |
<?import javafx.scene.control.Label?> | |
<?import javafx.scene.control.Tab?> | |
<?import javafx.scene.control.TabPane?> | |
<?import javafx.scene.control.TextField?> | |
<?import javafx.scene.control.Tooltip?> | |
<?import javafx.scene.layout.*?> | |
<?import javafx.scene.layout.BorderPane?> | |
<?import javafx.scene.layout.VBox?> | |
<?import javafx.scene.web.*?> | |
<BorderPane xmlns:fx="http://javafx.com/fxml" fx:controller="tabvalidation.Controller"> | |
<center> | |
<TabPane fx:id="tabPane"> | |
<tabs> | |
<Tab fx:id="nameTab" text="Name"> | |
<content> | |
<BorderPane> | |
<bottom> | |
<Label fx:id="nameTabErrorInstructions" styleClass="error-instructions" text="Items in red need attention" BorderPane.alignment="CENTER"> | |
<BorderPane.margin> | |
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" fx:id="x1" /> | |
</BorderPane.margin> | |
</Label> | |
</bottom> | |
<center> | |
<GridPane hgap="10.0" vgap="10.0"> | |
<children> | |
<Label text="First Name:" GridPane.columnIndex="0" GridPane.rowIndex="0" /> | |
<TextField fx:id="firstNameTextField" GridPane.columnIndex="1" GridPane.rowIndex="0"> | |
<tooltip> | |
<Tooltip text="First name" /> | |
</tooltip> | |
</TextField> | |
<Label text="Last Name:" GridPane.columnIndex="0" GridPane.rowIndex="1" /> | |
<TextField fx:id="lastNameTextField" GridPane.columnIndex="1" GridPane.rowIndex="1" /> | |
</children> | |
<columnConstraints> | |
<ColumnConstraints halignment="RIGHT" hgrow="NEVER" /> | |
<ColumnConstraints halignment="LEFT" hgrow="ALWAYS" /> | |
</columnConstraints> | |
<padding> | |
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> | |
</padding> | |
<BorderPane.margin> | |
<Insets /> | |
</BorderPane.margin> | |
</GridPane> | |
</center> | |
<top> | |
<TitledPane fx:id="nameTabErrorList" animated="false" expanded="false"> | |
<content> | |
<VBox fx:id="nameTabErrorMessages" spacing="5.0" /> | |
</content> | |
</TitledPane> | |
</top> | |
</BorderPane> | |
</content> | |
</Tab> | |
<Tab fx:id="contactTab" text="Contact"> | |
<content> | |
<BorderPane> | |
<bottom> | |
<Label id="nameTabErrorInstructions" fx:id="contactTabErrorInstructions" alignment="CENTER" styleClass="error-instructions" text="Items in red need attention" BorderPane.alignment="CENTER" BorderPane.margin="$x1" /> | |
</bottom> | |
<center> | |
<GridPane hgap="10.0" vgap="10.0"> | |
<children> | |
<Label text="Email:" GridPane.columnIndex="0" GridPane.rowIndex="0" /> | |
<TextField fx:id="emailTextField" GridPane.columnIndex="1" GridPane.rowIndex="0"> | |
<tooltip> | |
<Tooltip text="Email address" /> | |
</tooltip> | |
</TextField> | |
<Label text="Zip code:" GridPane.columnIndex="0" GridPane.rowIndex="1" /> | |
<TextField fx:id="zipCodeTextField" GridPane.columnIndex="1" GridPane.rowIndex="1" /> | |
</children> | |
<columnConstraints> | |
<ColumnConstraints halignment="RIGHT" hgrow="NEVER" /> | |
<ColumnConstraints halignment="LEFT" hgrow="ALWAYS" /> | |
</columnConstraints> | |
<padding> | |
<Insets bottom="10.0" left="10.0" right="10.0" top="10.0" /> | |
</padding> | |
<BorderPane.margin> | |
<Insets /> | |
</BorderPane.margin> | |
</GridPane> | |
</center> | |
<top> | |
<TitledPane fx:id="contactTabErrorList" animated="false" expanded="false"> | |
<content> | |
<VBox fx:id="contactTabErrorMessages" spacing="5.0" /> | |
</content> | |
</TitledPane> | |
</top> | |
</BorderPane> | |
</content> | |
</Tab> | |
<Tab fx:id="confirmationTab" text="Confirmation"> | |
<content> | |
<BorderPane> | |
<center> | |
<WebView fx:id="browser" /> | |
</center> | |
<top> | |
<Label text="Thanks. You may now see the discussion." BorderPane.alignment="CENTER" /> | |
</top> | |
</BorderPane> | |
</content> | |
</Tab> | |
</tabs> | |
</TabPane> | |
</center> | |
</BorderPane> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package tabvalidation; | |
import javafx.application.Application; | |
import javafx.fxml.FXMLLoader; | |
import javafx.stage.Stage; | |
import javafx.scene.Parent; | |
import javafx.scene.Scene; | |
public class TabValidationExample extends Application { | |
@Override | |
public void start(Stage primaryStage) { | |
try { | |
Parent root = FXMLLoader.load(getClass().getResource("TabValidation.fxml")); | |
Scene scene = new Scene(root, 800, 600); | |
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm()); | |
primaryStage.setScene(scene); | |
primaryStage.show(); | |
} catch(Exception e) { | |
e.printStackTrace(); | |
} | |
} | |
public static void main(String[] args) { | |
launch(args); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package tabvalidation; | |
import java.util.Arrays; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
import java.util.regex.Pattern; | |
import java.util.stream.Collectors; | |
import javafx.beans.InvalidationListener; | |
import javafx.beans.binding.Bindings; | |
import javafx.beans.binding.BooleanBinding; | |
import javafx.beans.binding.ListBinding; | |
import javafx.beans.binding.LongBinding; | |
import javafx.collections.FXCollections; | |
import javafx.collections.ObservableList; | |
import javafx.css.PseudoClass; | |
import javafx.fxml.FXML; | |
import javafx.scene.Node; | |
import javafx.scene.control.Label; | |
import javafx.scene.control.Tab; | |
import javafx.scene.control.TabPane; | |
import javafx.scene.control.TextField; | |
import javafx.scene.control.TitledPane; | |
import javafx.scene.control.Tooltip; | |
import javafx.scene.layout.Pane; | |
import javafx.scene.web.WebView; | |
public class Controller { | |
@FXML | |
private TextField firstNameTextField ; | |
@FXML | |
private TextField lastNameTextField ; | |
@FXML | |
private TextField emailTextField ; | |
@FXML | |
private TextField zipCodeTextField ; | |
@FXML | |
private Tab nameTab ; | |
@FXML | |
private Tab contactTab ; | |
@FXML | |
private Tab confirmationTab ; | |
@FXML | |
private TabPane tabPane ; | |
@FXML | |
private TitledPane nameTabErrorList ; | |
@FXML | |
private Pane nameTabErrorMessages ; | |
@FXML | |
private TitledPane contactTabErrorList ; | |
@FXML | |
private Pane contactTabErrorMessages ; | |
@FXML | |
private WebView browser ; | |
@FXML | |
private Label nameTabErrorInstructions ; | |
@FXML | |
private Label contactTabErrorInstructions ; | |
public void initialize() { | |
// Bit of a hack. Probably need a ValidationBinding extends BooleanBinding with a message property: | |
Map<BooleanBinding, String> messages = new HashMap<>(); | |
tabPane.getSelectionModel().select(nameTab); | |
BooleanBinding firstNameInvalid = emptyTextFieldBinding(firstNameTextField, "First Name is required", messages); | |
BooleanBinding lastNameInvalid = emptyTextFieldBinding(lastNameTextField, "Last Name is required", messages); | |
final Pattern emailPattern = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*@[a-zA-Z][a-zA-Z0-9_]+(\\.[a-zA-Z][a-zA-Z0-9_]*)+"); | |
BooleanBinding emailInvalid = patternTextFieldBinding(emailTextField, emailPattern, "You must enter a valid email", messages); | |
final Pattern zipPattern = Pattern.compile("[0-9]{5}"); | |
BooleanBinding zipInvalid = patternTextFieldBinding(zipCodeTextField, zipPattern, "You must enter a 5-digit zip code", messages); | |
BooleanBinding[] nameTabBindings = { firstNameInvalid, lastNameInvalid } ; | |
BooleanBinding[] contactTabBindings = { emailInvalid, zipInvalid } ; | |
BooleanBinding nameTabInvalid = any(nameTabBindings); | |
BooleanBinding contactTabInvalid = any(contactTabBindings); | |
contactTab.disableProperty().bind(nameTabInvalid); | |
confirmationTab.disableProperty().bind(nameTabInvalid.or(contactTabInvalid)); | |
nameTabErrorInstructions.visibleProperty().bind(nameTabInvalid); | |
contactTabErrorInstructions.visibleProperty().bind(contactTabInvalid); | |
bindMessageLabels(nameTabBindings, nameTabErrorMessages.getChildren(), messages); | |
final LongBinding nameTabErrorCount = count(nameTabBindings); | |
nameTabErrorList.textProperty().bind(Bindings.format("%d %s on this page", nameTabErrorCount, | |
Bindings.when(nameTabErrorCount.isEqualTo(1)).then("error").otherwise("errors"))); | |
bindMessageLabels(contactTabBindings, contactTabErrorMessages.getChildren(), messages); | |
final LongBinding contactTabErrorCount = count(contactTabBindings); | |
contactTabErrorList.textProperty().bind(Bindings.format("%d %s on this page", contactTabErrorCount, | |
Bindings.when(contactTabErrorCount.isEqualTo(1)).then("error").otherwise("errors"))); | |
browser.getEngine().load("http://stackoverflow.com/questions/22772364/javafx-prevent-selection-of-a-different-tab-if-the-data-validation-of-the-selec/"); | |
} | |
private void bindMessageLabels(BooleanBinding[] validationBindings, List<Node> labelList, Map<BooleanBinding, String> messages) { | |
ListBinding<Node> nodeListBinding = new ListBinding<Node>() { | |
{ | |
// calling bind(...) here won't work, neither will using WeakInvalidationListeners. Not sure why.... | |
InvalidationListener invalidationListener = obs -> invalidate(); | |
Arrays.stream(validationBindings).forEach(binding -> | |
binding.addListener(invalidationListener)); | |
} | |
@Override | |
protected ObservableList<Node> computeValue() { | |
return FXCollections.observableList( | |
Arrays.stream(validationBindings) | |
.filter(BooleanBinding::get) | |
.map(messages::get).map(Label::new) | |
.collect(Collectors.toList()) | |
); | |
} | |
}; | |
Bindings.bindContent(labelList, nodeListBinding); | |
} | |
private BooleanBinding emptyTextFieldBinding(TextField textField, String message, Map<BooleanBinding, String> messages) { | |
BooleanBinding binding = Bindings.createBooleanBinding(() -> | |
textField.getText().trim().isEmpty(), textField.textProperty()); | |
configureTextFieldBinding(binding, textField, message, messages); | |
return binding ; | |
} | |
private BooleanBinding patternTextFieldBinding(TextField textField, Pattern pattern, String message, Map<BooleanBinding, String> messages) { | |
BooleanBinding binding = Bindings.createBooleanBinding(() -> | |
!pattern.matcher(textField.getText()).matches(), textField.textProperty()); | |
configureTextFieldBinding(binding, textField, message, messages); | |
return binding ; | |
} | |
private void configureTextFieldBinding(BooleanBinding binding, TextField textField, String message, Map<BooleanBinding, String> messages) { | |
messages.put(binding, message); | |
if (textField.getTooltip() == null) { | |
textField.setTooltip(new Tooltip()); | |
} | |
String tooltipText = textField.getTooltip().getText(); | |
binding.addListener((obs, oldValue, newValue) -> { | |
updateTextFieldValidationStatus(textField, tooltipText, newValue, message); | |
}); | |
updateTextFieldValidationStatus(textField, tooltipText, binding.get(), message); | |
} | |
private BooleanBinding any(BooleanBinding[] bindings) { | |
return Bindings.createBooleanBinding(() -> | |
Arrays.stream(bindings).anyMatch(BooleanBinding::get), bindings); | |
} | |
private LongBinding count(BooleanBinding[] bindings) { | |
return Bindings.createLongBinding(() -> | |
Arrays.stream(bindings).filter(BooleanBinding::get).collect(Collectors.counting()), bindings); | |
} | |
private void updateTextFieldValidationStatus(TextField textField, | |
String defaultTooltipText, boolean invalid, String message) { | |
textField.pseudoClassStateChanged(PseudoClass.getPseudoClass("validation-error"), invalid); | |
String tooltipText ; | |
if (invalid) { | |
tooltipText = message; | |
} else { | |
tooltipText = defaultTooltipText; | |
} | |
if (tooltipText == null || tooltipText.isEmpty()) { | |
textField.setTooltip(null); | |
} else { | |
Tooltip tooltip = textField.getTooltip(); | |
if (tooltip == null) { | |
textField.setTooltip(new Tooltip(tooltipText)); | |
} else { | |
tooltip.setText(tooltipText); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment