Estou trabalhando em uma aplicação JavaFX + Spring Boot usando o padrão MVVM. Usamos o ControlsFX ValidationSupport para validar campos em um formulário de login. Nosso objetivo é:
Mostrar ícones de erro vermelhos ("decorações") imediatamente quando a página carrega, se os campos estiverem vazios ou inválidos
Mantenha o botão OK desabilitado até que todas as regras de validação sejam aprovadas
Exemplo:
Temos uma caixa de diálogo para criar um novo usuário com estes campos:
- Nome de usuário (TextField)
- Senha (PasswordField)
- Repita a senha (PasswordField)
Registramos Validadores assim:
@Component
public class ValidationHelper {
public void registerUserRegistrationValidations(ValidationSupport validationSupport, TextField userName,
PasswordField password, PasswordField repeatPassword) {
registerFocusLostValidation(userName, getEnteredUserNameDataLengthValidator(), validationSupport);
registerFocusLostValidation(password, getEnteredPasswdDataLengthValidator(), validationSupport);
registerFocusLostValidation(repeatPassword, getEnteredPasswdDataLengthValidator(), validationSupport);
registerFocusLostValidation(repeatPassword, getEnteredPasswordsEqualValidator(repeatPassword), validationSupport);
}
private Validator<Object> getEnteredUserNameDataLengthValidator() {
return Validator.createPredicateValidator(
userName -> userName != null && ((String) userName).length() > 2,
"user name too short");
}
private Validator<Object> getEnteredPasswdDataLengthValidator() {
return Validator.createPredicateValidator(
pin -> pin != null && ((String) pin).length() > 2,
"Password too short");
}
private Validator<Object> getEnteredPasswordsEqualValidator(PasswordField passwdField) {
return Validator.createPredicateValidator(
password -> password != null && password.equals(passwdField.getText()),
"Passwords do not match");
}
private <T> void registerFocusLostValidation(Control control, Validator<T> validator, ValidationSupport validationSupport) {
validationSupport.registerValidator(control, false, validator);
}
}
Também fazemos isso para vincular um sinalizador global:
BooleanBinding isInvalid = Bindings.createBooleanBinding(
() -> !validationSupport.getValidationResult().getErrors().isEmpty(),
validationSupport.validationResultProperty()
);
validationState.formInvalidProperty().bind(isInvalid);
Então no controlador de rodapé:
okButton.disableProperty().bind(validationState.formInvalidProperty());
O Problema
Isso geralmente funciona, mas somente depois que o usuário começa a digitar.
Inicialmente:
- A página carrega sem nenhum círculo vermelho de validação
- O botão OK fica ativo muito cedo (após digitar a primeira senha)
- A validação para repetição de senha só é acionada após perda de foco
Queremos que a validação apareça assim que o formulário for exibido, sem interação do usuário. Tentativas
- Tentamos Platform.runLater(validationSupport::revalidate)
- Tentamos definir required = true em registerValidator
- Também tentamos chamar validationSupport.getValidationResult().getErrors() manualmente, mas o estado inicial é sempre visto como "válido".
Alguma ideia de como fazer a validação ser acionada imediatamente e garantir que o botão OK se comporte corretamente desde o início?
Testado com o ControlsFX versão 11.1.1 (ou o que você estiver usando) e JavaFX 22.
Exemplo Mínimo Reproduzível
ControlsFxApp.java
package controlsFx;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.ConfigurableApplicationContext;
import java.io.IOException;
import java.net.URL;
@SpringBootApplication
public class ControlsFxApp extends Application {
private static final String RESOURCE = "sample.fxml";
private ConfigurableApplicationContext springContext;
@Override
public void init() {
springContext = new SpringApplicationBuilder(ControlsFxApp.class).run();
}
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = load(RESOURCE);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
public Parent load(String fxmlPath) throws IOException {
URL location = getClass().getResource(fxmlPath);
FXMLLoader fxmlLoader = new FXMLLoader(location);
fxmlLoader.setControllerFactory(springContext::getBean);
return fxmlLoader.load();
}
}
FooterController.java
package controlsFx;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import org.springframework.stereotype.Controller;
@Controller
public class FooterController {
private final ValidationState validationState;
public Button register;
public FooterController(ValidationState validationState) {
this.validationState = validationState;
}
@FXML
private void initialize() {
register.setOnAction(event -> {
System.out.println("Validation requested for the current step...");
});
register.disableProperty().bind(validationState.formInvalidProperty());
}
}
Controlador de Registro.java
package controlsFx;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.fxml.FXML;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.decoration.GraphicValidationDecoration;
import org.springframework.stereotype.Controller;
@Controller
public class RegistrationController {
private final ValidationHelper validationHelper;
private final ValidationSupport validationSupport = new ValidationSupport();
private final ValidationState validationState;
public TextField userName;
public PasswordField password;
public PasswordField repeatPassword;
public RegistrationController(ValidationHelper validationHelper, ValidationState validationState) {
this.validationHelper = validationHelper;
this.validationState = validationState;
}
@FXML
public void initialize() {
validationSupport.setValidationDecorator(new GraphicValidationDecoration());
validationHelper.registerUserRegistrationValidations(validationSupport, userName, password, repeatPassword);
Platform.runLater(() -> {
validationSupport.revalidate();
BooleanBinding isInvalid = Bindings.createBooleanBinding(
() -> !validationSupport.getValidationResult().getErrors().isEmpty(),
validationSupport.validationResultProperty()
);
validationState.formInvalidProperty().bind(isInvalid);
});
}
}
ValidationHelper.java
package controlsFx;
import javafx.scene.control.Control;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.springframework.stereotype.Component;
@Component
public class ValidationHelper {
public void registerUserRegistrationValidations(ValidationSupport validationSupport, TextField userName,
PasswordField password, PasswordField repeatPassword) {
registerFocusLostValidation(userName, getEnteredUserNameDataLengthValidator(), validationSupport);
registerFocusLostValidation(password, getEnteredPasswdDataLengthValidator(), validationSupport);
registerFocusLostValidation(repeatPassword, getEnteredPasswdDataLengthValidator(), validationSupport);
registerFocusLostValidation(repeatPassword, getEnteredPasswordsEqualValidator(repeatPassword), validationSupport);
}
private Validator<Object> getEnteredUserNameDataLengthValidator() {
return Validator.createPredicateValidator(
userName -> userName != null && ((String) userName).length() > 2,
"user name too short");
}
private Validator<Object> getEnteredPasswdDataLengthValidator() {
return Validator.createPredicateValidator(
pin -> pin != null && ((String) pin).length() > 2,
"Password too short");
}
private Validator<Object> getEnteredPasswordsEqualValidator(PasswordField passwdField) {
return Validator.createPredicateValidator(
password -> password != null && password.equals(passwdField.getText()),
"Passwords do not match");
}
private <T> void registerFocusLostValidation(Control control, Validator<T> validator, ValidationSupport validationSupport) {
validationSupport.registerValidator(control, false, validator);
}
}
Estado de Validação.java
package controlsFx;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.springframework.stereotype.Component;
@Component
public class ValidationState {
private final BooleanProperty formInvalid = new SimpleBooleanProperty(true);
public BooleanProperty formInvalidProperty() {
return formInvalid;
}
}
centro.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<AnchorPane xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx"
fx:controller="controlsFx.RegistrationController"
prefHeight="400.0" prefWidth="600.0">
<VBox AnchorPane.bottomAnchor="30" AnchorPane.leftAnchor="30" AnchorPane.rightAnchor="30"
AnchorPane.topAnchor="30">
<Label>User Name:</Label>
<TextField fx:id="userName" id="userName"/>
<Label>Password:</Label>
<PasswordField fx:id="password" id="password"/>
<Label>Repeat Password:</Label>
<PasswordField fx:id="repeatPassword" id="password"/>
</VBox>
</AnchorPane>
rodapé.xml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns:fx="http://javafx.com/fxml"
xmlns="http://javafx.com/javafx" prefHeight="400.0" prefWidth="600.0"
fx:controller="controlsFx.FooterController">
<Button fx:id="register" layoutX="33.0" layoutY="187.0" prefHeight="25.0" prefWidth="534.0" text="Register"/>
</AnchorPane>
amostra.fxml
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.text.Font?>
<BorderPane xmlns:fx="http://javafx.com/fxml" xmlns="http://javafx.com/javafx" prefWidth="200" prefHeight="200"
fx:id="borderPaneId">
<top>
<AnchorPane BorderPane.alignment="CENTER">
<Label text="Registration">
<font>
<Font size="24.0"/>
</font>
</Label>
<BorderPane.margin>
<Insets/>
</BorderPane.margin>
</AnchorPane>
</top>
<center>
<fx:include source="center.fxml"/>
</center>
<bottom>
<fx:include source="footer.fxml"/>
</bottom>
</BorderPane>
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>controlsFx</groupId>
<artifactId>validationdemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>17</java.version>
<javafx.version>22</javafx.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.controlsfx</groupId>
<artifactId>controlsfx</artifactId>
<version>11.1.2</version>
</dependency>
</dependencies>
</project>