Object-Oriented Widgets
- By Robb Shecter
- December 15, 1999
A WHILE AGO I had a problem with a program I was writing. I was working on a
dialog for a simple database application with contact information. The dialog was to
display a
List of names from the database, and when a
user clicked on one of the names, detailed information about the person would be shown.
I was using the AWT
List class, and the more I thought
about the problem, the harder it got. When an item got selected, the
List
class would report the first and last name of the person. In that case, my program would not receive
the unique ID of the person—and to retrieve it, I'd have to somehow parse out the user's name
and do a query. In order to make the parsing and query easier, I might have to display the names
in the
List like "
LastName,
FirstName," instead of the way I had originally wanted the user to see
them: "
FirstName LastName." And, what would happen if two
users had the same name? The quick solution that first came to mind was to maintain a parallel
Vector of
Person objects
or
Person ID numbers. The program would add a
Person object to this Vector every time it put his/her name in
the
List. Then, when a name was selected, I could fish out the
real information from the Vector. But this would get messy fast. I would have to make sure that every
piece of code that made changes to the
List would also make changes
to this parallel
Vector. I also didn't like this solution because I'd
seen the results of similar hacks done in very un-object-oriented (OO) C programming.
The true problem was that although the Java class libraries are OO, they only partially support OO
programming. The
AWT List widget
only works with strings, so it's difficult to use it to view and select objects. The
Swing
JTable class can also be difficult to use. The information
you want to display has to be somehow put into a
TableModel and
then plugged into a
JTable. Programming in a truly OO style may
mean implementing several different
TableModels that extract the
information you want to show. Put another way, the problem is similar to the "impedance mismatch" that
occurs when relational databases are used with OO systems; only part of a program works easily and
natively with objects, forcing them to be "flattened out" into scalar values, and then later "unflattened"
back into true objects.
After realizing what my real problem was, I considered how I'd like to use widgets and objects. Here's
what I came up with: It would be cool if I could take a bunch of objects and throw them to a widget,
which would then display them in the best way it could. The widget would let the user select one or
more of the objects. If the program asked the widget for the thing that got selected, it would get
a reference to a real object, not just a description of it. An advanced widget would even let the
user manipulate or edit the objects in some way.
I solved several user interface (UI) problems by extending an AWT and a Swing widget to make them act
more OO. The work I describe here can easily be applied to other UI classes.
Extending the AWT List
The AWT
List class has the following methods (among others) for
adding and retrieving strings:
- addItem(String)
- getSelectedItem()
The AWT
List class would be much more usable if it had a method
like
addObject(Object, String), which would add the given object
to its
List and represent it with the given
String. It could also have a method
addObject(Object) as a convenience that would call
Object.toString() to obtain the String representation.
Finally, it would have
getSelectedObject(), parallel to
the original
getSelectedItem().
Implementing this was fairly easy: I just applied "standard" OO refactoring to my original hack.
If I had kept on using the "parallel
Vector" idea, I would have
ended up with lots of applications, all using instances of
List
and all maintaining their own parallel
Vectors. My solution was
to have a special
List class that maintains its own parallel
Vector internally, eliminating redundant coding. The
Vector would be private, and manipulated when a public method
is called. Subclassing the
List class was the best way to implement
this. My new
OOList class would support the full
List API, allowing it to be used in IDEs or programs where the
old
List is. My new class would simply be a set of wrappers
around the original methods, all very similar to this:
public synchronized void removeAll() (
super.removeAll();
objectList.removeAllElements();
);
Besides creating an OO interface and eliminating redundancy, this code is much safer. The
internal
Vector is guaranteed not to get out of sync
with the
List because clients can access it only through
the public methods. This achieves a key goal of OO design: An object should guarantee the
integrity of its own data. Listings
1
and
2 show two simple classes that
represent typical domain objects.
OOListTest.java in
Listing 3 uses them to demonstrate
how the new
OOList class can be used. Figure 1 shows the results.
Figure 1. OOList
Extending the Swing JTable
Swing brought many improvements to Java. The
JList class, in fact,
allows any object type to be added to its list. This fixes the AWT
List's
String-only problem, when Swing can be used. There are still some areas of Swing, though, that can use
improvement.
I soon found myself in a typical database programmer's situation: I was writing lots of applications where
I needed to display information from a database, let the users change some of it, and then store the
updates. I had already made an OO API for my data storage (which was relational), but making the UIs
was turning into a lot of work. The programs were designed for a biological laboratory. Typically, a
user would want to look at a set of 50 or so samples, each one having 10 properties such as temperature
or quantity. The user could then edit some of the properties and save the modified objects.
The Swing
JTable was the natural choice for displaying this data. The
easiest way to store information in a
JTable is to use the
DefaultTableModel, which accepts a two-dimensional array, or
a
Vector of
Vectors. JavaSoft
doesn't recommend this approach, and sure enough, my needs were too complex for the simple
DefaultTableModel class. When I realized this, I started to
rewrite my code to use the
JTable as JavaSoft intended. I
made custom
TableModel implementations that get plugged into
a
JTable. The easiest way to do this is to subclass
AbstractTableModel and implement the following methods:
- int getColumnCount()
- int getRowCount()
- Object getValueAt(int row, int col)
The
JTable will then call these methods when it needs to. Optionally,
other methods can be overridden in order to let the
JTable change the
table data, as well as display more detailed information. Here are some of the more interesting ones:
- boolean isCellEditable(int row, int col)
- void setValueAt(Object, int row, int col)
- String getColumnName(int index)
- Class getColumnClass(int index)
The table model would know about the particular properties of my domain objects, and let the
JTable edit and view them. This worked better—my code was
cleaner, but I soon ran into another problem. Every dialog I was writing needed to show a different
set of objects, and for each type of object, I found myself making a new
TableModel implementation.
It occurred to me that, once again, I was writing somewhat un-OO code in order to work with UI widgets:
I had classes that dissembled my objects into scalar values, and reassembled them again. It was turning
into a maintenance problem, because changing my domain classes (the actual classes I wanted to work with
like "
Sample") caused me to change the
TableModels that know how to work with them. I started thinking about how I'd like to use
JTables if I could. Instead of giving the
JTable a two-dimensional array of scalar values, it would be better if I could give it a
one-dimensional array of the objects themselves. Objects would be displayed one per row; and the
columns would somehow automatically show the objects' properties. But how would this happen? I realized
that the JavaBean system would be perfect. The new development process would look something like this:
First, a special
TableModel,
OOTableModel, is implemented. This class accepts a one-dimensional array of objects in its
constructor. It analyzes the objects' class using the JavaBean
Introspector to get a
List
of the class'
PropertyDescriptors.
Each
PropertyDescriptor has information about a single JavaBean
property, including:
- A "display name" for the property.
- A "read method" that gives access to the property.
- Possibly a "write method" that sets a new value for the property.
Here's how the basic
TableModel interface is implemented:
- getRowCount()—Return the length of the object array.
- getColumnCount()—Return the length of property List.
- getColumnName(i)—Return the display name of property[i].
- getValueAt(row, col)—Get the read method of property[col], and execute it on object[row].
- setValueAt(object, row, col)—Similar to above, but get and execute the property's write method if there is one.
Finally, this last method gives a
JTable information about whether a
user should be allowed to edit a column:
- isEditable(col)—Get property[i], and return true if it has a write method.
Once the
OOTableModel was completed, I needed to make sure that
my domain classes adhered to the JavaBean standard. This isn't hard to do: For any
property
abc, a class should provide access to it via
methods named
getAbc and
setAbc, if
it's writable. Alternatively,
Beaninfo classes can be generated or
written that give detailed information to the
Introspector.
Animal.java and
Dog.java from
the previous example are basic JavaBeans. Animal has three writable
properties: "
age," "
name,"
and "
healthy."
Dog
subclasses
Animal and adds a read-only property,
"
ageInDogYears."
Figure 2. OOLTable
Finally, using the new classes is very easy.
Listing 4 00JTableTest.java, gives an example. Figure 2 shows the results. Whenever I have a
List of objects that I want to show the user, I instantiate a new
OOTableModel with the object
List, pass it to a
JTable, and display it. The
JTable automatically displays the objects one per line, and their properties one per column. The user can click on the cells and change the values. Properties that don't have a set method will be read-only and the
JTable won't let the user edit them. It's a bit difficult to see in a screenshot, but the
Dog's "ageInDogYears" property is read-only and no edit field appears when the user clicks on it. I also made a subclass of
JTable called
OOJTable as a convenience to save myself the step of separately instantiating the table model.
Notification
The
JTable enhancement described so far is just the beginning of what
can be done. One issue that isn't resolved yet is what happens if the objects change while they're being
displayed. The
TableModel and
JTable classes have a nice notification protocol that's easy to use when subclassing
AbstractTableModel like I have. The
AbstractTableModel has several methods for telling a
JTable that something has changed, such as:
- fireTableDataChanged(...)
- fireTableRowsUpdated(...)
- fireTableCellUpdated(...)
The
JTable can then query the
TableModel for the new values and repaint the changed cells. Figure 3 shows the sequence of
notification actions. A domain object lets the
TableModel know
that it has changed.
Figure 3. Notification.
Another part of the JavaBean specification describes "bound properties." A JavaBean automatically
notifies
Listeners whenever any of these properties are changed.
Using bound properties is like using the
Observer/Observable
classes, except bound properties are more flexible because the
Observable object isn't required to subclass a particular class. Instead, the
PropertyChangeSupport class takes care of most of the work involved.
Listing 5,
Animal2.java, and
Listing 6,
Dog2.java, show the modified JavaBeans, which now have bound properties. All that remained was to change
OOTableModel in two ways. First, it must try to register itself as a property listener of each object it receives. I did this by using reflection and ignoring any exceptions that are thrown. The
TableModel must also be modified to react correctly when it's notified about a change. Its implementation of the
propertyChange(PropertyChangeEvent) method acts like a relay, catching the property change notifications and, in turn, sending out notifications about
TableModel changes.
With these additions, a
JTable will automatically repaint itself
when the data changes.
Benefits of OO Widgets
I discovered that being able to directly display real objects in UI widgets encourages better and more
flexible designs:
- References to many kinds of objects can be passed to the OOTableModel, including persistent database objects, domain objects, report objects, and remote RMI objects. Objects that are cast as abstract classes or interfaces can also be used.
- The system uses standard JavaBean features, which means that most classes don't have to be modified to work with the OOTableModel. Third-party classes can be used without modification or access to their source code.
- The need to spread information about domain objects throughout programs is reduced. Normally, a UI class must know something about the classes it displays. This introduces redundancy, which is a maintenance problem. With OO widgets, objects can be conveniently described just once where they should be—in the class source code. The system then reuses this description at runtime.
- OO design and polymorphism can be used to create interesting effects such as database-like "views" onto the objects. Note the difference between the "Animals" table and the "Dogs" table in Figure 2.
- OOTableModel and OOJTable are consistent with Swing design and can be combined with other Swing features like cell renderers.
Possible Applications
The most obvious use for these new widgets is in easy-to-construct dialogs. Very little extra coding needs
to be done aside from checking whether an "
OK" or
"
Cancel" button was clicked. The application can then proceed using
the objects like it normally would. Moving beyond this, the notification/update feature has some interesting
possibilities. A small system to monitor the state of a running program could be made. Some key objects in
the system would be passed to OO widgets, where a user could observe changes that occur in them over the
lifetime of the program.
All object references are considered equal and a remote reference can be displayed just as easily. This
feature could be used in a distributed application control system. For example, a collection of references
to remote servers could be passed to an OO
JTable, allowing an
administrator to view their status, shut them down, and start them up.
Ideas for the Future
A neat feature of the model-view design is that the pieces can be separated and used in other contexts.
The
OOTableModel doesn't have to be used with a
JTable. Having table-like access to a set of objects and their
properties can be helpful in other settings. For example, I've been playing with classes that use a
TableModel to generate HTML in servlets. Coupling these with the
OOTableModel has made it easy to quickly write Web applications.
Taking this further, these HTML producing classes could create a layer between DHTML or HTTP that would
change browser events into sets and gets to a
TableModel.
An object browser could be created by extending the OO widget idea. For example, maybe clicking the right
mouse button over a cell in a
JTable would cause that object to be
"inspected" in a new window. All of its properties would then be displayed and could be edited. These
properties could then themselves be inspected, etc.
The
OOTableModel class could be easily modified to accept a
Vector or
Enumeration in
its constructor. This would make it easier to use in typical applications. In these cases, though,
there wouldn't be any guarantees that all objects would be the same type, or could be displayed.
Changes in objects can be detected with the notification enhancements I describe. However, changes to the
List of objects itself are not accounted for. This might be a handy feature: detecting additions and
deletions from the List of objects, and notifying Listeners about them.
Finally, it would be interesting to enhance other Swing model interfaces. As I mentioned in the HTML
example, a
TableModel is a useful tool for abstraction apart from
Swing. Having easy-to-use extensions added to the other Swing interfaces could enable new types of flexible
applications to be built around them.