Data Validation
Data validation refers to checking the correctness of information entered by the user. Validation ensures that user changes do not produce a data model that is invalid from the perspective of the application domain.
Example
Suppose we have the following simple application:
public class MainController implements Initializable {
@FXML
private ListView<Pet> petsList;
@FXML
private TextField nameField;
@FXML
private TextField speciesField;
@FXML
private Button addButton;
ObservableList<Pet> pets = FXCollections.observableArrayList();
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
petsList.setItems(pets);
addButton.setOnAction(_ -> addPet());
}
private void addPet() {
Pet pet = new Pet();
pet.setName(nameField.getText());
pet.setSpecies(speciesField.getText());
pets.add(pet);
nameField.clear();
speciesField.clear();
}
}
The application contains a list to which pets can be added, represented by the Pet class.
At the moment, the user can add pets without a name or a species, which is not a good idea.
We should prevent users from adding pets that have no name or no species.
Simple Validation
The simplest form of validation can be implemented by checking the domain requirements before creating the model object.
For example, we could check inside the addPet() method whether the name and species fields contain text:
private void addPet() {
// TODO: Perform validation here.
// Add the pet only if the information is valid.
Pet pet = new Pet();
pet.setName(name);
pet.setSpecies(species);
pets.add(pet);
nameField.clear();
speciesField.clear();
}
Validation Method
Validation is often easy to implement as a separate method in the controller:
/**
* Checks whether the contents of the pet fields are valid.
* If not, highlights the invalid fields.
*
* @return true if validation succeeds, otherwise false
*/
private boolean validatePet() {
// Reset field styles
nameField.setStyle("");
speciesField.setStyle("");
// Retrieve field contents
String name = nameField.getText();
String species = speciesField.getText();
// Validation: Name must not be empty
if (name.isBlank()) {
// Highlight the invalid field
nameField.setStyle("-fx-border-color: red; " + "-fx-background-color: #ffcccc;");
return false;
}
// Validation: Species must not be empty
if (species.isBlank()) {
speciesField.setStyle("-fx-border-color: red; " + "-fx-background-color: #ffcccc;");
return false;
}
// true = validation succeeded
return true;
}
private void addPet() {
// Validate before adding
if (!validatePet()) {
return;
}
Pet pet = new Pet();
pet.setName(nameField.getText());
pet.setSpecies(speciesField.getText());
pets.add(pet);
nameField.clear();
speciesField.clear();
}
Moving Validation into the Data Model
From the perspective of testing and separation of responsibilities, it is often clearer if validation is part of the data model itself.
In that case, validation can be implemented directly within the model class.
In this example, validateErrors is part of the Pet class.
At the same time, let's create an enumeration type ValidationError that represents possible error situations.
public enum ValidationError {
NAME_EMPTY,
SPECIES_EMPTY
}
public class Pet {
// ...
// Actual validation method
public ValidationError validateErrors() {
if (getName().isBlank()) {
return ValidationError.NAME_EMPTY;
}
if (getSpecies().isBlank()) {
return ValidationError.SPECIES_EMPTY;
}
return null;
}
}
Adding a pet still happens inside addPet(), but now validation of the model object is performed directly through the model class:
private void addPet() {
nameField.setStyle("");
speciesField.setStyle("");
// Create the model object and assign values
Pet pet = new Pet();
pet.setName(nameField.getText());
pet.setSpecies(speciesField.getText());
// Check whether the model object is valid
ValidationError errorResult = pet.validateErrors();
// If validation fails, display
// an appropriate error indication
if (errorResult != null) {
if (errorResult == ValidationError.NAME_EMPTY) {
nameField.setStyle("-fx-border-color: red; " + "-fx-background-color: #ffcccc;");
}
if (errorResult == ValidationError.SPECIES_EMPTY) {
speciesField.setStyle("-fx-border-color: red; " + "-fx-background-color: #ffcccc;");
}
return;
}
// Add the pet only if it is valid
pets.add(pet);
nameField.clear();
speciesField.clear();
}
Instead of relying on null checks, it is often much clearer to use the Optional type, which explicitly represents a situation where an error may or may not exist.
public Optional<ValidationError> validateErrors() {
if (getName().isBlank()) {
return Optional.of(ValidationError.NAME_EMPTY);
}
if (getSpecies().isBlank()) {
return Optional.of(ValidationError.SPECIES_EMPTY);
}
return Optional.empty();
}
Whereas null is an invisible contract that requires separate documentation (which, unfortunately, is often never written), the Optional type makes the intent explicit.
The validation code then becomes cleaner:
private void addPet() {
// ...
Optional<ValidationError> errorResult = pet.validateErrors();
// If a validation error exists,
// display the corresponding feedback
if (errorResult.isPresent()) {
ValidationError error = errorResult.get();
if (error == ValidationError.NAME_EMPTY) {
nameField.setStyle("-fx-border-color: red; " + "-fx-background-color: #ffcccc;");
}
if (error == ValidationError.SPECIES_EMPTY) {
speciesField.setStyle("-fx-border-color: red; " + "-fx-background-color: #ffcccc;");
}
return;
}
// ...
}