Loosely Coupled Open Action (Part 1)

A big flashing warning sign that your NetBeans Platform application isn't as mature & idiomatic, i.e., as loosely coupled, as it could be is if you have an Open Action (or something similar) that performs things like this:

@ActionID(category = "Customer",
id = "org.customer.viewer.nodes.OpenCustomerActionListener")
@ActionRegistration(displayName = "#CTL_OpenCustomerActionListener")
@Messages("CTL_OpenCustomerActionListener=Open Customer")
public final class OpenCustomerActionListener implements ActionListener {

    private final Customer context;

    public OpenCustomerAction(Customer context) {
        this.context = context;
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        TopComponent tc = WindowManager.getDefault().findTopComponent("CustomerEditorTopComponent");
        tc.open();
        tc.requestActive();
    }
    
}

What you see above in the highlighted code is bad in several ways. Firstly, you've got a hard dependency on a TopComponent with a very specific name. Secondly, how are you going to pass the context to the specified TopComponent? Are you going to create a "setCustomer" method in that TopComponent and then cast the "TopComponent" above to a "CustomerEditorTopComponent"? That's very ugly, plus the above TopComponent cannot be in a different module to where the ActionListener defined above is found, unless you're OK with setting a module dependency. Finally, the above is also suboptimal because a much better approach exists.

So, while the above works fine, it is really suboptimal and leads, in one or more different ways, to a tightly coupled scenario.

Let's change the above ActionListener to this instead:

@ActionID(category = "Customer",
id = "org.customer.viewer.nodes.OpenCustomerActionListener")
@ActionRegistration(displayName = "#CTL_OpenCustomerActionListener")
@Messages("CTL_OpenCustomerActionListener=Open Customer")
public final class OpenCustomerActionListener implements ActionListener {

    private final OpenCapability context;

    public OpenCustomerAction(OpenCapability context) {
        this.context = context;
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        context.open();
    }
    
}

Now that's a completely different definition, isn't it. Instead of becoming enabled when a "Customer" object is in the Lookup, the Action will now become enabled when an "OpenCapability" is in the Lookup.

The question is... how will the OpenCapability be made available in the Lookup? Well, in the domain module, i.e., the same module where the Customer object is found, you also need to define an OpenCapability, as follows:

public interface OpenCapability {
    void open();
}

OK. Now you have an interface. Let's implement it. Where shall we implement it? We will implement it in the place where we need to be able to put it into the Lookup. Where is that place, i.e., where (and when) do we want to put an implementation of our interface into the Lookup? We want to put an implementation of our interface into the Lookup whenever the OpenActionListener is performed. When is it performed? When the user invokes the Action. What needs to be available in the Lookup in order for the user to invoke the Action? An implementation of the OpenCapability interface. How will an implementation of the OpenCapability be put into the Lookup? Via the definition of the Lookup of the Node, which is as follows:

public class CustomerNode extends BeanNode {

    public CustomerNode(Customer bean) throws IntrospectionException {
        this(bean, new InstanceContent());
    }

    private CustomerNode(final Customer bean, final InstanceContent ic) throws IntrospectionException {
        super(bean, Children.LEAF, new AbstractLookup(ic));
        setDisplayName(bean.getName());
        ic.add(new OpenCapability() {
            @Override
            public void open() {
                ic.add(bean);
                ic.remove(bean);
            }
        });
    }

    @Override
    public Action[] getActions(boolean context) {
        List customerActions = Utilities.actionsForPath("Actions/Customer");
        return customerActions.toArray(new Action[customerActions.size()]);
    }
    
}

