Platinum Partner
java

Getting Even Further with Spring RCP (1)

In Getting Further with Spring RCP, we rebuilt part of the "Simple" sample from the Spring RCP distribution from scratch. We looked at a number of Spring RCP classes—"AbstractObjectTable", "AbstractView", "CommandGroup", "AbstractForm", "TableFormBuilder", "TitledPageApplicationDialog", and "DefaultRulesSource".

In this part, we'll go a few steps further. We'll get acquainted with the "CompositeForm" class, which we'll use to create two different layouts for changing our data, as shown in the two screenshots below, first a tabbed view and then a tree-based hierarchical view:

The cool thing about the above two dialogs is that the difference between them is only one line of code.

After that, we'll change the table display of our data to a tree hierarchy:

Finally, we'll look at some other ways of presenting our data, such as these:

Again, the difference between the two data views above is one line of code!

Table of Contents

 

Note: The completed example, from the end of the second section of this part, is available as a NetBeans project, as part of the Spring RCP Tooling plugin in the NetBeans Plugin Portal, from version 1.4 of the plugin onwards. Open the New Project wizard (Ctrl-Shift-N) and you should find "Spring RCP Tutorial Part 2" in the "Samples | Spring Rich Client" category. The sample "Spring RCP Tutorial Part 3" provides the code for the second part of this article, which discusses some alternative explorer views that you could use to display your data to the user.

 

Creating a Composite Dialog

In Getting Further with Spring RCP, we created a Customer Properties dialog. It extended the Spring RCP "TitledPageApplicationDialog" class. It displayed the content of one single Spring RCP "AbstractForm" class, the CustomerForm. However, what if you want to display multiple forms within the same dialog? Conceptualizing this is important—it underlines the difference between a dialog and a form, in the Spring RCP world, because they're not the same thing. You'll see, by the end of this section, that an "AbstractForm" can be seen as a display unit that is decoupled from the dialog that displays it.

To really understand this, I split my CustomerForm class into two separate form classes—one called "CustomerForm" and the other "AddressForm". For example, this is what the CustomerForm looks like now, with all address-related content removed into the separate "AddressForm" class, which contains nothing but address-related content:

public class CustomerForm extends AbstractForm {

private JComponent firstNameField;

public CustomerForm(Customer customer) {
super(customer);
setId("customer");
}

@Override
protected JComponent createFormControl() {
TableFormBuilder formBuilder = new TableFormBuilder(getBindingFactory());
formBuilder.setLabelAttributes("colGrId=label colSpec=right:pref");
formBuilder.addSeparator("General");
formBuilder.row();
firstNameField = formBuilder.add("firstName")[1];
formBuilder.add("lastName");
formBuilder.row();
return formBuilder.getForm();
}

public boolean requestFocusInWindow() {
return firstNameField.requestFocusInWindow();
}

}

 

However, now that we have TWO forms, how do we let the user work with both simultaneously? The answer is twofold—firstly, by means of the Spring RCP "TabbedDialogPage" class; secondly (alternatively), by means of the Spring RCP "TreeCompositeDialogPage" class. These are containers that organize other dialog pages into a tabbed structure or a tree structure. Let's first look at the code that relates to the "TabbedDialogPage" class. The result of that code will be a dialog with two tabs, each displaying one of the two forms, together providing one user interface for editing the data in question:

 

And here's the code:

private class PropertiesExecutor extends AbstractActionCommandExecutor {

private CustomerForm customerForm;
private AddressForm addressForm;
private CompositeDialogPage compositePage;
private TitledPageApplicationDialog dialog;

private void createDialog() {

customerForm = new CustomerForm(new Customer());
addressForm = new AddressForm(new Customer());

compositePage = new TabbedDialogPage("customerProperties");
compositePage.addForm(customerForm);
compositePage.addForm(addressForm);

dialog = new TitledPageApplicationDialog(
compositePage, getWindowControl(), CloseAction.HIDE) {

@Override
protected void onAboutToShow() {
customerForm.requestFocusInWindow();
setEnabled(compositePage.isPageComplete());
}

@Override
protected boolean onFinish() {
return true;
}

};

}

@Override
public void execute() {
if (dialog == null) {
createDialog();
}
customerForm.setFormObject(customerTable.getSelectedCustomer());
addressForm.setFormObject(customerTable.getSelectedCustomer());
dialog.showDialog();
}

}

 

