Java Solaris Communities My SDN Account Join SDN
 
Core Java Technologies Tech Tips

Using PropertyChangeListener and Using Action in Your UI

 
 
In This Issue

Welcome to the Core Java Technologies Tech Tips for December 2006. Core Java Technologies Tech Tips provide tips and hints for using core Java technologies and APIs provided in the Java 2 Platform, Standard Edition (Java SE).

In this issue provides tips for the following:

» Using PropertyChangeListener
» Using Action in your UI

These tips were developed using Java SE 6. You can download Java SE 6 from the Java SE Downloads page.

Using PropertyChangeListener to Bind Components
By Joshua Marinacci, NetBeans IDE Engineer  

When you build applications, you often need to hook multiple components together so that components do things in response to events in other components. When you build custom components, you may be tempted to build a custom set of listeners as well. This seems like good component etiquette. After all, this is how most of the javax.swing.* components are built. Still, that's a big pain to create new listener types just for observing simple changes. Plus, it tightly couples your classes which can make your code brittle when making changes later. There must be a better way. And there is!

Swing follows the Java Beans pattern for naming properties. Additionally, nearly all properties of Swing components are bound properties. All JComponent subclasses have an addPropertyChangeListener method. Most component properties are already set up to send events when they change. For example, JButton can notify listeners when its background color changes. The JComponent class also has a firePropertyChange method which lets your custom Swing components send their own change events without ever having to create new event classes. The result is a very easy way to hook components together with minimal new code and absolutely no new interfaces or classes.

This tech tip uses a bitmap tile editor example. The following image shows the unfinished user interface (UI).

tile map

The "+" button makes the grid larger. When you press the button, the grid editor panel makes itself bigger. When the grid becomes bigger, a small label should show the grid's new size. A chain of events must be implemented that links your pressing the "+" button, the grid resizing, and the label change. You could create custom events (like GridSizeChangeEvents or something equally horrendous), but that's a lot of work for what boils down to a simple "update yourself" request. Instead, consider how this would work with a PropertyChangeListener.

The editor is a custom JPanel with a setGridWidth method. Each time the grid width changes, the panel rebuilds the internal grid data and then fires a "gridWidth" property change event about it. The following code sample shows part of the editor panel:

public class TileBuilderEditorPanel extends JPanel {
    ...
    public void setGridWidth(int gridWidth) {
        this.oldGridWidth = this.gridWidth;
        this.gridWidth = gridWidth;
        rebuildGrid();
    }
    private void rebuildGrid() {
        int[][] newgrid = new int[gridWidth][gridHeight];
        ...
        this.gridcells = newgrid;
        repaint();
        firePropertyChange("gridWidth", oldGridWidth, gridWidth);
    }
}

Back in the main class, you need to update the label whenever the grid size changes. This is simple with an anonymous listener class like this:

editor.addPropertyChangeListener("gridWidth", new PropertyChangeListener() {
    public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
       viewer.repaint();
       grid_size.setText(realeditor.getGridWidth() + " x "
                         + realeditor.getGridHeight());
    }
});

Notice that the addPropertyChangeListener method takes an argument to specify the property to listen for. By telling the component that you want "gridWidth" events, you don't have to check for the specific property name inside of the listener. Also, for this example, the listener doesn't even need to know the "gridWidth" value, so it doesn't need to access any data inside the PropertyChangeEvent object. However, the data is there if you ever need it.

The next step is to create an action listener for the "+" button that controls the grid width. For this example, this button is called the "width_plus" button. The listener should make the grid width one cell bigger whenever the width_plus button is pressed. The following code shows the event handler for width_plus button events.

private void width_plusActionPerformed(java.awt.event.ActionEvent evt) {
// TODO add your handling code here:
    editor.setGridWidth(realeditor.getGridWidth()+1);
}

Many integrated development tools, like NetBeans IDE 5.5, can create and attach event handlers for you. NetBeans created the width_plusActionPerformed method stub and then attached it to the actual width_plus button behind the scenes, which means less code to write.

When you press the width_plus button, its event handler will call the editor.setGridwidth method, which will update the internal data structure, repaint itself, then fire a gridWidt property event. An anonymous listener will look for the event and then update the label. It's that simple!

For more information on the PropertyChangeListener class, read these:

Using Action in Your UI
By Scott Violet, Swing Architect  

