A Java/Swing GUI Framework

Bill Wohler
2004-09-19


Next: , Previous: (dir), Up: (dir)

Table of Contents

--- The Detailed Node Listing ---

The ui Package


Next: , Previous: Top, Up: Top

1 Introduction

WARNING: This paper is terribly out of date. Most of the ideas described here are found in the Swing Application Framework (JSR 296) and the Event Bus project. You no longer have to build your own wheels.

Graphical user interfaces can be developed in different ways. This paper discusses several specific frameworks utilizing the Java Foundation Classes, or Swing. While most of the paper is targeted towards UI developers, there are features such as the Session which have general applications. The goals in using common frameworks and methodologies include being able to quickly understand code written by others and being able to reuse code easily; both reduce maintenance burdens.

The first topic covers the ui package which contains general classes that can be used in every application. Then, Actions are introduced which encapsulate behavior in a single class that can be bound to multiple UI elements. Other topics which are not necessarily UI-specific include Sessions (an MVC or Model-View-Controller implementation), and error handling.


Next: , Previous: Introduction, Up: Top

2 The ui Package

The ui package contains general UI classes that can be used in any application. No application-specific code may be added here. Several of the classes and sub-packages are covered here. The classes' documentation is an additional source of information. The Action and Session classes will covered elsewhere.

Classes should not depend on classes in other packages. The reason for this is to allow the ui package to be used in a wide range of applications. Unnecessary dependencies add excess baggage and may make the package unusable in certain configurations. A few exceptions are listed below.


Next: , Previous: Swing Wrappers, Up: Swing Wrappers

2.1 The ui.swing Package

The ui.swing package contains wrappers for many of the Swing classes. The main purpose of this technique is flexibility and maintainability. For example, if JTextField is used throughout and we want to later change the font of all of the text fields in all of the applications, we would have to touch most of the UI code. This is tedious, could be a source of errors, and some fields might be overlooked. However, if we use a wrapper, we can simply add the font to the constructor in the wrapper and we are done.

What do you wrap? If you have need for a Swing class that is not already wrapped, you will need to decide whether that class needs to be wrapped or not: the Swing library is too large to wrap all classes. All high-level classes that start with a J such as JTextField, JTree, and so on are wrapped. Layout managers, events, and lower-level classes usually are not. Factories are usually wrapped so that additional local classes to be returned by the factory can be added. A sure sign that a class should be wrapped is if you find that you are making the same customizations wherever it is used. If you are unsure, discuss this with your colleagues.

How do you wrap? In general, you start with the company's template, and then copy the constructors from Sun's source code, including their documentation. Then, change the name of the constructors to match the wrapper's class name, and then edit the body of each constructor. Ideally, all but one constructor has a single method call to this and that lone constructor contains a single method call to super.

Except in rare circumstances, application code must not use any Swing classes that begin with J.

Obviously, code in this package depends on the javax.swing classes and is thus an exception to the rule that code in the ui hierarchy should not depend on external packages.


Next: , Previous: ui.swing, Up: Swing Wrappers

2.2 The ui.widgets Package

The ui.widgets package is used to house widgets that can be used in any application.


Previous: ui.widgets, Up: Swing Wrappers

2.3 Miscellaneous UI Classes

The ui package proper contains versions of the Action and Session classes, and these will be discussed later.

The ui package also contains an interface called UIConstants that contains general constants like COMPONENT_GAP and COMPONENT_GROUP_GAP. These two constants are set to 5 and 11 respectively and are derived from the Java Look and Feel Design Guidelines.

A suggested convention is for applications to define their own constants interface. Use the name of the application's package followed by the string "Constants" in the name of this interface. For example, if the application Frazzle is in the package frazzle, then name the interface FrazzleConstants.


Next: , Previous: Swing Wrappers, Up: Top

3 Actions

Let's say you have to add a button to a user interface. First you add the button to the panel and add a listener to the button. One method is make the listener for all the UI elements in the panel the panel itself which has the benefit of reducing the number of classes that need to be loaded. The problem with this method is that its actionPerformed method turns into a very long if-then-else clause to determine which action to perform when a button is pushed. This alone has a bad smell (according to Refactoring by Martin Fowler) and can be remedied by using anonymous classes. However, code is scattered throughout the UI to enable or disable the button or menu item depending on the state.

Thus, for every new element added to the interface, the class containing the main panel is touched in at least four different places, and the class grows indefinitely over time reducing maintainability further.

There is a better way.

Sun introduced Actions in Version 1.2 of Java 2 so that behavior could be encapsulated in a single class. This class can then be used in buttons, menu items, context menu items, and so on–it can even be used programmatically. Behavior includes the text in the label, the enabled or disabled state, and the code that is executed when the button, for example, is pushed.

