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

Version 2



    Swing Event Handling Basics
    Tra mpolines
    A Trampoline Based onEventHandler
    Swing Actions: Sharing Behavior and Visuals
    Localizing Actions with Resource Bundles
    Commands: InternationalActions
    Disclaimers and Thanks

    This article is about defining Swing application behavior. It's about combining low-level J2SE primitives, likeActions, ResourceBundles, andUIDefaults, 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 aResourceBundle that's loaded through Swing'sUIDefaults 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 theirActionListeners 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 aJButton:

    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 variablenActions). All of the event handling linkage shown here is statically type checked by the compiler, so there will be fewer surprises at runtime.


    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 ofif/else mess is to create listener "trampoline" classes that can call a method likedoActionN 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) { = 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 perEventListener type employed by your application. Chances are good that you'd elect to make the actionsclass, 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 onEventHandler

    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 usingEventHandler:

    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.Proxyto create just one (internal to the implementation) trampoline class (at runtime) for each listener interface it encounters. AndEventHandler can be used to create listeners of any type; see theEventHandler 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 theAction class.

    Swing Actions: Sharing Behavior and Visuals

    A Swing Action is just anActionListener 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 Actionobjects by creating a JMenuItem or aJButton, 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 anAction 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 (or a ResourceBundlesubclass called MyResources_en), German values, and suffix) would contain values to use if the default locale didn't match a localized resource file.

    The Swing UIDefaults class supportsResourceBundles. An application can load itsResourceBundles into an instance ofUIDefaults and then use typed methods, likeUIDefaults.getString(), to look up localized values for Action properties likeAction.SHORT_DESCRIPTION. The only useful types that the current UIDefaults class lacks areKeyStroke and KeyCode, so we'll add those in a UIDefaults subclass calledAppDefaults:

    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 itsAction properties from a ResourceBundleproperty 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 anAppDefaults object and passed it along to theCommand 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 toActionTrampoline 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 = 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 betweenActions and ResourceBundles.


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