The Source for Java Technology Collaboration
User: Password:



   

Asserting Control Over the GUI: Commands, Defaults, and Resource Bundles Asserting Control Over the GUI: Commands, Defaults, and Resource Bundles

by Hans Muller
01/31/2005

Contents
Swing Event Handling Basics
Trampolines
A Trampoline Based on EventHandler
Swing Actions: Sharing Behavior and Visuals
Localizing Actions with Resource Bundles
Commands: International Actions
Disclaimers and Thanks
Resources

This article is about defining Swing application behavior. It's about combining low-level J2SE primitives, like Actions, ResourceBundles, and UIDefaults, in a way that's appropriate for moderately large desktop Java applications. The article begins with a review of event handling, Swing actions, and conventional approaches for defining them. The second half of the article demonstrates how one can separate an Action's visual properties into a ResourceBundle that's loaded through Swing's UIDefaults API. In addition to enabling localization, this approach shifts some of the GUI from code to a declarative representation. That's an advantage for large applications, because the declarative aspect of the application can be developed independently from the code.

Swing Event Handling Basics

Swing components manage lists of observers, called event listeners, for different types of events. There are listeners for event types like raw mouse and keyboard input, as well as for component state changes, such as changes to a component's properties. Swing components notify their listeners by applying each one to an event that defines what happened. The most common Swing event type is called ActionEvent. All of the basic controls, like buttons and menu items, notify their ActionListeners when they detect a triggering input gesture such as a mouse button press. To complete the review, here's an example that adds an ActionListener to a JButton:

final JButton myButton = new JButton("Press Me");
ActionListener doMyAction = new ActionListener() {
    private int nActions = 0;
    public void actionPerformed(ActionEvent e) {
        nActions += 1;
        myButton.setText("Pressure: " + nActions);
    }
};
myButton.addActionListener(doMyAction);

There's much to like about this. We're using a class to define the button's behavior, which means we can encapsulate any state associated with handling the ActionEvent (the variable nActions). All of the event handling linkage shown here is statically type checked by the compiler, so there will be fewer surprises at runtime.

Trampolines

It seemed as though it was only seconds after the debut of this approach to event handling (in about 1996) that developers had created applications with thousands of controls and a commensurate number of EventListener subclasses. At the time, there was a substantial fixed overhead cost (size) for each class, and this made developers who were trying to deliver their apps to dialup modem users pretty unhappy. Once the app arrived on a user's machine, the bloat also adversely affected startup time and footprint. This led to some appalling hacks, notably using a single listener class for many components by switching on the component's label text or the component itself:

ActionListener doMyActions = new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        Object src = e.getSource();
        if (src == myButton1) {
           doAction1(e);
        }
        // Don't do this. Ever.
        else if ((JButton)(src).getText() == "Button2") {
           doAction2(e);
        }
        // etc ...
    }
};

The effects of localization and changes to the component hierarchy ensure a spot in the programmer's hall of shame for employing logic like this. To be fair to those hall of shame inductees, some of the code that followed these patterns was the legacy of AWT's original event model. Examples and documentation advocating this approach can be found in the Java 1.0 releases.

A common and sensible alternative to this kind of if/else mess is to create listener "trampoline" classes that can call a method like doActionN on a well-known object using reflection. We call such a class an event trampoline because the event just bounces from the trampoline's method to the real handler. For example, if we create an object that contains all of our action methods, a singe ActionListener trampoline will suffice:

private final Object actions = new Object() {
    public void doAction1(ActionEvent e) { ... }
    public void doAction2(ActionEvent e) { ... }
    ...
};
private class ActionTrampoline extends ActionListener {
    private final String name;
    ActionTrampoline(String name) { this.name = name; }
    public void actionPerformed(ActionEvent e) {
        // exception handling elided
	Class cls = actions.getClass();
        Method m = cls.getMethod(name, ActionEvent.class);
        m.invoke(actions, e);
    }
}
button1.addActionListener(new ActionTrampoline("doAction1"));
button2.addActionListener(new ActionTrampoline("doAction2"));
...

