Property infrastructure rewrite proposal
Version: 1.0, June 26, 2003
Author: Tim Boudreau,
Sun Microsystems/NetBeans
- Abstract:
- Describes a proposed replacement for the current property handling
infrastructure in NetBeans (
Node.Property and
Node.PropertySet
- Document History:
- [06/26/2003] : version 0.1: {Initial draft}
Contents:
Preface
The proposal and sample code herein should be considered an
early draft.
1. Overview
A definition of properties
Properties represent individual characteristics of a java object which
are presented to the user for customization via NetBeans UI. The object/properties
model is used pervasively throughout NetBeans UI, for manipulation of
source files, configuration of IDE settings, and presenting individual
controls for configuration in wizards and other parts of NetBeans
UI. There are two UI classes for rendering properties:
- PropertySheet — presents all the properties available on an
object in a table-like UI control
- PropertyPanel — is a control which can be configured from a
single property
A Property is a data binding between a
textual name and methods to fetch and set the value on a specific
object. NetBeans defines Properties (primarily) via the class
Node.Property, a subclass of
java.beans.FeatureDescriptor.
A definition of property editors
Modifications to the data-binding that is a Property is mediated by a
property editor (
java.beans.PropertyEditor) which may provide additional
logic for painting the value of the property, validating
values to be set and converting values between text and non-text
representations.
Property editors are defined in the JavaBeans specification.
Their primary design features are:
- Provide a generic interface for setting/reading values of objects via
the
setValue() and getValue() methods. In
keeping with the JavaBeans specification, properties may be
read-only, write-only or read-write. In practice the write-only
pattern is rarely encountered.
- Provide a means to convert to and from textual representations
via the
getAsText() and setAsText()
methods
- Optionally support enumerated values via the
getTags() method
- Optionally provide a means of painting a property value into an arbitrary
rectangle on a graphics canvas via the
paint() method
- Indicate prospective changes to the property value by firing
PropertyChangeEvents.
- Optionally provide a custom editor component which can do detailed
configuration of the property value
NetBeans use of property editors and extensions to the property editor concept
Properties are rendered by the UI, optionally allowing
the user to change a property's value
In NetBeans’ original design, the way to present a Property
in the UI was to create an instance of Node.Property and
implement the appropriate setters and getters. This includes
providing a property editor to convert values and optionally
provide painting logic. While property editors mix presentation
logic and business logic, which is generally not a good idea,
they offer a few benefits:
- Reuse of the standard property editors from the JDK
- Allow a single module to register a property editor for a given
value class, which will then be used for all objects of
that class to provide a better ui.
- Allow individual Node.Property objects with the same
value class to provide a custom property editor rather
than the default editor provided by the JDK or core,
to meet its unique requirements.
Item 3 above is significant: In some cases it may be
more beneficial to the user to provide behavior different
than the baseline behavior of a standard property editor.
For example, a property representing the boolean enabled
state of a module may be provide a better user experience
by providing the Strings "enabled" and "disabled"
than the default "true" and "false"
strings provided by the boolean property editor’s
getTags() method.
It is predictable that certain types of property editor
customizations prove so useful that, in the interest of
code reuse, support for them should be implemented in
a property editor for the type(s) commonly requiring them.
For example, it is just as likely that someone will want
a boolean editor that supplies "yes" and "no"
as display text for its values.
In order to achieve such code reuse, a mechanism needs to
exist so that the provider of the Property may communicate
to the standard property editor (not written by the author
of the property) that it should use some other text for
its tags - or in some other way behave differently than
it does by default. This is accomplished in NetBeans
via the class ExPropertyEditor, a base class
for property editors which can be given "hints"
that affect their behavior, and PropertyEnv,
which primarily exists to provide access to the Node.Property
object as an instance of its parent class, FeatureDescriptor.
ExPropertyEditor provides the method attachEnv(PropertyEnv env)
method. Implementations of ExPropertyEditor are then expected
to exploit an otherwise unused method of FeatureDescriptor,
getValue(String key). A collection of non-normative
hints are documented in the NetBeans API documentation, describing
what hints are possible for what value classes.
NetBeans implementation and extensions to the Properties concept
Whether discussing bean properties (i.e.
someClass.getFoo()
and
someClass.setFoo()) which are exposed to the
user as Node.Properties (for example, using
PropertySupport.Reflection), or explicitly created
Node.Property instances, we are typically dealing with
a bean-style getter/setter pair. The principle value
of doing this with an explicit Node.Property object is
that the overhead of reflection does not come into play.
There are three basic ways to invoke property-rendering
infrastructure in NetBeans:
- By explicitly creating an instance of Node.Property, which can
be displayed in a Property Sheet and implements logic for
getting/setting the value
- Using PropertySupport.Reflection, which is passed a
name for the property and implements getter/setter logic
using reflection
- Using PropertyPanel. PropertyPanel attempts not
to depend on Node.Property, and instead by driven by a
PropertyModel - an interface designed to abstract the
concept of a property without requiring that the property
be implemented as a Node.Property (though it usually is).
Most often the PropertyModel instances used by PropertyModel
are actually PropertyPanel.SimpleModel (refactored as
NodePropertyModel in the upcoming property sheet rewrite).
So in practice a hard dependency on Node.Property exists -
it is even coded into the current PropertyPanel class.
More on this PropertyModel vs. Node.Property connundrum later.
Problems with the current situation
Property support in NetBeans reflects the ongoing evolution
of the APIs from their earliest days, and a significant
amount of cruft has accumulated. The property sheet
component is currently being
rewritten to eliminate some of the most fragile
code in NetBeans codebase and solve the UI problems
of the original property sheet. This rewrite has
brought to light some of the architectural problems
and unnecessary complexity in NetBeans handling of
properties.
Specific problems are enumerated below:
Dependancy on the Nodes package
One of the laudable goals of the current implementation of
PropertyPanel is allowing objects which have no knowledge
of Nodes or Node.Properties to expose properties to the
user for presentation/editing with very little effort.
Yet because the implementation depends on Node.Property,
in practice, an application which wants the functionality
of PropertyPanel but has no use for Nodes and their ilk must
carry them around anyway to satisfy compile-time dependancies.
Node.Property and PropertyModel - grabbing the elephant by the trunk and the tail at the same time
PropertyModel is an attempt to address the
problems this document addresses, but which applies only
to PropertyPanel, the result being a sort of schizophrenic
design in the propertysheet package - half the code is
Node.Property dependant; the other half goes out of its
way to avoid such a dependancy, but ends up having one
anyway. One of the goals of this document is to reconcile
this situation.
The problems with the PropertyModel interface are that
- It was designed with only PropertyPanel in mind - it does
not supply all of the information needed for the
PropertySheet to be able to use it without recourse
to Node.Property. In particular, it provides no
means of obtaining the property editor a Node.Property
may supply. Further, the property sheet's API is
driven by Node.Property instances - yet it must
also deal with PropertyModel instances - the property
model may contain additional logic for setting/getting
the value (duplicating/supplanting such logic in
the property editor).
- It makes the propertysheet package's contents
somewhat schizophrenic. There are two ways to do
the same thing. Given a bean with a property to
expose in the UI, one may either use PropertySupport.Reflection
and a PropertyPanel will wrap it in an instance
of PropertyPanel.SimpleModel (now NodePropertyModel),
or create an instance of DefaultPropertyModel (which
uses reflection to find getters/setters as does
PropertySupport.Reflection, the main difference being
no dependancy on Node). It is unclear which is the
preferred case - the one thing which is clear is
that the primary usage of properties in NetBeans
is via Node.Property objects - the rest are fringe
cases.
- It has already been insufficient and extended
once, as ExPropertyModel. ExPropertyModel adds the
method
getFeatureDescriptor(), again
really a path to gain access to the original
Node.Property object, so that hints may be provided
to an instance of ExPropertyEditor if one is
provided.
- Performance - Node.Property does not implement any
of the PropertyModel interfaces - even if the PropertyModel interface
were sufficient for the property sheet to be driven
purely by instances of PropertyModel with no recourse
to Node.Property, it would require that an instance
of PropertyModel be created for each property to be
rendered, again and again each time the property sheet is
painted (it is possible to cache such objects, but
added memory consumption is precisely what we must
avoid to improve NetBeans performance). The PropertySheet
rewrite hacks around this problem with a single
reusable instance that is reconfigured for each
property rendered, much as its JTable implementation
reconfigures a handful of renderers for each property
rendered. It should be noted that this approach works,
but is not thread-safe (a ThreadLocal version could be
created with a performance penalty) and seriously complicates the
maintenance and readability of the property sheet code.
- Scalability - Node and Node.PropertySet both surface
their properties by means of returning arrays of PropertySet
and Property objects. This means very little scalability
can be had without hacks - if a node were to expose 1000
properties, all of the required Property objects would need
to be instantiated even if the only thing needed were to
ascertain the number of them, or display the first five.
A List-based implementation would offer more opportunities
for optimization.
PropertyEnv - the "waterboy" design pattern?
PropertyEnv evolved for a variety of purposes:
- Don't force property editors to depend on the Nodes package -
since Node.Property extends java.beans.FeatureDescriptor, the
PropertyEnv method
getFeatureDescriptor means that
for common cases, such property editors do not have a compile-time
dependancy on the Nodes package, and are thus usable outside
NetBeans. Such property editors will nonetheless need to depend on
org.openide.explorer.propertysheet.editors.ExPropertyEditor,
so this distinction is of dubious value.
- Provide hints to property editors to change their behavior -
the real mechanism for this is providing access to the
FeatureDescriptor method
getValue(String key)
which any Property, as a subclass of FeatureDescriptor, will
have
- Encapsulate the "state" of a property (for those property objects
that support it - in practice this is only Node.Property)
and provide an object a property editor may attach listeners
to to listen for changes in state. This is a requirement for
custom editor dialogs. PropertyEnv provides three states for
a property: Valid, Needs Validation and Invalid. These states
are used to influence its rendering in the property sheet (displaying,
for example, a red X beside properties that require editing for
the user to use the object they occur on, and to manage the
enabled state of the OK button in custom editor dialogs (the
dialog component is provided by NetBeans' infrastructure,
while the component it contains is provided by the property
editor).
- Provide a reference to the object(s) the property belongs
to via the
Object[] getBeans() method. It is unclear what
this method is used for, since I can find no code which calls
it in the NetBeans core, but presumable it meets the requirement
of some property editor implementation. Anecdotally, the
purpose is to allow, for example, the Form Editor to get a
reference to a source file and populate a list of, for example
event handlers already existing in that source file which
match the signature of the one needed. It appears, however,
that the Form Editor is actually still using an older, deprecated
means of acheiving this.
- Performance - simply to have a sufficient conversation with
a property editor to gather the necessary information to render
it requires that an instance of PropertyEnv be provided. As with
PropertyModel, the new property sheet implementation hacks around
this requirement by reconfiguring a single instance when rendering.
It is worth noting that the original purpose of PropertyEnv was
to provide a binding for the valid/invalid state of a property,
and that this binding is only truly needed when a custom editor
is open - Properties generally do not spontaneously go from
invalid to valid by themselves. The secondary purpose of
providing hints is an architectural hack to avoid dependancies
on Node.Property and offers no particular value.
Conclusions
It is clear that we have a rather organically grown API/SPI for
properties, with a great deal of complexity growing out of a
fairly simple task - to provide a generic way to call methods
of objects and render the results in the UI.
Looks, the death of Nodes and the property sheet/property panel rewrite
The Looks API is soon to replace the Nodes API. One of its principle
differences is its scalability and in the number of objects that
must be created in order to perform tasks such as displaying an
object and its children. It uses a pull, rather than push
approach to providing nodes - a client of a Look calls, for example,
getIcon(Object representedObject) rather than having a
Node object wrapping the underlying object to provide such display
logic. This both reduces the number of objects that need to be created
and improves scalability, as objects that do not need to be displayed
do not need to be created.
The introduction of Looks as a replacement for Nodes affords an
opportunity to review and redesign the way Properties are handled
in NetBeans. The complexity of the existing system adds impeteus
for such a redesign.
The property sheet rewrite makes a major improvement to NetBeans UI
for properties, but its implementation is handicapped by the need to
work with a complex and cumbersome existing API. Any next step to
further evolution of property support in NetBeans should involve
a cleanup of the existing API.
There are a number of performance problems with PropertyPanel - this
single-property property editor/displayer component still relies on
the infrastructure of the old propertysheet, in particular the
class SheetButton, one of the most complex and fragile
pieces of code in NetBeans. It is desirable to rewrite it to use
the same lightweight rendering infrastructure the new property sheet
uses. It particularly causes performance problems in
org.openide.explorer.view.TreeTableView.
However, this is fundamentally difficult: The property sheet depends
on Node.Property objects, indeed, it must depend on them -
Node.Property objects may provide a custom property editor; PropertyModel
posesses no method for fetching a property editor.
PropertyPanel, on the other hand, goes out of its way to avoid any
reference to Node.Property. Any reimplementation of PropertyPanel to use
the new property sheet's rendering infrastructure requires either
that that infrastructure be hacked to handle cases where a
PropertyModel with no underlying Node.Property is used (a
comparatively rare case), or the property sheet must be hacked in
the opposite direction, to deal with PropertyModel objects and
cast and hack to get access to a Node.Property object where
that is necessary. Neither of these scenarios are desirable.
A short-term hack, in order to enable PropertyPanel to use the
new PropertySheet's rendering infrastructure is possible - write
a Node.Property subclass which wraps an instance of PropertyModel,
so that it will always be driven by Node.Property objects - then
both components can be driven by a common infrastructure. However,
such an approach will hurt the long-term maintainability of an
already complex codebase, and I do not recommend it.
2.1. Property rendering lifecycle - how a property gets to the screen
2.1.1 - Property lifecycle in the original property sheet
- The node to render is set on a property sheet instance
- The new node is queried for its properties
- The property sheet discards its existing component contents and
builds a new component tree of instances of PropertyPanel
embedding instances of SheetButton to display the properties
and property names, accessor buttons for custom editors, etc.
- Instances of sheet button cache references to the Property objects,
the property editors, the property editor tag set, etc.
- The user clicks a property
- The PropertyPanel instance creates a new instance of an
appropriate editor component and places it in edit mode
- The user changes the value
- The editor component fires a property change
- The property editor is updated
- The property editor fires a property change
- The property model is updated (this may either mean a property change
is fired and the property is updated, or the property model value
setter wraps the Property object's setter)
- The property object setter sets the value on the underlying object
Problems with this design
Scalability: Components are created for every property, whether
shown or not. A great deal of component destruction and construction
occurs whenever the selected node changes (e.g. when the editor
caret is moved). Usability: PropertyPanel/SheetButton have a
number of serious usability and keyboard focus management problems.
2.1.2 - Property lifecycle in the new property sheet
- The node to render is set on a property sheet instance, triggering a repaint
- The new node is queried for its properties
- Each property is rendered, JTable-style, by one of three static renderer
components
- The user clicks a property
- An appropriate (text field, combo, etc.) singleton instance of
InplaceEditor is bound to the edited property for the duration of
the editor component's presence onscreen
- The user changes the value and closes the editor
- The inplace editor fires an action event
- The table cell editor fetches the value from the inplace editor
and the property model from the inplace editor
- The value is set on the model/Property from the inplace editor
- The property object setter sets the value on the underlying object
Problems with this design
On the whole, this design is much more scalable than the original
property sheet, and does much less object creation. The main down-sides
are impedance mismatches with an API that wasn't designed for
scalability in the first place. In particular, a number of hacks
are necessary to preserve compatibility:
- Use of a singleton PropertyEnv subclass which is reconfigured
as properties are rendered, to avoid creating one new PropertyEnv
object for every property each time the property sheet paints.
- Use of a singleton PropertyModel subclass which is reconfigured
as properties are renderered, to avoid creating one new PropertyEnv
object for every property each time the property sheet paints.
- Lack of scalability in the original Node API - the return
values of
getPropertySets() and getProperties()
are arrays, requiring that Property objects be instantiated even
if they will never be used.
2. Requirements
Abstracting from the above, it seems that what is needed is a common
set of interfaces for properties and property containers that meet all
of the requirements of PropertySheet and PropertyPanel. It is clear
that PropertyEnv evolved for two reasons: To make up for deficiencies
in the Node.Property interface (such as providing state management)
and as an architectural hack to avoid forcing property editors into
a dependancy on Node.Property (even though the only time they'll receive
any hints from a PropertyEnv instance is when they are being used to
edit a Node.Property). PropertyModel evolved as an attempt to move
in a more architecturally separate direction, abstracting the concept
of properties into a model, but it addresses only the use case of
PropertyPanel.
The following requirements can be gleaned from the above:
- Interfaces meeting all the requirements for clients of
property-containers and properties should be created.
- These interfaces should be scalable and not force creation of
properties optimistically
- A property interface must enable listening for state changes
on it and have a concept of valid/invalid state
- A property interface must be able to provide non-normative hints
to property editors to influence their behavior and appearance
- A property interface must, at least for some cases, make it
possible to obtain a reference to the object supplying the
property (the form editor use case)*
- Some mechanism for listening for changes in the property
must be available
- Some mechanism for listening for changes in the available
properties must be possible, at least for backward compatibility.
- The means of fetching properties should be at least potentially
scalable, using either indexed getters and setters or some interface
from the Java Collections framework.
- Support for default values for objects
*For the Looks-style solution below, this
requirement is met by the fact that it is impossible to get any
data about a property without knowing the object that owns the property
3. Solutions
There are two possible approaches, either of which can solve
the above problems and meet the above requirements:
Solution 1 - Comprehensive Property and PropertySet interfaces which
can be implemented on any object and bridged or implemented on
Node.Property/PropertySet
One approach is to take the existing PropertyModel concept - good
except that it only covers PropertyPanel's use-case, and create
a new interface that overlaps as much as possible with Node.Property,
but add missing components such as state validation. Deprecate
ExPropertyEditor and
PropertyEnv and
create a bridge infrastructure for backward compatibility.
Rewrite PropertySheet/PropertyEnv to render instances of this
new interface. Repackage and split off Property Sheet as
three modules:
- The new interfaces which providers of properties may implement,
such that modules which may want to provide properties do not
need to depend on a rendering infrastructure for them (the
property sheet
- The property sheet and property panel components (deprecate
the existing property sheet and make it a subclass of the new
one)
- A set of compatibility bridge classes for things like
PropertyModel and PropertyEnv
Advantages to this approach
The concepts should be familiar to module authors.
Disadvantages of this approach
This approach still requires creation of Property objects,
increasing memory footprint in order to provide essentially
a simple data binding. Also, Property objects lend themselves
to inheritance-based, rather than composition-based design,
a pattern which has already proven problematic in NetBeans'
history.
Implications for API usability and conversion
This approach merely updates the Property interface to match
its current use cases in a more scalable way. If the new interface
can be directly implemented on Node.Property, few or no changes
must be made in modules that wish to support the new interface.
Modules supplying instances of ExPropertyEditor should be
updated to the new property editor interface which can
communicate directly with the new property interface.
Solution 2 - A more radical approach
An alternative, possibly a preferable one is modelled on the
Looks concept. In Looks, you have a typical method
signature:
getSomething(Object representedObject).
In some ways, there is not much difference between a Node and
a Property - from a mile-high-view, a nodes property sets and
properties are simply an alternate set of child objects, similar
to a Node's children. The way they are rendered is different,
and a property binds two methods to a name, instead of one
object to a name and icon, but they are conceptually similar.
Such an approach means that a single PropertiesLook would
provide means of fetching the relevant data for the properties
of a given object. A rough sketch of such an interface could look
like:
public interface PropertiesLook {
public String[] getPropertyNames(Object representedObject);
public Object getPropertyValue(Object representedObject, String propertyName);
//note the method below could return an int compatible with getState instead of
//throwing a checked exception
public void setPropertyValue(Object representedObject, String name) throws InvocationTargetException;
public boolean isReadOnly (Object representedObject, String propertyName);
public Object getHint (Object representedObject, String propertyName, String key);
public int getState (Object representedObject, String propertyName);
public PropertyEditor getPropertyEditor(Object representedObject, String name);
public boolean hasDefaultValue (Object representedObject, String name);
public boolean isDefaultValue (Object representedObject, String name);
public void restoreDefaultValue (Object representedObject, String name) throws InvocationTargetException;
//XXX listener methods not included
}
Note that the above code does not address the existence of
property sets, but that problem is easily solved in a variety of
ways - it is simply a mapping of property names to categories.
We have the additional issue of allowing for property editor instances
which can do all of the things which current implementations of
ExPropertyEditor do. The current ExPropertyEditor
interface includes a single attachEnv (PropertyEnv env)
method, which allows such an editor to initialize itself with
data such as hints, set its valid state, etc. In practice,
attachEnv() is always called
immediately after the property editor constructor is run - there
is no use case for either repeatedly calling attachEnv()
or manipulating a property editor programmatically and then
calling attachEnv(). So I propose to move the functionality
of attachEnv() either to a required constructor or a
factory method. A rough sketch of such an interface could be:
public abstract class ExExExPropertyEditor extends PropertyEditor {
/** Allows info currently provided by the PropertyEnv to be given to the property editor. */
public static ExExExPropertyEditor create (Object[] representedObjects, boolean isReadOnly,
Object[] hints, Object initialValue, int initialState);
public int getState();
/** Allow code to interact with generic property editors in cases where
* outside code may determine the state based on context */
public void setState(int i);
/** Support state changes using standard change listener support */
public void addChangeListener (ChangeListener listener);
public void removeChangeListener (ChangeListener listener);
}
Advantages of this approach
With such an approach, the burden of providing scalability is built into
the API - an object providing a large number of properties does not
need to provide a custom list implementation or anything else - the
rendering infrastructure only needs to request data about those
properties it needs to display. Only the property names for all
properties must be available. Further, such an approach will have an
overall smaller memory footprint, since even Property do not need
to exist.
Implications for API usability and conversion
As a radical departure from the existing notion of properties
as objects which bind a name and getter/setter methods,
this approach implies some conceptual overhead for module
authors. The concept of a Property as an object is an
intuitive one.
At the same time, Looks is disposing of the Nodes concept,
and offering a properties solution which is conceptually
consistent with Looks seems a reasonable approach.
The absence of concrete Property objects is quite solvable
by providing good support
classes. For example, an implementation of PropertyLook may
be provided which allows users to define properties using
a descriptor object, which are then stored by the look. This
approach would be similar to creating a single GridBagConstraints
object, and then reconfiguring it to add several components.
To illustrate, a sample of what such constructor code could
look like:
public class MyObjectLook extends AbstractPropertiesLook {
public MyObjectLook() {
AbstractPropertiesLook.Descriptor desc = new AbstractProperties.Descriptor();
desc.configure ("hashCode", Integer.TYPE, false, //...
putProperty (desc);
desc.configure (//configure for another property...
putProperty (desc);
}
}
This registration mechanism can take other alternate forms as
well, including XML/layer based registration of properties for
specific object types.
2.1.2 - Property lifecycle using the Looks-style approach
- A property sheet instance is set to render the selected object
- The property sheet finds the look for the selected object and
requests its PropertiesLook
- The property sheet requests the property names, fetches the
values for those to be displayed (default large-model JTable approach)
and paints them
- The user clicks a value to edit it
- An appropriate inplace editor is created and bound to the property editor
- The user changes the value and indicates acceptance (presses enter, etc.)
- The inplace editor fires an action event
- The table cell editor sets the value on the property editor from the
inplace editor
- The property editor fires a property change
- The infrastructure calls setValue() on the PropertyLook for the object,
with the value.
A further consideration - property editors - what are they good for?
As is clear from above, the property editor provides limited value
in this situation. They exist to provide an abstraction for
validation, and in particular, to provide handling of property values
based on the object type, so that generic functionality may
be plugged in independant of who implements a particular property.
The other aspects of property editors, painting logic and
providing custom editors, is a less than desirable mixing of
visualization logic with other logic.
It would be possible to further simplify the number of layers
in the system by eliminating (or hiding) the use of property
editors altogether. However, this would complicate the
PropertyLook interface or require some registration mechanism
for the validation and custom editor functionality.
Conclusions
We are currently at a juncture in NetBeans APIs history which
offers some unique opportunities to modernize and redesign
NetBeans internal infrastructure to be more scalable and
maintainable. It is a good time to make such changes as are
proposed in this document. The current properties management
code in NetBeans is neither simple nor easily maintainable,
and a solution which keeps the functionality while eliminating
the complexity is desirable.