Using the Swing Application Framework (JSR 296)

By John O'Conner, July 2007

If you've developed many applications using a Swing-based graphical user interface (GUI), you've probably solved some common problems over and over again. Those problems include managing the application life cycle, event handling, threading, localizable resources, and maybe even persistence.

To save time and effort, you can develop reusable libraries and small application frameworks to use on each new project. An application framework can provide much of the common infrastructure that most applications share. A framework is a reusable library of classes and functionality that help you design and implement applications using consistent designs and patterns.

If you develop Swing applications, you can benefit from the Swing Application Framework, which is currently being developed as part of Java Specification Request (JSR) 296.

Framework Scope

The term framework can cause apprehension because frameworks can be large, complicated, and overbearing. For small applications, a framework can introduce more complexity than the original system it supposedly helps. However, the Swing Application Framework has goals that minimize any burdensome effects that a larger framework might cause.

The framework's primary goal is to provide the kernel of a typical Swing application, helping programmers to get started quickly and to adopt best practices for just a few elements common to every Swing application: architecture, lifecycle, resource management, event handling, threading, session state, and local storage.

Framework Project Files

As you read about the framework, you may be tempted to experiment with it yourself. Don't resist. You can download the Swing Application Framework project files from its online project location at java.net. Navigate to the project's documents and files section. The project is in the early stages at this time. The examples and source code in this article run correctly with version .50, the project's current version.

Note that the JSR 296 reference implementation libraries, documentation, and source files change frequently, so you may have to adjust or modify the source code in this article as a result. Project files generally follow the following naming conventions, where <version> represents a changing version number:

  • ApplicationFramework-<version>.jar
  • ApplicationFramework-<version>-doc.jar
  • ApplicationFramework-<version>-src.zip

You can use the framework by downloading just the library and documentation. If you're interested in implementation details, consider downloading the source as well, which is easily compiled with an ANT project-based integrated development environment (IDE) such as the NetBeans IDE. Once you have the framework implementation, put its jar file in your compiler and runtime classpath so that your application can use its application programming interface (API).

The framework's API exists in just one package: application. Although a few subpackages hold text and icon resources, all the API is in the application package itself.

Framework Architecture

Two classes help you manage your application: ApplicationContext and Application. The Application and ApplicationContext objects have a one-to-one relationship.

Those services include the following:

  • Localizable resource management
  • Task services and monitoring
  • Event-action management
  • Session-state storage

All Swing Application Framework applications must subclass either the Application class or its SingleFrameApplication subclass. The Application class provides lifecycle methods for launching the application, starting the user interface (UI), and shutting down.

The SingleFrameApplication adds a default main GUI frame, retrieves and injects default resources, and uses the ApplicationContext to save and restore simple session state. Session state includes UI component location, size, and configuration.

Both superclasses provide an ApplicationContext, but the SingleFrameApplication class provides additional default behaviors that use the context. You probably will not use Application as your superclass. Most likely, the SingleFrameApplication class provides the default behavior you need. In the future, other subclasses should be available to handle multiframe applications as well. Regardless of the superclass, your application will use its ApplicationContext instance to access most services that the framework provides.

Application Life Cycle

All applications have a life cycle of events that are called in a specific order. Your primary Application or SingleFrameApplication subclass should start in its static main method and should launch the application from that point. The supported life cycle includes the following methods, called in this order:

  1. launch -- You must call this framework method.
  2. initialize -- The framework will invoke this optional overridden method.
  3. startup -- The framework will invoke this overridden method.
  4. ready -- The framework will invoke this optional overridden method.
  5. exit -- You must call this framework method.
  6. shutdown -- The framework will invoke this optional overridden method.

Your application's minimum requirement is to call the launch method and to override the startup method. By calling the launch method, your application begins the life cycle. The launch method will then call the initialize, startup, and ready methods. You should also handle your application frame's closing by calling the exit method, which will eventually call the shutdown method. A few examples should help make the sequence more clear.

Application Launch, Initialization, and Startup

You must create and initialize your GUI on the Swing event dispatch thread (EDT). Many application developers forget this important step. Code Example 1 shows a basic Swing application without the framework, and the code examples that follow add the framework.

Code Example 1

public class BasicApp implements Runnable {

    JFrame mainFrame;
    JLabel label;

    public void run() {
        mainFrame = new JFrame("BasicApp");
        label = new JLabel("Hello, world!");
        label.setFont(new Font("SansSerif", Font.PLAIN, 22));
        mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        mainFrame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                mainFrame.setVisible(false);
                // Perform any other operations you might need
                // before exit.
                System.exit(0);
            }
        });
        mainFrame.add(label);
        mainFrame.pack();
        mainFrame.setVisible(true);
    }

    public static void main(String[] args) {
        Runnable app = new BasicApp();
        try {
            SwingUtilities.invokeAndWait(app);
        } catch (InvocationTargetException ex) {
            ex.printStackTrace();
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }
}