To use the Spring RCP "TreeCompositeDialogPage" class instead, there's literally nothing more you need to do than change line 13 above to the following:

compositePage = new TreeCompositeDialogPage("customerProperties");

 

The result of the above, i.e., the result of changing ONE line of code, would be the following:

 

You wil notice, though, that when you use any of the above dialogs, and you make a change, the change is NOT stored. That's because we need to commit changes back to the model, which is the typical way in which Spring RCP works. The model is, normally, provided by the Spring RCP "FormModel" class. However, in this case, we're not dealing with forms anymore, we're primarily dealing with the container that contains those forms. Therefore, we need to introduce the Spring RCP "HierarchicalFormModel" class. Rewrite all the code above to the following, taking note of the "HierarchicalFormModel" class used in lines 3, 14, 16, 17, and 33:

private class PropertiesExecutor extends AbstractActionCommandExecutor {

private HierarchicalFormModel ownerFormModel;

private CustomerForm customerForm;
private AddressForm addressForm;

private CompositeDialogPage compositePage;

private TitledPageApplicationDialog dialog;

private void createDialog() {

ownerFormModel = FormModelHelper.createCompoundFormModel(new Customer());

customerForm = new CustomerForm(FormModelHelper.createChildPageFormModel(ownerFormModel, null));
addressForm = new AddressForm(FormModelHelper.createChildPageFormModel(ownerFormModel, null));

compositePage = new TreeCompositeDialogPage("customerProperties");
compositePage.addForm(customerForm);
compositePage.addForm(addressForm);

dialog = new TitledPageApplicationDialog(compositePage, getWindowControl(), CloseAction.HIDE) {

@Override
protected void onAboutToShow() {
customerForm.requestFocusInWindow();
setEnabled(compositePage.isPageComplete());
}

@Override
protected boolean onFinish() {
ownerFormModel.commit();
return true;
}

};

}

@Override
public void execute() {
if (dialog == null) {
createDialog();
}
customerForm.setFormObject(customerTable.getSelectedCustomer());
addressForm.setFormObject(customerTable.getSelectedCustomer());
dialog.showDialog();
}

}

 

Now, when you click Finish, thanks to line 33 the changes will be saved to the model and reflected back in the view. Finally, let's look at the related "messages.properties" file. These are the key/value pairs that are relevant here:

customerProperties.title=Customer Properties

customerProperties.customer.title=Customer
customerProperties.customer.description=Changes customer properties

customerProperties.address.title=Address
customerProperties.address.description=Change address properties

 

Take careful note of the above. As with all Spring RCP messages, the "." in the key is significant. The first message uses the ID of the container (i.e., "customerProperties"), followed by the property ("title"). However, the others make use of two IDs: the ID of the container ("customerProperties"), followed by the ID of the form ("customer" or "address"), which is then followed by the property ("title" or "description").

Hurray, you've created your first Spring RCP "CompositeForm", making use of two separate form classes, decoupled from each other and from the dialog in which they're displayed. In this way, you can mix and match multiple forms, reusing them in different ways across different dialogs. Also, look at how easy it was to create completely different layouts—it took ONE change to switch from a tabbed view to a tree view. Bear these concepts in mind for the duration of this article, because it will come back again in a different context later.

 

Replacing the Table with a Tree

