Tuesday, November 13, 2012

JavaFX Field Validation

This post won't be about OpenGL, it will be about JavaFX. I'm currently writing a tool in JavaFX and wanted to give the user visual feedback about the validity of fields (mainly TextFields). I came across this post but while it achieves what I had in mind, it is pretty verbose and you would have to define everything over again for multiple fields.

I wanted to do a more generic version of what I had read and this is how it is used:


Valid Textfield


Textfield with warning


Textfield with error


ErrorValidator<String> idValidator = new ErrorValidator<>(idTextField.textProperty(), new ITypeValidator<String>() {
 @Override
 public ErrorValidator.State validate(String typeToValidate) {
  // Whatever validation code is required
  int occurences = countOccurences(typeToValidate, '!');
  if (occurences == 0) {
   return State.VALID;
  } else if(occurences < 3) {
   return State.WARNING;
  } else {
   return State.ERROR;
  }
 }
}, ErrorValidator.State.VALID);

idValidator.addStyleTargets(idErrorLabel, idTextField);

idValidator.stateProperty().addListener(new ChangeListener<State>() {
 @Override
 public void changed(ObservableValue<? extends State> observable, State oldValue, State newValue) {
  switch (newValue) {
  case ERROR:
   idErrorLabel.setText("Too many Exclamation Marks!!!");
   break;
  case WARNING:
   idErrorLabel.setText("Be careful not to use too many Exclamation Marks!!!");
   break;
  }
 }
});
Usage of the ErrorValidator Class
So what happens here? The Form visible in the screenshots consists of 6 Nodes: 2 Labels and a Textfield for the username and password. The 2 Labels in front of the textfields are static and don't do much, so it's basically a Textfield and Label for the username and password field.

The code at the top introduces the validation functionality. Lines 1-14 create an ErrorValidator (code at the bottom) that is bound to the textProperty of the textfield. It is created with a validator that checks how many exclamation marks are used in the username. Depending on the count it returns a state. This validator is called whenever the textProperty of the textfield changes. Line 14 sets the initial state of the validator which is optional. The ErrorValidator can be used for any type, not just Strings.

The username Textfield and Label are added to the validator on line 16. The ErrorValidator will set a styleclass (validation_valid, validation_warning and validation_error) on them, depending on the current validation state. Any type of node is accepted, not just labels and textfields. The Nodes are styled with CSS. This is the CSS used for the screenshots:

.label.validation_valid {
  visibility: hidden;
}
.label.validation_error {
 -fx-graphic: url("./icon/delete.png");
  -fx-text-fill: red;
  visibility: visible;
}
.label.validation_warning { 
 -fx-graphic: url("./icon/error.png");
  -fx-text-fill: orange;
  visibility: visible;
}

.text-field.validation_error {
 -fx-background-color: red,
       linear-gradient(
        to bottom,
        derive(red,70%) 5%,
        derive(red,90%) 40%
       );
}

.text-field.validation_warning {
 -fx-background-color: orange,
       linear-gradient(
        to bottom,
        derive(orange,70%) 5%,
        derive(orange,90%) 40%
       );
}
CSS used in the Example (used icons, famfamfam)

The CSS is used for basic visual styling: setting the color on the textfields and labes, making the error label invisible when the validation is valid and setting icons on the labels. The only thing that can't be done with pure CSS is changing the text of the label (well, it could be done using multiple lables).

To do this, a ChangeListener is registered to the state property of the ErrorValidator (lines 18-30). The text is changed depending on the state of the ErrorValidator.

I'm pretty happy with how the ErrorValidator works and right now as it fills my needs. Will it change in the future? Likely. Setting the text on a Label is still ugly so that's one area where improvements are possible.

Code of the ErrorValidator

/**
 * Class for giving visual feedback about the validation state of {@link ObservableValue}s.
 * 
 * @param <T>
 *            The type that is validated
 */
public class ErrorValidator<T> {
 private static final String BASE_STYLE = "validated";

 public static enum State {
  VALID("validation_valid"), WARNING("validation_warning"), ERROR("validation_error");

  public final String style;

  private State(String style) {
   this.style = style;
  }
 }

 private final List<Node> styleTargets = new ArrayList<>();
 private ObjectProperty<State> state = new SimpleObjectProperty<>(State.VALID);

 /**
  * Initializes this {@link ErrorValidator} with a state that can be different from the actual validation result.
  * 
  * @param property
  * @param validator
  * @param initState
  */
 public ErrorValidator(final ObservableValue<? extends T> property, final ITypeValidator<? super T> validator, State initState) {
  Preconditions.checkNotNull(initState, "The state may not be null!");
  state.set(initState);

  property.addListener(new ChangeListener<T>() {
   @Override
   public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {
    setState(validator.validate(newValue));
   }
  });
 }

 /**
  * Initializes this {@link ErrorValidator} with a state depending on the current validation result.
  * 
  * @param property
  * @param validator
  */
 public ErrorValidator(final ObservableValue<? extends T> property, final ITypeValidator<? super T> validator) {
  this(property, validator, validator.validate(property.getValue()));
 }

 private void setState(State newState) {
  if (state.get() == newState) {
   return;
  }
  Preconditions.checkNotNull(newState, "The state may not be null!");

  for (Node node : styleTargets) {
   node.getStyleClass().remove(state.get().style);
   node.getStyleClass().add(newState.style);
  }

  this.state.set(newState);
 }

 /**
  * Adds a new {@link Node} that should receive styleclasses depending of the validation state of this validator.
  * 
  * @param node
  */
 public void addStyleTarget(Node node) {
  styleTargets.add(node);
  node.getStyleClass().add(state.get().style);
  node.getStyleClass().add(BASE_STYLE);
 }

 /**
  * Adds new {@link Node}s that should receive styleclasses depending of the validation state of this validator.
  * 
  * @param nodes
  */
 public void addStyleTargets(Node... nodes) {
  for (Node node : nodes) {
   addStyleTarget(node);
  }
 }

 public ReadOnlyObjectProperty<State> stateProperty() {
  return state;
 }
}
The ErrorValidator class

The ErrorValidator also uses the Guava class to check for null values.

public interface ITypeValidator<T> {
 ErrorValidator.State validate(T typeToValidate);
}
The ITypeValidator class

5 comments:

  1. Hi, I'm trying to use your code, but I keep getting an error on this line:
    int occurences = countOccurences(typeToValidate, '!');

    Where do you get that method from?

    Thanks

    ReplyDelete
    Replies
    1. It's just a placeholder method for this example and counts the occurrences of the character '!' in the given string. Think of it as something like the following:
      public int countOccurrences(String s, char searched) {
      int occurrences = 0;
      for (int i = 0; i < s.length(); i++) {
      if (s.charAt(i) == searched) {
      occurrences++;
      }
      }
      return occurrences;
      }

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. FYI, if you change the type of styleListeners to contain elements that implement the Styleable interface instead of Nodes, you can then style tooltips and other things that aren't nodes. Thanks!

    ReplyDelete