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:

  1. Reuse of the standard property editors from the JDK
  2. 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.
  3. 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

  1. The node to render is set on a property sheet instance
  2. The new node is queried for its properties
  3. 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.
  4. Instances of sheet button cache references to the Property objects, the property editors, the property editor tag set, etc.
  5. The user clicks a property
  6. The PropertyPanel instance creates a new instance of an appropriate editor component and places it in edit mode
  7. The user changes the value
  8. The editor component fires a property change
  9. The property editor is updated
  10. The property editor fires a property change
  11. 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)
  12. 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

  1. The node to render is set on a property sheet instance, triggering a repaint
  2. The new node is queried for its properties
  3. Each property is rendered, JTable-style, by one of three static renderer components
  4. The user clicks a property
  5. 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
  6. The user changes the value and closes the editor
  7. The inplace editor fires an action event
  8. The table cell editor fetches the value from the inplace editor and the property model from the inplace editor
  9. The value is set on the model/Property from the inplace editor
  10. 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:
  1. 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
  2. The property sheet and property panel components (deprecate the existing property sheet and make it a subclass of the new one)
  3. 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

  1. A property sheet instance is set to render the selected object
  2. The property sheet finds the look for the selected object and requests its PropertiesLook
  3. The property sheet requests the property names, fetches the values for those to be displayed (default large-model JTable approach) and paints them
  4. The user clicks a value to edit it
  5. An appropriate inplace editor is created and bound to the property editor
  6. The user changes the value and indicates acceptance (presses enter, etc.)
  7. The inplace editor fires an action event
  8. The table cell editor sets the value on the property editor from the inplace editor
  9. The property editor fires a property change
  10. 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.

Project Features

About this Project

openide was started in November 2009, is owned by Antonin Nebuzelsky, and has 17 members.
By use of this website, you agree to the NetBeans Policies and Terms of Use (revision 20160708.bf2ac18). © 2014, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo
 
 
Close
loading
Please Confirm
Close