Build user interfaces for object-oriented systems, Part 2: The visual-proxy architecture

A scalable architecture for building object-oriented user interfaces

1 2 3 4 5 6 7 Page 4
Page 4 of 7

The final piece of the UI-assemblage puzzle is the form-creation process. An Employee object creates the form in its constructor. That is, the Employee object initializes itself. This strategy -- of self-initialization by interacting with a user -- is commonplace in object-oriented systems. Don't create a dialog to get the information that you need to call a constructor; instead, have the constructor get the information itself. This way you can easily modify the initialization form if new fields are added to the implementation.

The current object attaches itself to the form at the top of the constructor (Listing 2, line 66). This operation causes the Form to request proxies for the two attributes and get them installed at the right place in the form. The constructor then calls a "hook" method -- create_initialization_form(...) (Listing 2, line 85) -- that's provided so that derived-class attributes, should they exist, can also be initialized. The derived class would override this method. It would create a form of its own to initialize its own attributes and embed the base-class's Form (passed as an argument) into its own form. The returned value is the derived-class form (which now contains the base-class form). The default implementation just returns its argument.

The Employee constructor now creates a Frame to hold the form and makes it pop up. (I could create a JDialog instead of a JFrame, but I wanted to demonstrate how to do a modeless form.) The final call -- wait_for_close() -- suspends the current thread (the one that just popped up the form) until the Frame is closed by the user. You don't have to wait, however. The modeless form could remain visible indefinitely with the creating thread just using the associated values as needed. Remember that two threads will be active if you don't wait -- the Swing event-processing thread and the "main" thread that created the Employee -- so you'll have to be careful to synchronize properly if you use this second strategy.

Listing 2: Employee.java
01  import javax.swing.*;
02  import javax.swing.text.*;
03  
04  import java.awt.*;
05  import java.awt.event.*;
06  
07  import com.holub.ui.User_interface;
08  import com.holub.ui.AncestorAdapter;
09  import com.holub.ui.Form;
10  import com.holub.tools.Std;
11  
12  public class Employee implements User_interface
13  {
14    private static Form form = new Form();
15      static
16      {   form.add_field( new JLabel  ("Name:"),
17                            new Rectangle ( 0,  0,  75,  20 ) );
18                                      //    x   y  width height
19  
20        form.add_field( "Employee", "name",
21                            new Rectangle ( 75, 0,  200, 20 ) );
22  
23          form.add_field( new JLabel  ("Salary:"),
24                            new Rectangle ( 0,  30, 75,  20 ) );
25  
26          form.add_field( "Employee", "salary",
27                            new Rectangle ( 75, 30, 200, 20 ) );
28      }
28      //------------------------------------------------------------
20  
31    private double   salary = 1000000000.0;     // works for Microsoft
32    private Document name   = new PlainDocument();
33  
34      //------------------------------------------------------------
35    public JComponent visual_proxy( String attribute )
36      {   JComponent proxy = null;
37  
38          if( attribute.equals("salary") )
39          {   proxy = new JLabel( "" + salary );
40          }
41          else if( attribute.equals("name") )
42        {   proxy = new JTextField( name, null, 0 );
43          }
44          else
45          {   Std.err().println("Illegal attribute requested");
46          }
47  
48          return proxy;
49      }
50      //------------------------------------------------------------
51  
52    public String toString()
53      {   try
54          {   String name_value =   name.getLength() == 0
55                                  ? "n/a"
56                                  : name.getText(0,name.getLength())
57                                  ;
58  
59              return "name=" + name_value + ", salary=" + salary;
60          }
61          catch( BadLocationException e ) // shouldn't happen
62          {   throw new Error("Internal error, bad location in Employee name");
63          }
64      }
65  
66    public Employee()
67      {   form.attach(this);  // attach attributes of current object to
68                              // the form.
69  
70          // could be a JDialog rather than a JFrame if you want it to
71          // be modal.
72  
73          JComponent complete_form = create_initialization_form( form );
74  
75          JFrame frame = new JFrame("New Employee");
76          frame.getContentPane().add( complete_form );
77          frame.pack();
78          frame.show();
79  
80          // Wait for the form to shut down:
81  
82          form.wait_for_close();
83      }
84  
85    protected JComponent create_initialization_form( JComponent base_class_attributes )
86      {   return base_class_attributes;
87      }
88  
89      //------------------------------------------------------------
90    public static void main( String[] args ) throws Exception
91      {   Employee bill = new Employee();
92          Std.out().println( bill.toString() );
93          System.exit(0);
94      }
95  }

The Form class is implemented in Listing 4, and the implementation is a straightforward translation of the model that I presented earlier. Let's look at the implementation details, however. The "aggregation" relationship of a Form to its fields is translated into the fields collection (Listing 4, line 24). I've implemented the aggregation relationship as a Vector, but because the reference is to a generic Collection, I can replace the Vector with a different data structure at in the future without impacting the code that uses the Collection reference. (I suppose that the form could organize its Element objects in a hashtable, keyed by class and containing Vectors of Elements that represented attributes of that class. Because most forms have only a few fields on them, the minimal performance improvement that you'd get with this approach didn't seem worth the effort.)

The next batch of variables and methods are concerned with preferred size. INSET (Listing 4, line 30) defines the space around the outside edge of the form. I'm implementing this by hand rather than using an EmptyBorder object to simplify the current code. The preferred size of the form is stored in preferred_size (Listing 4, line 31), and is accessed by the layout manager in whatever window contains the form via the getPreferredSize() and getMinimiumSize overrides on the next couple lines. The update_preferred_size(...) method (Listing 4, line 36) is called internally whenever a new field is added to the form in order to update the preferred size. It's passed the location rectangle of the newly added field, and updates the size as necessary.

