Validation

Manuel Mauky edited this page Jul 2, 2018 · 12 revisions

Validation aspects and the problem with existing libraries

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.

mvvmFX validation overview and terminology

All validation classes of mvvmFX are located in the package (and subpackages of) de.saxsys.mvvmfx.utils.validation.

Validator

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.

ValidationStatus

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.

ValidationMessage

The ValidationStatus contains observable lists of ValidationMessages. A ValidationMessage is an immutable class containing a string message and a Severity. Severity is an enum with two constants: WARNING and ERROR.

ValidationVisualizer

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.

ValidationVisualizerBase

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.

ControlsFxVisualizer

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 NoClassDefFoundErrors and ClassNotFoundExceptions.


Validator implementations

At the moment we provide 3 implementations of the Validator interface that can be used for different use cases:

FunctionBasedValidator

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:

Using a Predicate:

You can provide a Predicate<T> and a ValidationMessageas parameters. The predicate has to return true when the given input value is valid, otherwise false. When the predicate returns true 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!");
Using a Function<T, ValidationMessage>:

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);

ObservableRuleBasedValidator

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 f.e. 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.

CompositeValidator

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);

Examples

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());
    }
}

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

You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.