Adding the basic framework for this example does not really reduce your workload, but you have to start somewhere. Still, even with an example this simple, the framework does several important jobs. Review Code Example 2, which implements the same "Hello, world!" functionality within the framework, and see the description that follows it.

Code Example 2

public class BasicFrameworkApp extends Application {
    private JFrame mainFrame;
    private JLabel label;

    @Override
    protected void startup() {
        mainFrame = new JFrame("BasicFrameworkApp");
        mainFrame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        mainFrame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                mainframe.setVisible(false);
                exit();
            }
        });
        label = new JLabel("Hello, world!");
        mainFrame.add(label);
        mainFrame.pack();
        mainFrame.setVisible(true);
    }

    public static void main(String[] args) {
        Application.launch(BasicFrameworkApp.class, args);
    }
}

Even in this simple example, the framework does several important jobs. First, by using the launch method, you always ensure that the UI starts on the EDT, something many developers simply forget. For more information about interacting with the EDT, you can read Improve Application Performance With SwingWorker in Java SE 6. Second, this code uses a distinct, well-defined startup method to create and display the UI components. Finally, by using the Application.launch method, you begin the application life cycle, which means that the framework will call your overridden lifecycle methods -- such as startup -- at important points in the application lifetime.

All framework applications must override the startup method. Use this method to create and display the UI. The framework will call this method on the EDT as a result of your invoking the launch method. In Code Example 2, the startup method creates a frame, adds a window listener for closing the frame and exiting, and displays the simple frame.

The launch method calls the application's optional initialize method just prior to calling the startup method. You can use the initialize method to perform any initial configuration or setup steps. For example, you can process command-line arguments from within the initialize method. You can also check a database connection or set system properties. In short, the framework provides this method for any non-UI related setup that your application may need before displaying the UI. The Application and SingleFrameApplication classes provide an empty method body for the initialize method. The method does nothing by default.

Code Example 3 subclasses the SingleFrameApplication class. Subclassing the SingleFrameApplication class has many benefits over using the Application class. For example, a SingleFrameApplication already has a primary JFrame instance as its main window. The superclass already overrides many of the lifecycle events to provide default behavior. The SingleFrameApplication class injects resources, implements a simple WindowAdapter object to handle window closings, implements a shutdown method, and performs basic operations to save and restore a session. In general, you should probably avoid subclassing the Application class directly. Using SingleFrameApplication provides your application with helpful default behaviors.

Code Example 3

public class BasicSingleFrameApp extends SingleFrameApplication {
    JLabel label;

    @Override
    protected void startup() {
        getMainFrame().setTitle("BasicSingleFrameApp");
        label = new JLabel("Hello, world!");
        label.setFont(new Font("SansSerif", Font.PLAIN, 22));
        show(label);
    }

    public static void main(String[] args) {
        Application.launch(BasicSingleFrameApp.class, args);
    }

}

As you can see, the basic "Hello, world!" application gets noticeably shorter in Code Example 3. The only new APIs here are the show and getMainFrame method calls. A SingleFrameApplication subclass can use the show method to provide its main, default UI component. The superclass adds the component -- a label in the case of Code Example 3 -- to the main frame and displays the main frame with that component.

The Ready State

After initialization and startup, the framework calls another lifecycle method that you can optionally override. The framework calls your application's ready method after all initial GUI events related to your UI startup have been processed. Overriding the ready method is your opportunity to perform tasks that will not delay your initial UI. Place here any work that depends on your UI being ready and visible.

Application Exit and Shutdown

The Application class implements an exit method to gracefully shut down the application. According to the Application implementation, a graceful shutdown involves asking any ExitListener objects whether exiting is possible, then alerting those same listeners that the Application will actually shut down, calling the shutdown method, and finally calling the System.exit method.

But the Application class does not call the exit method directly. Your application should do this if it subclasses the Application class. However, the SingleFrameApplication class does this for you when you close the main window frame. It implements a WindowListener that calls the exit method when you close the application's main window frame. Regardless of how you finally call the exit method, you should override the shutdown method to perform application-specific cleanup before the application terminates completely. The shutdown method is your opportunity to close database connections, save files, or perform any other final tasks before your application finally quits.

The SingleFrameApplication superclass implements a simple shutdown method. It saves its window-frame session state and includes all secondary frame state as well. For this reason, you should remember to call super.shutdown() if you override this method. Code Example 4 shows you what to do.

Code Example 4

@Override
protected void shutdown() {
    // The default shutdown saves session window state.
    super.shutdown();
    // Now perform any other shutdown tasks you need.
}

Implement the Application.ExitListener interface to allow your application the chance to veto or approve requests to exit the application. The default exit algorithm includes calls to all listeners before calling the shutdown method. By implementing the ExitListener interface, you can alert your customers or users of the impending shutdown operation and even allow them to stop the shutdown.

