User interfaces for object-oriented systems, Part 3

The incredible transmogrifying widget

1 2 3 Page 2
Page 2 of 3

The implementation in Listing 2 is, as usual, a straightforward translation of the design. The methods that pass through to the contained Collection start with add(...) (Listing 2, line 96). The methods that modify the Collection's state come first. All that they do, other than chain through to the equivalent method of the contained Collection, is call changed(...) (Listing 2, line 206), which notifies any existing proxies that the data they're displaying has changed. This way they can update themselves dynamically.

The Collection methods that don't modify the state of the underlying Collection object start at Listing 2, line 120. These methods literally do nothing but call their counterparts in the contained object. The designate_top(...) method (Listing 2, line 173) lets you (or the proxy) designate a new "top" item. It takes care of notifying all the action listeners, and it also updates the proxies -- in the for loop on line 189. This loop modifies the state of a visible Swing object. But, as I discussed back in February, Swing is not thread safe. Since the Bag might be modified by a random thread, it's important to play by the rules.

With this in mind, the proper way to instruct a Swing object to modify itself is to post a request -- in the form of a Runnable Command object -- on the Swing event thread by calling SwingUtilites.invokeLater. Swing will eventually process this request synchronously. (This aspect of Swing is an example of the Active-Object design pattern that I discussed in June.)

You should note, by the way, that the Bag is thread safe with respect to internal communications with Swing, but it's not externally thread safe for the same reason that the Java.util Collection classes aren't thread safe: I didn't want to incur the overhead of synchronization unnecessarily. It's not safe for two threads to simultaneously call add(), for example. If you want thread safety, you should write a thread-safe wrapper along the lines of the one that's returned from Collections.synchronizedCollection(). (This method returns a Decorator object that implements synchronized versions of the Collection methods that do nothing but call the similarly named methods in the decorated object.)

The changed(...) method (Listing 2, line 206) notifies the proxies of changes in size of the underlying Collection. It also uses invokeLater() to get Swing to process requests synchronously.

The top() item

Bag extends Collection conceptually in only one way: by introducing the notion of an item at the top of the bag -- the one you'll reach first when you stick your hand in. The top() item is the item the user selected via the list or combo box displayed by the Proxy. You can access the item via the top (Listing 2, line 65) method. (Yea I know, it reeks of being a get function, but it's the nature of a Bag to be a container of data. That is, accessing the top element is part of the problem-domain description of what a Bag does. At least you can't modify the element through top().) The Bag also supports the registration of ActionListeners, which are notified whenever a user, working through a proxy, selects a new top item.

The visual proxy

visual_proxy(...) (Listing 2, line 240) creates the visual proxy. Besides instantiating the proxy, it sets up an AncestorListener that adds or removes the proxy object from the proxy list maintained by the Bag. The Proxy object is added to the list when it's added to its container, and removed from the list when it's removed from the container (or the container shuts down).

This list manipulation is essential because the reference on the list of proxies will prevent the Proxy object from being garbage collected, even when it's not displayed. Note that, in the case of a container window that's simply hidden, rather than destroyed, the container window itself will keep a reference to the Proxy object, thereby preventing it from being destroyed.

The Proxy class itself comes next (Listing 2, line 275). As you can see, it creates the JComboBox, JList, and JButton. And it installs a common renderer and model into them. It also sets up an ActionListener on the button to get the popup to appear on cue, and a ComponentListener on itself (on line 348) to perhaps swap user interfaces when the component changes size.

The UI is actually installed by install_ui() (Listing 2, line 365), which checks the panel size against the preferred size of the combo box, and installs the correct UI accordingly. The combo box is used if there's enough width and if the height of the panel is less than three times the preferred height of the combo box.

If the panel is smaller, the button is used, if it's larger, the list is used. Nothing at all happens if the correct UI delegate has already been installed.

The changed(...) method (Listing 2, line 413) makes sure that the indexes on the various controls are set correctly when a new top item is selected. The rest of the proxy methods are self explanatory.

The Model and Renderer

The Model (Listing 2, line 461) and Renderer (Listing 2, line 498) classes come next. They are straightforward implementations, much like the examples you'll find in any decent Swing book. Notice that the changed(...) method (Listing 2, line 483) must fire off an event to the UI delegate to tell it that the model's state has changed. This changed() method is called indirectly by the changed() method we looked at earlier (in the Bag's add() method, for example), which is called when the Collection changes state.

The only interesting part of the Renderer class is the code on lines 522 to 534, where the component whose paint() method is called to render the cell is actually created. If the Collection member implements User_interface, then the visual proxy for that object renders the cell; otherwise, it creates a JLabel to hold the string returned by the Collection element's toString() method.


1 2 3 Page 2
Page 2 of 3