Skip to main content

Swing Panel Beater

Posted by evanx on June 15, 2006 at 7:43 AM PDT

Prequels

"When you think of things, you find sometimes that a Thing which seemed very Thingish inside you is quite different when it gets out into the open and has other people looking at it." Winnie the Pooh

In Turn Tables we introduced an "explicit properties" approach, where the property descriptors are absorbed into our TableColumn objects, which are declared in our custom TableModel.

In The SQL} we apply the explicit properties approach to object-relational mapping (ORM), to support "native queries," for loading entity beans.

Another (technical) Swing article I wrote in addition to "Swing turntables" is Event DTs

As always, this is a noisy article. The noisy bits are italicised, just so you know.

Introduction

"I didn't look around because if you look around and see a Very Fierce Heffalump looking down at you, sometimes you forget what you were going to say. It's hard to be brave, when you're only a Very Small Animal." Piglet

The application archetype I'm mostly interested in is database front-ends. You could call these CURD applications, for Create, Update, Retrieve and Delete. Some other people call them CRUD applications, and that's OK ;)

So we use an ORM to load our "entity beans" from the database. We view and edit
these in our Swing application. Finally we'll save them back to the database (using our ORM's entity manager). Or we create new entity beans in Swing, and save those to the database. Or we delete entity beans from the database. It's all the same, urm, crud.

JTable is great for browsing the database. But in this article we look at viewing and editing an entity object using a Swing "form." This is a JPanel with components on it like JTextField, JComboBox, and JCheckbox.

Colleagues and I have (re)implemented a similar approach for a few projects. Most recently it is implemented in java.net/projects/greenscreen}, which is used by java.net/projects/aptframework. This paper presents a refined design compared to greenscreen, in particular with respect to validation, which will make it's way into greenscreen in coming weeks (or months).

It is a simple Swing GUI data binding framework, e.g. one can implement 80% of it from scratch in a few days. Because 80% of greenscreen took me three days... and nights. I forgot about the nights, actually. The remaining 20% might take a little while longer ;) It has a few downsides I'm sure, but it works for us. I'm sure it breaks a few laws of OO, gravity and what-not. So at least it acheives that objective! ;)

tigger-hacking

Entity bean

"And the Small and Sorry Rabbit rushed through the mist at the noise, and it suddenly turned into Tigger; a Friendly Tigger, a Grand Tigger, a Large and Helpful Tigger, a Tigger who bounced, if he bounced at all, in just the beautiful way a Tigger ought to bounce." A. A. Milne

My friend Tigger wrote this bouncy Swing app to keep a handy list of our friends in the forest. Like Roo, Piglet, Eeyore, Rabbit and Owl. Tigger asked me to write this article for him. Because he has a rule about writing documentation. That rule is that he gets me to do it for him.

Let's start with our entity bean. That is to say, let's climb up the tree, and then look down, rather than the other way around.

/**
* This is a for my friends.
* @author Tigger
*/
public class FriendEntityBean {
   protected String friendName; // e.g. "Pooh"
   protected FriendType friendType; // an enum, e.g. POOH_BEAR
   protected ForestFood favouriteFood; // e.g. a reference to honey in Pooh's case
   protected Date birthday;
   protected int age; // when the system is first deployed

   ... // getters and setters 
}

Presentation Model bean

"It's a funny thing, how everything looks the same in a mist." Piglet

In a simplistic CURD application, the bean we view and edit in our form might be our raw entity bean. But to get flexibility, Tigger likes to wrap his entity beans into more chewy beans, as follows.

public class FriendFormBean {
   protected FriendEntityBean entityBean;
   protected FriendEntityBean originalEntityBean; // for isChanged()
   protected boolean created = false;
   protected boolean deleted = false;
  
   public FriendFormBean() {
      // we are creating a new entity to capture
      entityBean = new FriendEntityBean();     
      originalEntityBean = entityBean.clone();
      created = true;
   }

   public FriendFormBean(FriendEntityBean entityBean) {
      // we are editing an entity we read from the database
      this.entityBean = entityBean;
      originalEntityBean = entityBean.clone();
   }

   public boolean isChanged() {
      return entityBean.compareTo(originalEntityBean) != 0;
   }

   public String getValidationMessage() {
      if (getFriendName() == null) return "Friend's name is null";
      if (getFriendName().trim().length() == 0) return "Friend's name is empty";
      ...
      return null;
   }
  
   public String getFriendName() {
      return entityBean.getFriendName();
   }

   @IntegerRangeValidationAnnotation(minimumValue = 0, maximumValue = 9)
   public void setAge(int age) {
      if (age < 0 || age > 9) {
         throw new GValidationException(
            "They're a funny thing, accidents.\n" +
            "You never have them til you're having them.\n" +
            "Like now. - Piglet");
      }
      entityBean.setAge(age);
   }