The ExitListener interface has two methods:

  • public boolean canExit(EventObject e)
  • public void willExit(EventObject e)

Use the canExit method to respond to the exit request. Return a true value to allow the exit, false otherwise. The willExit method is simply an alert notification, but you can also use it for any preparations you need for the ensuing shutdown.

Code Example 5 shows how you might implement an ExitListener object. Notice that the example calls the exit method, which is implemented by the Application superclass. The exit method notifies all ExitListener objects and calls the shutdown method only if all listeners approve the request to exit.

Code Example 5

public class ConfirmExit extends SingleFrameApplication {
    private JButton exitButton;

    @Override
    protected void startup() {
        getMainFrame().setTitle("ConfirmExit");
        exitButton = new JButton("Exit Application");
        exitButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                exit(e);
            }

        });
        addExitListener(new ExitListener() {
            public boolean canExit(EventObject e) {
                boolean bOkToExit = false;
                Component source = (Component) e.getSource();
                bOkToExit = JOptionPane.showConfirmDialog(source,
                                "Do you really want to exit?") ==
                                JOptionPane.YES_OPTION;
                return bOkToExit;
            }
            public void willExit(EventObject event) {

            }
        });
        show(exitButton);
    }

    @Override
    protected void shutdown() {
        // The default shutdown saves session window state.
        super.shutdown();
        // Now perform any other shutdown tasks you need.
        // ...
    }

    /**
     * @param args the command-line arguments
     */
    public static void main(String[] args) {
        Application.launch(ConfirmExit.class, args);
    }

}

Code Example 5 defines a SingleFrameApplication that contains a single JButton in its main frame. When you click that button or attempt to close the main window, the application's ExitListener confirms the user request. Figure 2 shows the application responding through its listener interface.

Resource Management

Most applications use common resources such as text, icons, colors, and font definitions. If you ever want to localize your application to other locales or platforms, you should externalize resources to facilitate easy modifications or translations to other languages. Resources are defined in ResourceBundle implementations, which are usually ListResourceBundle subclasses or properties files.

The framework helps you define and organize resources for individual classes and for the entire application. Resources that are shared throughout the application should exist in a ResourceBundle file named after your Application subclass name. Resources for a specific form or class should exist in a ResourceBundle file named after that specific class. In both cases, the ResourceBundle implementations must exist in a resources subpackage immediately below that of the class for which they provide resources.

Table 1 shows the relationship among an application class or form, its ResourceBundle name, and the ResourceBundle file name.

Table 1. Relationships Among Application Class, ResourceBundle Name, and ResourceBundle File Name

Class ResourceBundle Name ResourceBundle File Name
demo.MyApp demo.resources.MyApp demo/resources/MyApp.properties
demo.hello.HelloPanel demo.hello.resources.HelloPanel demo/hello/resources/HelloPanel.properties
demo.hello.ExitPanel demo.hello.resources.ExitPanel demo/hello/resources/ExitPanel.properties

Instead of loading and working with ResourceBundle files directly, you will use the ResourceManager and ResourceMap framework classes to manage resources. A ResourceMap contains the resources defined in a specific ResourceBundle implementation. A map also contains links to its parent chain of ResourceMap objects. The parent chain for any class includes the ResourceMap for that specific class, the application subclass to which the class belongs, and all superclasses of your application up to the base Application class.

The ResourceManager is responsible for creating maps and their parent chains when you request resources. You will use the ApplicationContext to retrieve ResourceManager and ResourceMap objects.

You have three options for working with resources:

  • Manually load and use ResourceMap objects and their resources.
  • Use the framework to automatically inject resources into the UI components that need them.
  • Use the framework to automatically inject resources into the object fields that need need them.

Manual Resource Management

The ApplicationContext class provides access to the ResourceManager and its ResourceMap instances. Retrieve the context using the getContext method of your Application instance. Use the context to retrieve a resource manager and map. Use the map to retrieve resources for your application or specific class. Code Example 6 shows how to retrieve resources and apply them to UI components.

Code Example 6

public class HelloWorld extends SingleFrameApplication {
    JLabel label;
    ResourceMap resource;

    @Override
    protected void initialize(String[] args) {
        ApplicationContext ctxt = getContext();
        ResourceManager mgr = ctxt.getResourceManager();
        resource = mgr.getResourceMap(HelloWorld.class);
    }

    @Override
    protected void startup() {
        label = new JLabel();
        String helloText = (String) resource.getObject("helloLabel", String.class);
        // Or you can use the convenience methods that cast resources
        // to the type indicated by the method names:
        // resource.getString("helloLabel.text");
        // resource.getColor("backgroundcolor");
        // and so on.
        Color backgroundColor = resource.getColor("color");
        String title = resource.getString("title");
        label.setBackground(backgroundColor);
        label.setOpaque(true);
        getMainFrame().setTitle(title);
        label.setText(helloText);
        show(label);
    }
    // ...
}