We receive a Customer object from the ChildFactory and pass it, along with a new InstanceContent (a container for dynamic objects) to a private constructor (as Ernest has pointed out a few times, so that other developers can't accidentally start using this internal constructor, and I keep forgetting about, but not this time). There the InstanceContent is put into the Lookup as a placeholder for the objects that we'll add to it later (and remove from it). When the "open" method is called, which happens when the user invokes the Action, the Customer object is put into the Lookup of the Node. Then if the CustomerEditorTopComponent is listening to the global Lookup, it will detect that there is a new Customer object available and update itself as needed with information from the new data.

Immediately, the Customer object is removed again from the Lookup of the Node, so that the Customer will no longer be in the Lookup of the Node. If the Customer were to remain in the Lookup of the Node, then moving up and down with the keyboard in the node hierarchy would result in the current Customer object being published. We don't want that in this scenario, because in this scenario we want the OpenActionListener to control the publication of Customer objects, rather than the Node selection.

The benefit of all this? Well, take a look again at your OpenActionListener, simpler and cleaner is hard to imagine. And it is completely loosely coupled:

@ActionID(category = "Customer",
id = "org.customer.viewer.nodes.OpenCustomerActionListener")
@ActionRegistration(displayName = "#CTL_OpenCustomerActionListener")
@Messages("CTL_OpenCustomerActionListener=Open Customer")
public final class OpenCustomerActionListener implements ActionListener {

    private final OpenCapability context;

    public OpenCustomerActionListener(OpenCapability context) {
        this.context = context;
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        context.open();
    }
    
}

Code sample:

http://java.net/projects/nb-api-samples/sources/api-samples/show/versions/7.1/misc/OpenCapabilityDemo

Comments:

Maybe I overlook something, but doesn't this use case apply only for TopComponents that are already open and listening to the lookup ?

"..the Customer object is put into the Lookup of the Node. Then if the CustomerEditorTopComponent is listening to the global Lookup, it will detect that there is a new Customer object available and update itself as needed with information from the new data.."

How do you get a TopComponent that is not a singleton to open and listen to the global lookup ?
As far as I understand this example applies only for TopComponents that are already open.

Posted by guest on March 11, 2012 at 05:15 AM PDT #

Good question and here's the solution:

https://blogs.oracle.com/geertjan/entry/loosely_coupled_open_action_part

Posted by Geertjan on March 12, 2012 at 01:18 AM PDT #

"the Customer object is removed again" - this is bizarre and looks very wrong. The CustomerNode's lookup should permanently contain the Customer, since that is what it represents (in fact this is the default behavior of BeanNode IIRC).

Use the standard Openable, not OpenCapability.

The Openable impl can simply find an open TopComponent whose lookup has some other interface with a method like void setCustomer(Customer) and call it. Or define some singleton interface that tracks an "active" Customer.

Of course, all these tricks seem to be oriented toward replacing the Customer in a nonsingleton TC, which is itself bizarre and nonstandard UI. There are two normal possibilities:

1. The TC is a singleton - so all the usual annotations, including @OpenActionRegistration - which tracks the Customer selection, much like e.g. the Navigator would. No need for Openable; to open or focus the window, you use a menu item from the Windows menu, generated by @OAR.

2. Each Customer gets its own TC, which is permanently associated with that Customer, and Openable finds that TC (creating it on demand) and focuses it. This is how editor-type windows work.

Posted by Jesse Glick on March 12, 2012 at 07:43 AM PDT #

Thanks a lot. The problem is that there needs to be an "Open" menu item on the Node. When the "Open" menu item is clicked, a TopComponent for editing the node's object needs to open. However, the TopComponent displaying the node hierarchy and the TopComponent for editing the node's object need to be in separate modules and have no dependency on each other.

So, the editor TC cannot be a singleton, so possibility 1 is not appropriate here. The problem with possibility 2 is that there is no way for the Openable to create the editor TC on demand -- it can't get any reference to it because the Openable is in the node, which is in module A, while the editor TC is in module B. (That's why I used the ModuleInstall class to create the editor TC in module B.)

Posted by Geertjan on March 12, 2012 at 01:07 PM PDT #

Awesome post! You have used an anonymous class for the OpenCapability interface which makes sense, however, that approach seems to be convenient since you have access both to the customer bean and the ic (InstanceContent) object. If I wanted to define a class OpenCustomerCabilityImpl instead of using the anonymous class then I no longer have access to the objects I need to make available in the Lookup. Are there any alternatives for creating an OpenCustomerCapabilityImpl object and injecting it into the CustomerNode object externally?

Posted by guest on April 24, 2012 at 05:26 PM PDT #

Post a Comment:
  • HTML Syntax: NOT allowed
About

Geertjan Wielenga (@geertjanw) is a Principal Product Manager in the Oracle Developer Tools group living & working in Amsterdam. He is a Java technology enthusiast, evangelist, trainer, speaker, and writer. He blogs here daily.

The focus of this blog is mostly on NetBeans (a development tool primarily for Java programmers), with an occasional reference to NetBeans, and sometimes diverging to topics relating to NetBeans. And then there are days when NetBeans is mentioned, just for a change.

Search

Archives
« April 2014
SunMonTueWedThuFriSat
  
12
13
14
18
19
20
21
22
23
24
25
26
27
28
29
30
   
       
Today