To generalize this example, you'd need one trampoline class per EventListener type employed by your application. Chances are good that you'd elect to make the actions class, which is private inner and anonymous in the previous example, an ordinary top-level class. Similarly, it's usually not a good idea to dump all event handling methods into a single class; it's often helpful to segregate them according to purpose, requirements for access to shared state, and so on.

A Trampoline Based on EventHandler

The EventHandler interface, included in the 1.4 Java release, makes it possible to create this kind of trampoline (as well as more elaborate variations) automatically and at runtime. Here's the previous example written using EventHandler:

public class MyActions {
    public void doAction1() { ... }
    public void doAction2() { ... }
    ...
};
Class alc = ActionListener.class;
button1.addActionListener(
    EventHandler.create(alc, actions, "doAction1"));
button2.addActionListener(
    EventHandler.create(alc, actions, "doAction2"));

This approach has the same space-saving advantages as the first example did. EventHandler uses java.lang.reflect.Proxy to create just one (internal to the implementation) trampoline class (at runtime) for each listener interface it encounters. And EventHandler can be used to create listeners of any type; see the EventHandler Javadoc for more information. EventHandler, which was created to support IDEs that allowed users to connect data derived from a GUI control or event to a GUI-agnostic controller method, doesn't provide any special support for ActionListeners. Most Swing GUIs are mostly ActionListeners, so it makes sense to provide additional support for them and Swing does, in the Action class.

Swing Actions: Sharing Behavior and Visuals

A Swing Action is just an ActionListener with a little hashtable of properties that are used to define the appearance of a control that has the action. Several controls can share one action; e.g., the same action might appear in a pop-up right-button menu and on a toolbar. Actions keep track of the controls they've been added to, and one can disable all of them, by disabling the action (see Action.setEnabled() ). Menus and toolbars even support adding Action objects by creating a JMenuItem or a JButton, respectively. Just to complete the review, here's an example of using an Action that disables itself. The AbstractAction constructor argument becomes the value of the action's Action.NAME, which is used for the title of buttons and menus and menu items.

Action action = new AbstractAction("Disable All") {
   public void actionPerformed(ActionEvent e) {
       setEnabled(false);
   }
};
KeyStroke accKey = KeyStroke.getKeyStroke("ctrl D"));
action.putValue(Action.SHORT_DESCRIPTION, "the tooltip");
action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_D);
action.putValue(Action.ACCELERATOR_KEY, accKey);

JButton button = new JButton(action);
JMenu menu = new JMenu(action);
menu.add(action);
JToolBar toolbar = new JToolBar();
toolbar.add(action);

The problem with this is that we're back to square one for big applications with hundreds of ActionListeners. Once again, we're defining one class for each listener.

Localizing Actions with Resource Bundles

If you're at all sensitive to the internationalization requirements for applications, you'll have also noticed that we've wired the action's tooltip and label text down in the code. That's not good, either. The way to overcome both problems is to create an Action trampoline class, like the one we defined earlier, and a subclass of Action that configures itself from a resource file.

Java applications store localized text and other data in tables of key/value pairs called ResourceBundles. Resource bundles are usually loaded by the app at initialization time with a statement like this:

ResourceBundle rb = 
    ResourceBundle.getResource("MyResources");

The getResource call looks for a class or a .properties file named MyResources_<default locale>. An app developer would store English localizations in a file called MyResources_en.properties (or a ResourceBundle subclass called MyResources_en), German values in MyResources_de.properties, and MyResources.properties (no suffix) would contain values to use if the default locale didn't match a localized resource file.

The Swing UIDefaults class supports ResourceBundles. An application can load its ResourceBundles into an instance of UIDefaults and then use typed methods, like UIDefaults.getString(), to look up localized values for Action properties like Action.SHORT_DESCRIPTION. The only useful types that the current UIDefaults class lacks are KeyStroke and KeyCode, so we'll add those in a UIDefaults subclass called AppDefaults:

