HEAD PREVIOUS

Chapter 14
Customized Views and Editors

It's nice for a framework to support the automatic generation of views for various objects. We've also seen how the base user interface can be augmented with calendaring features, wizards, and support for producing PDFs. However, it's equally important for a framework to remain flexible and to allow for the construction of custom views as well.
JMatter's primary view mechanism at the moment is its Swing-based view mechanism. Other view mechanisms might be constructed in the future, including possibly a web-based user interface.
In this chapter I'd like to demonstrate through examples various ways in which custom views can be integrated into the Swing view mechanism.

14.1  Complex, or Composite Views

The demo-apps subdirectory contains a project, CustomUI, designed specifically to illustrate how this is done. It is similar to our contact manager: There's a Contact class and an Address class. The idea is that we'd like to be able to customize the way Addresses appear on the screen; specifically, the way that the form view for addresses is laid out. The default form view is nice. Figure 14.1 displays the default view for the address property of the Contact class.
figures/CustomView-1.png
Figure 14.1: Standard Form View the Contact.address Field
Let's say that we'd prefer to display addresses in such a way that the field captions are placed above the field editors (instead of appearing to their left), and that we'd like to lay out the city, state, and zip fields all on the same line, instead of having them appear one below the other, as shown in Figure 14.2.
figures/CustomView-2.png
Figure 14.2: Desired view for Addresses
It turns out that JMatter's FormView is already parametrized such that, if you prefer, you can designate that labels appear above their editors instead of to their left. All you need to do is revise the value for the labelEditorLayoutHorizontal property in your application's spring configuration context file src/applicationContext.xml as shown below:
<bean id="view-mechanism" class="com.u2d.view.swing.SwingViewMechanism"
      factory-method="getInstance">
   <property name="appSession" ref="app-session" />
   <property name="labelEditorLayoutHorizontal" value="false" />
</bean>
Let's proceed by pretending that this feature was not available. We must do two things:
  1. Override the method getMainView() on the Address class, to return a new view
  2. Implement the custom view for addresses; this process is similar to the way we traditionally build views in Swing
The first step is straightforward:
public EView getMainView()
{
   return new CustomAddressView(this);
}
Now, for the implementation. We could implement the UI by extending from Swing's JPanel, like this:
public class CustomAddressView extends JPanel
      implements ComplexEView, Editor
{
   private Address _addr;
   JComponent line1View, line2View, cityView, stateView, zipView;
   
   public CustomAddressView(Address address)
   {
      _addr = address;
      buildUI();
   }
   private void buildUI()
   {
      line1View = (JComponent) _addr.getLine1().getView();
      line2View = (JComponent) _addr.getLine2().getView();
      cityView = (JComponent) _addr.getCity().getView();
      stateView = (JComponent) _addr.getStateCode().getView();
      zipView = (JComponent) _addr.getZipCode().getView();
      
      FormLayout layout = new FormLayout("pref, 10px, pref, 10px, pref", 
                         "pref, pref, 10px, pref, pref, 10px, pref, pref");
      CellConstraints cc = new CellConstraints();
      DefaultFormBuilder builder = new DefaultFormBuilder(layout, this);
      // add caption..
      builder.add(new JLabel("Line 1:"), cc.xyw(1, 1, 5));
      builder.add(line1View, cc.xyw(1, 2, 5));
      
      builder.add(new JLabel("Line 2:"), cc.xyw(1, 4, 5));
      builder.add(line2View, cc.xyw(1, 5, 5));
      
      builder.add(new JLabel("City:"), cc.xy(1, 7));
      builder.add(new JLabel("State:"), cc.xy(3, 7));
      builder.add(new JLabel("Zip:"), cc.xy(5, 7));
      
      builder.add(cityView, cc.xy(1,8));
      builder.add(stateView, cc.xy(3, 8));
      builder.add(zipView, cc.xy(5, 8));
   }
   public EObject getEObject() { return _addr; }
   
   // as a composite view, this particular class may not
   // necessarily be interested in binding to the model
   // and listen to changes.  to the extent that i use
   // the jmatter views for the subparts of the address,
   // they will be listening directly to the parts.
   public void detach() { }
   public void stateChanged(ChangeEvent e) { }
   public void propertyChange(PropertyChangeEvent evt) { }
   public boolean isMinimized() { return false; }
   public int transferValue()
   {
      int result = 0;
      result += ((Editor) line1View).transferValue();
      result += ((Editor) line2View).transferValue();
      result += ((Editor) cityView).transferValue();
      result += ((Editor) stateView).transferValue();
      result += ((Editor) zipView).transferValue();
      return result;
   }
   public void setEditable(boolean editable)
   {
      ((Editor) line1View).setEditable(editable);
      ((Editor) line2View).setEditable(editable);
      ((Editor) cityView).setEditable(editable);
      ((Editor) stateView).setEditable(editable);
      ((Editor) zipView).setEditable(editable);
   }
   public boolean isEditable()
   {
      return ((Editor) line1View).isEditable();
   }
}
Let's review this code. Here I am using the excellent JGoodies Forms framework to layout a form "by hand," so to speak. JGoodies' DefaultFormBuilder makes this pretty easy. Similar code could also be produced with the help of a visual form designer, such as the Abeille Forms Designer. Note however, that there are additional responsibilities that this class must fulfill:
  1. The class must implement the ComplexEView interface
  2. If the class will also participate in the editing process, it must implement the Editor interface
