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.
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.
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:
- Override the method getMainView() on the Address class, to
return a new view
- 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:
- The class must implement the ComplexEView interface
- 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.
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:
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.