The Action class in the ui package extends Swing's AbstractAction class. It implements the Command design pattern through several execute methods that are called when the button is pushed, for example. The label of the button, for example, can be set by passing a string into the constructor.

Thus, create an action class for each UI element that subclasses Action directly or indirectly, and then create the element as in the following example:

         box.add(new JButton(new AddAction(session)));

That is all the code that is necessary in the panel that holds the button. Quite a contrast to the previous implementation! In addition, the same action object can be used in both the button as above, and in a menu item, for example.

See the documentation in the Action class for more information.


Next: , Previous: Actions, Up: Top

4 Session

A Session is the mechanism that we use to implement an MVC (Model-View-Controller) system. The Session class is found in the ui package. It extends the Observable class and adds a postUpdate method which simply calls Observable's setChanged and notifyObservers methods. While simple, its power is the framework that it is the basis of. As a wrapper to one of Sun's classes, it makes it easy to add code that has global effects.

Most UI components will want to observe the session so that they can be notified of changes to the system. Similarly, components will use the postUpdate method to notify other components of those changes. These notifications are typically called messages. This methodology effectively decouples all components so that the addition of new components is immediate–no other code needs to be changed after inserting the new code.

There are currently three classes in the ui package that components can use to manage the session. They are SessionAction, SessionDialog, and SessionPanel.

For example, all panels should extend SessionPanel which in turn extends JPanel and either pass a Session object into the constructor, or call setSession. The setSession method takes care of removing the panel as an observer of another session and adding itself to the given session. The SessionPanel class also provides convenience methods for the Session's postUpdate method which is used to post messages and the Observer's update method which is overridden to receive messages.

Applications might extend these classes further. For example, if the Frazzle application mentioned above uses dingbats in all of its panels, it may provide a DingbatPanel class which overrides the update method to set the dingbat attribute based upon the selected dingbat:

         public void update(Observable o, Object arg) {
             if (arg instanceof DingbatSelectedMessage) {
                 setDingbat(((DingbatMessage) arg).getDingbat());
             }
             ...
         }

Then the various panels in Frazzle can always simply call getDingbat and be assured they get the current dingbat without having to write any redundant code.

Usually, the argument to the postUpdate method is an object of the Message class (in the util package). The Message class contains two attributes which can be used by the recipients of the message: the source and object. The Message class is often subclassed to pass on additional information.

One type of message is the ConsumableMessage which is also in the util package. This message extends Message and implements Consumable which allows recipients to consume messages and check isConsumed before processing the message. This is very useful in avoiding posting duplicate error messages (discussed in the next section).


Next: , Previous: Session, Up: Top

5 Errors

In general, error handling should be as high level as possible. Errors are usually generated down in low level code which is an inappropriate level for interacting with a user. Also, you do not want to bludgeon the user with hundreds of duplicate messages.

Thus, in DingbatPanel.update we might see:

         public void update(Observable o, Object arg) {
             ...
             if (!((ConsumableMessage) arg).isConsumed()) {
                 try {
                     update(o, (DingbatMessage) arg);
                 } catch (Exception e) {
                     if (!seenAnError) {
                         JOptionPane.showErrorDialog(e);
                         // Deselect dingbat to suppress additional errors.
                         postUpdate(new DingbatSelectedMessage(this, null));
                     }
                     ((ConsumableMessage) arg).consume();
                 }
             }
     
             // If we (or some other observer) consumed the message,
             // there is an error.
             seenAnError = ((ConsumableMessage) arg).isConsumed();
             ...
         }
     
         public void update(Observable o, DingbatMessage arg)
             throws DingbatException, IOException {
         }

The first thing to notice is that lower level code does not have any try/catch blocks. Instead, the lower level methods declare that they throw exceptions which percolate up to the section of code shown above.

We do not process the message if it has already been consumed. Otherwise, we process the message by calling the abstract method update(Observable, DingbatMessage) which subclasses could override to provide more than the default functionality (which does nothing).

If an exception is thrown, we only display an error dialog if we have not done so already. In addition, we send a message of ourself to deselect the defective dingbat which might help the situation. We then consume the message.

Finally, we set the seenAnError attribute if the message has not been consumed since in this particular case, we only consume the message if there has been an error. This flag prevents a cascade of error messages–the user only sees the first one. The only way to clear this flag to receive a message that has not been consumed, and to operate upon it without error.

In this example, consuming messages is used as a flag to other components that an error has occurred. Obviously, if the components consume messages as a part of normal processing, a special message will have to be created that contains something like an errorProne attribute to communicate to other components that an error has occurred.


Previous: Errors, Up: Top

Index