An ActionListener object encapsulates application specific functionality that is usually triggered by a user interface gesture (this isn't always true, javax.swing.Timer also uses ActionListener). For example clicking the back button in a browser might notify an implementation of ActionListener that is responsible for navigating to the previous page. This might look something like:



private class GoBackActionListener implements ActionListener {
  public void actionPerformed(ActionEvent e) {
    browserComponent.show(getLastURL());
  }
}
backButton.addActionListener(new GoBackActionListener());
backButton.setIcon(...);
backButton.setEnabled(false);

If you've written any Swing/AWT applications, you've probably have code like this. It's simple, and it works.

Consider what happens if you want to conditionally enable the button based on whether you can go back a page. In this case you need a handle to the backButton object so that you can invoke its setEnabled method. If you also have a menu item with the same functionality you'll need to keep a handle to that menu item or any other user interface component that needs to use the GoBackActionListener functionality. This is a bit painful. In particular it leads to all sorts of references back to the hosting components in your application.

The following code shows configuring a menu item and wiring it to a GoBackActionListener:

backMenuItem.addActionListener(new GoBackActionListener()); backMenuItem.addActionListener(new GoBackActionListener());
backMenuItem.setText(...);
backMenuItem.setMnemonic(...);
backMenuItem.setDisplayedMnemonicIndex(...);
backMenuItem.setAccelerator(...);
backMenuItem.setEnabled(false);

The purpose of the Action interface is to solve this problem. Action not only encapsulates the functionality part of an ActionListener, but also includes a description of that functionality that's appropriate for user interface components. Included in that description is information about whether the user interface component should be enabled. When the enabled state of the Action changes the user interface components that listen to the Action will update appropriately. If the previous examples were replaced with Action you would change the enabled state of the Action, and as a consequence the button and menu item would update appropriately. The code for this could be something like:

private class GoBackAction extends AbstractAction {
  public GoBackAction() {
    putValue(Action.NAME, "...");
    putValue(Action.LARGE_ICON_KEY, ...);
    putValue(Action.SMALL_ICON, ...);
    putValue(Action.MNEMONIC_KEY, ...);
    putValue(Action.DISPLAYED_MNEMONIC_INDEX_KEY, ...);
    putValue(Action.ACCELERATOR_KEY, ...);
    putValue(Action.SHORT_DESCRIPTION, ...);
    setEnabled(false);
  }
  public void actionPerformed(ActionEvent e) {
    browserComponent.show(getLastURL());
  }
}
Action goAction = new GoBackAction();
backButton.setHideActionText(true);
backButton.setAction(goAction);
backMenuItem.setAction(goAction);

You'll notice a number of differences with this example and the earlier ones. In particular, rather than configuring the text, icon and other properties of the user interface component, the GoBackAction contains all this information. When you attach GoBackAction to the user interface component, the user interface component configures its state from the Action. A JButton component, for example, will get its icon, text, mnemonic, mnemonic index, tool-tip text, action command key and enabled state from the Action object. This is nice in that the go back event description is encapsulated in a single place and does not need to be replicated to all the hosting components.

Those that haven't followed the latest changes in Java SE 6 will notice some new keys and methods. In particular DISPLAYED_MNEMONIC_INDEX_KEY, LARGE_ICON_KEY values and the setHideActionText method are all new in Java SE 6. Not shown in this example, SELECTED_KEY is also new. Included in the Java SE 6 release are centralized descriptions of what properties each component supports. See the class-level documentation of Action for all the specifics.

When using ActionListener, you have to explicitly update the enabled state of each user interface component that generates the event like this:

backButton.setEnabled(...);
backMenuItem.setEnabled(...);

Using the Action class, the code becomes:

goBackAction.setEnabled(...);

Any user interface components attached to the Action will update appropriately whenever the Action changes. Now you just reference the Action and not all the various components.

The ability to change the Action directly and to have its status reflected appropriately in user interface components comes in very handy in a number of situations. For example, undo/redo menu items often have their text reflect what the undo/redo operation will do. For example, the text 'Undo Paste' or 'Undo Edit' might label the undo/redo menu items. If you change the NAME property of the Action, the menu item and all other components that use the Action will change appropriately.

For more information about the Action class, read these:

Rate and Review
Tell us what you think of the content of this page.
Excellent   Good   Fair   Poor  
Comments:
Your email address (no reply is possible without an address):
Sun Privacy Policy

Note: We are not able to respond to all submitted comments.