You can also retrieve a resource map using the convenience method in the ApplicationContext instance:

resource = ctxt.getResourceMap(HelloWorld.class);

In Code Example 6, the HelloWorld class uses three resources: a label's text, a color for the label background, and text for the frame's window title. It gets those resources from a resource map for the HelloWorld class:

resource = mgr.getResourceMap(HelloWorld.class);

Provide the Class instance that represents either your Application class or another specific class in your application. In this example, the resource manager will search and load a ResourceMap that contains resources from the resources/HelloWorld resource bundle. In this case, the bundle implementation is a file named resources/HelloWorld.properties. Code Example 7 shows part of the file.

Code Example 7

helloLabel = Hello, world!
color = #AABBCC
title = HelloWorld with Resources

Figure 3 shows the HelloWorld application. Each of the resources -- the window title, the label text, and the label background color -- were manually retrieved and applied to each specific component. If you use manual resource management, you must provide code to perform all steps of retrieving and using the resources in the appropriate component.

Component Resource Injection

The framework can automatically inject resources into your components. Using the ResourceMap object's injectComponents method, you tell the framework to retrieve resources from its resource map chain and to apply them to components. This works very well when you provide your application's root window because the method recursively travels down containers, injecting UI components as it traverses the container hierarchy. To use the injectComponents method, you must use a naming convention that helps the framework match component names and their resources.

The naming convention is simple. Just use the setName method on your UI components to give them a name. In your ResourceBundle files, use that name to define resources for that component. In the resource file, append a period ( .) and the property name to the component name to define a resource for a specific property.

For example, if you want to define the text property of a button, you should name the button and use the button's name in the resource file. If the button's name is btnShowTime, you can define its resource text in the resource file using the same btnShowTime name. Because you want to set the text of the button, the resource name should be btnShowTime.text.

Code Example 8 shows several resource definitions in a resources/ShowTimeApp.properties file. Use UI component names and their property names to define injectable resources.

Code Example 8

btnShowTime.text = Show current time!
btnShowTime.icon = refresh.png

txtShowTime.text = Press the button to retrieve time.
txtShowTime.editable = false

Code Example 8 shows resources for two components: btnShowTime and txtShowTime. The btnShowTime.text resource defines the text that should be injected into a button component. The btnShowTime.icon resource defines the icon that the same button will display. The second component name is txtShowTime, which is a text component in the application.

You can call injectComponents directly, but you can save yourself some effort just by subclassing the SingleFrameApplication framework class. This class defines a show method that automatically injects component resources. Code Example 9 shows how to use component naming conventions, the SingleFrameApplication superclass, and the properties file shown in Code Example 8 to inject resources.

Code Example 9

public class ShowTimeApp extends SingleFrameApplication {
    JPanel timePanel;
    JButton btnShowTime;
    JTextField txtShowTime;

    @Override
    protected void startup() {
        timePanel = new JPanel();
        btnShowTime = new JButton();
        txtShowTime = new JTextField();

        // Set UI component names so that the
        // framework can inject resources automatically into
        // these components. Resources come from similarly
        // named keys in resources/ShowTimeApp.properties.
        btnShowTime.setName("btnShowTime");
        txtShowTime.setName("txtShowTime");

        btnShowTime.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Date now = new Date();
                txtShowTime.setText(now.toString());
            }
        });

        timePanel.add(btnShowTime);
        timePanel.add(txtShowTime);
        show(timePanel);
    }
    // ...
}

Code Example 9 does not explicitly load a ResourceMap. The framework does that for you. All you have to do is follow the naming conventions for components, create a ResourceBundle file using the application class name, and use the component names appropriately in the resource file. The framework will find and inject the resources for you. Figure 4 shows the ShowTimeApp running. Notice that the button label and icon are set correctly using resources from the resources/ShowTimeApp.properties file.

Field Resource Injection

The framework supports field resource injection too. You can inject resources into fields with types such as String, Color, and Font by marking those fields with the @Resource annotation. When you use the ResourceMap object's injectFields method, the framework will automatically inject resources into fields that you mark with this annotation.

Field resource injection is similar to component resource injection because the framework handles the retrieval for you. However, important differences do exist. For example, field resource injection does not work on container hierarchies. When you inject field resources, the injectFields method works on a single target object's fields and does not traverse containers.

Again, you should put the resources in your application-level or class-level resource file. Remember to load those resources using the getResourceMap method, passing in the class object if you want class-level resources instead of application-level resources.

By default, field resources should have a name that contains their class name as a prefix. For example, if the class MyApp has a field named myIcon, the resource will have the name MyApp.myIcon in the ResourceBundle file and in the corresponding ResourceMap object.