In this case, the ComplexEView interface is fairly simple. Some of the methods have no-op implementations (detach(), stateChanged(), propertyChange()). Every view can, at its discretion, attach itself as a listener to the object model and thus receive model change notifications. In this case, we're dealing with a composite view, and each of the sub-views already listens to changes to the parts, so we don't technically need to listen to model changes here. Views must also make sure to "detach" themselves from the model objects when they're destroyed. It's very important that the detach() method properly do this. A careless implementation can easily introduce a memory leak into the application as views are created and their memory not reclaimed because they may still be attached to a model object whose lifetime is typically longer than its views'.
The Editor interface is also fairly straightforward. The setEditable() method is called when the model object's state toggles from Read to Edit (and back), thus giving the user interface a chance to update itself accordingly (if so desired). Notice also that transferValue() is called before an object is saved, allowing the view a chance to bind the newly entered data back to the model object (or possibly raise a validation exception, in which case the save operation is vetoed. The integer value returned by this method is an indication of the number of validation errors, which is also displayed by the framework in such circumstances.

14.2  A Custom View for Sympster Sessions

Here's a more compelling illustration of customizing a view for an object: the Sympster demo application was recently further customized with a custom view for its Session class. In this situation, I wanted to reuse JMatter's default FormView which handles the editing process beautifully and saves me a lot of work. On the other hand, I wanted a more compelling view for sessions in read state, as shown in figure 14.3.
figures/custom-sessionview.png
Figure 14.3: Customized View for Session
JMatter provides a class named CustomReadView which automatically composes JMatter's FormView in edit state with a custom view that you provide for the read state, as shown in the following code snippet from the class Session where the getMainView() method is overridden correspondingly:
   public EView getMainView()
   {
      return new CustomReadView(new SessionView(this));
   }
Please refer to the implementation of SessionView in the Sympster demo application for the details of its implementation. One note worthy of mentioning is that the implementation leverages JMatter's css4swing library to style itself, which I discuss in chapter .

14.3  The Self demo application

