Using State Guard in GUI

The State Guard pattern works very well with the patterns Builder, Chained Creator and Context Switcher but here we will demonstrate how it can be used all by itself in a graphical user interface to handle validation and transition to a valid state. The example is written in Java and uses Swing with the layout manager SpringLayout. The source code is hosted at GitHub.

Lets say we want to create this GUI:

The program starts by executing the class Main:
package nu.tengstrand.stateguard.guiexample;

import nu.tengstrand.stateguard.guiexample.person.Person;
import nu.tengstrand.stateguard.guiexample.person.PersonStateGuard;

import javax.swing.*;

public class Main {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                final PersonStateGuard personStateGuard = new PersonStateGuard();

                new PersonFrame(personStateGuard, new SaveCommand() {
                    public void save() {
                        Person person = personStateGuard.asValidState();
                        new PopupFrame(person);
                   }
                });
            }
        });
    }
}
  • row 13:
    the class PersonStateGuard holds all the attributes used in the GUI.
  • row 15:
    references of PersonStateGuard and the interface SaveCommand is sent in to the constructor of PersonFrame:
package nu.tengstrand.stateguard.guiexample;

import nu.tengstrand.stateguard.Validatable;
import nu.tengstrand.stateguard.guiexample.person.PersonStateGuard;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ResourceBundle;

public class PersonFrame extends JFrame {
    static ResourceBundle resourceBundle = ResourceBundle.getBundle("validationMessages");

    public PersonFrame(final PersonStateGuard person, final SaveCommand saveCommand) {
        setTitle("State Guard example - by Joakim Tengstrand");
        setPreferredSize(new Dimension(450, 190));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        Container contentPane = getContentPane();
        SpringLayout layout = new SpringLayout();
        contentPane.setLayout(layout);

        // Name
        JLabel nameLabel = new JLabel("Name: ");
        JTextField nameTextField = new JTextField("", 15);
        JLabel nameError = new JLabel();
        nameError.setForeground(Color.RED);
        contentPane.add(nameLabel);
        contentPane.add(nameTextField);
        contentPane.add(nameError);

        // Age
        JLabel ageLabel = new JLabel("Age: ");
        JTextField ageTextField = new JTextField("", 5);
        JLabel ageError = new JLabel();
        ageError.setForeground(Color.RED);
        contentPane.add(ageLabel);
        contentPane.add(ageTextField);
        contentPane.add(ageError);

        // Country
        JLabel countryLabel = new JLabel("Country: ");
        JTextField countryTextField = new JTextField("", 10);
        JLabel countryError = new JLabel();
        countryError.setForeground(Color.RED);
        contentPane.add(countryLabel);
        contentPane.add(countryTextField);
        contentPane.add(countryError);

        // Save button
        final JButton saveButton = new JButton("Save");
        saveButton.setEnabled(false);
        saveButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                saveCommand.save();
            }
        });
        contentPane.add(saveButton);

        // Validation explanation
        JLabel validationErrorExplanationLabel = new JLabel("* = Mandatory field");
        contentPane.add(validationErrorExplanationLabel);

        connectTextFieldToModel(person.name(), nameTextField, nameError, person, saveButton);
        connectTextFieldToModel(person.age(), ageTextField, ageError, person, saveButton);
        connectTextFieldToModel(person.country(), countryTextField, countryError, person, saveButton);

        // Spring layout constraints
        layout.putConstraint(SpringLayout.WEST, nameLabel, 5, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, nameLabel, 5, SpringLayout.NORTH, contentPane);
        layout.putConstraint(SpringLayout.WEST, nameTextField, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, nameTextField, 5, SpringLayout.NORTH, contentPane);
        layout.putConstraint(SpringLayout.WEST, nameError, 20, SpringLayout.EAST, nameTextField);
        layout.putConstraint(SpringLayout.NORTH, nameError, 5, SpringLayout.NORTH, contentPane);

        layout.putConstraint(SpringLayout.WEST, ageLabel, 5, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, ageLabel, 25, SpringLayout.NORTH, nameTextField);
        layout.putConstraint(SpringLayout.WEST, ageTextField, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, ageTextField, 25, SpringLayout.NORTH, nameTextField);
        layout.putConstraint(SpringLayout.WEST, ageError, 20, SpringLayout.EAST, ageTextField);
        layout.putConstraint(SpringLayout.NORTH, ageError, 25, SpringLayout.NORTH, nameTextField);

        layout.putConstraint(SpringLayout.WEST, countryLabel, 5, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, countryLabel, 25, SpringLayout.NORTH, ageTextField);
        layout.putConstraint(SpringLayout.WEST, countryTextField, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, countryTextField, 25, SpringLayout.NORTH, ageTextField);
        layout.putConstraint(SpringLayout.WEST, countryError, 20, SpringLayout.EAST, countryTextField);
        layout.putConstraint(SpringLayout.NORTH, countryError, 25, SpringLayout.NORTH, ageTextField);

        layout.putConstraint(SpringLayout.WEST, validationErrorExplanationLabel, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, validationErrorExplanationLabel, 30, SpringLayout.NORTH, countryLabel);

        layout.putConstraint(SpringLayout.WEST, saveButton, 80, SpringLayout.WEST, contentPane);
        layout.putConstraint(SpringLayout.NORTH, saveButton, 30, SpringLayout.NORTH, validationErrorExplanationLabel);

        pack();
        setVisible(true);
    }

    private void connectTextFieldToModel(final ValidatableStringValue validatableStringValue, JTextField textField, final JLabel error, final Validatable person, final JButton saveButton) {
        error.setText(validatableStringValue.validationMessages().firstMessage(resourceBundle));

        textField.getDocument().addDocumentListener(new UpdateTextListener() {
            public void setText(String text) {
                validatableStringValue.setValue(text);
                error.setText(validatableStringValue.validationMessages().firstMessage(resourceBundle));
                saveButton.setEnabled(person.isValid());
            }
        });
    }
}
  • row 16-63, 69-98:
    GUI setup code
  • row 54:
    Callback to Main via the SaveCommand interface.
  • row 65-67:
    When a text field is edited, this will happen:
    • the corresponding attribute (name, age, country) in PersonStateGuard is updated.
    • if the text field does not validate, the validation message is shown (asterisk if empty).
    • the save button is enabled/disabled depending on if the PesonStateGuard instance is valid (line 108).
