The Source for Java Technology Collaboration
User: Password:



Evan Summers

Evan Summers's Blog

Bean Curd 1F: Swing Panel Beater

Posted by evanx on June 15, 2006 at 07:43 AM | Comments (3)


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

eeyore_sit_crop_150.jpg In Bean Curd 1 we introduced an "explicit properties" approach, where the property descriptors are absorbed into our TableColumn objects, which are declared in our custom TableModel.

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

I wrapped up my May blogs in Mayhem Roundup, including the above Bean Curds 1 and 2, in particular the rationale of Bean Curd approach ie. "explicit properties with implicit binding." Mayhem Roundup also presented some of the comments and discussion from other May articles, including Java vs scripting, and GPL vs CDDL.

Another (technical) Swing article I wrote in addition to Bean Curd 1 is Swing and Roundabouts 1: Event DTs.

Health Warning: As always, this is a very 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 (ie. Bean Curd 1 plus this) for a few projects. Most recently it is implemented in greenscreen, which is used by 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, eg. 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_out_175.jpg
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; // eg. "Pooh"
   protected FriendType friendType; // an enum, eg. POOH_BEAR
   protected ForestFood favouriteFood; // eg. a reference to honey in Pooh's case
   protected Date birthday;
   protected int age; // when the system is first deployed

   ... // getters and setters  
}


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_125.jpg It isn't really a bean, it's just a POJO. As Owl will tell you. But Tigger likes to call everything a bean. And so I do too.

We show some simple validation mechanisms above. One, we can throw a GValidationException in our mutators, eg. setAge(). Two, we might have a method like getValidationMessage() to check that our data is OK eg. before we save it to the database. Three, we might put annotations on our mutators, eg. 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<FriendFormBean> 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 Bean Curd 1, 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, eg. 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.