public class AppDefaults extends UIDefaults {
    public KeyStroke getKeyStroke(String key) {
        return KeyStroke.getKeyStroke(getString(key));
    }
    public Integer getKeyCode(String key) {
        KeyStroke ks = getKeyStroke(key);
        return (ks != null) 
            ? new Integer(ks.getKeyCode()) 
            : null;
    }
}

Commands: International Actions

Now we can define an Action trampoline class, called Command, that initializes its Action properties from a ResourceBundle property file through an instance of AppDefaults. That means that instead of initializing component labels or tooltips in the code, we'll define all of them in a .properties file. For example, our application's quit Command is defined by the following property file entries:

quit.Name=Quit
quit.AcceleratorKey=control Q
quit.MnemonicKey=Q
quit.ShortDescription=Exit the application

The application creates and uses the quit command as you might expect. However, note that we've created an AppDefaults object and passed it along to the Command class:

public class MyCommands {
    public void quit() { ... }
}
AppDefaults defaults = new AppDefaults();
defaults.addResourceBundle("MyApplication");
...
MyCommands commands = new MyCommands();
Command quitCommand = new Command("quit", commands, defaults);
myMenu.add(quitCommand);  // invokes commands.quit();

The Command class is similar to ActionTrampoline except that rather than invoking a method on a implicit and private target object, the target (commands in the example above) is explicit. The other important difference is that Commands initialize themselves from an AppDefaults object:

private final static String actionKeys[] = {
    Action.NAME,
    Action.SHORT_DESCRIPTION,
    Action.LONG_DESCRIPTION,
    Action.SMALL_ICON,
    Action.ACTION_COMMAND_KEY,
    Action.ACCELERATOR_KEY,
    Action.MNEMONIC_KEY
};
public Command(Object target, String methodName, AppDefaults defaults) {
    super(methodName);  // methodName is the default label text
    this.target = target;
    this.methodName = methodName;
    for(String k : actionKeys) {
        String mk = methodName + "." + k;
        if (k == Action.MNEMONIC_KEY) {
            putValue(k, defaults.getKeyCode(mk));
        }
        else if (k == Action.ACCELERATOR_KEY) {
            putValue(k, defaults.getKeyStroke(mk));
        }
        else if (k == Action.SMALL_ICON) {
            putValue(k, defaults.getIcon(mk));
        }
        else {
            putValue(k, defaults.get(mk));
        }
    }
}

So there you have it. We've taken a pretty comprehensive tour of the support in Swing for binding GUI controls to behavior and have concluded with a some small but useful classes that separate a control's presentation into a ResourceBundle property file.

Disclaimers and Thanks

None of the material I've presented here is particularly novel. Variations on this theme have been produced by many other developers for as long as Swing has been around. In fact, if I may indulge in an analogy, action frameworks are to desktop Java developers what web application frameworks are to our browser-obsessed brethren. There's a reason for this. In a large application, it's often helpful to create an action framework (or to extend an existing one) that captures features and constraints that are unique to the application or its domain. Frameworks for applications with very large GUIs--think thousands of forms--often blend an action framework with a declarative representation for the forms, so that the forms can be loaded, discarded, and even generated dynamically. Applications that can be extended with plugin modules often use the kind of loose coupling described here to simplify morphing a live API.

My goal in writing this article was to show that a simple combination of the current J2SE classes are sufficient to bind Swing GUI components to actions in a declarative form that's easily localized. I hope that I've presented the material clearly enough for Swing novices and engagingly enough for old hands.

I'd like to thank Scott Violet for the time spent talking over the ideas presented here, notably the connection between Actions and ResourceBundles.

Resources

All of the code for the examples can be found in this source code .zip file.

Hans Muller is the CTO for Sun's Desktop division. He's been at Sun for over 15 years and has been involved with desktop GUI work of one kind another for nearly all of that time.

View all java.net Articles.

 Feed java.net RSS Feeds