Skip to main content

Bean Curd 1F: 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


eeyore_sit_crop_150.jpg align=left hspace=16 border=0 />

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 align=right hspace=16 vspace=8 />

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
*/
<b>public</b> <b>class</b> FriendEntityBean {
   <b>protected</b> String friendName; // eg. "Pooh"
   <b>protected</b> FriendType friendType; // an enum, eg. POOH_BEAR
   <b>protected</b> ForestFood favouriteFood; // eg. a reference to honey in Pooh's case
   <b>protected</b> Date birthday;
   <b>protected</b> <b>int</b> 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.

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

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

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

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

   @IntegerRangeValidationAnnotation(minimumValue = 0, maximumValue = 9)
   <b>public</b> <b>void</b> setAge(<b>int</b> age) {
      <b>if</b> (age < 0 || age > 9) {
         <b>throw</b> <b>new</b> 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()
   <b>public</b> <b>void</b> setFavouriteFood(ForestFood food) {
      entityBean.setFavouriteFood(food);  
   }
  
   @DateNotInFutureValidationAnnotation()
   <b>public</b> <b>void</b> 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 align=left hspace=16 />
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.

<b>public</b> FriendFormView <b>extends</b> JPanel {

   GFormHelper&lt;FriendFormBean&gt; helper = <b>new</b> GFormHelper(<b>this</b>, FriendFormBean.<b>class</b>);

   @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 = <b>false</b>)
   JCheckBox deleted = helper.createCheckBox();

   ...
  
   <b>public</b> FriendFormView() {  
      <b>super</b>();
      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 align=left hspace=16 />

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.

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

   <b>public</b> <b>void</b> 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
   }
     
   <b>public</b> <b>void</b> configure() {
      <b>for</b> (Field field : viewObject.getClass().getFields()) {
         field.setAccessible(<b>true</b>);
         Object object = field.get(viewObject);
         <b>if</b> (object <b>instanceof</b> GFieldComponent) {
            GFieldComponent component = (GFieldComponent) object;
            component.configure(field, beanInfo); // for field name, and annotations
         }
      }
      <b>for</b> (GLayoutComponent component : layoutComponentList) {
         viewObject.add(component, component.getLayoutConstraints());
      }
   }
  
   <b>public</b> <b>void</b> setModelBean(Bean bean) { // update fields from bean
      ignoreEvents = <b>true</b>;
      <b>try</b> {
         <b>for</b> (GFieldComponent component : fieldComponentList) {
            component.setModelBean(bean);
         }
      } <b>finally</b> {
         ignoreEvents = <b>false</b>;
      }  
   }

   <b>public</b> Bean getModelBean() { // update bean from field values
      <b>for</b> (GFieldComponent component : fieldComponentList) {
         component.getModelBean(bean);
      }
      <b>return</b> bean;
   }
  
   <b>public</b> <b>void</b> actionPerformed(ActionEvent event, GFieldComponent fieldComponent) {
      eventLogger.entering(event, ignoreEvents);
      <b>if</b> (!ignoreEvents) {
         ignoreEvents = <b>true</b>;
         <b>try</b> {
            fieldComponent.getModelBean(bean); // update the bean
            <b>if</b> (controllerObject <b>instanceof</b> ActionListener) {
               ActionListener actionListener = (ActionListener) controllerObject;
               actionListener.actionPerformed(event);
            }
            ... // invoke controllerObject.fieldChanged()
         } <b>catch</b> (GParseException pe) {
            showExceptionDialog(pe);
            fieldComponent.requestFocusInWindow();
         } <b>catch</b> (GValidationException ve) {
            showExceptionDialog(ve);
            fieldComponent.requestFocusInWindow();
         } <b>catch</b> (Exception e) {
            showExceptionDialog(e);
         } <b>finally</b> {
            ignoreEvents = <b>false</b>;
         }
      }  
   }
  
   ... // 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 align=right hspace=8 />

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.

<b>public</b> <b>interface</b> GFieldComponent&lt;Value&gt; <b>extends</b> GLayoutComponent {
   <b>public</b> <b>void</b> configure(Field field, GBeanInfo beanInfo);
   <b>public</b> <b>void</b> setFieldValue(Value value);
   <b>public</b> Value getFieldValue();
   <b>public</b> String format(Value value);
   <b>public</b> 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.

<b>public</b> <b>interface</b> GLayoutComponent {
   <b>public</b> GLayoutConstraints getLayoutConstraints();
}

We extend GridBagConstraints to make it chocolaty.

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

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

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

eeyore_rabbit_crop.jpg align=left hspace=16 vspace=4 />

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.

<b>public</b> <b>class</b> GTextField&lt;Value&gt; <b>extends</b> JTextField <b>implements</b> GFieldComponent&lt;Value&gt; {

   GTextFieldHelper&lt;Value&gt; helper = <b>new</b> GTextFieldHelper(<b>this</b>);

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

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

   <b>public</b> Value getFieldValue() {
      <b>return</b> parse(<b>super</b>.getText());
   }

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

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

eeyore_slouch_100.jpg align=right hspace=16 vspace=8 />
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.

<b>public</b> <b>class</b> GTextFieldHelper&lt;Value&gt; <b>extends</b> GFieldComponentHelper&lt;Value&gt; {
   GTextField&lt;Value&gt; textField;
  
   <b>public</b> GTextFieldHelper(GTextField&lt;Value&gt; textField) {
      <b>super</b>(textField);
      <b>this</b>.textField = textField;
   }
  
   <b>public</b> <b>void</b> configure(Field field, GBeanInfo beanInfo) {     
      <b>super</b>.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
*/   
<b>public</b> <b>class</b> GFieldComponentHelper&lt;Value&gt; <b>implements</b> ActionListener, FocusListener {
   GFormHelper formHelper;
   GFieldComponent&lt;Value&gt; fieldComponent;
   GLayoutConstraints layoutConstraints;
   GProperty&lt;Value&gt; property;
  
   /**
    * Boa Constructor.
    */   
   <b>public</b> GFieldComponentHelper(GFieldComponent&lt;Value&gt; fieldComponent) {
      <b>this</b>.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)
    */
   <b>public</b> <b>void</b> configure(Field field, GBeanInfo beanInfo) {     
      property = beanInfo.createProperty(field);
      fieldComponent.addActionListener(<b>this</b>); // forwards events to GFormHelper
   }  
  
   /**
    * Forward event to formHelper with reference to source field component.
    * @see GFormHelper#actionPerformed(ActionEvent)
    */
   <b>public</b> <b>void</b> actionPerformed(ActionEvent event) {
      formHelper.actionPerformed(event, fieldComponent);
   }
  
   /**
    * Format value to viewable string representation, by delegation to GProperty.
    * @see GProperty#format(Value)
    */
   <b>public</b> String format(Value value) {
      <b>return</b> property.format(value);
   }

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

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

   /**
    * Pull value from field component into bean.
    * @see GProperty#setValue(Bean, Value)
    */
   <b>public</b> <b>void</b> 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 align=left hspace=16 vspace=8 border=0 />

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.

<b>public</b> <b>class</b> GComboBox&lt;Value&gt; <b>extends</b> JComboBox <b>implements</b> GFieldComponent&lt;Value&gt; {

   GComboBoxHelper&lt;Value&gt; helper = <b>new</b> GComboBoxHelper(<b>this</b>);

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

   <b>public</b> Value getFieldValue() {
      <b>return</b> parse(<b>super</b>.getSelected().toString());
   }

   <b>public</b> String format(Value value) {
      <b>return</b> helper.format(value);
   }

   <b>public</b> Value parse(String string) {
      <b>return</b> 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."

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

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

   <b>public</b> <b>void</b> addAll(List&lt;Value&gt; valueList) {
      <b>for</b> (Value value : valueList) {
         add(value);
      }
   }
  
   <b>public</b> <b>void</b> addSelectNone(String label) {
      add(label, <b>null</b>);
      selectNoneLabel = label;
   }

   <b>public</b> <b>void</b> addSelectAll(String label) {
      add(label, <b>null</b>);
      selectAllLabel = label;
   }

   <b>public</b> String format(Value value) {
      <b>return</b> helper.comboBox.format(value);
   }

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

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

   <b>public</b> <b>boolean</b> isSelectAll() {
      <b>return</b> getSelected().equals(selectAllLabel);
   }
  
}

eeyore_lift_crop.jpg align=right hspace=16 />
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.

   <b>public</b> <b>void</b> addFriendTypes(GComboBox comboBox) {
      <b>for</b> (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.

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

}

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

<b>public</b> <b>class</b> GProperty&lt;Value&gt; {
   <b>protected</b> GBeanInfo beanInfo;
   <b>protected</b> PropertyDescriptor propertyDescriptor;
   <b>protected</b> String label;
   <b>protected</b> GPropertyType propertyType;
   <b>protected</b> Integer displayWidth;
   <b>protected</b> String format;
   <b>protected</b> List&lt;GValidator&gt; validatorList = <b>new</b> ArrayList();
   <b>protected</b> GFormatter formatter = GContext.getFormatter();
   ...
  
   <b>public</b> GProperty(GBeanInfo beanInfo, PropertyDescriptor propertyDescriptor) {
      <b>this</b>.beanInfo = beanInfo;
      <b>this</b>.propertyDescriptor = propertyDescriptor;
   }

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

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

   <b>public</b> Value validate(Value value)  { // throws GValidationException 
      <b>for</b> (GPropertyValidator validator : validatorList) {
         validator.validate(value);
      }
      <b>return</b> 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 align=left hspace=16 />

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.

<b>public</b> <b>class</b> GStringValidator <b>extends</b> GValidator&lt;String&gt; {   
   <b>protected</b> Integer minimumLength;
   <b>protected</b> Integer maximumLength;
   <b>protected</b> <b>boolean</b> nullable = <b>false</b>;
   <b>protected</b> <b>boolean</b> empty = <b>false</b>;
  
   ...
   <b>public</b> <b>void</b> validate(String value) {
      <b>if</b> (value == <b>null</b>) {
         <b>if</b> (!nullable) {
            <b>throw</b> <b>new</b> GValidationException(<b>this</b>, "is null");
         }
         <b>return</b>;
      }        
      <b>if</b> (!empty && value.trim().length() == 0) {
         <b>throw</b> <b>new</b> GValidationException(<b>this</b>, "is empty");
      }
      <b>if</b> (minimumLength != <b>null</b> && value.length() < minimumLength) {
         <b>throw</b> <b>new</b> GValidationException(<b>this</b>, "is shorter than " + minimumLength);
      }
      ...
   }
  
   <b>public</b> <b>void</b> 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.

<b>public</b> <b>class</b> GPropertyVerifier <b>extends</b> InputVerifier {
   GProperty property;
  
   <b>public</b> GPropertyVerifier(GProperty property) {
      <b>this</b>.property = property;
   }
  
   <b>public</b> <b>boolean</b> verify(JComponent component) {
      <b>if</b> (component <b>instanceof</b> JFormattedTextField) {
         JFormattedTextField textField = (JFormattedTextField) component;
         String text = textField.getText();
         <b>try</b> {
            property.parse(text);
         } <b>catch</b> (Exception e) {
            <b>return</b> <b>false</b>;
         }
      }
      <b>return</b> <b>true</b>;
   }
  
   <b>public</b> <b>boolean</b> shouldYieldFocus(JComponent component) {
      <b>return</b> 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 align=left hspace=8 />
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.

<b>public</b> <b>class</b> ForestFriendsFormatter <b>extends</b> GDefaultFormatter {
   ...
   <b>public</b> String format(Object value, GProperty property) {
      <b>if</b> (property.isDateFormat()) <b>return</b> dateFormat.format(value);
      <b>if</b> (value <b>instanceof</b> FriendType) <b>return</b> value.toString();
      <b>if</b> (value <b>instanceof</b> ForestFood) <b>return</b> format((ForestFood) value);
      ...
      <b>return</b> <b>super</b>.format(value, property);
   }
  
   <b>public</b> Object parse(String string, GProperty property) <b>throws</b> GParseException {
      <b>if</b> (property.isDateFormat()) <b>return</b> dateFormat.parse(string);     
      <b>if</b> (property.isTimestampFormat()) <b>return</b> timestampFormat.parse(string);
      <b>if</b> (property.isString()) <b>return</b> string;
      <b>if</b> (property.isInteger()) <b>return</b> <b>new</b> Integer(string);
      ...
      <b>return</b> <b>super</b>.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 align=left hspace=16 vspace=4 />
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!

<b>public</b> FriendTypeField <b>extends</b> GComboBoxField&lt;FriendType&gt; {

   <b>public</b> FriendTypeField() {
      <b>super</b>();
      populate();     
   }

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

   <b>public</b> FriendType parse(String string) {
      <b>return</b> <b>super</b>.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 = <b>new</b> FriendTypeField();
   ...
   <b>public</b> <b>void</b> 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.

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

   <b>protected</b> <b>void</b> clear() {
      friendFormBean = <b>new</b> FriendFormBean();
      rebind();
   }

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

   <b>public</b> <b>void</b> 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 align=right hspace=16 />
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 align=left hspace=16 vspace=24 />

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 align=right hspace=16 vspace=16 />
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.

Related Topics >>