The Form's constructor (Listing 4, line 45) sets up an AncestorListener that will be notified when a component's ancestors (for example, the Frame that holds the Form object) are resized or shut down. In the current case, I'm interested in finding out when I'm being shut down so I can release any threads that are waiting in a wait_for_close() call. The wait_for_close() method encapsulates a wait(); the release() (Listing 4, line 120) method encapsulates a notifyAll().

Ancestor listeners were added to Swing rather late in the development cycle, and unfortunately there's no equivalent AncestorAdapter, so I've invented one in Listing 3.

Listing 3: AncestorAdapter.java
01 package com.holub.ui;
02 
03 import javax.swing.event.*;
04 

/ ****************************************

Corrects a flaw in Swing, provides an AncestorListener implementation made up of empty methods.

/

05  public class AncestorAdapter implements AncestorListener
06  {   public void ancestorAdded   ( AncestorEvent event ){}
07    public void ancestorMoved   ( AncestorEvent event ){}
08    public void ancestorRemoved ( AncestorEvent event ){}
09  }

The various attach methods come next. They all chain to the first one, attach(User_interface) (Listing 4, line 77), which gets an iterator across the fields, asking each field in turn to attach the newly added object. The associated attach call in the Element's inner class (attach(...)) goes through the dynamic-model scenario I discussed earlier, getting the visual proxy from the object if its class name matches the one stored in the Element. The proxy is added to the form at this juncture as well.

The final implementation issue of interest to us is the doLayout() override (Listing 4, line 104), which is called by Swing to actually lay out the form. The implementation simply goes through the list of fields, activating each one in turn. The actual activation is done in the Element by activate() (Listing 4, line 182), which calls setBounds(location) and setVisible(true) to get the previously attached proxy to pop up in the right place.

As I mentioned earlier, you could easily implement this system as a layout manager, but I opted not to in order to clarify the architecture.

One gotcha

I sidestepped the main Form-related "gotcha" in the current example by not using any listeners to update the proxy. The main issue is unwanted references. When either the proxy or the model-side object registers itself as a listener to the other, there's a possibility that the implicit reference from the "publisher" to the "subscriber" object (for example, from a Component to that Component's listeners) will prevent the "subscriber" from being garbage-collected, even thought it's not used for anything.

Typically this problem doesn't come up when the proxy is notifying an abstraction-layer object because the form on which the proxy sits (and typically, the proxy itself) are more short-lived than the objects in the abstraction layer. You could have a problem if the proxy registers itself as a listener to the abstraction-layer object (so that it could automatically update itself when the model's state changes, for example). In this case, the proxy will continue to exist until that abstraction-layer object shuts down, unless the proxy removes itself from the model's listener list when the form shuts down.

This sort of object, which is hanging around but won't be used for anything, is an example of a "loiterer" -- a kind of memory leak. (Who says that garbage-collected languages can't have memory leaks?) The only reason the problem doesn't come up in the current example is that the Document object used to hold the state of the name attribute's proxy doesn't have any references to the TextComponent to which it's attached. That is, there's no setText() method in AbstractDocument for a reason. Simply changing the model's state won't update the associated UI Delegate. If the employee wanted to dynamically update the name, for example, it would pass a setText() method to the proxy, which in turn would modify the underlying PlainDocument object.

A good solution to this problem is to use the AncestorListener mechanism of the JComponent that I discussed earlier as a hook for removing itself from any publishers to which it has subscribed. The Form itself uses this mechanism in its constructor (Listing 4, line 45) to notify any waiting threads when the form shuts down.

Listing 4: Form.java

001  package com.holub.ui;
002  
003  import javax.swing.*;
004  import javax.swing.event.*;
005  import java.awt.*;
006  import java.util.*;
007  import java.io.*;
008  
009  import com.holub.ui.User_interface;
010  import com.holub.ui.AncestorAdapter;
011  import com.holub.tools.debug.Assert;
012  import com.holub.tools.debug.D;
013  

/ ****************************************

The Form class builds simple forms in such a way that the form layout and contents can be changed without impacting the underlying logical model (and vice versa). A fixed layout is used (Yeah, I know. Live with it.)

/

014  public class Form extends JComponent
015  {
016      /* Elements are just stored in a vector. Another possibility is to
017       * store them several small vectors, placed in a hash table,
018       * keyed by class name (or Class object). This way
019       * I wouldn't have to do a linear search on the fields to find
020       * the attributes of a particular object when its attached.
021       * For the time being, a simple vector will do.
022       */
023  
024    private final Collection fields      = new Vector();
025  
026      /* Stuff to keep track of the preferred size, which is updated
027       * every time a field is added.
028       */
029  
030    private static final int INSET = 10;
031    private final Dimension preferred_size = new Dimension();;
032  
033    public Dimension getPreferredSize(){ return preferred_size; }
034    public Dimension getMinimumSize  (){ return preferred_size; }
035  
036    private final void update_preferred_size( Rectangle location )
037      {   preferred_size.setSize
038          (   Math.max( preferred_size.width,  
039                        location.x + location.width  + 2*INSET  ),
040              Math.max( preferred_size.height,
041                        location.y + location.height + 2*INSET )
042          );
043      }
044  

/ ****************************************

One visual element of a form. Specifies where on the form the UI for a particular attribute of a particular class should be placed. Note that the attribute is specified by class, not object, so multiple objects of the same class cannot be represented directly. You can represent such a one-to-many mapping by writing a class to represent the aggregate object, however.

/

045    public Form()
046      {   addAncestorListener
047          (   new AncestorAdapter()
048            {   public void ancestorRemoved(AncestorEvent event)
049                  {   release();
050                 }
051             }
052         );
053     }
054 
Related:
1 2 3 4 5 6 7 Page 4
Page 4 of 7