--- The Detailed Node Listing ---
The ui Package
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.
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.
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.
The ui.widgets package is used to house widgets that can be used in any application.
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
.
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.
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).
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.