Skip to content
deanriverson edited this page Jan 29, 2012 · 8 revisions

This page is meant to be a collection of the lessons we've learned about writing NodeFactories and working with Groovy builders in general.

All Factories should extend AbstractGroovyFXFactory

If a factory overrides the onHandleNodeAttributes method then it should return super.onHandleNodeAttributes(...) at the end of the method. This ensures that the remaining attributes are processed by the default handling code in AbstractGroovyFX Factory. The only exception is when the overriding method processes all possible attributes itself and returns false to indicate that no other processing is required.

Never handle properties that a user might bind in the Factory#newInstance method

When you bind an attribute to a property, you are setting a PropertyBinding object on that property. Therefore the bindingAttributeDelegate needs to run in order to remove the bound property from the attribute map and complete the binding.

The bindingAttributeDelegate is a handler that is registered with SGB and gets first crack at all attributes in all nodes before they are processed by a factory's onHandleNodeAttributes method.

If a bound property is handled before the bindingAttributeDelegate processes it, or somehow skips being processed by the bindingAttributeDelegate, an exception will surely be thrown when the PropertyBinding object is assigned to the property.

Always try to return the actual object from the newInstance method rather than a wrapper, delegate, or builder.

This is especially true if the object is-a Node.

Avoid assumptions about what child nodes your code will receive

As an example, this code was in ControlFactory:

public void setChild(FactoryBuilderSupport builder, Object parent, Object child) {
    // ...
    if (parent instanceof SplitPane) {    
        if (child instanceof Node) {
            parent.items.add(child);
        } else if (child instanceof List) {
            parent.items.addAll(child);
        } else {
            def pcntx = builder.getParentContext();
            List dividers = pcntx.get(SceneGraphBuilder.CONTEXT_DIVIDER_KEY);
            dividers.add(child);
        }
    }
    // ...
}

The final else clause is meant to capture instances of dividerPosition nodes. The problem is that with code such as this:

splitPane(orientation: 'horizontal') {
    dividerPosition(index: 0, position: 0.62)

    tripsTable = tableView(items: bind(model.tripsProperty()),
                           columnResizePolicy: TableView.CONSTRAINED_RESIZE_POLICY) {
        tableColumn('ID', property: 'id')
        tableColumn('Start Time', property: 'start', converter: { it.toString(dateFormat) })
        tableColumn('Duration', property: 'duration', converter: { durationFormatter.print(it) })
        tableColumn('Miles', property: 'miles', converter: { decimalFormatter.format(it) })
        tableColumn('MPG', property: 'mpg', converter: { decimalFormatter.format(it) })
    }
    node(new WebMap())
}

Not only are the dividerPosition and tableView child nodes, but so too is the result of the bind( … ) call on tableView's items property. The result of the bind method is a FullPropertyBinding object which the catch-all else clause attempted to treat as a DividerPosition object leading to an exception.

Moral of the story: child nodes are like a box of chocolate, you never know what you're going to get.