Code Example 10 shows how you can inject field resources into your code. Not only does this example inject the field, but it also uses class-specific resources defined in a resources/NameEntryPanel resource bundle. Mark fields with the @Resource annotation to use field injection.

Code Example 10

public class NameEntryPanel extends javax.swing.JPanel {

    @Resource
    String greetingMsg;

    ApplicationContext ctx;
    ResourceMap resource;

    /** Creates new form NameEntryPanel. */
    public NameEntryPanel(ApplicationContext ctxt) {
        initComponents();
        ResourceMap resource = ctxt.getResourceMap(NameEntryPanel.class);
        resource.injectFields(this);
    }

    private void initComponents() {
        lblNamePrompt = new javax.swing.JLabel();
        txtName = new javax.swing.JTextField();
        btnGreet = new javax.swing.JButton();

        lblNamePrompt.setName("lblNamePrompt");
        btnGreet.setName("btnGreet");
        // ...
    }

    private void btnGreetActionPerformed(java.awt.event.ActionEvent evt) {
        String personalMsg = String.format(greetingMsg, txtName.getText());
        JOptionPane.showMessageDialog(this, personalMsg);
    }

    private javax.swing.JButton btnGreet;
    private javax.swing.JLabel lblNamePrompt;
    private javax.swing.JTextField txtName;
}

In order to inject the resources for the NameEntryPanel class, the resources must be defined in a class resource file. Code Example 11 shows the sample file, which defines a parameterized string with key NameEntryPanel.greetingMsg. You should name field resources in the resource file using the class name followed by a period ( .) and the field name: <classname>.<fieldname>. That naming pattern is the default, but you can override this by providing a different name using the @Resource annotation's key element.

Code Example 11

# resources/NameEntryPanel.properties
NameEntryPanel.greetingMsg = Hello, %s, this string was injected!

Conversions and Conveniences

The ResourceMap class provides automatic conversion for common resource types represented as text strings. This is useful because resource values in a .properties file are always simple text values. Resource converters, however, can convert the text representation of fonts, color, and icon resources into actual Font, Color, and Icon objects. Other ResourceConverter classes are available, but the following examples show how to represent these resource types. Of course, conversion of any .properties file value into a String type is always supported. Consult the framework documentation for information about other converters.

Code Example 12 shows the resource file and the ConverterApp source code that demonstrates how to create and use resources with ResourceConverter objects.

Code Example 12

# resources/ConverterApp.properties
Application.id = ConverterApp
Application.title = ResourceConverter Demo

msg = This app demos ResourceConverter.
font = Arial-BOLD-22
color = #BB0000
icon = next.png

**
public class ConverterApp extends SingleFrameApplication {
    protected void startup() {
        ApplicationContext ctx = getContext();
        ResourceMap resource = ctx.getResourceMap();

        String msg;
        Color color;
        Font font;
        Icon icon;
        JLabel label;

        // Use resource converters to convert text representations
        // of resources into Color and Font objects.
        msg = resource.getString("msg");
        color = resource.getColor("color");
        font = resource.getFont("font");
        icon = resource.getIcon("icon");

        label = new JLabel(msg);
        label.setOpaque(false);
        label.setForeground(color);
        label.setFont(font);
        label.setIcon(icon);

        show(label);
    }
    // ...
}

In Code Example 12, notice the format of the resource values. To use the resource converters, you must format the text representation of your resources in specific ways. Each resource type -- whether it be a font, icon, color, or other resource -- has a specific format.

For example, you should use the red (R), green (G), and blue (B) values -- commonly referred to as RGB values -- between 0 and 255 for colors. The accepted formats use RGB color values in any of these forms:

  • #RRGGBB for the hexadecimal representation of RGB values
  • #AARRGGBB for hexadecimal RGB values with an alpha channel (A)
  • R, G, B for decimal RGB values
  • R, G, B, A for decimal RGB values with an alpha channel

Represent fonts using the form <name>-<style>-<size>. The name element is simply the font face name. Common names are Arial and Helvetica, for example. Styles can include values such as PLAIN, BOLD, or ITALIC. Finally, the size element is the font size. Examples of valid font resource values are Arial-PLAIN-12 or Helvetica-BOLD-22.

Icon resources are represented by their file names in your classpath. For example, if you want to create an icon for an image file in your resources package, you just use the image's file name.

Figure 6 shows the result of running the ConverterApp code in Code Example 12. The color, font, and icon resources in the panel were converted by a ResourceConverter object.

The ResourceMap class provides support for parameterized strings. Code Example 10 uses a parameterized string in its resources but did not use ResourceMap to insert the parameter. The parameterized string is this:

NameEntryPanel.greetingMsg = Hello, %s, this string was injected!