In the previous part, we displayed our data in a table, using the Spring RCP "AbstractObjectTable" class. This time, instead, we'll display our data in a tree:

     

  1. Start by simply pasting the following into the existing CustomerView class:
    private void createTree() {
    DefaultMutableTreeNode rootNode =
    new DefaultMutableTreeNode("Customers");
    Object[] cutomers = customerDataStore.getAllCustomers();
    for (int i = 0; i < cutomers.length; i++) {
    Customer customer = (Customer) cutomers[i];
    DefaultMutableTreeNode customerNode =
    new DefaultMutableTreeNode(customer);
    customerNode.add(new DefaultMutableTreeNode(
    customer.getAddress().getAddress1()));
    customerNode.add(new DefaultMutableTreeNode(
    customer.getAddress().getCity()));
    customerNode.add(new DefaultMutableTreeNode(
    customer.getAddress().getState()));
    rootNode.add(customerNode);
    }
    treeModel = new DefaultTreeModel(rootNode);
    tree = new JTree(treeModel);
    tree.setShowsRootHandles(true);
    tree.setCellRenderer(getTreeCellRenderer());
    tree.setRootVisible(true);
    }

    public TreeCellRenderer getTreeCellRenderer() {
    return treeCellRenderer;
    }

    private DefaultTreeCellRenderer treeCellRenderer =
    new FocusableTreeCellRenderer() {
    @Override
    public Component getTreeCellRendererComponent(
    JTree tree, Object value, boolean sel,
    boolean expanded, boolean leaf, int row,
    boolean hasFocus) {
    super.getTreeCellRendererComponent(
    tree, value, sel, expanded, leaf, row, hasFocus);
    DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
    if (node.isRoot()) {
    this.setIcon(getIconSource().getIcon("folder.icon"));
    } else {
    Object userObject = node.getUserObject();
    if (userObject instanceof Customer) {
    Customer o = (Customer) userObject;
    this.setText(o.getFirstName() + " " + o.getLastName());
    this.setIcon(getIconSource().getIcon("icon1"));
    } else {
    this.setIcon(getIconSource().getIcon("icon2"));
    }
    }
    return this;
    }
    };
  2.  

  3. Next, change the "CustomerView.createControl" as follows:
    @Override
    protected JComponent createControl() {
    //No need to create the customer table anymore:
    //customerTable = new customerTableFactory().createCustomerTable();
    //Create the tree instead:
    createTree();
    JPanel view = new JPanel(new BorderLayout());
    //No need to add the JTable to the JScrollPane anymore:
    //JScrollPane sp = getComponentFactory().createScrollPane(customerTable.getControl());
    //Add the tree to the JScrollPane instead:
    JScrollPane sp = getComponentFactory().createScrollPane(tree);
    view.add(sp, BorderLayout.CENTER);
    return view;
    }

 

Notice something interesting—or, in fact, boring—we've done a lot of coding to replace our AbstractObjectTable with a JTree. And none of what we've done is specific to Spring RCP—we've only used a standard Swing JTree, after all. However, we're displaying the same data as before. When we were dealing with the Spring RCP forms, how much coding did we need to do to replace one dialog with another? Just one line. We replaced the instantiation of the "TabbedDialogPage" with the instantiation of the "TreeCompositeDialogPage". Wouldn't it be cool if we could do the same thing for our data... and not just for our forms? That's what the next section is all about.

 

Integrating Flexible Views

Here is the same view as before, but created very differently:

 

The above can be created like this, instead of the convoluted approach demanded by the typical complex Swing components, such as JTree and JTable, and so on:

private class ViewPanel extends javax.swing.JPanel implements ExplorerManager.Provider {

private ExplorerManager em;

private ViewPanel() {
setLayout(new BorderLayout());
em = new ExplorerManager();
em.setRootContext(CustomerNode.customers());
BeanTreeView view = new BeanTreeView();
view.setRootVisible(false);
add(view);
}

@Override
public ExplorerManager getExplorerManager() {
return em;
}

}

 

The "ExplorerManager" class that you see referred to above is sensitive to explorer views, such as the "BeanTreeView" instance you see referenced above, and displays its root context in any of the currently available views that it knows about. So now... we change ONE line of code—just compare line 9 above with line 9 below:

private class ViewPanel extends javax.swing.JPanel implements ExplorerManager.Provider {

private ExplorerManager em;

private ViewPanel() {
setLayout(new BorderLayout());
em = new ExplorerManager();
em.setRootContext(CustomerNode.customers());
IconView view = new IconView();
//view.setRootVisible(false);
add(view);
}

@Override
public ExplorerManager getExplorerManager() {
return em;
}

}

 

...and you get a completely different view:

 

Simply replace the dummy images with real ones (for example, headshots of the people referred to above) and then, for changing just one line of code, you've presented your users with a completely new view on top of their data. All you need to do, after creating JPanels such as the above, is add them to your "CustomerView.createControl":

@Override
protected JComponent createControl() {
ViewPanel panel = new ViewPanel();
return panel;
}

 