   @NotNullValidationAnnotation()
   public void setFavouriteFood(ForestFood food) {
      entityBean.setFavouriteFood(food);  
   }
  
   @DateNotInFutureValidationAnnotation()
   public void setBirthday(Date birthday) {
      entityBean.setBirthday(birthday);  
   }
  
   ... // other delegating getters and setters, isCreated(), isDeleted()
}

The above bean is a "model bean" representing the data to be displayed in our form. Since this is mostly data in the entity bean, we delegate to the entity bean a lot.

heffalump

It isn't really a bean, it's just a POJO. As Owl will tell you. But Tigger likes to call everything a bean.

We show some simple validation mechanisms above. One, we can throw a GValidationException in our mutators, e.g. setAge(). Two, we might have a method like getValidationMessage() to check that our data is OK e.g. before we save it to the database. Three, we might put annotations on our mutators, e.g. IntegerRangeValidationAnnotation. And finally... Oh, that's still to come, in the next section.

Formative Panel

"This writing business. Pencils and what-not. Overrated if you ask me. Silly stuff. Nothing in it." Eeyore

Tigger implements a "form" as a JPanel with a number of "fields" on it. With a whole honey pot of annotations. And ever since Tigger heard of this MVC thing, he tries to label things along those lines. So our form is called a "view." Of the "model bean" we introduced above.

public FriendFormView extends JPanel {

   GFormHelper helper = new GFormHelper(this, FriendFormBean.class);

   @PropertyAnnotation(label = "Name", mnemonic = 'N', displayWidth = 250)
   @LayoutAnnotation(anchor = NORTHWEST)
   @StringValidatorAnnotation(minimumLength = 1)
   @TextFieldAnnotation(maximumLength = 20) // custom annotations for text fields
   JTextField friendName = helper.createTextField();
  
   @PropertyAnnotation(label = "Type of Animal", displayWidth = 200)
   @LayoutAnnotation(fill = HORIZONTAL)
   @ComboBoxAnnotation(selectNoneLabel = "Not specified")
   JComboBox friendType = helper.createComboBox();

   @PropertyAnnotation(label = "Favourite food", displayWidth = 250)
   @LayoutAnnotation()
   @TextFieldAnnotation() // custom annotations for text fields
   JTextField favouriteFood = helper.createTextField(); // lookup field
  
   @PropertyAnnotation(label = "To be deleted")
   @LayoutAnnotation(flow = NEW_LINE, spacer = BOTH)
   @CheckBoxAnnotation(selected = false)
   JCheckBox deleted = helper.createCheckBox();

   ...
  
   public FriendFormView() {  
      super();
      helper.configure();
   }
  
}

As in Swing Turntables, we let our components get created in the order they are declared, and then finally invoke configure() in the constructor. This reflects on the declared fields, so that they can configure themselves using their own Field, e.g. they extract their own field name to be used as the implicit property name for beans binding. And they also extract annotations, which we can use to configure the component, including annotations for validation and layout, as above.


In the example above, we include the default label and display width for each field in the PropertyAnnotation. But we should translate/customise these in a resource bundle. In the case of the display width, we might save and load this using the Preferences API. For example, our fields might listen for a keystroke to increase and decrease their size.

We might also include layout settings in our LayoutAnnotation, e.g. gridx, gridy. We try to use GridBagLayout exclusively. But by default we use FlowLayout to flow fields horizontally in a subpanel, and then GridBagLayout to stack subpanels, and throw in a "spacer panel" or two, according to the annotations, e.g. fill, anchor, flow, and spacer. We have introduced flow and spacer, to compliment GridBagLayout's fill and anchor, you see.

In general, when writing business apps, enterprise apps, database-front-ends, and such like, we are more interested in keeping it bouncy than anything else. Our "screens" (and reports and what-not), must be quick to implement, and easy for developers to extend and change. So that their weekends are free as in beer. Tiggers are agile and fuzzy creatures, and like to write software that is agile and fuzzy too.

A GUI designer tool makes it easy to build beautiful GUIs, without having to be a Swing expert. Roo prefers GUI designers, and Tigger prefers IDE-agnostic frameworks. Who is right? Owl says they both are right. And that makes them both happy.

Swing honey pot

"It's always useful to know where a friend-or-relation is, whether you want him or whether you don't." Roo

There a few places where we can put our "framework" code. We could have a superclass (which extends JPanel). Or we could put it into a friendly helper class, which we delegate to. This is what Tigger does. That is to say, he prefers composition and delegation to inheritance. In this case, it is possible to have methods in the superclass, that delegate to the helper. And in other superclasses too, although none come to mind immediately.