To use this same resource and insert the parameter as you retrieve the NameEntryPanel.greetingMsg text, you must provide an additional argument list to the getString method. In addition to the resource key, you should provide an argument list that contains the data to insert into the parameterized string. Code Example 13 shows code that could replace the original action handler code in Code Example 10. This new code uses the additional convenience functionality of getString to insert a name into the parameterized resource string.

Code Example 13

private void btnGreetActionPerformed(java.awt.event.ActionEvent evt) {
    String personalMsg = resource.getString("NameEntryPanel.greetingMsg", txtName.getText());
    JOptionPane.showMessageDialog(this, personalMsg);
}

Actions

To handle UI events, you will implement an ActionListener object for a specific component. A more powerful form of an ActionListener is an AbstractAction object, which implements the javax.swing.Action interface. The Action interface allows you to define an event handler. In addition, it lets you associate an icon, text, mnemonic, and other visual elements with the event. Using the Action interface, often with an AbstractAction class, you can conveniently put all the relevant visual cues and the event-processing logic for a component in one place.

Another convenient benefit of using Action interfaces is that you can reuse the same action across multiple UI components. GUIs often provide multiple ways to accomplish a task. For example, to increase the font size of a piece of text, an application might provide both a menu and a button interface to perform the same task. The multiple different ways of accomplishing the same task should behave the same way, and you should enable or disable those actions simultaneously across all associated components -- something that Action interfaces will help you do.

Despite the convenience of using the Action interface, action objects have a few problems. First, visual properties should be localized. All too often, developers hardcode the action's visual elements, making localization difficult or impossible. Additionally, manually creating Action objects can be a chore, especially if you use all the visual components that are available.

The Swing Application Framework helps you manage actions in three ways:

  • The framework provides the @Action annotation to mark and name methods that will be used by Action implementations.
  • An ActionManager class creates javax.swing.ActionMap instances for classes that contain @Action methods.
  • The framework supports localization of Action elements such as icons, text, and mnemonics.

The following discussion about Action objects uses a demo application called ActionApp and a panel named ResizeFontPanel. Figure 7 shows the demo UI, which provides the context for the Action class and annotation descriptions.

The @Action Annotation

Define and name your Action objects using the @Action annotation. Mark Action event-handling methods with this annotation. The annotation has several optional elements, including a way to override the default name of the Action object. The default action name is the method name. Code Example 14 shows how to use this annotation with code that increases and decreases the font size for a text component.

Code Example 14

public class ResizeFontPanel extends javax.swing.JPanel {
    ...
    @Action
    public void makeLarger() {
        Font f = txtArea.getFont();
        int size = f.getSize();
        if (size < MAX_FONT_SIZE) {
            size++;
            f = new Font(f.getFontName(), f.getStyle(), size);
            txtArea.setFont(f);
        }
    }

    @Action
    public void makeSmaller() {
        Font f = txtArea.getFont();
        int size = f.getSize();
        if (size > MIN_FONT_SIZE) {
            size--;
            f = new Font(f.getFontName(), f.getStyle(), size);
            txtArea.setFont(f);
        }

    }
    ...
}

Code Example 14 defines two Action methods with default names: makeLarger and makeSmaller. When invoked, the actions reset the font for a text area, which is named txtArea.

The ActionMap class associates actions with UI components by using the component's setAction method. Use the Action object's name. In Code Examples 14 and 15, the names are makeLarger and makeSmaller. The ResizeFontPanel object has a button for both actions. Code Example 15 shows how to bind the Action objects with the components.

Code Example 15

// ctx is the ApplicationContext instance.
ResourceMap resource = ctxt.getResourceMap(ResizeFontPanel.class);
resource.injectComponents(this);
ActionMap map = ctxt.getActionMap(this);

btnMakeLarger.setAction(map.get("makeLarger"));
btnMakeSmaller.setAction(map.get("makeSmaller"));

Notice that the ApplicationContext provides the ActionMap for this class. As a side note, the code also uses the context to inject component resources. An ActionMap instance stores all the Actions that are associated with this class. In this case, only two Action objects exist: makeLarger and makeSmaller. Retrieve the Action objects using the ActionMap instance's get method, providing the Action instance name. Bind the UI component and Action using the component's setAction method.

Action Resources

Code Example 15 shows how to get the ActionMap for the ResizeFontPanel. It also shows how to call setAction on specific components using the Action objects defined by the @Action annotation. But where did the action's icon and text come from in Figure 7? That's also shown in Code Example 15. Take a closer look at the line that retrieves the ActionMap instance for the ResizeFontPanel object:

ActionMap map = ctxt.getActionMap(this);

This single line of code does a lot of work behind the scenes. First, as described earlier, it creates Action instances for each method marked with an @Action annotation in the target object. Second, it retrieves resources for the Action from the target's ResourceMap. The ResourceMap for this ResizeFontPanel class includes properties from the resources/ResizeFontPanel.properties file. This file defines a few resource key-value pairs and includes the data as shown in Code Example 16. The makeLarger and makeSmaller actions have text and icons defined by this resource file. The getActionMap method includes these resources when it creates the ActionMap for the ResourceFontPanel object.

