|
|
|
|
Allowing users to enter data is a vital part of almost every application. However, making sure that the data makes sense is a challenge in many different cases. Users might enter words in a field that requires only numbers, or they might create a password that is too small, or they might enter a phone number with the wrong number of digits.
To ensure the integrity of freely-entered data, Java introduced the InputVerifier in J2SE 1.3. Unfortunately, as others have noted, InputVerifier "is not very interesting. All it does is prevent the user from tabbing or mousing out of the component in question. That's pretty boring and also not very helpful to the user at helping them figure out why what they entered is invalid." A more flexible and more complete alternative is JGoodies Validation, created by Karsten Lentzsch, the creator of the previously-reviewed frameworks JGoodies Forms and JGoodies Binding.
Unlike InputValidator, the Validation framework allows validation at several points (at key change, at focus loss, etc.), presents several different ways to indicate an error condition (text fields, icons, color, etc.), and can give the user hints on what input is valid.
For this article, let's create a basic dialog form that could use validation. Imagine this as a user signup form, where a user will enter a name, create a username, and enter a phone number. Later, we will require values in all three fields, require a specific length for the username, and show a warning if the phone number does not match the standard American format.
This layout uses FormLayout from JGoodies Forms.
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.factories.ButtonBarFactory;
import com.jgoodies.forms.layout.FormLayout;
import javax.swing.*;
import java.awt.event.ActionEvent;
public final class Unvalidated {
private final JFrame frame = new JFrame("Unvalidated");
private final JTextField name = new JTextField(30);
private final JTextField username = new JTextField(30);
private final JTextField phoneNumber = new JTextField(30);
public Unvalidated() {
this.frame.add(createPanel());
this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.frame.pack();
}
private JPanel createPanel() {
FormLayout layout = new FormLayout("pref, 2dlu, pref:grow");
DefaultFormBuilder builder = new DefaultFormBuilder(layout);
int columnCount = builder.getColumnCount();
builder.setDefaultDialogBorder();
builder.append("Name", this.name);
builder.append("Username", this.username);
builder.append("Phone Number", this.phoneNumber);
JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(
new JButton(new OkAction()), new JButton(new CancelAction()));
builder.append(buttonBar, columnCount);
return builder.getPanel();
}
private void show() {
this.frame.setVisible(true);
}
private final class OkAction extends AbstractAction {
private OkAction() {
super("OK");
}
public void actionPerformed(ActionEvent e) {
frame.dispose();
}
}
private final class CancelAction extends AbstractAction {
private CancelAction() {
super("Cancel");
}
public void actionPerformed(ActionEvent e) {
frame.dispose();
}
}
public static void main(String[] args) {
Unvalidated example = new Unvalidated();
example.show();
}
}
Now that we have a place to start, let's look at the core classes and interfaces that define the framework.
Severity.OK,
Severity.WARNING, and Severity.ERROR.
Severity severity();
String formattedText();
Object key();
key() method allows a loosely-coupled
association between message and view. This association is established by
the message key that can be shared between messages, validators, views,
and other parties.
ValidationMessage interface are provided in the framework,
SimpleValidationMessage and
PropertyValidationMessage, both of which extend
AbstractValidationMessage.
ValidationResult encapsulates a list of
ValidationMessages that are created by a validation. This class
provides many convenience methods for adding messages, combining
ValidationResults, retrieving message text, retrieving all
messages of a certain Severity, retrieving the highest
severity represented in the list, etc.
ValidationResult validate();
Validator (or even
ValidationCapable before version 1.2). Because of this change and
a few other changes, version 2.0 is binary-incompatible with previous
versions.
ValidationResult validate(T validationTarget);
Validatable interface, and the signature of the validate(T
validationTarget) method in this interface has changed. Because
of this change and a few other changes, version 2.0 is
binary-incompatible with previous versions. Also, version 2.0 uses Java
5 features, as can be seen in the parameterization of this interface.
ValidationResult (which in turn holds
ValidationMessages). It provides bound, read-only properties for
the result, severity, error and messages state. ValidationResultModel interface are
provided in the framework, DefaultValidationResultModel
and ValidationResultModelContainer, both of which extend
AbstractValidationResultModel.
In addition to the core classes, there are utility classes like
ValidationUtils (very similar to StringUtils
in the Jakarta Commons framework, but with more validation-specific static
methods), some useful custom DateFormatters and
NumberFormatters, and some adapters for some Swing objects like
JTable and JList.
Note that these additional utility classes are the only parts of the framework that use Swing; there is no dependency on Swing in the core classes. That means that the core validation logic can be placed at a different level of the application than the GUI. It also means that the core of the Validation framework can be used for SWT applications or even for command-line applications.
Now that we have seen the core classes, let's use some of them to add validation to the form we used above (important additions are bold).
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.factories.ButtonBarFactory;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.validation.ValidationResult;
import com.jgoodies.validation.ValidationResultModel;
import com.jgoodies.validation.util.DefaultValidationResultModel;
import com.jgoodies.validation.util.ValidationUtils;
import com.jgoodies.validation.view.ValidationResultViewFactory;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class Validation {
private final JFrame frame = new JFrame("Validation");
private final JTextField name = new JTextField(30);
private final JTextField username = new JTextField(30);
private final JTextField phoneNumber = new JTextField(30);
private final ValidationResultModel validationResultModel =
new DefaultValidationResultModel();
private final Pattern phonePattern =
Pattern.compile("\\b\\d{3}-\\d{3}-\\d{4}");
public Validation() {
this.validationResultModel.addPropertyChangeListener(
new ValidationListener());
this.frame.add(createPanel());
this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.frame.pack();
}
private JPanel createPanel() {
FormLayout layout = new FormLayout("pref, 2dlu, pref:grow");
DefaultFormBuilder builder = new DefaultFormBuilder(layout);
int columnCount = builder.getColumnCount();
builder.setDefaultDialogBorder();
builder.append("Name", this.name);
builder.append("Username", this.username);
builder.append("Phone Number", this.phoneNumber);
//add a component to show validation messages
JComponent validationResultsComponent =
ValidationResultViewFactory.createReportList(
this.validationResultModel);
builder.appendUnrelatedComponentsGapRow();
builder.appendRow("fill:50dlu:grow");
builder.nextLine(2);
builder.append(validationResultsComponent, columnCount);
JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(
new JButton(new OkAction()), new JButton(new CancelAction()));
builder.append(buttonBar, columnCount);
return builder.getPanel();
}
private void show() {
this.frame.setVisible(true);
}
//validate each of the three input fields
private ValidationResult validate() {
ValidationResult validationResult = new ValidationResult();
//validate the name field
if (ValidationUtils.isEmpty(this.name.getText())) {
validationResult.addError("The Name field can not be blank.");
}
//validate the username field
if (ValidationUtils.isEmpty(this.username.getText())) {
validationResult.addError("The Username field can not be blank.");
} else if (!ValidationUtils.hasBoundedLength(
this.username.getText(), 6, 12)) {
validationResult.addError(
"The Username field must be between 6 and 12 characters.");
}
//validate the phoneNumber field
String phone = this.phoneNumber.getText();
if (ValidationUtils.isEmpty(phone)) {
validationResult.addError(
"The Phone Number field can not be blank.");
} else {
Matcher matcher = this.phonePattern.matcher(phone);
if (!matcher.matches()) {
validationResult.addWarning(
"The phone number must be a legal American number.");
}
}
return validationResult;
}
private final class OkAction extends AbstractAction {
private OkAction() {
super("OK");
}
public void actionPerformed(ActionEvent e) {
//don't close the frame on OK unless it validates
ValidationResult validationResult = validate();
validationResultModel.setResult(validationResult);
if (!validationResultModel.hasErrors()) {
frame.dispose();
}
}
}
private final class CancelAction extends AbstractAction {
private CancelAction() {
super("Cancel");
}
public void actionPerformed(ActionEvent e) {
frame.dispose();
}
}
//display informative dialogs for specific validation events
private static final class ValidationListener
implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
String property = evt.getPropertyName();
if (ValidationResultModel.PROPERTYNAME_RESULT.equals(property))
{
JOptionPane.showMessageDialog(null,
"At least one validation result changed");
} else
if (ValidationResultModel.PROPERTYNAME_MESSAGES.equals(property))
{
if (Boolean.TRUE.equals(evt.getNewValue())) {
JOptionPane.showMessageDialog(null,
"Overall validation changed");
}
}
}
}
public static void main(String[] args) {
Validation example = new Validation();
example.show();
}
}
There's a lot of code here, so let's go from the top to the bottom looking at the new code.
We have declared two new instance variables.
validationResultModel will hold onto and organize the
ValidationMessages for us. phonePattern uses a regex to
define the legal type of phone number we will accept (i.e. 314-555-1212); it
will be used in later validation.
In the constructor, we have added ValidationListener as a
listener to the validationResultModel to demonstrate some user
notification. The effect of this listener will be described in more detail
later.
Within createPanel() method, we have added a report list
created by ValidationResultViewFactory. This report list is a
custom JList that is blank if there are no known validation
problems, but then it shows a listing of the ValidationMessages
with an icon indicating the severity of each. The
ValidationResultViewFactory that creates this JList for
us also has methods that create convenient JTextArea and
JTextPane components as well.
The new validate() method is the core of the validation. Here,
we validate the following conditions:
ValidationUtils class mentioned above.
Within the OkAction, we added a check that will dispose of the
frame only if there are no validation errors. If we used
validationResultModel.hasErrors() instead of
validationResultModel.hasMessages(), then all warnings would have to
resolved, too.
The new ValidationListener inner class was added to the
validationResultModel in the constructor. The effect of this listener
is that the user is notified when the state of validation (pass or fail) has
changed (upon clicking the "OK" button), and when the list of
ValidationMessages changes. So, the first time an invalid state is
found, both "At least one validation result changed" and "Overall validation
changed" will be displayed in popup dialogs. From then on, whenever one or
more validations change (such as entering a value for the name field), the
"At least one validation result changed" message will be displayed. In a
real application, these popup dialogs would be annoying, but it is included
here as an example.
On launch, this version looks slightly different because of the space reserved for the report list:
If we click the OK button now, the current state will be evaluated, and an error will be recorded for each of the three fields. After disposing of the two notification dialogs described above, we see the following new state:
This shows very clearly the error messages in a list with icons indicating the severity.
In a first attempt to resolve the issues, we put a "z" in each field and
click the OK button again. Once again, the current state is evaluated. The
new value is legal for the name field, is illegal for the username field,
and causes a warning in the phone number field. Because we have changed the
validation state of one or more of the fields, we again have to close the
"At least one validation result changed" dialog which comes from listening
for changes to messages in the validationResultModel. However,
the "Overall validation changed" dialog does not appear because the overall
state (failure) has not changed. After disposing of the one dialog, this is
the state:
This again shows very clearly the validation messages in a list with icons indicating the severity (one is a warning and one is an error), and a message is given telling us how to resolve the problem.
The last step will be to change the username value to "validation," a legal
value. Now, when we click on the OK button, the "Validation has been
performed" dialog appears (because we have gone from an invalid state to a
valid state), and the frame is disposed. Note that the form will close even
though there is still a warning because of the invalid phone number; this
happens because we told the OkAction to check for errors, not
messages.
In addition to enabling the validation itself, the Validation framework
provides some nice hints and conveniences for the user. The following class
is based on Validation, but replaces the
ValidationListener with a FocusChangeHandler that
updates a JLabel with a hint based on the field with focus. It
also uses three different methods from the Validation Framework to provide a
visual indication that a field is required (in a real application, only one
of the three approaches would be used). Important additions since
Validation are bold.
import com.jgoodies.forms.builder.DefaultFormBuilder;
import com.jgoodies.forms.factories.ButtonBarFactory;
import com.jgoodies.forms.layout.FormLayout;
import com.jgoodies.validation.ValidationResult;
import com.jgoodies.validation.ValidationResultModel;
import com.jgoodies.validation.util.DefaultValidationResultModel;
import com.jgoodies.validation.util.ValidationUtils;
import com.jgoodies.validation.view.ValidationComponentUtils;
import com.jgoodies.validation.view.ValidationResultViewFactory;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class InputHints {
private final JFrame frame = new JFrame("InputHints");
private final JLabel hintLabel = new JLabel();
private final JTextField name = new JTextField(30);
private final JTextField username = new JTextField(30);
private final JTextField phoneNumber = new JTextField(30);
private final ValidationResultModel validationResultModel =
new DefaultValidationResultModel();
private final Pattern phonePattern =
Pattern.compile("\\b\\d{3}-\\d{3}-\\d{4}");
public InputHints() {
//create a hint for each of the three validated fields
ValidationComponentUtils.setInputHint(name, "Enter a name.");
ValidationComponentUtils.setInputHint(username,
"Enter a username with 6-12 characters.");
ValidationComponentUtils.setInputHint(phoneNumber,
"Enter a phone number like 314-555-1212.");
//update the hint based on which field has focus
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addPropertyChangeListener(new FocusChangeHandler());
this.frame.add(createPanel());
this.frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.frame.pack();
}
private JPanel createPanel() {
FormLayout layout = new FormLayout("pref, 2dlu, pref:grow");
DefaultFormBuilder builder = new DefaultFormBuilder(layout);
int columnCount = builder.getColumnCount();
builder.setDefaultDialogBorder();
//add the label that will show validation hints, with an icon
hintLabel.setIcon(ValidationResultViewFactory.getInfoIcon());
builder.append(this.hintLabel, columnCount);
//add the three differently-decorated text fields
builder.append(buildLabelForegroundPanel(), columnCount);
builder.append(buildComponentBackgroundPanel(), columnCount);
builder.append(buildComponentBorderPanel(), columnCount);
JComponent validationResultsComponent =
ValidationResultViewFactory.createReportList(
this.validationResultModel);
builder.appendUnrelatedComponentsGapRow();
builder.appendRow("fill:50dlu:grow");
builder.nextLine(2);
builder.append(validationResultsComponent, columnCount);
JPanel buttonBar = ButtonBarFactory.buildOKCancelBar(
new JButton(new OkAction()), new JButton(new CancelAction()));
builder.append(buttonBar, columnCount);
return builder.getPanel();
}
//mark name as mandatory by changing the label's foreground color
private JComponent buildLabelForegroundPanel() {
FormLayout layout = new FormLayout("50dlu, 2dlu, pref:grow");
DefaultFormBuilder builder = new DefaultFormBuilder(layout);
JLabel orderNoLabel = new JLabel("Name");
Color foreground = ValidationComponentUtils.getMandatoryForeground();
orderNoLabel.setForeground(foreground);
builder.append(orderNoLabel, this.name);
return builder.getPanel();
}
//mark username as mandatory by changing the field's background color
private JComponent buildComponentBackgroundPanel() {
FormLayout layout = new FormLayout("50dlu, 2dlu, pref:grow");
DefaultFormBuilder builder = new DefaultFormBuilder(layout);
ValidationComponentUtils.setMandatory(this.username, true);
builder.append("Username", this.username);
ValidationComponentUtils.updateComponentTreeMandatoryBackground(
builder.getPanel());
return builder.getPanel();
}
//mark phoneNumber as mandatory by changing the field's border's color
private JComponent buildComponentBorderPanel() {
FormLayout layout = new FormLayout("50dlu, 2dlu, pref:grow");
DefaultFormBuilder builder = new DefaultFormBuilder(layout);
ValidationComponentUtils.setMandatory(this.phoneNumber, true);
builder.append("Phone Number", this.phoneNumber);
ValidationComponentUtils.updateComponentTreeMandatoryBorder(
builder.getPanel());
return builder.getPanel();
}
private void show() {
//same as in Validation.java
}
private ValidationResult validate() {
//same as in Validation.java
}
private final class OkAction extends AbstractAction {
//same as in Validation.java
}
private final class CancelAction extends AbstractAction {
//same as in Validation.java
}
//update the hint label's text based on which component has focus
private final class FocusChangeHandler
implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
String propertyName = evt.getPropertyName();
if ("permanentFocusOwner".equals(propertyName)) {
Component focusOwner = KeyboardFocusManager
.getCurrentKeyboardFocusManager().getFocusOwner();
if (focusOwner instanceof JTextField) {
JTextField field = (JTextField) focusOwner;
String focusHint = (String) ValidationComponentUtils
.getInputHint(field);
hintLabel.setText(focusHint);
} else {
hintLabel.setText("");
}
}
}
}
public static void main(String[] args) {
InputHints example = new InputHints();
example.show();
}
}
Input hints are defined in the constructor for each of the three fields
using the ValidationComponentUtils utility, and the
FocusChangeHandler pulls the current focus hint from the
ValidationComponentUtils as necessary when the focus changes.
This is what the program looks like when the Phone Number field has focus:
Each of the three fields in required, and each uses a different visual indicator of the mandatory status.
Name is marked as mandatory in
buildLabelForegroundPanel() by simply changing the foreground
color of its label to
ValidationComponentUtils.getMandatoryForeground().Username is marked as mandatory in
buildComponentBackgroundPanel() by changing the background color
of the field to a specific color that the Validation framework uses for
mandatory fields.Phone Number is marked as mandatory in
buildComponentBorderPanel() by changing the color of the field's
border to a specific color that the Validation framework uses for
mandatory fields.
Interestingly, neither the Username field nor the Phone
Number field is directly modified to set the mandatory color.
Instead, each is first marked by a call to
ValidationComponentUtils.setMandatory(JComponent comp, boolean
mandatory). This call sets a per-instance value on the
JComponent
using the rarely-used putClientProperty(Object
key, Object value) method. Later, when either the
updateComponentTreeMandatoryBackground(Container) or
updateComponentTreeMandatoryBorder(Container) method is called on
ValidationComponentUtils, the
ValidationComponentUtils uses a visitor pattern to walk through the
Swing component tree and decorate all the mandatory fields with the
requested indicator. If we had more mandatory fields in the same
Container, they would all be decorated by the same single method
call.
In this example, the Username field and the Phone
Number field required different Containers in order to
demonstrate the different behavior decorated to the fields. Of course, in a
real application, the same approach would be used for all mandatory
decoration, so the fields would be on the same JPanel.
JGoodies Validation simplifies user input validation and notification for Swing applications. In this article, we have seen the power of the basic validation framework and the usability features of the framework that assist users with data requirements.
There are many other powerful features of the JGoodies framework, particularly when used in combination with the JGoodies Binding framework. To see even more power that the Validation framework gives you in the location, structure, timing, and presentation of validation and its results, look at JGoodies' excellent WebStart-powered Validation demo.
The code in this article was built using version 2.0.0 of JGoodies Validation and version 1.1.0 of JGoodies Forms, both available for free from JGoodies.
Lance Finney thanks Michael Easter, Tom Wheeler, and Rob Smith for reviewing this article and providing useful suggestions.
OCI is the leading provider of Object Oriented technology training in the Midwest. More than 3,000 students participated in our training program over the last 12 months. Targeted toward Software Engineers and the development community, our extensive program of over 50 hands-on workshops is delivered to corporations and individuals throughout the U.S. and internationally. OCI's Educational Services include Group Training events, Open Enrollment classes, and Courseware Licensing.
For further information regarding OCI's Educational Services programs, please visit our Educational Services section on the web or contact us at training@ociweb.com.
|
|
|