public class GFormHelper implements ActionListener, FocusListener {
   protected JPanel viewObject;
   protected Bean bean;
   protected GBeanInfo beanInfo;
   protected List layoutComponentList = new ArrayList();
   protected List fieldComponentList = new ArrayList();
   protected boolean ignoreEvents = false;
   protected Object controllerObject = null;
  
   public GFormHelper(JPanel viewObject, Class beanClass) {
      this.viewObject = viewObject;  
      viewObject.setLayout(new GridBagLayout());
      this.beanInfo = new GBeanInfo(beanClass);
   }
  
   public JTextField createTextField() {
      GTextField textField = new GTextField(this);
      configure(textField);
      return textField;
   }

   public void configure(GTextField textField) {
      layoutComponentList.add(textField); // for adding to the panel later in configure()
      fieldComponentList.add(textField); // for bean binding in getModelBean() and setModelBean()
      ... // other custom configuration
   }
     
   public void configure() {
      for (Field field : viewObject.getClass().getFields()) {
         field.setAccessible(true);
         Object object = field.get(viewObject);
         if (object instanceof GFieldComponent) {
            GFieldComponent component = (GFieldComponent) object;
            component.configure(field, beanInfo); // for field name, and annotations
         }
      }
      for (GLayoutComponent component : layoutComponentList) {
         viewObject.add(component, component.getLayoutConstraints());
      }
   }
  
   public void setModelBean(Bean bean) { // update fields from bean
      ignoreEvents = true;
      try {
         for (GFieldComponent component : fieldComponentList) {
            component.setModelBean(bean);
         }
      } finally {
         ignoreEvents = false;
      }  
   }

   public Bean getModelBean() { // update bean from field values
      for (GFieldComponent component : fieldComponentList) {
         component.getModelBean(bean);
      }
      return bean;
   }
  
   public void actionPerformed(ActionEvent event, GFieldComponent fieldComponent) {
      eventLogger.entering(event, ignoreEvents);
      if (!ignoreEvents) {
         ignoreEvents = true;
         try {
            fieldComponent.getModelBean(bean); // update the bean
            if (controllerObject instanceof ActionListener) {
               ActionListener actionListener = (ActionListener) controllerObject;
               actionListener.actionPerformed(event);
            }
            ... // invoke controllerObject.fieldChanged()
         } catch (GParseException pe) {
            showExceptionDialog(pe);
            fieldComponent.requestFocusInWindow();
         } catch (GValidationException ve) {
            showExceptionDialog(ve);
            fieldComponent.requestFocusInWindow();
         } catch (Exception e) {
            showExceptionDialog(e);
         } finally {
            ignoreEvents = false;
         }
      }  
   }
  
   ... // lots of other methods, e.g. createComboBox() et al
}

We use a trick for "automatic" event listener registration, where if our controller is an ActionListener, then we forward it action events from its components. Similarly with other types of events, e.g. FocusListener.

eeyore
Note that we ignore action events when we are reading from the bean into the form's fields in setModelBean(). In this case, the user is not editing a field, but the developer refreshing the fields from the current values of the bean properties.

When an ActionEvent occurs on the field, we invoke the overloaded actionPerformed() above, with a reference to the relevant field as the second argument. We then update the bean with the new value from the field. At this point, the bean's mutator might throw a GValidationException.

We make sure we catch exceptions that are thrown by event handlers, and display those to the user. Exceptions might be tiny unexpected errors, like null pointers, caused by the developer having too much to do, too soon. Or they might be expected exceptions, like validation exceptions, caused by the user making a big Heffalump mistake like typing the wrong thing at the wrong time, which is totally not allowed of course.

If the user enters a field and we throw a validation exception, we should keep focus on that field, so that the user can get his act together e.g. enter a valid value. In the case of a JFormattedTextField, we can use an InputVerifier to nail this too.

One field component

"The most wonderful thing about Tiggers is, I'm the only one!" Tigger

We extend the Swing components JTextField, JFormattedTextField, JComboBox, JPasswordField, JCheckBox etcetera, in a minimal way, in order to implement GFieldComponent so that they all start looking like nails, to be hammered into our form.

public interface GFieldComponent extends GLayoutComponent {
   public void configure(Field field, GBeanInfo beanInfo);
   public void setFieldValue(Value value);
   public Value getFieldValue();
   public String format(Value value);
   public Value parse(String string); // throws GParseException, GValidationException
   ... // other methods, e.g. requestFocusInWindow(), used by GFormHelper
}

Laying it out