Lets demonstrate this with a couple of pictures:

The asterisk shows the mandatory fields. When the Name is filled in, the asterisk goes away. This is handled by the class NameStateGuard (the property mandatory.field at row 9 is stored in the property file validationMessages):
package nu.tengstrand.stateguard.guiexample.person;

import nu.tengstrand.stateguard.StateGuard;
import nu.tengstrand.stateguard.guiexample.ValidatableStringValue;
import nu.tengstrand.stateguard.validator.NonEmptyString;

public class NameStateGuard extends StateGuard implements ValidatableStringValue {
    private NonEmptyString name = NonEmptyString.attributeName("name")
                                  .messageKey("mandatory.field");

    public NameStateGuard() {
        addValidator(name);
    }

    public void setValue(String value) {
        name.setValue(value);
    }

    @Override
    protected Name createValidState() {
        return new Name(name.value());
    }
}

When an invalid integer is typed, a validation error is shown. This is handled by the class AgeValidator in AgeStateGuard:
package nu.tengstrand.stateguard.guiexample.person;

import nu.tengstrand.stateguard.StateGuard;
import nu.tengstrand.stateguard.ValidationMessages;
import nu.tengstrand.stateguard.guiexample.ValidatableStringValue;
import nu.tengstrand.stateguard.validator.IntegerValidator;

public class AgeStateGuard extends StateGuard implements ValidatableStringValue {
    private AgeValidator age = new AgeValidator();

    private static final int MIN_AGE = 0;
    private static final int MAX_AGE = 150;

    public AgeStateGuard() {
        addValidator(age);
    }

    public void setValue(String value) {
        age.setValue(value);
    }

    @Override
    protected Age createValidState() {
        return new Age(age.value());
    }


    private static class AgeValidator extends IntegerValidator {
        AgeValidator() {
            super("age");
        }

        @Override
        public boolean isValid() {
            return super.isValid() && value() >= MIN_AGE && value() <= MAX_AGE;
        }

        @Override
        public ValidationMessages validationMessages() {
            if (isValid()) {
                return ValidationMessages.withoutMessage();
            }
            if (stringValue() == null || stringValue().length() == 0) {
                return ValidationMessages.message("*");
            }
            return ValidationMessages.message("Enter a valid age");
        }
    }
}
The AgeValidator also checks if the age is within the range 0 to 150 (line 35).
The validation of Country is handled by the class CountryValidator in CountryStateGuard. The only valid countries are Sweden and Norway (case insensitive).
Now, when all fields are filled in correct, the save button is enabled.
When the save button is clicked we now know that the instance of PersonStateGuard is valid so we can safely let it create a valid instance of Person at line 17 in Main. The resulting instance of Person is shown in the popup window PopupFrame.

Best Regards,
Joakim Tengstrand

0 comments

Post a Comment