ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Data Models for Desktop Apps Data Models for Desktop Apps

by Andrei Cioroianu
06/02/2004

This is the third article in a series that presents the prototype of a Java desktop application called JImaging. The first article described the three major Java GUI toolkits: AWT, Swing, and SWT. In the second article, I introduced the prototype, with its classes and packages; I presented the code of the Main class and I explained how I made several important technical decisions. In this article, I'll describe the data model of the prototype and I'll show how to use it.

What are Data Models and Why Use Them?

The term "model" means various things in different contexts. Java developers use it frequently when they discuss the Model-View-Controller (MVC) pattern, but you may also hear this term when somebody talks about JavaBeans, whose specification defines a "component model." In this article, a data model means an object structure keeping the data of the entire application or just the data visualized by a single GUI component.

JavaBeans are serializable components that expose a set of properties accessible with get and set methods. They may also have public methods, but instead of calling each other's methods, JavaBeans communicate through events. This requires more coding since it involves the so-called event listener interfaces and event classes, but these features make the components independent and reusable. All GUI components of AWT and Swing are JavaBeans. The java.awt.event and javax.swing.event packages define events and listeners such as ActionEvent and ActionListener.

JavaBeans are also used on the server side in JavaServer Pages (JSP). This is possible because JavaBeans don't have to use any specific GUI API. Non-GUI JavaBeans usually act as data models, and their public methods may be used to implement the application's logic, processing the data. There is nothing stopping us from using non-GUI JavaBeans on the client side, in desktop applications.

Related Reading

Java Cookbook
By Ian F. Darwin

Separating the data model from the GUI that visualizes the data has many benefits:

The model-view separation seems to increase the application's complexity because you have to implement the event-based communication. This requires a bit more coding and maybe a few more classes and interfaces, but the complexity is actually reduced because there are fewer dependencies between the components of the application. If implemented correctly, the data model doesn't normally have to know anything about the GUI that uses it. This makes the code more maintainable and reusable.

In addition, the application logic code, which processes the model's data, doesn't have to notify the GUI when the data is updated. Instead, the interested GUI components register themselves as event listeners to the data model, which notifies them through a common listener interface. When new components enhance the user interface, the model's code doesn't have to be changed. If the data model is enriched, existing components can be easily modified to take advantage of the new features.

Developing Data Models

The JImaging prototype has a custom component called PaintView, which allows users to draw annotations on an image, such as rectangles, ellipses, and lines. The prototype also can be used to add text notes, but these are handled by the MainPanel class, which extends JDesktopPane. The information about all annotations, including the text notes, are managed by an instance of the PaintModel class.

Extending the PropertyChangeSupport Class

The java.beans package contains a few classes that implement a generic mechanism that allows a JavaBean to notify other components when the values of its properties are changed. The components interested in getting notifications must implement the PropertyChangeListener interface, which has one method, named propertyChange(). This method takes a PropertyChangeEvent parameter whose methods return the property's name, the old value, and the new value.

The PropertyChangeSupport class provides the addPropertyChangeListener() and removePropertyChangeListener() methods for registering and unregistering event listeners. The support class also has several firePropertyChange() methods that call the propertyChange() method of the registered listeners. Therefore, you can notify the event listeners about a property change with one of the firePropertyChange() methods.

The PaintModel class extends PropertyChangeSupport because it needs its listener registration and event-firing mechanisms. If a JavaBean must extend another class, you can still use PropertyChangeSupport. In this case, your JavaBean would use a PropertyChangeSupport instance and would need to define its own addPropertyChangeListener() and removePropertyChangeListener() methods that would delegate those calls to the PropertyChangeSupport instance.

The PaintModel class defines a String constant for each property because the methods of PropertyChangeSupport work with property names:

package com.devsphere.articles.desktop.paint;
...
import java.beans.PropertyChangeSupport;
...
public class PaintModel extends PropertyChangeSupport {
    public static final String BACK_COLOR_PROPERTY
        = "BACK_COLOR_PROPERTY";
    public static final String BACK_IMAGE_PROPERTY
        = "BACK_IMAGE_PROPERTY";
    public static final String ZOOM_FACTOR_PROPERTY
        = "ZOOM_FACTOR_PROPERTY";
    public static final String TOOL_CLASS_PROPERTY
        = "TOOL_CLASS_PROPERTY";
    public static final String TOOL_COLOR_PROPERTY
        = "TOOL_COLOR_PROPERTY";
    public static final String TOOL_STROKE_PROPERTY
        = "TOOL_STROKE_PROPERTY";
    public static final String LAST_TOOL_PROPERTY
        = "LAST_TOOL_PROPERTY";
    ...
} 

Using String constants is not very elegant, but it's the price we have to pay for using a generic class such as PropertyChangeSupport. Most Swing data models define their own event objects and listener interfaces in the javax.swing.event package, such as TableModelListener and TreeModelListener. It doesn't make sense to design custom event listeners for a simple prototype, but this is necessary when developing reusable GUI components.

The Properties of the PaintModel Class

The PaintModel class has several properties, as described in the following table:

Property Name Property Type Description
backColor Color Background color
backImage Image Image being annotated
zoomFactor float Zoom factor
toolClass Class Current tool used for painting
toolColor Color Current color used for painting
toolStroke Stroke Current stroke used for painting
lastTool AbstractTool The last added painting tool
toolIterator Iterator Iterator for all painting tool objects
(see below for more details)

The values of these properties, except for those of lastTool and toolIterator, are kept in private fields, and are initialized in the PaintModel() constructor:

public static final Color BACK_COLOR_INIT_VALUE = Color.white;
public static final Image BACK_IMAGE_INIT_VALUE = null;
public static final float ZOOM_FACTOR_INIT_VALUE = 1.0f;
public static final Class TOOL_CLASS_INIT_VALUE = NoteTool.class;
public static final Color TOOL_COLOR_INIT_VALUE = Color.black;
public static final BasicStroke TOOL_STROKE_INIT_VALUE = 
       new BasicStroke(5, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
 
private Color backColor;
private Image backImage;
private float zoomFactor;
private Class toolClass;
private Color toolColor;
private Stroke toolStroke;
private LinkedList toolList;
 
public PaintModel(Object source) {
   super(source);
   backColor = BACK_COLOR_INIT_VALUE;
   backImage = BACK_IMAGE_INIT_VALUE;
   zoomFactor = ZOOM_FACTOR_INIT_VALUE;
   toolClass = TOOL_CLASS_INIT_VALUE;
   toolColor = TOOL_COLOR_INIT_VALUE;
   toolStroke = TOOL_STROKE_INIT_VALUE;
   toolList = new LinkedList();
}

Each property has a get method that returns its value and a set method that changes the property's value and notifies the registered listeners. For example, the getZoomFactor() and setZoomFactor() methods are implemented with the following code:

public float getZoomFactor() {
   return zoomFactor;
}
 
public void setZoomFactor(float newZoomFactor) {
   float oldZoomFactor = zoomFactor;
   zoomFactor = newZoomFactor;
   firePropertyChange(ZOOM_FACTOR_PROPERTY, new Float(oldZoomFactor),
                      new Float(newZoomFactor));
}

The firePropertyChange() method calls the propertyChange() method of the listeners registered with addPropertyChangeListener().

The Tool Classes Used by PaintModel

The application model keeps the image annotations in a java.util.LinkedList, whose elements are instances of the classes from the com.devsphere.articles.desktop.paint.tools package. These tool classes will be presented in a future article of this series. You may study their source code if you want, but at this moment, the only thing you need to know about them is that each tool object keeps information about an image annotation that was painted by the user with the mouse.

The getLastTool() method returns the last element of toolList:

public AbstractTool getLastTool() {
   if (toolList.isEmpty())
      return null;
   else
      return (AbstractTool) toolList.getLast();
}

The setLastTool() method adds an element to toolList:

public void setLastTool(AbstractTool newLastTool) {
   AbstractTool oldLastTool = getLastTool();
   toolList.add(newLastTool);
   firePropertyChange(LAST_TOOL_PROPERTY, oldLastTool, newLastTool);
}

You may use getToolIterator() to obtain all elements of toolList, one by one:

public Iterator getToolIterator() {
   return toolList.iterator();
}

The createTool() method creates an instance of the current toolClass, initializing its properties with the current color and stroke:

public AbstractTool createTool(int paintStyle) {
   AbstractTool tool = null;
   try {
      tool = (AbstractTool) toolClass.newInstance();
   } catch (IllegalAccessException e) {
      throw new InternalError(e.getMessage());
   } catch (InstantiationException e) {
      throw new InternalError(e.getMessage());
   }
   tool.setColor(toolColor);
   tool.setStroke(toolStroke);
   tool.setPaintStyle(paintStyle);
   return tool;
}

Finally, removeTool() can be used to remove an element from toolList:

public void removeTool(AbstractTool tool) {
   toolList.remove(tool);
}

The toolList could have been an indexed property, allowing direct access to its elements. However, the java.beans.PropertyChangeSupport class doesn't provide features for indexed properties. In addition, the GUI components need to be notified only when a tool object is added to the list, and this can be done with the lastTool property.

Using Data Models

As explained in the previous section, PaintModel keeps the annotations that are shown on screen by the MainPanel and PaintView components. Some of the properties of PaintModel are used and modified by the application's toolbar, which is created by the ToolBarBuilder class. Therefore, the state of the data model (PaintModel) is reflected on screen by three components: a desktop panel (MainPanel), a custom component (PaintView), and a toolbar (see Figure 1). These GUI components get the model's data using the get methods and change the model's state using the set methods. Some of these components register themselves as event listeners in order to be notified when the model's state is changed. The PaintModel class doesn't have to know anything about the GUI components.

Figure 1
Figure 1. The GUI components of the JImaging prototype

Creating a PaintModel Instance

An instance of the PaintModel class is created by the PaintView GUI component, which has a getModel() method that returns the model:

package com.devsphere.articles.desktop.paint;
...
public class PaintView extends JComponent {
   private PaintModel model;
   ...
 
   public PaintView() {
      model = new PaintModel(this);
      ...
   }
 
   public PaintModel getModel() {
      return model;
   }
   ...
}

The details of the PaintView class will be presented in the next article of this series.

Registering Listeners to PaintModel

MainPanel is one of the classes that register listeners to the data model with addPropertyChangeListener(). When the value of a model property is changed, the propertyChange() method of the event listener is called by a firePropertyChange() method inherited by PaintModel from PropertyChangeSupport. Depending on the name of the changed property, the event listener calls other methods of MainPanel:

package com.devsphere.articles.desktop.frames;
...
public class MainPanel extends JDesktopPane {
   public static final int INIT_WIDTH = 600;
   public static final int INIT_HEIGHT = 400;
   public static final Integer PAINT_LAYER = new Integer(10);
   public static final Integer NOTE_LAYER = new Integer(20);
 
   private PaintView paintView;
 
   public MainPanel() {
      paintView = new PaintView();
      setBackground(paintView.getModel().getBackColor());
      setDragMode(OUTLINE_DRAG_MODE);
      add(paintView, PAINT_LAYER);
      registerListeners();
   }
 
   public PaintView getPaintView() {
      return paintView;
   }
 
   protected void registerListeners() {
      paintView.getModel().addPropertyChangeListener(
       new PropertyChangeListener() {
          public void propertyChange(PropertyChangeEvent e) {
             String p = e.getPropertyName();
             if (p.equals(PaintModel.BACK_IMAGE_PROPERTY)
              || p.equals(PaintModel.ZOOM_FACTOR_PROPERTY))
                updateSize();
             if (p.equals(PaintModel.LAST_TOOL_PROPERTY)
              && e.getNewValue() instanceof NoteTool)
                addNoteFrame((NoteTool) e.getNewValue());
         }
      });
   }
   ...
}

When the backImage or zoomFactor properties are changed, their set methods use firePropertyChange(), which calls the propertyChange() method of the listener created and registered by MainPanel. The propertyChange() method invokes updateSize(), which changes the size of the MainPanel. Note that this panel is wrapped in a JScrollPane by MainFrame. The updateSize() method also sets the bounds of the PaintView component. This last operation is necessary because the main panel holding the paint component is actually a Swing desktop panel (MainPanel extends JDesktopPane).

public void updateSize() {
   int width = INIT_WIDTH;
   int height = INIT_HEIGHT;
   PaintModel paintModel = paintView.getModel();
   if (paintModel.getBackImage() != null) {
      Image backImage = paintModel.getBackImage();
      width = backImage.getWidth(this);
      height = backImage.getHeight(this);
   }
   float zoomFactor = paintModel.getZoomFactor();
   width = (int) (width * zoomFactor);
   height = (int) (height * zoomFactor);
   Dimension size = new Dimension(width, height);
   setPreferredSize(size);
   setSize(size);
   paintView.setBounds(0, 0, width, height);
}

The zoomFactor property is changed when the user modifies the selection of the combo box from the upper-left corner of the window. Figure 2 shows the annotated screenshot of Figure 1 within the JImaging prototype. The zoom factor is 200 percent:

Figure 2
Figure 2. Zoomed screenshot within JImaging

Saving Data Models

The PaintModel class is a data model with get and set methods -- it lets you register event listeners -- but PaintModel is not a fully conformant JavaBean, because it is not serializable. That means that you cannot save the model using Java Object Serialization, which is a mechanism for transforming objects into byte streams that may be saved into files or sent over a network to another computer. Those byte streams can be used to reconstruct the objects together with their links (references). See the Java Object Serialization specification for more details.

The easiest way to make a class serializable is to declare that your class implements java.io.Serializable, which is an interface with no methods. You would also have to make sure that all of its non-static, non-transient fields are serializable, too. Some of the fields of PaintModel and the tool objects are not serializable because their classes do not implement java.io.Serializable nor java.io.Externalizable. While Java Object Serialization sounds like a great idea, sometimes it doesn't work very well in practice. All Swing components implement java.io.Serializable, but their serialization usually fails because some fields of many Swing classes are not serializable. Also, changing the Java code of your classes may easily break the ability to deserialize the objects saved with the old version of the code.

J2SE 1.4 introduced an XML-based mechanism for archiving JavaBeans, which is exposed through the java.beans.XMLEncoder and java.beans.XMLDecoder APIs. This mechanism is fault-tolerant, meaning that it is not interrupted by an exception when an object cannot be archived (serialized) or reconstructed (deserialized). You can "listen" for such exceptions, but it can be difficult to make sure that your objects are archived properly because only the public JavaBean properties are saved without the internal state of the components, such as private fields that aren't mapped to properties.

The java.beans.XMLEncoder cannot save the state of the PaintModel, which has fields whose types aren't JavaBeans. We would have to build "persistence delegates" that know how to save the entire state of the objects that aren't JavaBeans. In the case of the JImaging application, another way to save its data model would be a custom file format, possibly based on XML, but this is beyond the scope of a prototype. In its current version, JImaging can save only the annotated images, as explained in a previous article.

Summary

How would this prototype look without a data model and without using events and listeners? As an exercise, try to take out the PaintModel class, moving its properties into the PaintView component. Instead of using PropertyChangeSupport and PropertyChangeListener, call methods directly when one component must notify another component about a property change. This creates a lot of unnecessary relationships between your classes. For a small application such as JImaging, it might not be a problem, but when you have hundreds or thousands of classes, the unnecessary dependencies make the code very hard to maintain. Model-view separation and event-listener mechanisms solve this problem. The next article of this series will focus on views, showing how to build custom Swing components.

Resources

Andrei Cioroianu is the founder of Devsphere and an author of many Java articles published by ONJava, JavaWorld, and Java Developer's Journal.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.