Code Example 16

# resources/ResizeFontPanel.properties
makeLarger.Action.text = Increase font size
makeLarger.Action.icon = increase.png

makeSmaller.Action.text = Decrease font size
makeSmaller.Action.icon = decrease.png

Because an Action object's resources exist in a ResourceMap, you can localize the resources as appropriate for a specific operating system (OS) platform or locale. Follow the usual naming standards for creating alternate ResourceBundle files. You can find out more about localization and ResourceBundle naming by consulting the ResourceBundle documentation .

Tasks

Always use a background thread for input/output (I/O) bound or computationally intensive tasks. Long-running tasks can block the event dispatch thread (EDT). The new Swing Application Framework provides support for starting, stopping, and monitoring the progress of background tasks. Although the SwingWorker class in the Java SE 6 platform has most of what's needed, the framework provides a Task class to support more functionality.

The Task class extends a SwingWorker implementation, which is similar to the SwingWorker available in Java SE 6. A TaskService class helps you execute tasks, and a TaskMonitor helps you monitor their progress.

The easiest way to use the Task class is to extend it, override one or more methods, and use the Task with an Action handler. You can can monitor the task and get intermediate result information as the task runs. This article will describe only the basic usage associated with event handling.

Code Example 17 creates a Task class. The NetworkTimeRetriever class overrides the doInBackground method, which is the method that performs your long-running task. The Task class defines several methods that communicate the success -- or failure -- of the completed task. One of those methods is the succeeded method. The Task superclass calls succeeded when the doInBackground method finishes its work. The succeeded method runs on the EDT, so you can use it to update GUI components when your task completes successfully. The NetWorkTimeRetriever is an inner class, so it has access to fields in the NetworkTimeApp class. The important field that it needs is the txtShowTime variable, which is a JTextField instance. The Task completes its duties when it finally displays the current time provided by the National Institute of Standards and Technology (NIST) time servers.

Code Example 17

public class NetworkTimeApp extends SingleFrameApplication {
    JPanel timePanel;
    JButton btnShowTime;
    JTextField txtShowTime;

    @Override
    protected void startup() {
        // Create components and so on.
        // ...

        // Retrieve and set Actions.
        ActionMap map = getContext().getActionMap(this);
        javax.swing.Action action = map.get("retrieveTime");
        btnShowTime.setAction(action);
        timePanel.add(btnShowTime);
        timePanel.add(txtShowTime);
        show(timePanel);
    }

    @Action
    public Task retrieveTime() {
        Task task = new NetworkTimeRetriever(this);
        return task;
    }
    // ...

    class NetworkTimeRetriever extends Task<Date, Void> {

        public NetworkTimeRetriever(Application app) {
            super(app);
        }

        @Override
        protected Date doInBackground() throws Exception {
            URL nistServer = new URL("http://time.nist.gov:13");
            InputStream is = nistServer.openStream();
            int ch = is.read();
            StringBuffer dateInput = new StringBuffer();;
            while(ch != -1) {
                dateInput.append((char)ch);
                ch = is.read();
            }
            String strDate = dateInput.substring(7, 24);
            DateFormat dateFormat = DateFormat.getDateTimeInstance();
            SimpleDateFormat sdf = (SimpleDateFormat)dateFormat;
            sdf.applyPattern("yy-MM-dd HH:mm:ss");
            sdf.setTimeZone(TimeZone.getTimeZone("GMT-00:00"));
            Date now = dateFormat.parse(strDate);
            return now;
        }

        @Override
        protected void succeeded(Date time) {
            txtShowTime.setText(time.toString());
        }
    }
}

In Code Example 17, a button prompts the user to retrieve the time from a network server. The button event handler has the @Action annotation, marking it as an Action handler in its class. The action has the name retrieveTime, and the application's ActionMap stores it. After you retrieve the Action and set the button's Action property, the button will contain the resources associated with it too. For example, the btnShowTime component has an icon and some text that are referenced in a ResourceBundle for the application. When you click on the button, its action handler method retrievetime will execute.

Notice that the retrieveTime method returns a Task. The method creates a NetworkTimeRetriever task and returns that object. The framework automatically runs it as a background thread. When the task completes successfully, it updates the UI with the network time. The combination of tasks, actions, and resource injection is a powerful and simple way to handle component event handling.

Figure 8 shows the running NetworkTimeApp application. This application is similar to the previous ShowTimeApp application. The primary difference is that the NetworkTimeApp class retrieves its time from a network time server. As a result, the "Retrieve time!" button must execute a long-running task as a background thread.

Session State

The Swing Application Framework provides a way to save session state when your application exits and restore the state when you restart. Session state is the graphical window configuration of your application. This state includes window size, internal frame locations, selected tabs, column widths, and other graphical properties. Saving the GUI session state allows you to restart the application and return to the same window or form at a later time.

