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 3
Page 3 of 7

At the risk of sounding like a broken record (does anyone remember vinyl records?): Note that the class and attribute names are stored as Strings, not as references to instantiated objects. Among other things, using strings makes it easy to define a form as an ASCII file or a table in a database. It also makes it really easy to modify the form simply by modifying this ASCII file. No recompilation is necessary. This flexibility is an essential part of decoupling the "view" from the "model" -- you can radically change the layout and composition of the forms without any need to write code. The model, in particular, remains unaffected.

Hooks are provided in the Form class for loading the Elements from a file in the form of the Form's store() and load() methods. These are the only non-final methods in the class definition and at present don't do anything other than throw an exception. You can customize these to support your own form-definition system by deriving a class from Form and overriding the methods. For example, I have a class (which I haven't included in the present article) that implements both methods as follows: The load() method initializes a Form by reading a comma-delimited ASCII file, one line per Element, each line taking the form:

x,y,width,height,class_name,attribute_name

The class name must be fully qualified (with the package name as well as the class name specified). Any class in the javax.swing package can be specified in the class_name field provided that that class has a String constructor. The attribute_name field is used as the argument to that constructor. If the attribute_name starts with a < character and ends with a > character, the attribute_name is assumed to be a URL. If the class_name is javax.swing.ImageIcon, the ImageIcon is created but is wrapped inside a JLabel.

For example, the input line

10,20,100,200,javax.swing.JButton, Hello World

is treated as

new Element( new Rectangle(x,y,width,height),
             new JButton( "Hello world" )  );

An icon is created with the input line

10,20,100,200,javax.swing.ImageIcon, foo.gif

which is treated as

new Element( new Rectangle(x,y,width,height),
             new JLabel( new ImageIcon("foo.gif") ) );

and

10,20,100,200,javax.swing.ImageIcon, <http://www.holub.com/images/mooney.jpg>

becomes

new Element( new Rectangle(x,y,width,height),
             new JLabel( new ImageIcon(
                    new URL("http://www.holub.com/images/mooney.jpg")
           )));

Of course, a system as primitive as this one is not very sexy, but it's easy to imagine a drag-and-drop visual layout tool that would output an ASCII file such as the one I've described.

Now let's see how these classes work together to get a form displayed on the screen. There are two scenarios of interest in the dynamic model: specifying the form (defining the fields) and activation (attaching runtime objects to the form and causing the form to be displayed). Because "specification" is easy -- you just call one or the other of the add_field() methods I discussed earlier -- I'll look at only the activation scenario here.

The UML dynamic-model diagram in Figure 3 shows the activation process. If you've never seen one of these diagrams before, the columns represent objects in the system at runtime. The horizontal lines indicate messages that are being passed from one object to another (in the direction of the arrow), and time moves down. (Messages toward the top of the diagram are sent before messages that are found lower down.)

Figure 3. PAC Dynamic Model

Some previous scenario has populated the Form object with fields, so our starting point is a fully defined Form -- already populated with Element objects. The activation process starts out by some abstraction-side "control" object passing the previously populated Form an attach(things) message, where things means one or more "displayable" objects -- objects that implement User_interface, that return a visual proxy when asked. The attach() message is a request that the form display various attributes of the object passed as an argument (as compared to whatever object used to be attached to the form). The exact set of attributes is controlled by the individual Element objects that comprise the fields.

Looking back up at the static model, three variants of attach() are supported by Form objects: one for single objects, a second for arrays, and a third that's passed an Iterator that has been initialized to traverse some Collection or Map. I've chosen to use a push model -- in which the objects that are displayed are "pushed" into the Form from outside -- to connect model-side objects to the Form. A pull model -- in which the Form extracts the objects that it needs to display from the model -- is equally viable, but harder to implement in a generic system.

When an object is attached to a Form, the Form turns around and tries to attach it to each of its elements in turn. If the element doesn't represent an attribute of the attached object's class, this request is silently ignored. The Element's version of attach() decides whether or not it can deal with the associated "displayable" object. The Element gets the object's class name, and then, if the class name matches the class name stored inside the Element, the Element requests from the "displayable" object a visual proxy for the attribute (or attributes) listed in the Element's attribute field. Again, note that attributes are requested by passing a string holding an attribute name (or names) into the "displayable" object in order to decouple the description of the element from the implementation as much as possible.

At this point, what happens is up to the "displayable" object. Typically, the "displayable" object would create a JComponent that represents the attribute, optionally hook itself up with the component so it can find out when the component is modified, then return the component. As I mentioned earlier, it could also create a Swing widget, install a local model, then return the widget. (I'll take this latter approach in a following example.) The proxy object that's created by the visual_proxy() request is then stored away inside the Element.

A visual proxy is not requested when the class name doesn't match the name stored in the Element. If the proxy field isn't null and the current Element doesn't represent something that's invariant (a label, icon, and so on), then the newly created proxy is added to the form. (If the object was invariant, the Element(Rectangle,Component) constructor will have added it to the form already).

Once all the "displayable" objects have been passed into the form, the control object now activates the form, typically by sending it a setVisible(true) or invalidate() request. This request is fielded by the Component base class of Form, but eventually a do_layout() is issued by the base class. doLayout() sends activate() messages to each of the Form's elements, which pass setBounds() and setVisible() requests to the associated proxies to get them to display themselves.

So, now the form is visible, and is effectively talking to the underlying "displayable" objects that provided proxies to it. Note, that user input flows directly from the proxy to the "displayable" thing. The surrounding Form object is not involved in this communication. Also, note that none of the actors in this scenario has a clue about how a given attribute is implemented. There are no "get" methods used to get data out of the abstraction-level objects, and no "set" methods are used to modify the state of those objects. Everything is done through the interface provided by the generic Swing JComponent. I can modify a form -- shuffle around its fields, add or remove fields, eliminate or add entire forms -- without having an impact on the abstraction-layer classes. That is, the classes that comprise the "model" don't even know that changes have occurred. By the same token, if I modify the implementation of the underlying model -- even radically -- the forms don't know that anything has happened as long as the model-level objects continue to provide proxies when asked. Moreover, because the proxy is usually implemented as an inner class of the abstraction-layer class, all the changes (both to the implementation and the presentation) for a given class are concentrated in one place. I do not have to search the entire application for all references to a given class to make sure that the code that uses that class still works. Only the names of the attributes and the names of the classes can't change, but in a decent design, these names shouldn't need to change.

For those of you naysayers who say that visual_proxy() is effectively a get request, the proxy really is an example of two Gang of Four design patterns: Proxy, of course, but also Memento. That is, though the proxy is provided by the model-side object, there's no way for anyone to use that proxy to find out anything about the object's implementation. In other words, the encapsulation is still intact in the sense that the implementation of the model-side object can change without the outside world knowing about it. Of course, the attribute set is exposed -- at least the names of the attributes are exposed as strings -- but if the attribute set changes, that means the initial design work was seriously flawed. In any event, adding an attribute is a non-issue in this system, and an object can always throw an exception or return null if asked for a nonexistent attribute, so the error can be detected easily enough.

An implementation

Now let's turn this theory into something concrete with an implementation. First, Listing 1 defines the User_interface class that must be implemented by objects that can create visual proxies. The interface has exactly one method in it, which is passed a String that names the desired attribute. The method returns a JComponent that serves as the proxy. It's assumed that the proxy will have been hooked up to the object that created it (so the two objects can communicate with each other).

Listing 1: User_interface.java
001  package com.holub.ui;
002  
003  import javax.swing.JComponent;
004  import java.awt.Container;
005  

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

The User_interface interface lets you make generic "forms" using a Presentation/Abstraction/Control, visual-proxy architecture. Objects that implement a User_interface return proxies ("views") for attributes of a "business" object when asked. (c) 1999, Allen I. Holub. All rights reserved.

@author Allen Holub

@version 1.1

/

006  public interface User_interface
007  {
/*****************************************

Return a visual proxy for the attribute specified as an argument or null if the requested attribute isn't recognized. All objects that implement User_interface must support a null "attribute" argument, which will return the "default" view, whatever that is. The individual proxies can use the JComponent's addAncestorListener() facility to find out when the surrounding form is displayed or shut down. Similarly, they can override addNotify() to find out who their parent is.

@param attribute_name The attribute that this proxy represents or null for the default type.

@return s

The proxy, or null if the requested attribute is not supported or recognized.

/
008    public JComponent visual_proxy( String attribute_name );
009  }

Listing 2 demonstrates how the User_interface might be implemented (and how the related Form class is used). The static form variable (Listing 2, line 14) references an empty Form object that I'll use to initialize Employee objects. It's static because the same form can be used over and over to initialize multiple objects. The static-initializer block just below the declaration defines the fields in the form. The first call to add_field adds an "invariant" field -- a label holding the string "Name:". Invariant fields will always be the same -- they aren't hooked up to an attribute of an object; look at them as attributes of the form itself. This particular label is in the upper-left corner of the form, and is 75 pixels wide and 20 pixels high. (The form automatically installs a 10-pixel empty border around the inside edge of the form, so you don't have to provide an offset to the fields: a location of (0,0) is automatically translated to (10,10).) Space for the proxy is allocated on line 20 of Listing 2. This call looks much like the previous one, but instead of passing in a JComponent, I'm specifying (in two strings) the name of the class and attribute that will be displayed.

That's it for defining the form. The next piece of the UI-assemblage puzzle is the visual_proxy(...) override (Listing 2, line 35) that's called by the Form object when the form is displayed. As you can see, it recognizes two attributes: "name" and "salary". The proxy for the salary is a read-only attribute (more to demonstrate how to do read-only attributes than because of any practical consideration), so a simple JLabel is returned as the proxy. The "name" attribute is a bit more complex because I wanted the user of the form to be able to modify the name. I've opted for a Swing-based approach, here. The internal value associated with the name attribute is a PlainDocument object, referenced by name (Listing 2, line 32). This is the "model" component required by Swing. The associated "presentation" object, a JTextField is created (and attached to the model) on line 42 of Listing 2. When the user modifies the text field, the associated PlainDocument object will be updated by the system to reflect the characters that the user typed. All we have to do is read it.

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