Working with forms

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

  • populate the input fields with their initial values,
  • react on the OK and Cancel buttons in an appropriate way,
  • perform validation of user input (which can happen when the user presses OK or already during editing, so that invalid input can be reported immediately,
  • and finally collect the data entered by the user and store it in the data model used by the application.

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!

Constructing a form

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:

  • Read and write access to the physical input component. This is actually done by another helper object: a 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.
  • Transformation of data. For instance, a text input field might actually be used for entering a numeric value. So, when populating the input fields when the form is opened the number obtained from the application's data model must be converted to a string. When the form is closed and its data is to be written back into the model the inverse transformation has to be performed.
  • Validation of data. In order to perform the transformations necessary, it has to be ensured that the data typed in by the user is syntactically correct (e.g. a text entered by the user is indeed a valid number). In most cases a semantic validation is required, too (e.g. the number must be between 0 and 100).
With 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:

  • A 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.
  • A 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.

The binding strategy

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.

Initializing input fields

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

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:

  1. Syntactic validation at the field level. This validation checks whether the data entered by the user actually conforms to the target data type.
  2. Logic validation at the field level. Here logic conditions can be checked after the data has been converted to the target data type.
  3. Logic validation at the form level. At this layer conditions referring to multiple fields can be checked.
We describe these validation levels in more detail and discuss how they are related to the validators that can be specified for a 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.

Form validators

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:

  • The 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).
  • In order to get the content of the form as a whole the validator can create a new model object and call the form's 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);
    }
}