tigger_beer_175.jpg 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, eg. 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, eg. 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<Bean> implements ActionListener, FocusListener {
   protected JPanel viewObject;
   protected Bean bean;
   protected GBeanInfo beanInfo;
   protected List<GLayoutComponent> layoutComponentList = new ArrayList();
   protected List<GFieldComponent> 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, eg. 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, eg. FocusListener.

eeyore_beer_crop.jpg 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 eg. 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<Value> 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, eg. 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, ie. our fields, but also buttons and what-not.

public interface GLayoutComponent {
   public GLayoutConstraints getLayoutConstraints();
}

We extend GridBagConstraints to make it chocolaty.

public class GLayoutConstraints extends GridBagConstraints {
   protected int flow; // to mix in flowing sub-panels, ie. using FlowLayout
   protected int spacer; // for adding spacer panels, ie. 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_rabbit_crop.jpg
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 eg. GTextFieldHelper. This helper is a thin customised extension of GFieldComponentHelper which handles the common functionality eg. 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<Value> extends JTextField implements GFieldComponent<Value> {

   GTextFieldHelper<Value> 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_slouch_100.jpg 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<Value> extends GFieldComponentHelper<Value> {
   GTextField<Value> textField;
   
   public GTextFieldHelper(GTextField<Value> 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<Value> implements ActionListener, FocusListener {
   GFormHelper formHelper;
   GFieldComponent<Value> fieldComponent;
   GLayoutConstraints layoutConstraints;
   GProperty<Value> property;
   
   /**
    * Boa Constructor.
    */    
   public GFieldComponentHelper(GFieldComponent<Value> 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, eg. an instance of FriendFormBean. And also to format, parse and validate property values to and from their string representations. Heh heh... Hm... Eh?

eeyore_sit_side_crop3.jpg
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<Value> extends JComboBox implements GFieldComponent<Value> {

   GComboBoxHelper<Value> 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."

public class GComboBoxModel<Value> extends DefaultComboBoxModel {
   GComboBoxHelper<Value> helper;
   List<Value> valueList = new ArrayList();
   Map<String, Value> 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<Value> 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);
   }
   
}

eeyore_lift_crop.jpg A null value might correspond to a selection of "All" or "None" or both. So we offer methods like isSelectAll() so we can check, ie. 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, eg. via a DAO method eg. foodComboBoxModel.addAll(entityManager.food.getEntityList()).

For very large tables, ie. "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<Bean> {
   protected BeanInfo beanInfo;
   protected Map<String, GProperty> 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<Value> {
   protected GBeanInfo beanInfo;
   protected PropertyDescriptor propertyDescriptor;
   protected String label;
   protected GPropertyType propertyType; 
   protected Integer displayWidth;
   protected String format;
   protected List<GValidator> 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.

Pooh_100.jpg
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, eg. 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<String> {   
   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, eg. "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 with 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

eeyore_fallen_small.jpg Unfortunately annotations do not support null values or inheritance, so we work around that, eg. using hacks like default values of -1 and empty strings ie. "" 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, eg. 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, eg. format(ForestFood)
}

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

eeyore_side_crop.jpg 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<FriendType> {

   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 ie. 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().

tigger_sit_125.jpg 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 Bean Curd 1, to apply "explicit properties" to the Swing JPanel and its fields.

We explicitly declare field components in our form, eg. 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, eg. 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 eg. the user is told.

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

bees_and_sunflowers_175.jpg
Coming sooner or later
"Before beginning a Search, it is wise to ask someone what you are looking for before you begin looking for it." Winnie the Pooh

Coming up in the Swing series is "Swing and Roundabouts 2: Lightweights, Canvas, Action!" on Swing Actions (for buttons, toolbars, menus and hotkeys), and "Swing and Roundabouts 3: Framewarez" on building a tabbed application frame for our "worksheets." After that, maybe "Swing and Roundabouts 4: My big fat geeky combo box" on database lookups.

In this Bean Curd series, "Bean Curd 2X: The Xtended Version" will look at applying those native queries to XML, "Bean Curd 3: On Form" will look at binding HTML forms, and a "Bean Curd 4: On the table" series will look at explicit properties for documents, like PDF, HTML and Excel ones.

In the meantime, check out our Featured Prequel...

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 yotophoto.com, and flickr.com, notably agnieszka.


Featured Prequel
"You can't always sit in your corner of the forest and wait for people to come to you... you have to go to them sometimes." Eeyore

tigger_honey_pot_125.jpg I'm gonna use this soapblog to punt my previous article, Mayhem Roundup, because if I don't, who will? ;) So this is a roundup of that Roundup article of my May blogs, including Bean Curds 1 and 2, in particular the rationale of their "explicit properties with implicit binding" approach used here.

First things first. My favourite NIH quote in that article is by Homer Simpson. "You couldn’t fool your mother on the foolingest day of your life if you had an electrified fooling machine." Heh heh. And my favourite quote of my own, is "I gotta respect other views that I might disagree strongly with. Not least because they might be right."

Mayhem Roundup presented some of the comments and discussion of my May articles. Especially my own. And especially the discussion related to using Java for "scripting" tasks, and building libraries rather than more scripting languages.

And also exploring the GPL, LGPL, CDDL, and SCA. Because boring legal stuff is well important, innit.


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • Hi Evan - a lot of the stuff in here is taken care of automatically in BeanView. If you have some cycles, I'd love to hear your feedback: www.beanview.com, http://attainware.com/blog/category/beanview-ui-framework/

    Cheers,
    -Will

    Posted by: wiverson on June 15, 2006 at 10:21 AM

  • I've done something simmilar a while back: http://freshmeat.net/projects/autoui/

    let me know if you thing some sort of colaboration would be worthwile.

    regards,
    Emil (http://thekirschners.com/software/testare/testare.html)

    Posted by: entzik on June 16, 2006 at 05:48 AM


  • Thanks, Will, i will download and have a look.

    I think that annotating the bean, and thereby providing automatic views (ie. creating the panel and fields and all), is tremendously useful. I have a friend doing the same, and we have many great debates on the issue :)

    The problem as i see it, is how do you reference your components explicitly, eg. friendNameField.setEnabled(false).

    Because i want a framework which enables me to leverage the IDE eg. no string references, just real object references, eg. of our components (and bean properties) , so that i get auto-completion, prompting and error detection. And most important, refactorability.

    Frameworks are such a tradeoff, innit! Where to draw the line?!
    It's a multi-dimensional graph of size/complexity of framework versus resulting size/complexity of application code, for a given size/complexity of the application requirements, versus agility to changing requirements.

    So in the face of all that, i would aim for an absolutely minimal framework, that does the absolute minimum, and so keeps us right up close to programming like we would be programming if there was no framework, and just takes away the roughest of the rough edges of programming with no framework.

    In my view, POJO bean binding is a necessity rather than a luxury.
    And should be and will be in the standard libraries, as we know from JSR 295? Not least because it is relatively simple to do, as i tried to demonstrate in this paper?


    Posted by: evanx on June 17, 2006 at 02:49 AM





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds