Soup up your Java classes with customization
Wow your beans users with cool customization features
Those of you who have been following this column have already read my first two installments on Java customization. The first,”Double Shot, Half Decaf, Skinny Latte: Customize your Java,” was an introduction to the concept of object properties and the various ways in which beans can be customized. Last month’s column, “The trick to controlling bean customization,” covered the general concepts of bean customization structure, and was a bit light on code and examples. This month, we’ll hit some more complex coding examples. The background to JavaBeans customization has been covered in previous articles, so this article will assume that you’ve either read those articles, or you have an equivalent understanding of the topic.
Because the more advanced JavaBean customization features are only necessary for more advanced beans, we’ll start with a walk-through of ScatterPlot, a bean that displays a scatter plot of data it retrieves from another object. The ScatterPlot
class will (perhaps predictably) provide us with some gnarly customization problems. Fortunately, we’ll have the tools at hand to solve them, armed as we are with the customization interface of the JavaBeans API. We’ll overcome some of the limitations of property sheets by writing and registering a property editor for a derived type, and we’ll finish by writing a complete customizer class.
Property editors vs. customizers
To recap from last month’s column, there’s a difference between a property editor and a customizer. A property editor is a class that implements the java.beans.PropertyEditor interface (although it often does so indirectly, by extending the “adapter” class java.beans.PropertyEditorSupport). Its purpose is to allow the user to edit a single property of a bean. The BeanBox container, from Sun, comes with several built-in property editors that it presents to application developers in a property sheet, as follows. (For more on the BeanBox, see my previous JavaWorld article, “The BeanBox: Sun’s JavaBeans test container.” Check-out the Resources section of this article to download the BeanBox.)
Class | Property Editor |
---|---|
Boolean | BoolEditor |
Byte | ByteEditor |
Color | ColorEditor |
Double | DoubleEditor |
Float | FloatEditor |
Font | FontEditor |
Int | IntEditor |
Long | LongEditor |
Number | NumberEditor |
Short | ShortEditor |
String | StringEditor |
Built-in BeanBox property editors
When the user selects a bean in the BeanBox, the BeanBox application builds a custom property sheet for the bean. It first introspects the bean to figure out what its properties are. Then, based on the class, the BeanBox places an instance of each of these property editors, one per property, along with labels identifying the properties. Any properties for which it either can’t find or (for some reason) can’t construct property editors are omitted from the property sheet. This means that any properties for which no editor exists, including properties of types you’ve created, won’t show up on the property sheet, even if appropriate setter
and getter
methods have been written for them. (This is an important point, as we’ll see later.)
Since there’s no built-in enumerated type in Java (though there is an interface, java.util.Enumeration
), there’s no enumerated type editor. (We’ve already seen how to add a property editor for an enumerated type, though, in last month’s column.)
These property editors also only allow editing of scalar properties — that is, single-valued properties, not indexed (array) properties. Properties that require an index to access simply won’t appear in the property sheet. And, what’s more, since you can write getter
and setter
methods for a property of any type, you may want an editor for a property to have its own little dialog box to edit a value for that property. (ColorEditor
already works this way.)
Why property editors aren’t enough
To add to all of these problems, even if accessible property editors exist for every kind of object, there are still other problems inherent to relying on a “property sheet”:
-
A property sheet is ugly and clumsy when too many properties appear on it.
-
There may be properties you want to hide from a user or hide from non-expert users. (Actually, as we saw last month, you can hide properties from a property sheet by leaving them out of the
PropertyDescriptor
array.) -
Some of the properties may interact, in which case allowing users to edit properties one at a time may place the bean in a state that’s inconsistent with its internal logic. For example, imagine a bean that displays both gray-scale and color images. If the bean is set to display “gray-scale,” showing a property that controls color balance may confuse the user.
-
Bean properties may have names like XDitherErrorDifusion, which, while they correctly reflect how the bean is implemented, aren’t necessarily what you want the user to see.
- In fact, you may want a single control in the customizer to update several properties at once. For example, in an image manipulation bean, you may want the user to be able to specify a single Render Quality in a list box (containing, say, fast, good, and best). Choosing one of these options might update four or five properties of the bean with pre-set values. In this example, “Render Quality” could be considered a “virtual” property, existing for ease of use but corresponding to several of the bean’s properties instead of just one. (The
BarChartBeanColorSchemeEditor
from last month’s column partially demonstrated this concept, though theColorScheme
property was actually a property of the bean itself.)
For all of these reasons, complex beans often benefit from having a separate customizer class; one which displays a bean’s properties as its user interface elements are manipulated. In the sections that follow, we’ll solve some of these problems and make the property sheet more useful. Then, we’ll get frustrated with the property sheet and throw it angrily in the trash, replacing it with a customizer. But first, we need a class that’s sufficiently ornery to need its own customization support.
Introducing the ScatterPlot bean
Here’s what the ScatterPlot bean looks like:
The bean has quite an impressive list of properties, each of which has corresponding setter
and getter
methods. Notice that some of these properties have non-standard types. These are:
- boolean cachePoints — a property that tells whether or not to cache the points from the input data set
- PlotSpot — an object that encapsulates how to draw each point. This is where the smileys come from (PSHappy.java)
- SPDataSource dataSource — an object that provides a set of points to a ScatterPlot for plotting
- String title — the title of the plot
- boolean autoRangeX, autoRangeY — tells whether to automatically figure out the x and y data ranges from the input data
- boolean numberPoints — tells whether or not to put numbers next to the points
- boolean showGrid — tells whether or not to display a grid
- int xMax, xMin, yMax, yMin — these are the min and max values for x and y axes
- int xTic, yTic — tells (in input units) how far apart grid lines should be for x and y
There are two special types here: PlotSpot and SPDataSource. Both are interfaces that other classes may implement so that a ScatterPlot bean can be used to plot those classes’ data. (Remember that an interface is like an abstract class, except that it is implemented rather than extended. If you’re not clear on this, see the section on interfaces in October’s column, “Keep Listening for Upcoming Events.”)
The first interface class is SPDataSource. Its definition is:
import java.awt.*;
//
// A ScatterPlot may be created with one of these
// as an argument. This is the data source for the
// scatterplot.
//
public interface SPDataSource
{
Point getPoint(int index);
void setPoint(int index, Point value);
int getPointCount();
};
ScatterPlot needs a data source in order to have something to plot. This interface specifies a method for ScatterPlot to ask its data source how many points will be returned, then it asks for the data points in sequential order, using Point getPoint(int i)
, from 0 to n-1 (where n is the number of data points to plot).
There’s also a method to set a data point, so conceivably you could move data points around with a mouse, and the data source would be notified of any changes. (This operation is not actually implemented in ScatterPlot.)
The cool thing about using an interface for the data source is that any class that wants to plot points on a ScatterPlot bean need only implement this interface, and the ScatterPlot can plot its data. The data can be anything: Weather data from last month; the number of hits on your Web site by day; or your weight vs. pulse rate after that grueling workout video. (I hate that tape, especially the ab work.) Note, however, that in a builder tool there’s no way to specify the data source, since there’s no property editor for class PSDataSource
.
The other interface used by ScatterPlot is PlotSpot, which has a single method:
import java.awt.*;
// A class that implements this interface draws a point
// into Graphics g representing the point (x, y).
public interface PlotSpot {
public void drawSpot(Graphics g, int x, int y, String label);
};
The ScatterPlot bean defers the task of drawing the individual data points to another class, so that client applications (written by you, perhaps) can draw data points any way they like: dots, bars, ovals — even little images. The ScatterPlot doesn’t care how the dots get drawn, because it defers that work to the PlotSpot
object, which was itself provided by another object (like, perhaps, a property editor) via the setPlotSpot()
method. This means it’s possible to extend the functionality of ScatterPlot without changing one character of the ScatterPlot code.
Of course, on a property sheet, you can’t set this plotSpot property, because there’s no property editor for the PlotSpot
. Yet. (In literature, this is called “foreshadowing.” Keep reading.)
So, let’s have a look at the property sheet for the ScatterPlot before we add any additional customization information for it:
The property sheet has most of the properties we want, but there are some glaring omissions. For one, although the bean has methods for setting the dataSource and plotSpot properties, there’s no property editor for either of them. Also, it’s possible to set the autoRangeX property (which automatically calculates xmin and xmax based on the data the bean receives), but there’s no way to keep the user from monkeying with xmin and xmax via the property editor. (This isn’t something we’ll be able to fix in the property sheet. We’ll have to wait for a customizer to do that.) And, of course, the property sheet is just plain ugly. Related properties aren’t grouped near one another, and the properties have ugly, techie-looking names next to them.
Property editor for a derived type
Let’s create a property editor for the plotSpot editor, and then we’ll add it to the property sheet. A property editor, as noted before, is a class that implements the interface java.beans.PropertyEditor. This interface provides quite a lot of functionality; you don’t usually want or need to implement all of the functions in the interface, and with plotSpot, you don’t have to.
The property editor for plotSpot must display a list of the available PlotSpot
implementations, and it needs to set the plotSpot property of the (ScatterPlot) bean it is customizing by calling setPlotSpot()
.
Note, though, that it’s the builder tool, not the property editor, that calls setPlotSpot()
, because the property editor uses a listener interface (see Resources below) to connect the bean to the property editor. The application builder tool (or “IDE”) detects that there’s a property called plotSpot of type plotSpot. It then finds the PlotSpotEditor
class we’re about to write, and puts the editor in the property sheet just as it would for any other class. (How it “finds” the editor class is covered in the next section.)
So, the PlotSpotEditor
class needs to provide the following functionality:
- A way to display a list of the available PlotSpot implementations from which to choose
- A way to register the bean with the editor so the editor can fire property change events at it, thus telling it to change its PlotSpot
- A way to set and get the name of the PlotSpot we want as a text string (because that’s what’s being picked from the list)
- A way to get the actual value of the PlotSpot (as an object), so the IDE can pass it to the bean by calling
setPlotSpot()
.
These functions are specified in the PropertyEditor
interface:
getTags()
returns the names of the valid PlotSpot implementationsaddPropertyChangeListener()
hooks the editor to the bean being editedgetAsText()
and setAsText() get and set the editor’s current “value” object (the PlotSpot) as a stringgetValue()
returns the actual PlotSpot object to the caller
As in a lot of cases, the JavaBeans Guys and Gals (at Sun’s JavaSoft division) have already done a lot of the work for you by providing the base class java.beans.PropertyEditorSupport, which you can subclass to provide the functionality you want, instead of having to implement all of the methods for the interface java.beans.PropertyEditor. In particular, the PropertyEditorSupport
class maintains a local member called value, accessed via getValue()
and setValue()
. These two accessors keep track of the PlotSpot
object that the IDE passes to the ScatterPlot’s setPlotSpot
method when a property change event fires. In addition, java.beans.PropertyEditorSupport
implements the property change listener functions for you.
Here, then, is the implementation of the PlotSpotEditor
class:
import java.beans.*;
import PlotSpot;
// Note we have to import all PlotSpot subclasses here, since
// we "new" them.
import PSBox;
import PSHappy;
import PSTri;
public class PlotSpotEditor extends PropertyEditorSupport
{
protected String myName;
// Create an editor.
public PlotSpotEditor()
{
super();
myName = new String("Box");
}
// Return valid names of PlotSpotEditor subclasses.
public String[] getTags()
{
System.out.println("PlotSpotEditor.getTags");
String[] spots = {
"Box",
"Happy",
"Tri"
};
return spots;
}
// Given the name of a PlotSpot, set the value of the
// object we're editing, and then fire a property change event
// at the bean.
public void setAsText(String sValue)
{
System.out.println("PlotSpotEditor.setAsText(" + sValue + ")");
myName = sValue;
if (sValue == "Box") {
setValue(new PSBox());
} else if (sValue == "Happy") {
setValue(new PSHappy());
} else if (sValue == "Tri") {
setValue(new PSTri());
}
firePropertyChange();
}
// Get the name of the PlotSpot being edited
public String getAsText()
{
System.out.println("PlotSpotEditor.getAsText");
return myName;
}
// Override this just so we can print a message (to demonstrate
// that the IDE is calling it.)
public void addPropertyChangeListener(PropertyChangeListener p)
{
System.out.println("PlotSpotEditor.addPropertyChangeListener");
super.addPropertyChangeListener(p);
}
};
I’ve put println statements in the methods of this class so that you can experiment in the BeanBox and see when the methods are being called. The getTags() function simply returns a list of enumerated values to the caller (the BeanBox). The BeanBox interprets a non-null result from this function as a request to create a drop list, which is what it does. It also hooks up the bean being edited to the editor by calling addPropertyChangeListener()
. When the user changes the drop list in the property editor, the function setAsText()
is called with the new selected value as an argument. As you see above, the editor turns this name into a PlotSpot
object of the appropriate type, calls setValue()
(setting the value property of the editor, which then calls setPlotSpot()
on the bean), and then calls firePropertyChange()
, telling the bean to redraw itself with its new PlotSpot
.
Registering a property editor for a type
Last month, we saw an example of how to associate a property editor with a class by calling setPropertyEditorClass
on the PropertyDescriptor
for the class. Clear as mud? Here’s the code from last month:
PropertyDescriptor psc = new PropertyDescriptor("colorScheme",
myClass);
psc.setPropertyEditorClass(BarChartColorSchemeEditor.class);
This code’s fine as long as you’re using PropertyDescriptors
, which means that in any class in which you want a property of type (for example) PlotSpot
, you don’t mind implementing BeanInfo
. But there’s an easier way to tell the IDE what property editor to use: You can register the property editor class with the class java.beans.PropertyEditorManager by calling its static member function registerEditor()
. (You’ll remember that static member functions are functions that exist in a class but don’t need a class instance in order to be called. See the upcoming example.)
Where should you call this function? You probably want it in a place from which it’s only going to be called once. I like to do it in the getBeanDescriptor()
method of the BeanInfo
class for the bean, assuming I’m even writing one. Here’s how I do it in ScatterPlotBeanInfo
:
public BeanDescriptor getBeanDescriptor()
{
// ... Other code
// Register some property editors
PropertyEditorManager.registerEditor(PlotSpot.class,
PlotSpotEditor.class);
// ... Other code
}
You simply pass to registerEditor()
the class object for the type you want to edit and the class object for the editor class, and the PropertyEditorManager
hangs onto them for you. When the IDE asks the PropertyEditorManager
, “Hey, PEM, what class do you use to edit this here property I found called ‘PlotSpot’”, the PropertyEditorManager
responds, “Oh, that would be PlotSpotEditor
, don’t’cha know.” (The “don’t’cha know” part only applies in Minnesota and thereabouts.)
Of course, if you conform to the naming convention for property editors, you don’t have to register the PlotSpotEditor
at all, because the PropertyEditorManager
‘s smart: It knows that, if there’s no editor registered for, say, class wibble.wobble.PlotSpot
, it will search the CLASSPATH for the fully qualified pathname wibble.wobble.PlotSpotEditor
, and if it can’t find that, it just looks for a class called PlotSpotEditor
. In this case, in response to the above question from the IDE (“Hey, PEM,…” and so on), the answer is more along the lines of, “Well, I don’t got nothing registered for PlotSpot, but here’s something I found for PlotSpotEditor
. Why don’t you try using that class, then?” The point is, if you follow the naming convention classnameEditor for property editors, you don’t need to register your property editor: PropertyEditorManager
will find it for you.
Writing a customizer
This property sheet is a little better than the default sheet that the BeanBox generates, but it’s still a property sheet — meaning it’s still ugly, it still allows you to input junk data to some extent, and it’s still not as easy as it could be to configure the ScatterPlot. We need something better: A Customizer class.
The interface java.beans.Customizer is, in contrast with the PropertyEditor interface, very simple. It provides methods to:
- Set the object being edited (
setObject()
) - Add a property change listener (
AddPropertyChangeListener
) - Remove a property change listener (
RemovePropertyChangeListener
)
A Customizer should be a subclass of java.awt.Component, typically a java.awt.Panel that the IDE can embed into a GUI object (often, though not always, a frame), and present to the user for interaction. The user interface elements correspond to the properties (and “virtual properties,” like “Render Quality”) that you want the user to see; as they change, the Customizer notifies its listeners via PropertyChange
. The listener is, of course, the bean being configured. So, the Customizer is simply a component that fires PropertyChange events at a bean in response to user interface activity. These events cause the bean to change state (since it’s listening for these events), thereby configuring it.
When it finds a customizer for a class, the BeanBox adds a menu item Customize… to its Edit… menu. When you activate that item, the Customization dialog box appears:
Partial source code for the customizer appears below:
import java.beans.*;
import java.awt.*;
import ScatterPlot;
import ScatterPlotCustomizerPanel;
public class ScatterPlotCustomizer
extends ScatterPlotCustomizerPanel
implements java.beans.Customizer
{
PropertyChangeSupport pcs_; // handles propertychange stuff
ScatterPlot sp_; // hang on to the object we're customizing
// Initialize
public ScatterPlotCustomizer()
{
super();
pcs_ = new PropertyChangeSupport(this);
}
public void addPropertyChangeListener(PropertyChangeListener pcl)
{
pcs_.addPropertyChangeListener(pcl);
}
public void removePropertyChangeListener(PropertyChangeListener pcl)
{
pcs_.removePropertyChangeListener(pcl);
}
// The bean to be configured is passed in here.
public void setObject(Object obj)
{
if (obj instanceof ScatterPlot)
sp_ = (ScatterPlot)obj;
// Initialize contents of GUI elements
Title.setText(sp_.getTitle());
//... and so on for other properties
}
protected void TitleEnterKeyPressed( Event event )
{
String oldText = sp_.getTitle();
String newText = Title.getText();
Object oldobj = (Object)oldText;
Object newobj = (Object)newText;
String prop = new String("title");
super.TitleEnterKeyPressed(event);
sp_.setTitle(newText);
pcs_.firePropertyChange(prop, oldobj, newobj);
}
// And so on for other properties...
};
Complete source code is available for download (see Resources below). The ScatterPlotCustomizerPanel
, from which the ScatterPlotCustomizer
is derived, is a JDK 1.0-style panel I created in an excellent (if somewhat behind-the-times) application builder that does not yet support JDK 1.0. By placing all of the JDK 1.1+ functionality in a subclass, it’s possible to continue using these older tools and still take advantage of packages such as java.beans. Unfortunately, with this scheme, we’re still stuck with the pre-Java 1.1 event handling structure.
Registering a Customizer
Registering a customizer is similar to registering a PropertyEditor
for a class. It can be done in the BeanInfo
class by creating a java.beans.BeanDescriptor object with two arguments: The class and the Customizer
class. The JavaBeans Specification says that the BeanDescriptor
class is the place to look for a Customizer, so that’s where we put it:
import java.beans.*;
import sun.beans.editors.*;
import PlotSpotEditor;
public class ScatterPlotBeanInfo extends SimpleBeanInfo
{
private final static Class myClass = ScatterPlot.class;
public BeanDescriptor getBeanDescriptor()
{
// Register some property editors
PropertyEditorManager.registerEditor(PlotSpot.class,
PlotSpotEditor.class);
// Create bean descriptor (including the customizer class)
BeanDescriptor desc = new BeanDescriptor(myClass,
ScatterPlotCustomizer.class);
desc.setShortDescription("Scatter Plot");
desc.setDisplayName("ScatterPlot");
return desc;
}
};
Conclusion
Once again, we’ve gone over a great deal of material. We discussed why property editors and customizers are necessary, and then implemented one of each as examples. Next month, we’ll take a look at the Java concept of class loaders, and begin talking about serialization and instantiation of JavaBeans at runtime. Yet another article on types of property editors will follow in a later column.