|
|
||
Evan Summers's BlogBean Curd 1F: Swing Panel BeaterPosted by evanx on June 15, 2006 at 07:43 AM | Comments (3)
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.
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! ;)
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
}
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.
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.
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.
We might also include layout settings in our LayoutAnnotation, eg. gridx, gridy. (Tigger knows GridBagLayout and often uses that.) 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.
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.
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.
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 the same type of nail. 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
}
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.
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
}
GComboBox's getFieldValue() delegates to its GComboBoxModel to lookup the object value associated with it's getSelected() label, as we will see later.
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?
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!"
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);
}
}
There is Another Big Issue. Usually we want to support auto-completion for combo boxes. But we gonna dodge that issue for now.
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!
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.
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);
}
}
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.
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(string);
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.
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?
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 the programming even more fluffy and 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().
OK, let's wrap up. Time for a nap!
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!
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.
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. CommentsComments are listed in date ascending order (oldest first) | Post Comment
| ||
|
|