The SingleFrameApplication class implements session storage for you. When you restart any application subclass of SingleFrameApplication, all GUI geometry should be restored exactly as it was when you exited the application.

If you use the Application class and want to store the session state, you must save and restore the state yourself. Fortunately, this is not difficult. Like most functionality in the framework, session storage management starts with the ApplicationContext class. Use the ApplicationContext and SessionStorage classes in the application startup method to restore state. Use these classes in the shutdown method to save state. Code Example 18 shows how to save and restore session state using the SessionStorage class:

Code Example 18

// ...
String sessionFile = "sessionState.xml";
ApplicationContext ctx = getContext();
JFrame mainFrame = getMainFrame();

@Override protected void startup() {
  //...
  /* Restore the session state for the main frame's component tree.
   */
  try {
    ctxt.getSessionStorage().restore(mainFrame, sessionFile);
  }
  catch (IOException e) {
    logger.log(Level.WARNING, "couldn't restore session", e);
  }
  // ...
}

@Override protected void shutdown() {
  /* Save the session state for the main frame's component tree.
   */
  try {
    ctxt.getSessionStorage().save(mainFrame, sessionFile);
  }
  catch (IOException e) {
    logger.log(Level.WARNING, "couldn't save session", e);
  }
  // ...

Note that applications will not usually need to create their own SessionStorage instance. Instead, you should use the ApplicationContext object's shared storage:

SessionStorage ss = ctxt.getSessionStorage();

Local Storage

Session storage depends on the framework class LocalStorage. The LocalStorage class helps the SessionStorage class do its work, but it can also help you to store simple XML-encoded representations of any JavaBean component. The LocalStorage class uses the XMLEncoder and XMLDecoder classes to encode and decode XML files using your application's objects.

Again, the ApplicationContext provides access to a shared LocalStorage instance. You should retrieve the LocalStorage instance like this:

LocalStorage ls = ctxt.getLocalStorage();

Now you can use the object's save and load methods to encode and decode objects to your local storage. The LocalStorage class uses your home directory as a base subdirectory for determining the default location of storage files.

Code Example 19 shows how to use the LocalStorage class to store a list of phone numbers named phonelist.xml. In this example, a JList component model is both loaded and saved with an application context's shared LocalStorage instance. The variable file contains the file name that will contain the list's contents. You can see the entire program listing for this and all other code examples in the demo source code, which is provided as a downloadable link at the end of this article.

Code Example 19

@Action
public void loadMap() throws IOException {
    Object map = ctxt.getLocalStorage().load(file);
    listModel.setMap((LinkedHashMap<String, String>)map);
    showFileMessage("loadedFile", file);
}

@Action
public void saveMap() throws IOException {
    LinkedHashMap<String, String> map = listModel.getMap();
    ctxt.getLocalStorage().save(map, file);
    showFileMessage("savedFile", file);
}

Figure 9 shows the My Little Black Book application. Notice the tool tip and labels. All these resource items are in an application resource file. All the buttons and text fields have associated actions that provide labels, tips, and event handling.

The text field at the bottom of the application window shows the file name of the phone list. Files created by the LocalStorage class always exist in platform-specific locations on your host system. The files are, however, always stored under the application user's home directory.

Summary

The Swing Application Framework (JSR 296) provides a basic architecture and a set of commonly used services for Swing applications. Most applications must implement and manage lifecycle events, UI component event handling, threading, localizable resources, and simple persistence. The framework provides services for managing all these common needs, allowing you to concentrate on your application's unique functionality.

Framework architecture includes Application and ApplicationContext classes. The ApplicationContext instance will provide help for session and local storage, task management, resource management, and most other services that the framework provides.

Your framework application has a well-defined life cycle. Each lifecycle stage has a corresponding method that your application can override to provide its unique functionality.

Framework applications have support for application- and class-level resources. Using the ResourceManager and ResourceMap classes, you can automatically inject those resources into your application, making them easy to localize.

The @Action annotation helps you create UI component event handlers. Using this annotation and ResourceMap objects, you can combine an action's functionality and visual elements in a single place, making the resulting Action object reusable and localizable.

Some event handlers should run as background threads. Use the Task class to define background threads. The framework makes it easy to combine Action and Task instances to make your GUI run smoothly and reliably with long-running event handlers.

Finally, the framework provides support for session state and local storage. Session state includes component geometry, screen location, selected tabs, column widths, and other UI state. You can save session state before the application exits, and you can restore the same state when the application starts again. Additionally, the framework gives you a simple storage API that lets you store local objects on your host platform.

The Swing Application Framework is a work in progress. Expect some changes as the JSR 296 expert group revises both the specification and reference implementation before its final release. The expert group hopes to include the framework in a future release of the Java platform.

For More Information