Clearly, this is the same idea as the Spring RCP dialogs discussed at the start of this article, except that this time the idea is applied to the display of data instead of forms. And where do the above classes (i.e., ExplorerManager, IconView, and BeanTreeView) come from? From the NetBeans Platform! And there are about 5 or 6 other classes just like them.

To make the above code possible, simply add the following JARs from the NetBeans IDE "platform" folder to your classpath: org-openide-util.jar, org-openide-nodes.jar, org-openide-explorer.jar, org-openide-awt.jar, org-openide-actions.jar, and org-openide-dialogs.jar. One might consider this a lot of new JARs to introduce to a project simply for creating new data views. On the other hand, there's many lines of boilerplate Swing component code that you don't need to write anymore and remember that... code you don't need to write is code that you don't need to test, debug, or maintain either. And aside from the less coding, it's clearly just as easy to switch from "BeanTreeView" to "IconView" (or any other view) as it is to switch from "TabbedDialogPage" to "TreeCompositeDialogPage". In other words, conceptually this part of the NetBeans Platform integrates seamlessly with Spring RCP. Good news for everyone.

 

Constructing a Node Hierarchy

Line 8 of the two main snippets shown in the previous section retrieves the data via a call to "CustomerNode.customers".

That call is to a class that extends "org.openide.nodes.AbstractNode", which is one of the classes from the NetBeans Platform JARs referred to previously.

In effect, in this class you model the nodes in the view, which represent the data in your model.

Looking at it another way, in this class you build your data nodes in the way that you build your form via the Spring RCP Form Builder.

Perhaps you can, therefore, think of the code below as an example of a Data Node Builder in action!

I believe the code below, though slightly lengthy, is relatively understandable without too much explanation.

First the parent node is created, then its children, which each get their own node:

public final class CustomerNode extends AbstractNode {

private static Customer customer;

private static CustomerDataStore customerDataStore = new CustomerDataStore();

private CustomerNode(Customer c) {

super(new CustomerChildren(c));
customer = c;

}

public static Node customers() {

AbstractNode n = new AbstractNode(new CustomerChildren(customer));
return n;

}

private static final class CustomerChildren extends Children.Keys<Customer> {

Customer customer;

public CustomerChildren(Customer customer) {
this.customer = customer;
}

@Override
protected void addNotify() {
if (customer == null) {
Customer[] objs = customerDataStore.getAllCustomers();
setKeys(objs);
}
}

@Override
protected Node[] createNodes(Customer c) {
AbstractNode n = new AbstractNode(new AddressChildren(c.getAddress()));
n.setDisplayName(c.getFirstName() + " " + c.getLastName());
return new Node[]{n};
}

}

private static final class AddressChildren extends Children.Keys<Address> {

Address address;

CustomerView view;

public AddressChildren(Address address) {
this.address = address;
}

@Override
protected void addNotify() {

Customer[] objs = customerDataStore.getAllCustomers();
for (int i = 0; i < objs.length; i++) {
if (objs[i].getAddress().getAddress1().equals(address.getAddress1())) {
Customer customer = objs[i];
setKeys(new Address[]{customer.getAddress()});
}
}

}

@Override
protected Node[] createNodes(Address a) {

AddressNode addressNode = new AddressNode();
addressNode.setDisplayName(a.getAddress1());

AddressNode cityNode = new AddressNode();
cityNode.setDisplayName(a.getCity());

AddressNode stateNode = new AddressNode();
stateNode.setDisplayName(a.getState());

AddressNode zipNode = new AddressNode();
zipNode.setDisplayName(a.getZip());

return new Node[]{addressNode,cityNode,stateNode,zipNode};

}

}

public static final class AddressNode extends AbstractNode {

private AddressNode() {

super(Children.LEAF);

}

}

}

 

 

Conclusion

In this part of the Spring RCP series, I've tried to show how composite dialogs are created, via loose coupling of forms, which in that sense can be seen as "display units" that delegate rendering to Spring RCP containers—TabbedDialogPage and TreeCompositeDialogPage. I've also tried to show how, instead of recoding complex Swing components (and their models) whenever you need to display your data in a new view, you can very easily integrate NetBeans Platform JARs and thereby reuse a central concept used by that platform—the separation of data from views, which is comparable to the Spring RCP approach of separating forms from dialogs.

(And now read on... in the next part of this series!) 

 

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}