BeansBinding Across Modules in a NetBeans Platform Application
- by Geertjan
Here's two TopComponents, each in a different NetBeans module. Let's use BeansBinding to synchronize the JTextField in TC2TopComponent with the data published by TC1TopComponent and received in TC2TopComponent by listening to the Lookup.
The key to getting to the solution is to have the following in TC2TopComponent, which implements LookupListener:
private BindingGroup bindingGroup = null;
private AutoBinding binding = null;
@Override
public void resultChanged(LookupEvent le) {
if (bindingGroup != null && binding != null) {
bindingGroup.getBinding("customerNameBinding").unbind();
}
if (!result.allInstances().isEmpty()){
Customer c = result.allInstances().iterator().next();
// put the customer into the lookup of this topcomponent,
// so that it will remain in the lookup when focus changes
// to this topcomponent:
ic.set(Collections.singleton(c), null);
bindingGroup = new BindingGroup();
binding = Bindings.createAutoBinding(
// a two-way binding, i.e., a change in
// one will cause a change in the other:
AutoBinding.UpdateStrategy.READ_WRITE,
// source:
c, BeanProperty.create("name"),
// target:
jTextField1, BeanProperty.create("text"),
// binding name:
"customerNameBinding");
bindingGroup.addBinding(binding);
bindingGroup.bind();
}
}
I must say that this solution is preferable over what I've been doing prior to getting to this solution: I would get the customer from the resultChanged, set a class-level field to that customer, add a document listener (or action listener, which is invoked when Enter is pressed) on the text field and, when a change is detected, set the new value on the customer. All that is not needed with the above bit of code.
Then, in the node, make sure to use canRename, setName, and getDisplayName, so that when the user presses F2 on a node, the display name can be changed. In other words, when the user types something different in the node display name after pressing F2, the underlying customer name is changed, which happens, in the first place, because the customer name is bound to the text field's value, so that the text field's value will also change once enter is pressed on the changed node display name.
Also set a PropertyChangeListener on the node (which implies you need to add property change support to the customer object), so that when the customer object changes (which happens, in the second place, via a change in the value of the text field, as defined in the binding defined above), the node display name is updated.
In other words, there's still a bit of plumbing you need to include. But less than before and the nasty class-level field for storing the customer in the TC2TopComponent is no longer needed. And a listener on the text field, with a property change listener implented on the TC2TopComponent, isn't needed either. On the other hand, it's more code than I was using before and I've had to include the BeansBinding JAR, which adds a bit of overhead to my application, without much additional functionality over what I was doing originally. I'd lean towards not doing things this way. Seems quite expensive for essentially replacing a listener on a text field and a property change listener implemented on the TC2TopComponent for being notified of changes to the customer so that the text field can be updated. On the other other hand, it's kind of nice that all this listening-related code is centralized in one place now.
So, here's a nice improvement over the above. Instead of listening for a customer, listen for a node, from which the customer can be obtained. Then, bind the node display name to the text field's value, so that when the user types in the text field, the node display name is updated. That saves you from having to listen in the node for changes to the customer's name. In addition to that binding, keep the previous binding, because the previous binding connects the customer name to the text field, so that when the customer display name is changed via F2 on the node, the text field will be updated.
private BindingGroup bindingGroup = null;
private AutoBinding nodeUpdateBinding;
private AutoBinding textFieldUpdateBinding;
@Override
public void resultChanged(LookupEvent le) {
if (bindingGroup != null && textFieldUpdateBinding != null) {
bindingGroup.getBinding("textFieldUpdateBinding").unbind();
}
if (bindingGroup != null && nodeUpdateBinding != null) {
bindingGroup.getBinding("nodeUpdateBinding").unbind();
}
if (!result.allInstances().isEmpty()) {
Node n = result.allInstances().iterator().next();
Customer c = n.getLookup().lookup(Customer.class);
ic.set(Collections.singleton(n), null);
bindingGroup = new BindingGroup();
nodeUpdateBinding = Bindings.createAutoBinding(
AutoBinding.UpdateStrategy.READ_WRITE,
n, BeanProperty.create("name"),
jTextField1, BeanProperty.create("text"),
"nodeUpdateBinding");
bindingGroup.addBinding(nodeUpdateBinding);
textFieldUpdateBinding = Bindings.createAutoBinding(
AutoBinding.UpdateStrategy.READ_WRITE,
c, BeanProperty.create("name"),
jTextField1, BeanProperty.create("text"),
"textFieldUpdateBinding");
bindingGroup.addBinding(textFieldUpdateBinding);
bindingGroup.bind();
}
}
Now my node has no property change listener, while the customer has no property change support. As in the first bit of code, the text field doesn't have a listener either. All that listening is taken care of by the BeansBinding code.
Thanks to Toni for help with this, though he can't be blamed for anything that is wrong with it, only thanked for anything that is right with it.