Adding SWT Input Validation the Easy Way
Join the DZone community and get the full member experience.
Join For FreeAny input provided by a user in a GUI application must typically be validated in one way or another. There is a number of ways this gets done, while some applications have just ignored the matter altogether.
When crafting an Eclipse RCP application, there are some help provided by SWT and JFace. We can add ModifyListeners and VerifyListeners to certain SWT widgets. JFace also provides ControlDecorations to help us indicate to the user where a problem with a specific input value exists.
The problem is that these are at a low level, and we need to do a lot of "monkey"-coding just to add basic validation and error indication to a widget, and then we're not even touching the world of input masks. If you're like me, you want to concentrate on solving your business problem, and don't want to write lots of basic UI code over and over.
This is where the RCP Toolbox is very useful. It provides a light-weight validation framework (among other features) that makes it much easier to add validation and input masks to SWT Text, Combo and CCombo widgets.
The goal
Let us have a look at how to define a basic wizard for creating a new Booking. This wizard must capture the following fields from the user:
And of course we want indicators next to each field when an error or warning condition exists in the field, as well as a message being written to the WizardDialog's message area. For fun we want the user to be able to get a quick-fix option on the date field for setting it to the current time.
The Validation framework
The RCP Toolbox provides a number of custom widgets and a easy to use validation framework. Adding validation starts with the ValidationToolkit class. This class gets instantiated to work with a specific type of contents, and is then used to create ValidatingField instances that can handle that type of contents.
The rest of the framework deals with interfaces and default implementations to facilitate the validation of contents, definition of input masks, provision of quick-fixes, error-handling and conversion of the input text to specific class types.
The WizardPage and the ValidationToolkits
We start by first defining our BookingWizardPage class and instantiating the necessary ValidationToolkit instances.
//Not all imports are shown
import com.richclientgui.toolbox.validation.IFieldErrorMessageHandler;
import com.richclientgui.toolbox.validation.ValidationToolkit;
import com.richclientgui.toolbox.validation.converter.DateStringConverter;
import com.richclientgui.toolbox.validation.converter.IntegerStringConverter;
import com.richclientgui.toolbox.validation.string.StringValidationToolkit;
public class BookingWizardPage extends WizardPage {
private static final int DECORATOR_POSITION = SWT.TOP | SWT.LEFT;
private static final int DECORATOR_MARGIN_WIDTH = 1;
private static final int DEFAULT_WIDTH_HINT = 150;
private StringValidationToolkit strValToolkit = null;
private ValidationToolkit<Date> dateValToolkit = null;
private ValidationToolkit<Integer> intValToolkit = null;
private final IFieldErrorMessageHandler errorMessageHandler;
public BookingWizardPage() {
super("booking.pageone","New Booking Entry", null);
errorMessageHandler = new WizardPageErrorHandler();
}
public void createControl(Composite parent) {
final Composite composite = new Composite(parent, SWT.NONE);
composite.setLayout(new GridLayout(2, false));
strValToolkit = new StringValidationToolkit(DECORATOR_POSITION,
DECORATOR_MARGIN_WIDTH, true);
strValToolkit.setDefaultErrorMessageHandler(errorMessageHandler);
intValToolkit = new ValidationToolkit<Integer>(new IntegerStringConverter(),
DECORATOR_POSITION, DECORATOR_MARGIN_WIDTH, true);
intValToolkit.setDefaultErrorMessageHandler(errorMessageHandler);
dateValToolkit = new ValidationToolkit<Date>(new DateStringConverter(),
DECORATOR_POSITION, DECORATOR_MARGIN_WIDTH, true);
dateValToolkit.setDefaultErrorMessageHandler(errorMessageHandler);
//TODO: create ValidatingFields
setControl(composite);
}
}
The StringValidationToolkit class we instantiate in line 28 is a ValidationToolkit that deals specifically with ValidatingFields that have normal String contents. In line 32 we instantiate a typed instance of ValidationToolkit that will create ValidatingFields that only takes Integers as input.
We must provide a way that the contents of the fields are converted from a String to the correct content type. This is done with a set of coverter classes provided by the framework. In lines 32 and 36 we specify a IntegerStringConverter and a DateStringConverter to convert Integer and java.util.Date values respectively.
The framework makes use of the JFace org.eclipse.jface.fieldassist.ControlDecoration and related classes to indicate whether a field has an error or warning condition, whether it is a required field, and if there is a quick-fix available (by right-clicking on the decorator icon) for the current error or warning condition. The position of these decorator icons relative to the input widgets as well as the margin width between the decorator icon and the widget can be specified when constructing a ValidationToolkit. All the fields created by this ValidationToolkit instance will use the same settings for there decorator icons. In lines 9 - 10 we have defined some constants for the decorator position and margins, and we use this for constructing all the ValidationToolkit instances.
Handling the error messages
We also make use of an IFieldErrorMessageHandler to get feedback from the validation process. The validation framework will call these error handlers when error or warning conditions occur, and allow us to do something with those messages. By default these messages are only displayed in the tooltips of the decorator icons. A default error handler can be specified for each toolkit instance, or a separate handler can be set for each ValidatingField if so required.
The inner class WizardPageErrorHandler implements the IFieldErrorMessageHandler interface and basically just set the messages on the WizardPage's message area.
//inner class of BookingWizardPage
class WizardPageErrorHandler implements IFieldErrorMessageHandler {
public void handleErrorMessage(String message, String input) {
setMessage(null, DialogPage.WARNING);
setErrorMessage(message);
}
public void handleWarningMessage(String message, String input) {
setErrorMessage(null);
setMessage(message, DialogPage.WARNING);
}
public void clearMessage() {
setErrorMessage(null);
setMessage(null, DialogPage.WARNING);
}
}
The actual error or warning messages are generated by the various IFieldValidator implementations (we'll get to those), and can easily be customized by implementing custom validators.
Creating a simple ValidatingField
Of course just having some toolkit instances does not help us much. We need actual input widgets that are being validated. The first step is to update the createControl method.
public void createControl(Composite parent) {
final Composite composite = new Composite(parent, SWT.NONE);
composite.setLayout(new GridLayout(2, false));
strValToolkit = new StringValidationToolkit(DECORATOR_POSITION,
DECORATOR_MARGIN_WIDTH, true);
strValToolkit.setDefaultErrorMessageHandler(errorMessageHandler);
intValToolkit = new ValidationToolkit<Integer>(new IntegerStringConverter(),
DECORATOR_POSITION, DECORATOR_MARGIN_WIDTH, true);
intValToolkit.setDefaultErrorMessageHandler(errorMessageHandler);
dateValToolkit = new ValidationToolkit<Date>(new DateStringConverter(),
DECORATOR_POSITION, DECORATOR_MARGIN_WIDTH, true);
dateValToolkit.setDefaultErrorMessageHandler(errorMessageHandler);
createNameField(composite);
createDateField(composite);
createNumberPersonsField(composite);
createTelephoneNumberField(composite);
setControl(composite);
}
Then we can look at creating our first validated input field that makes use of a SWT Text widget to capture the name of the person doing the booking.
private void createNameField(Composite composite) {
new Label(composite, SWT.NONE).setText("Booking Name:");
final ValidatingField<String> nameField = strValToolkit.createTextField(
composite, new IFieldValidator<String>(){
public String getErrorMessage() {
return "Name may not be empty.";
}
public String getWarningMessage() {
return "That's a very short name...";
}
public boolean isValid(String contents) {
return !(contents.length()==0);
}
public boolean warningExist(String contents) {
return contents.length() < 3;
}
}, true, "");
GridData gd = new GridData(SWT.LEFT, SWT.CENTER, false, false);
gd.widthHint = DEFAULT_WIDTH_HINT;
nameField.getControl().setLayoutData(gd);
}
Since this field works with String contents, we make use of the strValToolkit instance to create the field in line 4 above. We specify the parent composite that the input widget must be added to, the IFieldValidator that will be used to validate the field contents, whether this is a required field or not (thus whether the required decorator icon must be shown or not) and an initial empty string value for the field. Note that this call will also create a Text widget to be used for the field, but the API allows that you can create your own Text, Combo or CCombo instance and pass that to the toolkit to use when creating a new ValidatingField.
An anonymous inner class implementation of IFieldValidator is specified in lines 5 - 23. We're doing some very basic validation checks in this example, but it is easy to implement validators that makes use of other heavy-weight business validation frameworks. Our validator will indicate an error condition if the contents of the field is empty (line 16), in which case the error message "Name may not be empty." will be displayed (line 8). This validator will also indicate a warning condition if the name field contains less than 3 characters (line 20) with the message "That's a very short name..." (line 12).
In lines 24 - 26 we set the layout of the input widget on the composite.
Dating an input mask
Dates have always been a difficult input type to deal with. The easiest way for us developers to deal with them are to use widgets that pop up a calendar from where the user can choose a day, and possibly a time as well. However, this way of dealing with dates are not always a favourite with touch-typing end-users. They prefer some masked field where they only need to fill in the bits of the date that matter. Using the mouse should be restricted as far as possible. Luckily the RCP Toolbox provides a way of specifying input masks, as well as specific implementations of validators and converters for dealing with Dates.
We want to create a field that only takes dates as input, where the date entered must be of the form yyyy-MM-dd HH:mm (e.g 2008-07-19 21:00), and fall in the date range starting at the current time and ending at the end of the year 2008.
private void createDateField(Composite composite) {
new Label(composite, SWT.NONE).setText("Booking Time:");
final Date endYear = getEndYearDate();
//we create a Date field that takes input of form yyyy-MM-dd HH:mm
//and only allows values from now till the end of the year
final ValidatingField<Date> rangedDateField
= dateValToolkit.createTextField(composite,
new RangedDateFieldValidator(
DateFieldValidator.DATE_TIME_HHMM_DASH,
dateValToolkit.getStringConverter(),
new Date(), endYear),
true,
new Date());
GridData gd = new GridData(SWT.LEFT, SWT.CENTER, false, false);
gd.widthHint = DEFAULT_WIDTH_HINT;
rangedDateField.getControl().setLayoutData(gd);
}
Here we used the dateValToolkit instance to create the field as the contents of the field must be a java.util.Date. We specify an instance of RangedDateFieldValidator (provided by the framework) that makes use of the specified Date input mask pattern DateFieldValidator.DATE_TIME_HHMM_DASH (line 9) to validate the contents of the field. Other patterns are available, or custom ones can also be defined. The DateStringConverter specified when dateValToolkit was constructed will be used to convert from Dates to Strings and vice versa.
In line 11 we specify the valid date range for the field, from the current date and time to the end of the year, and in line 13 we set the initial value of the field to the current time.
Providing quick-fixes
I found that developers using Eclipse RCP to develop their applications like to make the experience for the end-user as good as possible. So we should not stop at just validating input; we must also try and help them quickly fix mistakes, where possible. In this example we can do that by specifying a IQuickFixProvider.
//add at end of createDateField(..) method
//we add a quickfix that will set it to the current date
rangedDateField.setQuickFixProvider(new IQuickFixProvider<Date>(){
public boolean doQuickFix(ValidatingField<Date> field) {
field.setContents(new Date());
return true;
}
public String getQuickFixMenuText() {
return "Set to current time";
}
public boolean hasQuickFix(Date contents) {
//would typically first check contents to determine if quickfix
//is possible
return true;
}
});
The above is a very simple quick-fixer. It always says it has a quick-fix available (line 17), where a more complex provider will first check the contents of the field to determine if there is a quick-fix. When the user performs the quick-fix, it just sets the contents of the field to the current date and time (line 6).
When the validation framework detects there is an error condition on a field, it will see if there is a IQuickFixProvider available with a quick-fix. If this is the case, it will add the quick-fix option to a context-menu on the decorator icon with the text specified in line 11. All the user then needs to do is right-click on the decorator icon and select the quick-fix to perform.
Validating Combos
A Combo widget would be just the thing to use for capturing the number of persons for the booking. Our requirements say we must limit the number to a maximum of 10 people (and a minimum of 1 goes without saying). Once again we do not want to force the user to use numerous mouse-clicks or keystrokes to select the number, so we do not make the Combo read-only, and rather decide to add validation to it.
private void createNumberPersonsField(Composite composite) {
new Label(composite, SWT.NONE).setText("Number of persons:");
final ValidatingField<Integer> numberPersonsField
= intValToolkit.createComboField(
composite, new StrictRangedNumberFieldValidator<Integer>(1, 10){
public String getErrorMessage() {
return "Bookings for groups bigger than 10 not allowed";
}
public String getWarningMessage() {
return null;
}
public boolean warningExist(Integer contents) {
return false;
}
},
true,
2,
new Integer[]{1,2,3,4,5,6,7,8,9,10});
GridData gd = new GridData(SWT.LEFT, SWT.CENTER, false, false);
gd.widthHint = DEFAULT_WIDTH_HINT;
numberPersonsField.getControl().setLayoutData(gd);
}
Here we make use of the intValToolkit.createComboField method to create a field containing a Combo widget with contents of type Integer. We specify a StrictRangedNumberFieldValidator (line 6) to ensure that the entered value only consists of digits and falls in the range 1 to 10. No warning conditions are checked. In line 22 we populate the Combo with a list of Integers from 1 to 10, and in line 21 we select a default value of 2. As easy as counting to 5.
And don't forget the telephone number
Freeform telephone number fields are used a lot, but unfortunately end-users can easily make mistakes in such fields. We want to force our user to input the telephone number in a specific form, thus at least preventing the cases where digits are missed. To do this, we make use of the framework's TelephoneNumberValidator. This validator allows telephone number to be entered in either international format (e.g. +44 (55) 555-5555) or in domestic format (e.g. (055) 555-5555).
private void createTelephoneNumberField(Composite composite) {
new Label(composite, SWT.NONE).setText("Contact Telephone Nr:");
final ValidatingField<String> telephoneField
= strValToolkit.createTextField(
composite,
new TelephoneNumberValidator(true),
true,
"+44 (55) 555-4321");
GridData gd = new GridData(SWT.LEFT, SWT.CENTER, false, false);
gd.widthHint = DEFAULT_WIDTH_HINT;
telephoneField.getControl().setLayoutData(gd);
}
We are using strValToolkit to create the field, since the contents will be managed as a String. Then we specify a TelephoneNumberValidator as the validator in line 7, with the true parameter indicating that we want to use the international format. In line 9 we provide an initial value.
Conclusion
This article describes a very simple example of how to add validation to SWT widgets using the RCP Toolbox. In a real-world application the actual business validations to be done might be more complex, but if this validation framework is used, the UI code related to validation would remain as simple as the code in these examples.
This validation framework really made our development much easier, allowing us to concentrate on the business code. It is very easy to extend the framework with custom validators, converters, quick-fix providers and error handlers that ties into existing business code or other validation code, rules engines etc.
Note that the examples in this article need Eclipse RCP 3.3 or 3.4 as well as RCP Toolbox v1.0.1, created and distributed by www.richclientgui.com.
Opinions expressed by DZone contributors are their own.
Comments