User interfaces for object-oriented systems, Part 3

The incredible transmogrifying widget

This month's Java Toolbox continues the object-oriented design/GUI theme by looking at a Collection wrapper that makes it easy to present a visual proxy, as I described in the September Java Toolbox. The wrapper, called a Bag, works just like a Collection -- it implements the java.util.Collection interface. It can, however, create a visual proxy for itself when asked.

The associated proxy can display the list of elements as a list; a button that, when pressed, pops up a frame containing the list; or a combo box. The display strategy is automatic, controlled by the size of the proxy. The list is used if there's enough space, the button is used when space is tight, and the combo box is used in an in-between situation. Moreover, several such proxies can simultaneously display the same underlying Collection (presumably, on different screens). When the Collection is changed at the model level -- by someone adding or removing a member, for example -- all the proxies will redraw themselves to reflect the change. The notion of selection is supported: When a user selects an element from the UI, a model-level object that's registered as a listener will receive notification. If several proxies are displayed, they can stay in synch with each other. If you select from one, the selections in the others will change as well. (This behavior isn't required, however.)

You may want to download the code from my Web site (see Resources) and play with this thing a bit to see how it works. Figure 1 illustrates three visual proxies simultaneously displaying the same underlying Collection. The proxy on the left is large enough to have represented itself as a list. The one at the top is a bit smaller, so it has represented itself as a combo box, while the one in the bottom right is smaller still, so it has represented itself as button, which when pressed pops up the frame window that's shown. (The button is disabled as long as the frame window is visible; it's re-enabled automatically when you close the frame.) If you resize any of these windows, they'll transmogrify into one or the other of these three forms as they get larger or smaller. Moreover, if you select a line in any of them, the other two will change to reflect that selection.

Figure 1. The Bag's visual proxy in action

This sort of dynamic adaptability is essential when implementing user interfaces using the architecture that I described in the July and September columns (which you should read before proceeding -- I assume that you're familiar with the architecture I presented in those articles). In this architecture, a control layer builds a presentation from visual proxies of attributes that are provided by an abstraction-layer object. The abstraction layer cannot know the context in which the proxy will be asked to display itself, so the proxy must accommodate whatever space is available automatically.

If the proxy wasn't adaptive in this way, the coupling between the abstraction and presentation layers would be too tight. In an HR application, for example, an employee's identity attribute might want to display a name, an employee ID, and a photograph. Moreover, it will indeed display all three items when there's enough space. If space is tight, the proxy might display itself as a button with the employee's name as the label. If pressed, the button will throw up a frame with the additional information. Adaptability is the key. In any event, all well-written Java applications should make no assumptions about the display environment. The same application that's runs on the 20-inch monitor on your desktop might also have to run on a Palm Pilot at some juncture.

Unfortunately, most of Java's built-in widgets don't work in an adaptable way out of the box. We all wish that the font size on a button's label would grow as the button grows (if it's placed in a GridBag or Grid, for example), but that's not how it works. Instead, you usually end up with minuscule text in on a huge gray button or a button that's too small for the label to print in its entirety. On the other hand, since most Swing widgets support the notion of a renderer -- an object whose job is to draw all or part of the widget -- it's relatively easy to build a set of adaptive widgets on top of Swing. That's what I've done here.

Letting the proxy out of the Bag

Before looking at the implementation, let's look at how it's used. Listing 1 shows the test class from the Bag implementation. Notice that a Bag is as easy to use as a Collection. Indeed, it's just a Collection that can produce its own UI when asked.