Yet another way to augment JMatter with hand-crafted views and widgets is illustrated through the bundled demo application named Self. The model for this application is trivial on purpose: it consists of a Space that contains Balls. The space has one additional property: the temperature within the space.
In this particular scenario, it is quite unpallatable to view these objects through forms when it would be so much more compelling if their visual representations were represented directly in the UI.
Any command you write that returns an object of type View or JComponent will be displayed by JMatter upon invocation. The Self demo application takes advantage of this feature:
public class Space
{
...
   @Cmd(mnemonic='s')
   public View Show(CommandInfo cmdInfo)
   {
      return new SpaceView(this);
   }
...
Running this application, we can create a space, and add a few balls into our space. We can further set the temperature. I created two balls: a filled red ball with radius 30, and a blue ball with radius 80, and my temperature was 30 degrees celsius. Proceeding with invoking the Show command, I can expose my custom view SpaceView directly in JMatter:
figures/Self.png
Figure 14.4: The Self Demo Application
The screenshot doesn't really do it justince since the balls are moving withing the space at a certain speed. Try to change the temperature within the space to, say, 50 degrees. This will cause the balls to move faster. Technically, this custom view could also have been implemented by wrapping it in a CustomReadView as we did in the previous example.

14.4  Customizing Layout

JMatter provides a means through which a layout for a form can be defined by using the open-source Abeille Forms Designer. By placing placeholder components in the layout and giving each a name matching a model object's field name, JMatter can be made to use the specified layout when displaying the form in question. Abeille allows us to serialize the form design's definition to a .jfrm file which we place alongside our source code (JMatter's build file automatically copies these files to the classpath as part of the build process) in a file we name after the model object whose view we want to customize. The CustomUI demo application provides an example implementation.

14.5  Atomic Views

JMatter defines the notion of atomic types. Examples include representations for booleans, dates, text, zip codes, social security numbers, percentages, integers, floats, etc.. They're defined in the package com.u2d.type.atom.
This section describes how to write a custom view and/or editor for a given type. Let's take BooleanEO as an example. Here is the implementation for the default renderer for boolean's:
public class BooleanRenderer extends JLabel implements AtomicRenderer
{
   public void render(AtomicEObject value)
   {
      BooleanEO eo = (BooleanEO) value;
      setText((eo.booleanValue()) ? "Yes" : "No");
   }
   public void passivate() { }
}
The interface AtomicRenderer has essentially a single method. You can ignore the passivate() method for now (we're considering making a design revision where views are pooled, and then re-used for different atomic types, in which case this method could be used to "clean up" the view before it's re-used).
So basically one implements the render() method. This method is handed a model object that it must render. So in this case, the renderer is given an instance of a BooleanEO which it uses to "paint" the text Yes or No on a JLabel.
To write a custom Editor for a type, you must implement two methods: render() and bind(). Here's the definition of the interface that one must implement:
public interface AtomicEditor extends AtomicRenderer
{
   public int bind(AtomicEObject value);
}
And here's an example implementation, again for the BooleanEO type:
public class BooleanCheckboxEditor extends JCheckBox
      implements ItemListener, AtomicEditor, ActionNotifier
{
   public BooleanCheckboxEditor()
   {
      addItemListener(this);
   }
   public void itemStateChanged(ItemEvent e)
   {
      setText( (isSelected()) ? "Yes" : "No" );
   }
   public int bind(AtomicEObject value)
   {
      BooleanEO eo = (BooleanEO) value;
      eo.setValue(isSelected());
      return 0;
   }
   public void render(AtomicEObject value)
   {
      BooleanEO eo = (BooleanEO) value;
      setSelected(eo.booleanValue());
      itemStateChanged(null); // text synch with checkbox
   }
   public void passivate() { }
}
The bind() method is responsible on setting the newly edited value back to the model object. Here's a second implementation for a BooleanEO editor that uses a pair of radio buttons instead:
public class BooleanRadioEditor extends JPanel implements AtomicEditor
{
   private JRadioButton _yesBtn, _noBtn;
   public BooleanRadioEditor()
   {
      _yesBtn = new JRadioButton("Yes");
      _yesBtn.setOpaque(false);
      _noBtn = new JRadioButton("No");
      _noBtn.setOpaque(false);
      _yesBtn.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent evt)
         {
            _yesBtn.setSelected(true);
         }
      });
      _noBtn.addActionListener(new ActionListener()
      {
         public void actionPerformed(ActionEvent evt)
         {
            _noBtn.setSelected(true);
         }
      });
      ButtonGroup group = new ButtonGroup();
      group.add(_yesBtn);
      group.add(_noBtn);
      FormLayout layout = new FormLayout("pref, 3px, pref", "pref");
      DefaultFormBuilder builder = new DefaultFormBuilder(layout, this);
      CellConstraints cc = new CellConstraints();
      builder.add(_yesBtn, cc.xy(1, 1));
      builder.add(_noBtn, cc.xy(3, 1));
   }
   public void render(AtomicEObject value)
   {
      BooleanEO eo = (BooleanEO) value;
      JRadioButton btn = (eo.booleanValue()) ? _yesBtn : _noBtn;
      btn.setSelected(true);
   }
   public int bind(AtomicEObject value)
   {
      BooleanEO eo = (BooleanEO) value;
      eo.setValue(_yesBtn.isSelected());
      return 0;
   }
   public void passivate() { }
}
The JMatter codebase is rife with examples of atomic editors and renderers. Use them as the basis for creating your own implementations.

14.5.1  Specifying the Default Editor and Renderer

Specifying the default renderer and editor for a given type is straightforward. JMatter defines the interface ViewMechanism. The Swing implementation of that interface is SwingViewMechanism. This interface defines a litany of methods that specify the components for the entire user interface. By revising the corresponding method for a given view mechanism, you control the user interface.
For our above implementations of a boolean editor and renderer, and assuming the SwingViewMechanism, you'd revise the implementations of getBooleanRenderer() and getBooleanEditor() respectively for the BooleanEO type:
public AtomicRenderer getBooleanRenderer() {
  return new BooleanRenderer();
}
public AtomicEditor getBooleanEditor() {
  return new BooleanRadioEditor();
}
Of course, if you're going to be implementing a custom view for a more complex or composite object, then you're in complete control of the rendering and editing process.
The framework's view mechanism in general is extremely modular. It's easy to override one small aspect and plug it right into the existing view mechanism to fundamentally change one aspect of the user interface, whether it's by providing your own implementation of a FormView (or embellishing the existing one), or by customizing the widgets that represent associations.

HEAD NEXT