-
Notifications
You must be signed in to change notification settings - Fork 104
Validation
package: de.saxsys.mvvmfx.utils.validation
Validation is an important issue in most applications. In the JavaFX world there are some libraries available that provide support for validation, for example ControlsFX.
Validation consists of two aspects:
- validation logic: What conditions does an input value fulfill to be considered "valid"? Under which conditions is a value "invalid"?
- visualizing validation errors: When an input value is invalid, how do we present the error to the user? We could f.e. use a red border around the input control and show a message next to the control.
From the point of view of MVVM it is clear where these aspects should be handled: The validation logic has to be placed in the ViewModel while the visualization is the job of the View. Sadly, the ControlsFX libraries mixes both aspects and doesn't support a real separation. See the example from the ControlsFX javadoc:
ValidationSupport validationSupport = new ValidationSupport();
validationSupport.registerValidators(textField, Validator.createEmptyValidator("Text is required"));
Using mvvmFX, where should this code be placed? The textField
is part of the View. But the View shouldn't know the validation logic (createEmptyValidator
). If we would place this code in the View we couldn't easily test the validation logic. On the other hand if we would put this code in the ViewModel we would introduce a dependency from the ViewModel to the View and to view-specific classes (TextField
) which again would reduce testability as we would need to run the tests on the JavaFX UI Thread. For this reason we have introduced our own validation support for mvvmFX.
All validation classes of mvvmFX are located in the package (and subpackages of) de.saxsys.mvvmfx.utils.validation
.
We have different types of validators that all implement the interface Validator
.
Validators are part of the ViewModel layer. Validators are containing the validation logic.
Typically you will have a validator instance for each field in you viewModel resp. each control in your view.
Each Validator
has a ValidationStatus
object that can be obtained with the Validator.getValidationStatus()
method. The ValidationStatus
represents the current state of the validation. It has a boolean property valid
and observable lists for error/warning messages.
The ValidationStatus
object is reactive, which means that it's content will be updated automatically after a (re-)validation was executed.
The ViewModel should provide the ValidationStatus
for each validator/field/control to the View. The View can access the status without knowing what valdiator lays behind this status. If the status changes the view can update itself accordingly.
The ValidationStatus
contains observable lists of ValidationMessage
s. A ValidationMessage
is an immutable class containing a string message and a Severity
. Severity
is an enum with two constants: WARNING
and ERROR
.
In the View we need a component that can visualize validation errors. This is the task for implementations of ValidationVisualizer
. The View can instantiate a visualizer and use it to "connect" the ValidationStatus
from the ViewModel with the input control in the View. This is done with the method initVisualization
which takes the status and the Control
as arguments (and a optional third parameter to determine if the field is mandatory or not). The visualizer will decorate the control when the status is updated.
The ValidationVisualizerBase
class implements the ValidationVisualizer
interface and is used to simplify the implementation of custom Visualizers. It takes care for the management of the ValidationStatus (like handling listeners for the observable lists of messages in the ValidationStatus
object). An implementor of a custom visualizer only needs to define how a single messages should be displayed and how a mandatory field should be decorated.
At the moment we are providing an implementation of ValidationVisualizer
that uses the ControlsFX library for decoration of error messages.
Please Note: To use the ControlsFxVisualizer
you need to add the ControlsFX library to the classpath of the application. Otherwise you will get NoClassDefFoundError
s and ClassNotFoundException
s.
At the moment we provide 3 implementations of the Validator
interface that can be used for different use cases:
The FunctionBasedValidator<T>
is used for simple use cases. An instance of this validator is connected to a single observable value that should be validated. This validator has a generic type <T>
that
is the type of the observable. There are two "flavours" of the FunctionBasedValidator
:
You can provide a Predicate<T>
and a ValidationMessage
as parameters. The predicate has to return true
when the given input value is valid, otherwise false
. When the predicate returns false
the given validation message will be present in the ValidationStatus
of this validator.
StringProperty firstname = new SimpleStringProperty();
Validator firstnameValidator;
...
Predicate<String> predicate = input -> input != null && ! input.trim().isEmpty(); // predicate as lambda
firstnameValidator = new FunctionBasedValidator<>(firstname, predicate, ValidationMessage.error("May not be empty!");
You can provide a Function as parameter, that returns a ValidationMessage
for a given input value. The returned validation message will be present in the validation status. If the input value is valid, the function has to return null
.
StringProperty firstname = new SimpleStringProperty();
Validator firstnameValidator;
...
Function<String,ValidationMessage> function = input -> {
if(input == null) {
return ValidationMessage.error("May not be null2");
} else if (input.trim().isEmpty()) {
return ValidationMessage.warning("Should not be empty");
} else {
return null; // everything is ok
}
};
firstnameValidator = new FunctionBasedValidator<>(firstname, function);
For more complex validation logic you can use the ObservableRuleBasedValidator
. This validator is not connected with a single field. Instead you can add Rules to the validator. A "rule" consists of an observable boolean value and a validation message. When a rule is violated (the observable evaluates to false
) the given message is present in the validation status.
This way you have the full flexibility of the JavaFX Properties and Data-Binding API to define your rules. You can for example define rules that are depended of multiple fields (cross-field-validation).
You can add multiple rules to the validator. This way it is possible to have more then one validation message to be present in the validation status object when multiple rules aren't fulfilled.
Example:
StringProperty password = new SimpleStringProperty();
StringProperty passwordRepeat = new SimpleStringProperty();
ObservableRuleBasedValidator passwordValidator = new ObservableRuleBasedValidator();
BooleanBinding rule1 = password.isNotEmpty();
BooleanBinding rule2 = passwordRepeat.isNotEmpty();
BooleanBinding rule3 = password.isEqualTo(passwordRepeat);
passwordValidator.addRule(rule1, ValidationMessage.error("Please enter a password"));
passwordValidator.addRule(rule2, ValidationMessage.error("Please enter the password a second time"));
passwordValidator.addRule(rule3, ValidationMessage.error("Both passwords need to be the same"));
Starting with version 1.6.0 you can also add more complex rules to the ObservableRuleBasedValidator
.
Instead of defining an ObservableValue<Boolean>
and a fixed message you can add a ObservableValue<ValidationMessage>
as rule. If this observable has a value of null
it is considered to be valid. If this observable contains an actual validation message then it is considered to be invalid and the message is used for the validator. This way you can dynamically use different validation messages based on the actual error.
StringProperty password = new SimpleStringProperty();
ObservableValue<ValidationMessage> rule = Bindings.createObjectBinding(() -> {
if(password.get() == null) {
return ValidationMessage.error("Please enter a password");
} else if(password.get().length() < 6) {
return ValidationMessage.warning("Your password is really short");
} else {
return null;
}
}, password);
ObservableRuleBasedValidator passwordValidator = new ObservableRuleBasedValidator();
passwordValidator.addRule(rule);
You can combine both types of rules in a single validator.
The CompositeValidator
is used to compose other validators. The ValidationStatus
of this validator will
contain all messages from all sub-validators. It will be only valid when all sub-validators are valid.
The main use case for this class is to create forms: Each field in the form has it's own validator (of type FunctionBasedValidator
or ObservableRuleBasedValidator
). Additionally you define a CompositeValidator
and add all validators of all fields to it. The status of the compositeValidator can then be used to disable the "OK" button of the form. This way, only when all fields are valid, the OK button will be enabled.
As the CompositeValidator itself implements the Validator
interface it is possible to add other CompositeValidators to a CompositeValidator. This way you can create a hierarchy of validators.
Validator firstnameValidator = new FunctionBasedValidator(....);
Validator lastnameValidator = new ObservableRuleBasedValidator();
Validator formValidator = new CompositeValidator();
formValidator.addValidators(firstnameValidator, lastnameValidator);
This is how we would implement validation:
public class MyViewModel implements ViewModel {
private StringProperty firstname = new SimpleStringProperty();
private StringProperty lastname = new SimpleStringProperty();
private FunctionBasedValidator firstnameValidator;
private ObservableRuleBasedValidator lastnameValidator;
private CompositeValidator formValidator;
public MyViewModel() {
firstnameValidator = new FunctionBasedValidator(
firstname,
input -> input != null && !input.trim().isEmpty(),
ValidationMessage.error("Firstname may not be empty"));
lastnameValidator = new ObservableRuleBasedValidator();
lastnameValidator.addRule(lastname.isNotEmpty(), ValidationMessage.error("Lastname may not be empty"));
formValidator = new CompositeValidator();
formValidator.addValidators(firstnameValidator, lastnameValidator);
}
public StringProperty firstnameProperty() {
return firstname;
}
public StringProperty lastnameProperty() {
return lastname;
}
public ValidationStatus firstnameValidation() {
return firstnameValidator.getValidationStatus();
}
public ValidationStatus lastnameValidation() {
return lastnameValidator.getValidationStatus();
}
public ValidationStatus formValidation() {
return formValidator.getValidationStatus();
}
}
public class MyView implements FxmlView<MyViewModel> {
@FXML
TextField firstnameInput;
@FXML
TextField lastnameInput;
@FXML
Button okButton;
@InjectViewModel
private MyViewModel viewModel;
public void initialize() {
firstnameInput.textProperty().bindBidirectional(viewModel.firstnameProperty());
lastnameInput.textProperty().bindBidirectional(viewModel.lastnameProperty());
ValidationVisualizer visualizer = new ControlsFxVisualizer();
visualizer.initVisualization(viewModel.firstnameValidation(), firstnameInput, true);
visualizer.initVisualization(viewModel.lastnameValidation(), lastnameInput, true);
okButton.disableProperty().bind(viewModel.formValidation().validProperty().not());
}
}
If you, for example need a validation only when an input is entered, you can use the FunctionBasedValidator
. With this Validator you can implement optional fields, that will be only validated, if the input is entered.
Example for an validation of an optional e-mail address:
public class EMailValidator extends FunctionBasedValidator<String> {
private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle("bundle");
private static final Pattern SIMPLE_EMAIL_REGEX = Pattern
.compile("^$|[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}");
private static final Function<String, ValidationMessage> FUNCTION = input -> {
if (input == null) {
// input can be null
return null;
} else {
if (SIMPLE_EMAIL_REGEX.matcher(input).matches()) {
// input is empty or valide
return null;
} else {
return ValidationMessage.error(RESOURCE_BUNDLE.getString("validation.error.email"));
}
}
};
public EMailValidator(ObservableValue<String> source) {
super(source, FUNCTION);
}
}
Another example of a cross-field-validation can be seen in the code at: https://github.com/sialcasa/mvvmFX/tree/develop/mvvmfx-validation/src/test/java/de/saxsys/mvvmfx/utils/validation/crossfieldexample
In our Contacts example we are using the validation in the dialog to add/edit contacts: https://github.com/sialcasa/mvvmFX/tree/develop/examples/contacts-example/src/main/java/de/saxsys/mvvmfx/examples/contacts/ui/contactform