X

Geertjan's Blog

  • September 28, 2010

Modular Strategies for NetBeans RCP Applications

Geertjan Wielenga
Product Manager
Let's say you're creating a customer application where you have different toolbar buttons and menu items that need to be enabled depending on various states of the underlying customer object. For example, let's say you're analyzing resellers of a product. You're creating an application that includes handling responses to the state of what a reseller has done with the product you've sold them. So, one state may be "the reseller has paid", another state may be "the reseller has advertized the product to others", while another state may be "we have received feedback from the reseller".

So, for each reseller, you have three booleans in the domain object: "paid", "advertized", and "receivedFeedback". Now you need to provide UI elements (e.g., toolbar buttons) for handling combinations of these three states. I.e., if the customer has paid AND has advertized AND you have received feedback from them, you'll want to thank them and give them a free ticket to great seats at the superbowl. On the other hand, if they've paid for the product but you have not received feedback, you'd like to send them an e-mail that says "thanks for paying, now we'd like some feedback about the product".

So, let's look at some screenshots. In the first screenshot, the customer has paid and has advertized the product, but hasn't sent feedback. Hence, the "Request Feedback" button is enabled, while "All Done" is disabled:

In this case, however, the feedback from the customer has been received, as well as the other status items being satisfied, hence the "All Done" action is enabled, with the "Request Feedback" being disabled:

The question for this blog entry is, therefore, how to effectively implement the above. Should you, for example, create a new Action for each and every combination? That would be a lot of work, especially since that's not necessary.

Here's the complete code sample discussed below:

http://blogs.sun.com/geertjan/resource/WakeUpActionsDemo.zip

To solve the above scenario effectively, we create a module where we have an interface defining something we name "EnableStrategy":

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public interface EnableStrategy extends ActionListener{
public boolean isEnabled(Object o);
@Override
public void actionPerformed(ActionEvent e);
}

Next, we create an Action that will receive strategies registered in the layer and will process them:

public class StateDependentCapability extends AbstractAction
implements LookupListener, Presenter.Toolbar {
private Lookup.Result result;
private JButton toolbarBtn;
private EnableStrategy delegate;
public StateDependentCapability(FileObject config) {
//Get, from the layer, the FileObject's attributes:
super((String)config.getAttribute("displayName"),
new ImageIcon(ImageUtilities.loadImage((String)config.getAttribute("iconBase"))));
delegate = (EnableStrategy) config.getAttribute("enableStrategy");
//Get the Customer from the Lookup:
result = Utilities.actionsGlobalContext().lookupResult(Customer.class);
result.addLookupListener(this);
resultChanged(new LookupEvent(result));
}
//This will be used from the layer, to create new strategies:
public static StateDependentCapability createFactory(FileObject config){
return new StateDependentCapability(config);
}
@Override
public void actionPerformed(ActionEvent e) {
if (null != result && 0 < result.allInstances().size()) {
Customer v = (Customer) result.allInstances().iterator().next();
if (null != v) {
//do something to to the reseller,
//as defined in the strategy:
delegate.actionPerformed(e);
}
}
}
//This comes from the Toolbar.Presenter, which we need
//to access the toolbar button so that we can enable/disable it,
//note that below it is disabled by default,
//enabled only in the ResultChanged:
@Override
public Component getToolbarPresenter() {
if (null == toolbarBtn) {
toolbarBtn = new JButton(this);
toolbarBtn.setEnabled(false);
}
return toolbarBtn;
}
@Override
public void resultChanged(LookupEvent arg0) {
//Enable the action and the button
//based on current the strategy:
if (result.allInstances().size() > 0) {
Customer v = (Customer) result.allInstances().iterator().next();
setEnabled(delegate.isEnabled(v));
if (null != toolbarBtn) {
toolbarBtn.setEnabled(delegate.isEnabled(v));
}
}
//Activate the window, to make it the current window,
//so that a change in the Properties window causes an immediate
//update of the menu item and toolbar button:
WindowManager.getDefault().invokeWhenUIReady(new Runnable() {
@Override
public void run() {
TopComponent tc = WindowManager.getDefault().findTopComponent("CustomerTopComponent");
tc.open();
tc.requestActive();
}
});
}
}

