One of the main features of the JGUIraffe library is its support for forms. Typically, the creation and management of a form is a task most developers are not crazy about. Though not really complicated, there is a lot of boiler-plate code that has to be written in order to
The Form
class and other helper classes located in the
net.sf.jguiraffe.gui.forms
package are designed to simplify the
work with forms to a great extent. In the following sections the usage of
the Form
class is described. This knowledge is required if a
Form
instance is to be used directly (which already should be
of great help compared with standard programming of user interface
components). When used together with the builder framework most of the
interaction with forms is done under the hood by the framework. In fact, it
is then hardly necessary to access a Form
object at all!
A Form
object stores an internal model of all its input fields. It is not a visible
object, but only provides access to user input. Each input field is
represented by an object implementing the
FieldHandler
interface. The FieldHandler
acts as a kind of adaptor between
a logic input field and its data and the physical input component that
is visible to the user. It provides the following functionality:
ComponentHandler
. The ComponentHandler
knows how to
interact with an input component of a specific type. For instance, there
are special ComponentHandler
implementations for text fields,
checkboxes, radio buttons, list fields, etc.
DefaultFieldHandler
there is a fully functional implementation
of the FieldHandler
interface. A DefaultFieldHandler
can be initialized with all the helper objects like transformers, validators, and a
ComponentHandler
it needs for fulfilling its tasks.
To construct a Form
object the form constructor has to be called;
then the FieldHandler
objects representing the input fields
have to be added by calling the addField()
method. The
constructor of the Form
class expects two helper objects:
TransformerContext
object: This object is needed by the transformers
and validators used by the form, e.g. to resolve resources, determine the
current Locale
, and get current properties.
TransformerContext
is a super interface of
ApplicationContext
. So the current implementation of this
interface can be directly obtained from the central
Application
object.
BindingStrategy
object: A binding strategy is responsible
for the communication between the Form
object and the data
model used by the application. This is discussed in detail a little later.
For each input field to be managed by the form the addField()
method has to be called. addFields()
expects the (logic) name
of the input field and its corresponding FieldHandler
as
arguments. Constructing the FieldHandler
objects with all their
dependent helper objects is not a trivial task. Doing this manually can be
cumbersome. In this area the form builder framework is a great
help.
Every Form
object is associated with a
BindingStrategy
. As was already pointed out, the
BindingStrategy
is responsible for the interaction with the
data model used by the application.
The idea behind a BindingStrategy
is that applications may use
different ways for storing the data to be managed by forms: Some applications
may use standard Java beans (e.g. Hibernate objects or JPA entities fall
into this category). If beans are used as data model for a form, the input
fields typically have to be mapped to bean properties. Other applications
may operate on special data transfer objects, e.g. Service
Data Objects (SDO) or map-based objects. A Form
does not
have to know about the underlying model objects; this lies in the
responsibility of the BindingStrategy
.
The BindingStrategy
interface is pretty lean: it defines one
method for reading a property from a model object, and another one for
writing a property. It should not be too complicated to implement this
interface to support a custom type of model objects. The default binding
strategy is
BeanBindingStrategy
, which operates on standard Java beans. It
uses reflection for reading and writing properties from and to model objects,
which must conform to the Java beans standard.
When a form is opened its input fields typically have to be initialized with data from the model. As an example consider a form that allows editing an existing entity; of course, the data of this entity has to be displayed.
The Form
defines the initFields()
method for this
purpose. The method is passed a model object, which contains the data to be
displayed. This model object must be compatible with the
BindingStrategy
used by the form (see above)! It iterates over
all input fields contained in the form and asks the BindingStrategy
to read the corresponding properties from the model object. The values of
these properties are then transformed - according to the transformers
defined for the FieldHandler
objects - and written into the
visual input components of the form.
There is also an overloaded version of initFields()
that
additionally accepts a set with the names of the fields to be initialized.
That way only a subset of the form's fields can be initialized, which may
be useful in some situations. Many of the methods provided by the
Form
class allow restricting their operation on a subset of
fields.
Validation of a form means checking that all input entered by the user in the form's input fields conforms to certain criteria enforced by so-called validator objects associated with the input fields or the form as a whole. Typical validation criteria are rules like "Field X must not be empty", "Field Y must contain a valid number between 0 and 100", or "Field Z must contain a date that lies in the future". In most cases the logic of an application dictates the validation rules that must be checked.
However, ensuring that all input fields contain valid data is only one half
of the game. In most cases it is also desired that the data of the input
fields is extracted, converted into the correct data type (for instance, an
input field that accepts only numeric data should produce an
Integer
or Double
object rather than a string),
and passed to the data model of the application. The
Form
class supports this with the help of Transformer
and
Validator
objects.
A Transformer
is responsible for data conversion. When a form is
opened and the input fields are populated from the data model one
Transformer
- the so-called write transformer has to
covert the type of a model property to the expected data type for the input
component used. For instance, text fields only operate on strings, even if
they should accept only numeric data. So a number object obtained from the
data model has to be converted into a string. When the user clicks the OK
button and the form needs to be read the opposite transformation has to be
performed (in the example with the numeric input field the string obtained
from the input field has to be converted to a number, which is then stored
in the data model). This second transformation is done by another
Transformer
- the read transformer. Both types of
transformers are associated with the FieldHandler
representing
the corresponding input field. The
DefaultFieldHandler
class provides get and set methods for these
two Transformer
objects.
Transformations can only be successful if the data entered by the user is
valid. If the user has entered the text "ABC" in the numeric input field
Age, no Integer
can be created for that input. So
Validator
objects have to work together closely with
Transformer
objects to ensure correctness. The Form
class supports three levels of validation:
DefaultFieldHandler
instance.
The lowest level of validation is the syntax check of the form's input fields. It
is performed by the validator set by the setSyntaxValidator()
method of
DefaultFieldHandler
. Its purpose is to ensure that data
entered by the user can be successfully converted to the desired target
data type. For instance, if an input field accepts a numeric value, this
validation has to ensure that the string entered by the user is indeed a
valid number. Providing a correct syntax validator is important
because otherwise transformation to the target data type is impossible. The
syntax validator must be compatible with the read transformer
of the DefaultFieldHandler
, i.e. if it indicates a valid input,
transformation must be guaranteed to be successful.
The next validation level is the logic check of the form's input fields. This
phase is initiated after the read transformer has been invoked, and
it is executed by the validator set through the setLogicValidator()
method of DefaultFieldHandler
. The main difference to the
syntactic validation is that the object passed to the logic validator
is not the plain string entered by the user, but the result of the
read transformer, i.e. the data converted to the target data type.
In the example of the numeric input field, the logic validator
would be passed an Integer
object. This simplifies logic
checks. The purpose of the logic validation is to check for conditions on
the target data type, e.g. "Is the number between 0 and 100" or "Is the
date in the future".
The syntax and the logic validation operate on single input fields and are
therefore initiated by the FieldHandler
objects. (By the way,
there is an enumeration class
ValidationPhase
that assigns names to the single phases of
field validation.) In many use cases more complex conditions have to be
checked that involve multiple input fields. As an example consider a dialog
for entering payment options: here a validation rule could be defined that
the creditCardNo
field must contain a valid credit card number,
but only if the payment
radio button is enabled. In practice
arbitrarily complex validation rules are possible which include all input
fields of the form. So the whole data of the form must be available at this
stage. Responsible for this kind of validation are objects implementing the
FormValidator
interface. A Form
object can be
associated with a FormValidator
using its
setFormValidator()
method. The FormValidator
is
invoked after validation at the field level (both syntax and logic validation)
have been successful. We describe form validators in the next section.
After all that theory, how is validation triggered in practice? The good
news is that this is a matter of a single method call: just call the
Form.validate()
method and pass in a data model object.
validate()
first performs syntactic and logic validation at
the field level. If this is successful, the FormValidator
is
invoked if it is defined. If all validation steps have been successful, the
data is written into the passed in model object (which must conform to the
BindingStrategy
set for the form). Otherwise, if there is at
least one validation failure, the model object is not checked. In any case
validate()
returns an object of the class
FormValidationResults
. Using this object the result of the
validation (success or failure) can be checked. If there are validation
errors, the input fields affected and the corresponding error messages can
be retrieved. Basically, a FormValidationResults
object
contains all information required for giving the user feedback about a
failed validation.
The
FormValidator
interface has already been mentioned in the last
section. In this section we discuss how to implement a concrete form
validator and how to associate it with a Form
object.
The FormValidator
interface contains only a single method:
isValid()
. This method expects a Form
object as
parameter and returns a
FormValidationResults
with the results of the validation.
Typically form validators do very specific things that strongly depend on
application logic. Because of that the framework cannot provide any
meaningful base classes to extend from when writing custom form validators.
In any case the form validator will have to access the values of the form's
input fields for checking their current values. At the time the
isValid()
is triggered, validation at the field level was
already successful. This means the validator can assume that all fields
contain syntactically correct data, which has already been converted to the
appropriate target data type. There are two ways for accessing the data of
the single input fields:
FieldHandler
objects of specific fields can be obtained
by calling the getField()
method of Form
. The name
of the field has to be passed in. The FieldHandler
interface
defines a getMethod()
, which returns the current value of
this field (in the target data type).readFields()
method. readFields()
can be called after a successful
validation at the field level. It copies the current data of the input
fields into the passed in model data object (which must be compatible with
the BindingStrategy
used by the form).
The results of the validation are returned in form of a
FormValidationResults
object. This object not only has a boolean
valid flag, but - in case of a failed validation - also contains
information about the invalid fields and the validation errors. A default
implementation of FormValidationResults
is provided by the
DefaultFormValidationResults
class. An instance of this class
is constructed with a map of validation results for the single fields of
the form. The map uses the names of the input fields as keys and associates
them with
ValidationResult
objects (refer to the section about transformers
and validators for more information about ValidationResult
).
We will now present a small example of an implementation of the
FormValidator
interface. We assume that the validator checks
a form with shipment information of an online shop. It enforces the
validation rule: "If the user checked the field different delivery
address, the field delivery address must be filled". An
implementation could look as follows:
public class ShipmentFormValidator implements FormValidator { // Constants for the names of input fields private static final String FLD_DIFFERENT_ADDRESS = "differentDeliveryAdr"; private static final String FLD_DELIVERY_ADDRESS = "deliveryAdr"; public FormValidationResults isValid(Form form) { // Set up the default map with valid results Map<String, ValidationResult> results = DefaultFormValidationResults.validResultMapForForm(form); // Now implement validation checks Boolean differentAddress = (Boolean) form.getField(FLD_DIFFENT_ADDRESS).getData(); if (differentAddress.booleanValue()) { String s = (String) form.getField(FLD_DELIVERY_ADDRESS).getData(); if (s == null || s.length() < 1) { results.put(FLD_DELIVERY_ADDRESS, DefaultFormValidationResults.createValidationErrorResult(form, ValidationMessageConstants.ERR_FIELD_REQUIRED)); } } // further checks ommitted // Construct the result object return new DefaultFormValidationResults(results); } }
The actual validation check should be pretty clear. The remaining code for
setting up the results object deserves some explainations: The validator
must return a FormValidationResults
object and uses the
default implementation DefaultFormValidationResults
for this
purpose. The constructor of this class expects a map with
ValidationResult
objects for all form fields. With the static
method validResultMapForForm()
such a map is created that contains
only valid ValidationResult
objects. This is a good starting
point; during validation some of these objects may be replaced by
ValidationResult
objects with error messages.
DefaultFormValidationResults
defines a convenience method for
creating a ValidationResult
object for a failed validation:
createValidationErrorResult()
. This method is passed the key
of the error and otional parameters. Our example validator calls this
method if the validation rule for the delivery address is violated.
The implementation of FormValidator
in this example used direct
access to the form's imput fields. By using a model object the code could be
simplified a bit. We assume that a Java bean class named
ShipmentBean
exists with properties that correspond to the
form's fields. In this case the implementation could look like:
public class ShipmentFormValidator implements FormValidator { // Constants for the names of input fields private static final String FLD_DELIVERY_ADDRESS = "deliveryAdr"; public FormValidationResults isValid(Form form) { // Set up the default map with valid results Map<String, ValidationResult> results = DefaultFormValidationResults.validResultMapForForm(form); // Obtain the data from the form ShipmentBean data = new ShipmentBean(); form.readFields(data); // Now implement validation checks if (data.isDifferentAddress()) { String s = data.getDeliveryAddress(); if (s == null || s.length() < 1) { results.put(FLD_DELIVERY_ADDRESS, DefaultFormValidationResults.createValidationErrorResult(form, ValidationMessageConstants.ERR_FIELD_REQUIRED)); } } // further checks ommitted // Construct the result object return new DefaultFormValidationResults(results); } }