"Always watch where you are going. Otherwise, you may step on a piece of the Forest that was left out by mistake."

Components that we are gonna add to the form panel, need to implement the GLayoutComponent interface, i.e. our fields, but also buttons and what-not.

public interface GLayoutComponent {
   public GLayoutConstraints getLayoutConstraints();
}

We extend GridBagConstraints to make it chocolaty (see http://java.net/projects/gridbaglady).

public class GLayoutConstraints extends GridBagConstraints {
   protected int flow; // to mix in flowing sub-panels, i.e. using FlowLayout
   protected int spacer; // for adding spacer panels
      // i.e. JPanel with gbc.fill set to this spacer value
   ...
  
   public GLayoutConstraints() {
   }

   public GLayoutConstraints(int gridx, int gridy) {
      super(gridx, gridy, 1, 1, 0., 0., NORTHWEST, NONE, new Insets(0, 0, 0, 0), 0, 0);
   }
  
   public GLayoutConstraints horizontal() {
      super.fill = HORIZONTAL;
      return this;
   }
  
   ...
}

But for the purposes of keeping this article bear-shaped, let's just pretend we use the vanilla GridBagConstraints.

eeyore

Text field example

"It's a funny thing about Tiggers," whispered Tigger to Roo, "how Tiggers never get lost." "Why don't they, Tigger?" "They just don't," explained Tigger. "That's how it is."

Our field components implement the GFieldComponent interface, and delegate to a helper e.g. GTextFieldHelper. This helper is a thin customised extension of GFieldComponentHelper which handles the common functionality e.g. beans binding, for which it uses GProperty, as we will see later.

It sounds convoluted when put into words, so maybe Tigger lost the plot here. Which can happen when Winnie the Pooh comes over for a 11 o' clock smackerel of something. He disrupts Tigger's train of thought, you see.

public class GTextField extends JTextField implements GFieldComponent {

   GTextFieldHelper helper = new GTextFieldHelper(this);

   public GTextField(GFormHelper formHelper) {
      super();
      helper.setFormHelper(formHelper);
   }
  
   public void configure(Field field, GBeanInfo beanInfo) {
      helper.configure(field, beanInfo);
   }  

   public GLayoutConstraints getLayoutConstraints() {
      return helper.getLayoutConstraints();
   }
  
   public void setFieldValue(Value value) {
      super.setText(format(value));
   }

   public Value getFieldValue() {
      return parse(super.getText());
   }

   public String format(Value value) { // in case we wanna override
      return helper.format(value);
   }

   public Value parse(String string) { // in case we wanna override
      return helper.parse(string);
   }  
  
   ... // other methods of GFieldComponent interface, which we delegate out to our helper
}     

eeyore

In the case of GCheckBox, it's getFieldValue() will invoke isSelected() and return a Boolean.

GComboBox's getFieldValue() delegates to its GComboBoxModel to lookup the object value associated with it's getSelected() label, as we will see later.

Field component helpers

'What?' said Piglet, with a jump. And then to show that he hadn't been startled, he jumped up and down once or twice more in an exercising sort of way.

Our GTextFieldHelper keeps a reference to its peer GTextField, and performs any functionality specific to JTextField. But otherwise, the GFieldComponentHelper superclass is the main Heffalump.

public class GTextFieldHelper extends GFieldComponentHelper {
   GTextField textField;
  
   public GTextFieldHelper(GTextField textField) {
      super(textField);
      this.textField = textField;
   }
  
   public void configure(Field field, GBeanInfo beanInfo) {     
      super.configure(field, beanInfo); // PropertyAnnotation,
          // LayoutAnnotation, and validation annotations
      ... // process TextFieldAnnotation
   }     
}  

The configure() methods extract the configuration annotations from the Field.

GComboBoxHelper and GCheckBoxHelper are very much like the above. The Heffalump superclass is implemented as follows.

/**
* Superclass for GTextFieldHelper, GComboBoxHelper, GCheckBoxHelper, et al.
* @author Tigger
*/   
public class GFieldComponentHelper implements ActionListener, FocusListener {
   GFormHelper formHelper;
   GFieldComponent fieldComponent;
   GLayoutConstraints layoutConstraints;
   GProperty property;
  
   /**
    * Boa Constructor.
    */   
   public GFieldComponentHelper(GFieldComponent fieldComponent) {
      this.fieldComponent = fieldComponent;
   }

   /**
    * Get the GProperty from beanInfo, configured using PropertyAnnotation et al
    * Note, we should also invoke setFormHelper() to finally configure this baby.
    * @see GBeanInfo#createProperty(Field)
    */
   public void configure(Field field, GBeanInfo beanInfo) {     
      property = beanInfo.createProperty(field);
      fieldComponent.addActionListener(this); // forwards events to GFormHelper
   }  
  
   /**
    * Forward event to formHelper with reference to source field component.
    * @see GFormHelper#actionPerformed(ActionEvent)
    */
   public void actionPerformed(ActionEvent event) {
      formHelper.actionPerformed(event, fieldComponent);
   }
  
   /**
    * Format value to viewable string representation, by delegation to GProperty.
    * @see GProperty#format(Value)
    */
   public String format(Value value) {
      return property.format(value);
   }

   /**
    * Convert and validate string, by delegation to GProperty.
    * @see GProperty#parse(String)
    */
   public Value parse(String string) {
      return property.parse(string);
   }  

   /**
    * Push value into field component from bean.
    * @see GProperty#getValue(Bean)
    */
   public void setModelBean(Object bean) {
      fieldComponent.setFieldValue(property.getValue(bean));
   }

   /**
    * Pull value from field component into bean.
    * @see GProperty#setValue(Bean, Value)
    */
   public void getModelBean(Object bean) {
      property.setValue(bean, fieldComponent.getFieldValue());
   }
     
   ... // other methods
}     

We use the GProperty property descriptor wrapper, to read and write that property's values to and from our bean, e.g. an instance of FriendFormBean. And also to format, parse and validate property values to and from their string representations. Heh heh... Hm... Eh?

eeyore

Combo box

"I used to believe in forever, but forever is too good to be true." Winnie the Pooh

We implement GComboBox as follows. I don't understand this, so I'm just gonna present it. Any questions, post them. Then my people will get hold of Tigger's people and get back to your people.

public class GComboBox extends JComboBox implements GFieldComponent {

   GComboBoxHelper helper = new GComboBoxHelper(this);

   public GComboBox() {
      super();
   }
  
   public void configure(Field field, GBeanInfo beanInfo) {
      helper.configure(field, beanInfo);
   }  
  
   public void setFieldValue(Value value) {
      super.setSelected(format(value));
   }

   public Value getFieldValue() {
      return parse(super.getSelected().toString());
   }

   public String format(Value value) {
      return helper.format(value);
   }

   public Value parse(String string) {
      return helper.parse(string);
   }  
  
   ... // other methods of GFieldComponent interface, which we delegate out to the helper
}     

GComboBoxHelper in turn delegates the format() and parse() to GComboBoxModel. I asked Tigger why so much delegation, from the component to the helper, to the model. He said, "To make it more bouncy, of course!"

Combo box model

"I don't see much sense in that," said Piglet. "No," said Pooh humbly, "there isn't. But there was going to be when I began it. It's just that something happened to it along the way."

We implement a "generic" combo box model to be reusable by all combo boxes.

public class GComboBoxModel extends DefaultComboBoxModel {
   GComboBoxHelper helper;
   List valueList = new ArrayList();
   Map valueMap = new HashMap();
   String selectNoneLabel = "Select...";
   String selectAllLabel = "The whole honey pot";
  
   ...
   public void add(String label, Value value) {
      valueMap.put(label, value);
      valueList.add(value);
   }

   public void add(Value value) {
      add(format(value), value);
   }
  
   public void addAll(Value ... values) {
      for (Value value : values) {
         add(value);
      }
   }

   public void addAll(List valueList) {
      for (Value value : valueList) {
         add(value);
      }
   }
  
   public void addSelectNone(String label) {
      add(label, null);
      selectNoneLabel = label;
   }

   public void addSelectAll(String label) {
      add(label, null);
      selectAllLabel = label;
   }

   public String format(Value value) {
      return helper.comboBox.format(value);
   }

   public Value parse(String label) {
      return valueMap.get(label);
   }
    
   public Object getElementAt(int index) { // implementing ComboBoxModel
      return format(valueList.get(index));
   }

   public int getSize(int index) { // implementing ComboBoxModel
      return valueList.size;
   }
  
   public boolean isSelectNone() {
      return getSelected().equals(selectNoneLabel);
   }

   public boolean isSelectAll() {
      return getSelected().equals(selectAllLabel);
   }
  
}

A null value might correspond to a selection of "All" or "None" or both. So we offer methods like isSelectAll() so we can check, i.e. when the value is null, and we have "All" and "None" as available selections, when which is it?

There is Another Big Issue. Usually we want to support auto-completion for combo boxes. But we gonna dodge that issue for now.

Filling the combo box

"Company means Food... and Listening-to-Me-Humming and such like." Winnie the Pooh

We might populate a combo box model from an enum type as follows.

   public void addFriendTypes(GComboBox comboBox) {
      for (FriendType friendType : FriendType.values()) {
         comboBox.getComboBoxModel().add(friendType.toString(), friendType);
      }
   }

Another example would be populating from a database table, e.g. via a DAO method e.g. foodComboBoxModel.addAll(entityManager.food.getEntityList()).

For very large tables, i.e. "high volume" combo boxen, we gotta treat those as A Special Case. With auto-completion and reactive fetching and populating of the combo box. And multi-column combo boxes and database lookup-popups. Here's a suggestion, let's seriously forget about all that for now. And leave it for "Swing and Roundabouts: My big fat geeky combo box".

Hope they bought that? Phew, that was a close one. Tigger better work out how to do all that at some stage!

Bean wiring

"Poetry and Hums aren't things which you get, they're things which get you. And all you can do is go where they can find you." Winnie the Pooh

We should have introduced the GBeanInfo and GProperty wrappers sooner. Here they are, later.

public class GBeanInfo {
   protected BeanInfo beanInfo;
   protected Map propertyMap;
     
   public GBeanInfo(Class beanClass) {
      this.beanInfo = Introspector.getBeanInfo(beanClass);
      for (PropertyDescriptor propertyDescriptor : beanInfo.getPropertyDescriptors()) {
         GProperty property = new GProperty(this, propertyDescriptor)
         propertyMap.put(propertyDescriptor.getPropertyName(), property);
      }
   }
  
   public GProperty getProperty(String propertyName) {
      return propertyMap.get(propertyName);
   }
  
   public GProperty createProperty(Field field) {
      return propertyMap.get(field.getName()).configure(field);
   }

}

We use GProperty to read and write to the bean, basically.

public class GProperty {
   protected GBeanInfo beanInfo;
   protected PropertyDescriptor propertyDescriptor;
   protected String label;
   protected GPropertyType propertyType;
   protected Integer displayWidth;
   protected String format;
   protected List validatorList = new ArrayList();
   protected GFormatter formatter = GContext.getFormatter();
   ...
  
   public GProperty(GBeanInfo beanInfo, PropertyDescriptor propertyDescriptor) {
      this.beanInfo = beanInfo;
      this.propertyDescriptor = propertyDescriptor;
   }

   public GProperty configure(Field field) {
      // extract PropertyAnnotation, to set label, displayWidth, propertyType et al
      // extract validation annotations, to construct validatorList
      ...
      return this;
   }
  
   public Value getValue(Object bean) {
      return (Value) propertyDescriptor.getReadMethod().invoke(bean);
   }
  
   public void setValue(Object bean, Value value) {
      propertyDescriptor.getWriteMethod().invoke(bean, value);
   }  
  
   public String format(Value value) { // delegate to GFormatter
      return formatter.format(value, this);
   }

   public Value parse(String string) { // throws GParseException
      return validate(formatter.parse(string, this));
   }

   public Value validate(Value value)  { // throws GValidationException 
      for (GPropertyValidator validator : validatorList) {
         validator.validate(value);
      }
      return value;
   }

   ...
}     

We store meta-data extracted from annotations in GProperty, required to perform conversion and validation. So let's introduce GValidator and GFormatter implementations below, to see what we're getting.

Validation annotation station

Pooh looked at his two paws. He knew that one of them was the right, and he knew that when you had decided which one of them was the right, then the other was the left, but he never could remember how to begin.

Our list of GValidator's in GProperty is constructed from validation annotations, e.g. StringValidatorAnnotation, IntegerRangeValidatorAnnotation, DateRangeValidatorAnnotation, and any number of other validation annotations we wish to introduce.

Let's consider the following example.

public class GStringValidator extends GValidator {   
   protected Integer minimumLength;
   protected Integer maximumLength;
   protected boolean nullable = false;
   protected boolean empty = false;
   ...
   public void validate(String value) {
      if (value == null) {
         if (!nullable) {
            throw new GValidationException(this, "is null");
         }
         return;
      }        
      if (!empty && value.trim().length() == 0) {
         throw new GValidationException(this, "is empty");
      }
      if (minimumLength != null && value.length() < minimumLength) {
         throw new GValidationException(this, "is shorter than " + minimumLength);
      }
      ...
   }
  
   public void configure(Field field) {
      ... // extract StringValidatorAnnotation to set minimumLength et al
   }        
}

Our GValidator superclass keeps a reference to its GProperty, so that our validation exception messages can report the label of the invalid property to the user, e.g. "Friend's name is null".

We might create an InputVerifier for a JFormattedTextField as follows.

public class GPropertyVerifier extends InputVerifier {
   GProperty property;
  
   public GPropertyVerifier(GProperty property) {
      this.property = property;
   }
  
   public boolean verify(JComponent component) {
      if (component instanceof JFormattedTextField) {
         JFormattedTextField textField = (JFormattedTextField) component;
         String text = textField.getText();
         try {
            property.parse(text);
         } catch (Exception e) {
            return false;
         }
      }
      return true;
   }
  
   public boolean shouldYieldFocus(JComponent component) {
      return verify(component);
   }
}               

Annotation aches and panes

"Don't underestimate the value of Doing Nothing, of just going along, listening to all the things you can't hear, and not bothering." Roo

eeyoreg
Unfortunately annotations do not support null values or inheritance, so we work around that, e.g. using hacks like default values of -1 and empty strings i.e. "" to mimic null values. And cut and paste between annotations like a crazed Heffalump. Very messy stuff.

Tigger wishes that Annotations were POJOs. Where specifying values is like setting bean properties. But they aren't. So we always convert or extract annotations into objects right away, and then conveniently forget about them. Then we can use nulls for properties not specified in the annotation, e.g. in the above GStringValidator example, minimumLength is null by default.

Parsing and formatting

"I thought Tiggers were smaller than that." "Not the big ones," said Tigger

Our fields need to "format and parse." That is, they need to convert between objects and strings, and visa versa. Because Pooh and friends read and write strings, but computers are objects and references which are ones and zeroes.

One bouncy way of doing this is having one Heffalump helper to format and parse them all.

public class ForestFriendsFormatter extends GDefaultFormatter {
   ...
   public String format(Object value, GProperty property) {
      if (property.isDateFormat()) return dateFormat.format(value);
      if (value instanceof FriendType) return value.toString();
      if (value instanceof ForestFood) return format((ForestFood) value);
      ...
      return super.format(value, property);
   }
  
   public Object parse(String string, GProperty property) throws GParseException {
      if (property.isDateFormat()) return dateFormat.parse(string);     
      if (property.isTimestampFormat()) return timestampFormat.parse(string);
      if (property.isString()) return string;
      if (property.isInteger()) return new Integer(string);
      ...
      return super.parse(string, property);
   }
  
   ... // custom formatting, e.g. format(ForestFood)
}

Actually much of the above would be in the superclass i.e. GDefaultFormatter. We extend that to customise it, e.g. for our special types e.g. FriendType and ForestFood.

As you can see, we don't buy into anything too fancy here in the forest. We have visiting to do, afternoon naps and things that keep us busy. So when we write software, which we do strictly mornings only, we hope that it works. If it doesn't, we fix it tomoro, time permitting. And anyway... what are cyclic dependencies, by the way?

Custom fields

"Tiggers can't climb downwards, because their tails get in the way, only upwards." Tigger

We might implement custom fields, and overwrite format() and parse(), which are otherwise delegated by the GFieldComponent, firstly to its GFieldComponentHelper helper, then to its GProperty property descriptor, and finally to the GFormatter implementation. Uh Hm. This is what Tigger calls 'whOOPy' programming, all this delegation and inheritance and friends-and-relations, is what makes it so nice and bouncy!

public FriendTypeField extends GComboBoxField {

   public FriendTypeField() {
      super();
      populate();     
   }

   public FriendTypeField(GFormHelper helper) {
      super(helper);
      populate();     
   }
  
   protected void populate() {
      for (FriendType friendType : FriendType.values()) {
         comboBoxModel.add(friendType.toString(), friendType);
      }
   }
  
   public String format(FriendType value) {
      return "A " + value.toString();
   }

   public FriendType parse(String string) {
      return super.parse(string.substring(2));
   }
}  

A Small Problem is that our component factory creates regular text fields and what-not. So we can extend our factory i.e. GFormHelper to introduce a factory method like createFriendTypeField(), or we can use the GFormHelper.configure(comboBox) method, as follows.

   FriendTypeField friendTypeField = new FriendTypeField();
   ...
   public void configure() {
      helper.configure(friendTypeField);
      ...
   }

Or we can pass our GFormHelper context through to our custom combo box, so that its GComboBox superclass can invoke helper.configure(this) for us.

So many choices and decisions... Time to have Owl over for tea and a little smackerel of something, shall we?

Form programming

"Bounding up trees is easy for Tiggers. The difficulty is in the climbing down, backwards." Tigger

Our form controller class might be implemented as follows.

public FriendFormController implements ActionListener, GFieldListener {   
   FriendFormView friendFormView = new FriendFormView();  
   FriendFormBean friendFormBean = new FriendFormBean();  
   ...   
   public FriendFormController() {     
      friendFormView.helper.setController(this);
      clear();
      ...
   }

   protected void clear() {
      friendFormBean = new FriendFormBean();
      rebind();
   }

   protected void rebind() {
      friendFormView.helper.setModelBean(friendFormBean);
   }
  
   public void actionPerformed(ActionEvent event) {
      eventLogger.entering(event);
      // CURD events
      if (newAction.isSource(event)) {
         if (friendFormBean.isChanged()) {
            if (!guiHelper.showConfirmationDialog("You wanna loose changes?")) {
               return;
            }
         }
         clear();
      } else if (saveAction.isSource(event)) {
         if (!friendFormBean.isChanged()) {
            guiHelper.showMessageDialog("Nothing to save")) {
         } else if (friendFormBean.isDeleted()) {
            entityManager.friend.deleteEntity(friendFormBean.entityBean);
         } else if (friendFormBean.getValidationMessage() != null) {
            guiHelper.showMessageDialog(friendFormBean.getValidationMessage());
         } else {        
            entityManager.friend.saveEntity(friendFormBean.entityBean);
         }
      } else if (findAction.isSource(event)) {
         if (friendFormBean.getFriendName() == null) {
            guiHelper.showMessageDialog("Enter a name first");
         } else {
            FriendEntityBean friendEntity
               = entityManager.friend.getFriendByName(
                  friendFormBean.getFriendName());
            friendFormBean = new FriendFormBean(friendEntity);
            rebind();
         }        
      } else if (deleteAction.isSource(event)) {
         if (friendFormBean.isCreated()) clear();
         else friendFormBean.setDeleted(true);
      }     
   }
  
   public void fieldChanged(GFieldEvent event) {
      eventLogger.entering(event);
      if (friendFormView.friendName.isSource(event)) {
         traceLogger.finer(event.getFieldComponent(),
               event.getOldValue(), event.getNewValue());
         if (event.isEntered() || event.isFocusLost()) {
            if (friendFormBean.getFriendName().trim().length() == 0) {
               guiHelper.showMessageDialog("Who dat?");
               return;
            }
         }
      }
      // TODO get more bouncy here
   }

   public void documentChanged(GFieldEvent event) {
   }  
}  

To make it bouncy, we might introduce our own new event specifically for fields, for when the value of a field is changed by the user. When they tab out of the field (which is a "focus lost" FocusEvent), or press Enter (which causes an ActionEvent), we might invoke fieldChanged().


We might throw a documentChanged() event handler into our GFieldListener, which comes into play when the user is editing a text field or what-not, courtesy of our GFormHelper listening for a DocumentEvent and turning that into a GFieldEvent.

OK, let's wrap up. Time for a nap!

Summary

"When late morning rolls around and you're feeling a bit out of sorts, don't worry; you're probably just a little eleven o'clockish." Winnie the Pooh

We continue from Swing Turntables, to apply "explicit properties" to the Swing JPanel and its fields.

We explicitly declare field components in our form, e.g. JTextField, JComboBox et al. Bean binding is performed implicitly, using the field names of the components. Annotations are used to specify layout, validation and stuff. Oh, and we introduce an interface for fields, to make them all look like honey pots. Our field components delegate to helpers and property descriptors to do all the work.

We program to a beautiful bouncy POJO "form model" rather than heavy Swing models. We reference our components as necessary, e.g. to invoke setEnabled(), requestFocusInWindow(), et al, and also to identify them as the source of events in our event handlers.

But we don't have to worry about extracting, converting and validating values from fields. We put that into a framework, where it belongs. A very simple framework mind you.

The framework handles events nicely and politely for us too. It does automatic event registration, event filtering (eg. ignoring events when they should be ignored), and introduces a handy custom event for fields, to hide the vagarities of ActionEvent and FocusEvent, which are mixed up with other types of components. Finally, it makes sure that exceptions thrown by event handlers are handled e.g. the user is told.

And now for smackerel of something to eat, and then a lovely nap!

Sequels

"Before beginning a Search, it is wise to ask someone what you are looking for before you begin looking for it." Winnie the Pooh

The next article in this Swing series is Inside Action on Swing Actions (for buttons, toolbars, menus and hotkeys), and then Framewarez on building a tabbed application frame for our "worksheets."

Thanks

This blog was written using Netbeans, and previewed using Firefox (as hoofed in Netbeans, my weblogging tool. It was started in my sister's house, carried on in my brother's house, continued in my sister's office, and finished in my mom's cottage, on my new Windows XP notebook (as mooted in My Desktop OS: Windows XP so...
now you know!

Pictures from http://yotophoto.com and flickr.com, notably http://flickr.com/people/agnieszka/

Resources

https://code.google.com/p/vellum/ - where i will collate these articles and their code.

Related Topics >>