Now we've set up the infrastructure for our strategies. Let's use our infrastructure!

Create new modules for each strategy. (Remember, that's the title of this blog entry.) Each provider of a strategy needs entries like this in the layer:

<folder name="Actions">
<folder name="File">
<file name="org-company-strategy-alldone-AllDoneStrategy.instance">
<attr name="instanceCreate" methodvalue="org.w.api.StateDependentCapability.createFactory"/>
<attr name="enableStrategy" newvalue="org.company.strategy.alldone.AllDoneStrategy"/>
<attr name="iconBase" stringvalue="org/company/strategy/alldone/AllDoneIcon.png"/>
<attr name="displayName" stringvalue="All Done" />
</file>
</folder>
</folder>
<folder name="Toolbars">
<folder name="File">
<file name="org-company-strategy-alldone-AllDoneStrategy.shadow">
<attr name="originalFile" stringvalue="Actions/File/org-company-strategy-alldone-AllDoneStrategy.instance"/>
<attr name="position" intvalue="1300"/>
</file>
</folder>
</folder>

So, the above is for the "AllDoneStrategy". The "instanceCreate" will use "createFactory" in our "StateDependentCapability" class to create a new action, passing in the "enableStrategy", "iconBase", and "displayName". And here's the definition of the "AllDoneStrategy":

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JOptionPane;
import org.customer.Customer;
import org.w.api.EnableStrategy;
public class AllDoneStrategy implements ActionListener, EnableStrategy {
@Override
public boolean isEnabled(Object o) {
Customer c = (Customer) o;
if (c.isAdvertized() && c.isPaid() && c.isReceivedFeedback()) {
return true;
} else {
return false;
}
}
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(null, "all done");
}
}

So, the logic for defining the strategy is in a separate class, in a separate module. Nice, isn't it. You have only defined one action and are reusing it via the layer for all the strategies relevant to your application.

This is one of the great and practical examples that Toni created during the NetBeans Platform Training at Marakana, here, in San Francisco.

Join the discussion

Comments ( 3 )
  • James Reid Wednesday, September 29, 2010

    Very nice post.


  • R&eacute;mi Emonet Monday, October 4, 2010

    Hi Geertjan,

    Thank you for your blog as it is really interesting and a mine of information about the Netbeans platform.

    I am the original author of Omiscid Gui that you briefly cited in one of your post one year ago (http://blogs.sun.com/geertjan/entry/omiscid_gui_on_the_netbeans).

    For Omiscid Gui, I targeted maximum modularity for the actions:

    \* obviously, new actions can be added by different modules,

    \* and, more originally, the decision to enable or disable actions can also be taken by additional modules (not even the module providing the action).

    The approach I used is to implement a new kind of Lookup which enrich an existing delegate Lookup. The enrichment is done using an extensible set of "selectors" that can be registered via the layer file. The selectors can add "higher-level elements" to the lookup. The conditional actions can then rely on the presence of these "higher-level elements" in the Lookup to decide whether they are enabled.

    Taking an example of this approach, we can have:

    http://omiscid.gforge.inria.fr/download/doc-all/extending-omiscidgui/starter.xhtml#slide26

    \* a base lookup for a selection in a Tree

    \* a set of selectors that use the selection to produce "task descriptors" (multiple selectors can produce the same kind of tasks)

    \* a set of actions that each depend on the presence of some tasks (multiple actions can consume the same kind of tasks).

    I just published the code in case someone is interested: http://idiap.ch/~remonet/dl/SelectorBasedLookup.zip

    Also, examples taken from a simple tutorial can be found at http://omiscid.gforge.inria.fr/download/doc-all/extending-omiscidgui/DEMOModule/src/fr/prima/omiscidgui/demolive/

    This code is probably not perfect and all comments are welcome.

    If I see interested people here, I will probably post more details about this approach.


  • Geertjan Wielenga Tuesday, October 5, 2010

    Hi Rémi, it would be very interesting and useful if you'd write a short article about all of this on http://netbeans.dzone.com. Please contact me at geertjan dot wielenga at oracle dot com for the details.


Please enter your name.Please provide a valid email address.Please enter a comment.CAPTCHA challenge response provided was incorrect. Please try again.