The main() method (Listing 1, line 26) creates a Collection by wrapping a LinkedList in a Bag, then adds a few elements to the underlying Collection through the Bag. Note that the Bag is a Collection in a real sense, so you can treat it exactly like one without difficulty (or accessed through a Collection reference). The Bag contains the actual data structure, however. (It's passed in as a constructor argument.)

The Bag is an example of the Gang-of-Four Decorator pattern (see Resources). A Decorator adds capabilities to the decorated item without using implementation inheritance (extends). The java.io package includes many examples of the Decorator pattern. The BufferedInputStream, for example, decorates another InputStream by adding buffering. You don't know whether you're dealing with an actual InputStream or a decorated one when you call read(), however. The BufferedInputStream, therefore, adds the ability to buffer input to a raw InputStream. Similarly, a Bag decorates a Collection in order to add the ability to create a visual proxy on demand.

The Collection object is only wrapped -- it's passed the Bag constructor -- and you can access it directly, rather than through the wrapper, if you like. But as is the case with the java.io Decorators, you should use direct access with care. For example, it would be dangerous to directly access an InputStream that was simultaneously accessed via a BufferedInputStream Decorator elsewhere in the program. In the case of a Bag, it's perfectly safe to examine the wrapped Collection -- traverse it with an iterator, look something up in it, and so on -- without going through the Bag wrapper. Don't modify the wrapped Collection directly, however, do it via the Bag wrapper. If you do decide to directly modify the contained Collection (rather than via the Bag object), be aware that none of the exposed proxies will reflect your modifications. On the other hand, you may want such behavior.

As is the case with many Decorators, the Bag supports a few operations over and above the ones defined in the Collection interface. I invoke one of these -- addActionListener() -- on line 44 to add a listener that reports selected items by printing them onto the console window.

A few lines down, I create (and display) three identical visual proxies by calling create_ui(), which I'll examine momentarily. Then I enter a while loop that adds whatever lines you type on the console window to the current Collection. (This text will appear in all three of the proxy windows.)

The create_ui() method at the top of the listing mimics the behavior of the control object: It creates a frame, retrieves a visual proxy from the Bag (which it treats as a generic «displayable» object, as described last month), then sets up a listener to handle the window-closing event on the frame and pops the frame into existence.

 

Listing 1 (/src/com/holub/ui/Bag.java): Testing the Bag
1:   public static class Test
2:      {
3:       public void create_ui( Collection aggregate
)
4:          {
5:              JFrame frame = new JFrame();
6:  
7:              // Get the visual proxy and shove it into the Frame
8:  
9:              User_interface displayable = (User_interface)aggregate;
10:              frame.getContentPane().add(
11:                          displayable.visual_proxy("Attribute", true) );
12:  
13:              // Set up a window-closing handler and pop the frame up.
14:  
15:              frame.addWindowListener
16:                  (   new WindowAdapter()
17:                   {   public void windowClosing(
WindowEvent e )
18:                          {   System.exit(0);
19:                          }
20:                      }
21:                  );
22:              frame.pack();
23:              frame.show();
24:          }
25:  
26:       public static void main( String[] args ) throws Exception
27:          {   
28:              Collection aggregate = new Bag( new LinkedList(), "outer" );
29:              aggregate.add("A");
30:              aggregate.add("B");
31:              aggregate.add("C");
32:              aggregate.add("D");
33:  
34:              // You need to treat it as a Bag (as compared to a generic
35:              // Collection) to install an ActionListener, thus the cast.
36:              // It has to be final because it's referenced by the inner-
37:              // class object. Note that the listener will report all the
38:              // selections associated with displaying the initial UI.
39:              // There will be two such notifications (one for the list
40:              // and one for the drop-down) for each of the three proxies.
41:              // If you don't want this behavior, install the listener
42:              // after the visual proxy has been displayed.
43:  
44:           final Bag the_bag = (Bag) aggregate;
45:              the_bag.addActionListener
46:              (   new ActionListener()
47:               {   public void actionPerformed(
ActionEvent e )
48:                      {   System.out.println( "Selected " + the_bag.top() );
49:                      }
50:                  }
51:              );
52:  
53:              create_ui( aggregate );
54:              create_ui( aggregate );
55:              create_ui( aggregate );
56:  
57:              // Transfer all lines typed on the console to the collection.
58:  
59:              String s;
60:              while( (s = com.holub.tools.Std.in().readLine()) != null )
61:              {   com.holub.tools.Std.out().println( "->" + s );
62:                  aggregate.add( s );
63:              }
64:          }
65:      }
                                       
                                    

The nitty-gritty

Now let's look at the rest of the Bag.java file, in Listing 2. I've sketched out the Bag's static model in Figure 2. Starting with the Bag itself in the upper-left corner, a Bag both is a Collection and contains a Collection. That's one of the things that makes it a Decorator. Bag implements all of the methods of Collection, though the ones that don't modify the state of the contained object are just pass-through methods -- they do nothing but call the method with the same name on the contained Collection object. The Bag also implements the User_interface interface (described in September's column). In this sense it's a Gang-of-Four Adapter -- it makes a Collection appear to be a User_interface, so any "control" object can use it to build its presentation. To continue with the vocabulary I introduced last month, the Bag is an abstraction-layer object; a control-layer object can ask it to produce a presentation-layer object (a visual proxy) by calling visual_proxy().

Looking below the Bag box in Figure 2, the proxy is implemented as a JPanel derivative called Proxy. Proxy is a private inner class of Bag that implements a public interface (JPanel), so the outside world can access only those features of a Proxy that you've defined in this public interface. The other methods shown in the "operations" compartment are for internal communication between the Bag and its proxies.

The Proxy does its work by creating a single Swing model object that can serve as the model for both a Jlist and a JComboBox -- it extends AbstractListModel and implements ComboBoxModel. Since both of these UI Delegates share the same physical model object, they will always stay in synch with each other. The Model class, then, serves as an intermediary between the visual side of things and the actual data, stored in the Bag's contents field. (It's a somewhat degraded case of a Gang-of-Four Mediator.) Note that Model is an inner class of an inner class. The Bag doesn't know or care that it exists, so its definition properly nests inside the Proxy, which does care about it.

The bottom third of Figure 2 shows the actual display objects, which are contained inside the Proxy's panel: a JComboBox called drop_down; a JList called, oddly enough, list; and a JButton called button. All three of these exist at once, but only one is visible. The main job of the Proxy, from the perspective of UI, is to pick the correct one of these three objects to display, based on the size of the containing panel. The button, when clicked, creates and pops up an instance of Popup, which does nothing but provide a free-floating container for the same list object.

 

Figure 2. The Bag's static model

The Collection pass-throughs

Related:
1 2 3 Page 1