Build a Rich Client Platform To-Do Application in NetBeans IDE

by John N. Kostaras

Practice using NetBeans IDE features that improve code quality and increase developer productivity.

Published May 2014

This article shows how to use NetBeans IDE 7.4 to develop a Swing-based "to-do" application, and it demonstrates the use of a rich client platform (RCP). It is an update of "A Complete App Using NetBeans 5" by Fernando Lozano, which was originally published in NetBeans Magazine.

Note: NetBeans IDE 7.4 requires JDK 7, while earlier versions up through NetBeans IDE 7.3 can run with JDK 6.

You can download and then unzip the original application to compare it with the RCP to-do application that you'll develop in this article. To compare the applications, open the original application in NetBeans IDE and create a new project group by right-clicking somewhere inside the Project tab and selecting Project Group -> New Group. Give the project group a name and click Create Group. Later, we'll create another group for the new RCP to-do application. Then you can compare the two applications by switching between the two project groups by right-clicking and selecting Project Group and the appropriate group.

Note: The source code for the RCP to-do application developed in this article can be downloaded here.

Developing the RCP To-Do Application

The example application builds a to-do list, which is commonly found as part of privileged identity management (PIM) suites. It won't just demo the NetBeans IDE's RCP features; it also sticks to object-oriented best practices, showing that you can develop GUI applications quickly and interactively without compromising long-term maintenance and a sound architecture.

You'll develop the to-do application in three steps:

  1. Build a "static" visual prototype of the GUI, using the NetBeans IDE's visual GUI builder to build a task list window.
  2. Build a "dynamic" prototype of the application, coding user interface events and associated business logic and creating customized GUI components as needed.
  3. Code the persistence logic by modeling the classes and the database.

These steps form a process that starts with the View, then builds the Controller, and finally builds the Model (the familiar MVC architecture).

Your application requirements include the following:

  • Tasks should have a priority, so users can focus first on higher-priority tasks.
  • Tasks should have a due date, so users can focus on tasks that are closer to their deadline.
  • Tasks that are either late or near their deadlines should have visual cues.
  • Tasks can be marked as completed, but this doesn't mean they have to be deleted or hidden.

You will have two main windows for the to-do application: a task list window and a task editing form. Figure 1 shows a rough sketch of both.

rcp-todo-f1

Figure 1

Step 1: Build a Static Prototype of the GUI

To get started, click the New Project toolbar button and select NetBeans Platform Application in the NetBeans Modules category (see Figure 2). Enter TodoRCP as the project name and choose a suitable project location (anywhere on your hard disk), as shown in Figure 3. Then click Finish.

rcp-todo-f2

Figure 2

rcp-todo-f3

Figure 3

NetBeans creates the TodoRCP project containing an empty Modules folder and an Important Files folder, which act as a container for the modules that will be created in the rest of this article.

Right-click the Modules folder icon and choose Add New. Type View as the module name and click Next. Type todo.view in the Code Name Base field and then click Finish (see Figure 4).

rcp-todo-f4

Figure 4

In NetBeans IDE 7.4, the Generate XML Layer checkbox has been removed from the second step of the wizard. To create the XML layer, right-click the module and select New -> Other -> Module Development -> XML Layer.

The XML layer is a file named layer.xml, and each module can have one. NetBeans IDE's RCP functionality combines all layer.xml files during runtime and creates the central registry of the application. Right-click the View module and select Open Project (see Figure 5).

rcp-todo-f5

Figure 5

The new module contains a package called todo.view. You now need to create your view. But instead of creating a JFrame form, as you do in the Swing application, you’ll create its RCP equivalent, a TopComponent. If the View module isn't already open, right-click it and select New -> Window.

The dialog box asks you for the window position. The NetBeans IDE has various windows that can be positioned; the Editor window is the main area, Output is the lower area where messages are displayed, and so on. Make the selections shown in Figure 6 and click Next.

rcp-todo-f6

Figure 6

The Class Name Prefix field specifies the name of the frame or panel that will be created along with some other help files. Enter the name Tasks (instead of TasksWindow, as in the original article) and click Finish. (See Figure 7.)

rcp-todo-f7

Figure 7

Two files are created, TasksTopComponent.java and TasksTopComponent.form, and the form is opened in Design view in the Editor window along with the Palette window. Figure 8 shows the NetBeans IDE GUI and its components.

Note: If you can see the TasksTopComponent.form file in the Projects window but NetBeans IDE complains that it can't recognize the file when you open it, you need to activate the Java SE plugin by selecting Tools -> Plugins -> Installed.

rcp-todo-f8

Figure 8

Notice the location of the Projects and Navigator windows on the left, and the Editor window in the center. A red frame highlights the selected component (TopComponent). The Navigator displays all visual and nonvisual components of TopComponent, which is handy when you need to change the properties of a component that's hidden by another or that's too small to be selected in the drawing area.

To the right is the Palette window, which by default shows the standard Swing components (you can also add third-party JavaBeans). Also to the right is the Properties window. Properties are categorized to ease access to the ones most commonly used, and changed properties have their names highlighted in bold.

To change the visual layout of the IDE, you can drag each window to another corner of the main window or even leave some windows floating around by right-clicking their tab and selecting Float.

In previous versions of NetBeans IDE, layer.xml contained a TasksTopComponent displayed in Editor mode. In NetBeans IDE 7.4, this information exists in annotations. Click Source to view the annotations shown in Listing 1 and compare them with the dialog box shown in Figure 6.

@ConvertAsProperties(
        dtd = "-//todo.view//Tasks//EN",
        autostore = false
)
@TopComponent.Description(
        preferredID = "TasksTopComponent",
        //iconBase="SET/PATH/TO/ICON/HERE", 
        persistenceType = TopComponent.PERSISTENCE_ALWAYS
)
@TopComponent.Registration(mode = "editor", openAtStartup = true)
@ActionID(category = "Window", id = "todo.view.TasksTopComponent")
@ActionReference(path = "Menu/Window" /* , position = 333 */)
@TopComponent.OpenActionRegistration(
        displayName = "#CTL_TasksAction",
        preferredID = "TasksTopComponent"
)
@Messages({
    "CTL_TasksAction=Tasks",
    "CTL_TasksTopComponent=Tasks Window",
    "HINT_TasksTopComponent=This is a Tasks window"
})

Listing 1

Notice that mode = "editor" (if not, here's your chance to change it) and openAtStartup = true (again, if not, here's your chance to change it).

The NetBeans IDE visual editor is unlike other visual Java editors you might have seen. Just right-click inside TopComponent and select the Set Layout menu item. You see that the default choice isn't a traditional Swing/AWT layout manager; it’s something named Free Design. This means you're using the Matisse Visual GUI builder. Matisse configures TopComponent to use the GroupLayout layout manager developed in the SwingLabs java.net project, which is included as a standard layout manager in Java SE 6.

As shown in Figure 1, the task list consists of a menu bar, a toolbar, a table, and a status bar. Apart from the table, which in RCP is called OutlineView, the platform handles all the rest. To add OutlineView, you need to add a dependency to the Explorer & Property Sheet API. Right-click the Libraries folder of the View module and select Add Module Dependency, select Explorer & Property Sheet API, and click OK. Right-click TopComponent in the Design view and change its layout to BorderLayout.

Now we have two options:

  • Option 1: The first option is rather a hack, but it's faster. Drag a ScrollPane from the Palette window and drop it in the center of TasksTopComponent. Right-click JScrollPane in the Navigator window (look at the left bottom) and change its variable name to outlineView. In the Properties window, click Code, and then click the small button [...] of the Custom Creation Code property, and add new OutlineView(). Switch to the Source view in the Editor window, right-click, and select Fix Imports to fix the errors.
  • Option 2: The second option is to add the visual components of the Explorer & Property Sheet API to the Palette window. If the Palette window isn't visible, display it by selecting Window -> Palette. Right-click inside the Palette window and select Palette Manager. Create a new category by clicking New Category and name the category something like NetBeans RCP or NetBeans platform components. Then click the Add from JAR button, navigate to <Netbeans installation> -> platform -> modules, select org-openide-explorer.jar, and click Next. Select all available components (as shown in Figure 9) and click Next. Select the category you created previously and click Finish. Click Close to close the Palette Manager. Now you see the new category in the Palette window. Locate the OutlineView component and drag it inside the form.
rcp-todo-f9

Figure 9

When you run the application, you will see what's shown in Figure 10.

rcp-todo-f10

Figure 10

Look at how many things you get out of the box with NetBeans IDE, which in a normal Swing application you would have to develop yourself: a menu bar and a toolbar (which need customization), a status bar, and a strange tree-table component—and all of these have just one line of code.

Let's start by customizing the menu. A central registry holds information about every module of the RCP to-do application. You can find the menu bar in this central registry if you click Important Files -> XML Layer -> <this layer in context> -> Menu Bar (see Figure 11).

rcp-todo-f11

Figure 11

In Figure 1, you saw that we need only File, Edit, and Options menus.

So, keep File and Edit, rename Tools to Options (by selecting it, right-clicking, and selecting Rename), and remove the rest (by selecting them, right-clicking, and selecting Delete). The result should look something like Figure 12.

rcp-todo-f12

Figure 12

Now you can start building your prototype. If you followed Option 1 earlier, open TasksTopComponent and at the end of the constructor, add the lines of code shown in Listing 2:

OutlineView ov = (OutlineView)outlineView;

//Set the columns of the outline view,
//using the name of the property
//followed by the text to be displayed in the column header:
ov.setPropertyColumns(
       "priority", "Priority",
       "description", "Task",
       "alert", "Alert",
       "dueDate", "Due Date");

//Hide the root node, since we only care about the children:
ov.getOutline().setRootVisible(false);
TableColumnModel columnModel = ov.getOutline().getColumnModel();
ETableColumn column = (ETableColumn) columnModel.getColumn(0);
((ETableColumnModel) columnModel).setColumnHidden(column, true);

Listing 2

Or, if you chose Option 2 earlier, add the lines of code shown in Listing 3:

//Set the columns of the outline view,
//using the name of the property
//followed by the text to be displayed in the column header:
outlineView.setPropertyColumns(
       "priority", "Priority",
       "task", "Task",
       "alert", "Alert",
       "dueDate", "Due Date");

//Hide the root node, since we only care about the children:
outlineView.getOutline().setRootVisible(false);
TableColumnModel columnModel = ov.getOutline().getColumnModel();
ETableColumn column = (ETableColumn) columnModel.getColumn(0);
((ETableColumnModel) columnModel).setColumnHidden(column, true);

Listing 3

The only difference between Listing 2 and Listing 3 is the following line of code, which isn’t needed in the second case because you don't need to cast JScrollPane to an OutlineView:

OutlineView ov = (OutlineView)outlineView; 

To be able to compile the code, you need to add one more dependency to the ETable and Outline module. You remember how to do this, right? Here’s a hint: right-click the Libraries folder. If you are successful, your application should look like Figure 13:

rcp-todo-f13

Figure 13

Now create the status bar. To display a StatusBar in NetBeans, a class must implement the StatusLineElementProvider interface and declare it as a service. Right-click the todo.view package and select New -> Java Class. Name it StatusBar and click Finish. Then copy the code shown in Listing 4 and paste it in:

package todo.view;

import java.awt.Component;
import javax.swing.JLabel;
import org.openide.awt.StatusLineElementProvider;
import org.openide.util.lookup.ServiceProvider;

/**
 * The application's status bar.
 * @author jnk
 */
@ServiceProvider(service=StatusLineElementProvider.class, position=1)
public class StatusBar implements StatusLineElementProvider {

    @Override
    public Component getStatusLineElement() {
        return new Jlabel("There are no task alerts for today.");
    }
    
}

Listing 4

Don't forget to right-click and select Fix imports. Then right-click and select Format to format the code.


So, what's going on here? As you can see, the StatusLineElementProvider interface is flexible; you can return any component you want—a JLabel, JButton, JPanel, and so on. But of course, the magic thing is the first line, which declares the StatusBar class as a service provider of StatusLineElementProvider.class.

What does this mean? Well, this is where lookups come in. In short, this line adds your class to the application's default lookup, which NetBeans IDE then searches for StatusLineElementProvider.class, and then it adds all the providers it finds to the status bar.

Now it's time to create the toolbars. A toolbar contains actions, so you'll create the actions and insert them into the appropriate toolbars in the central registry. You'll need the icons from the original to-do application, so if you haven't done so, download the zip file and unzip it on your disk.

The actions are controllers, so create a new module called Controller as you learned to do at the beginning of this article, and use todo.controller as the code name base. Since you're here, also create the Model module using todo.model as the code name base.

Open the Controller module and create the packages todo.controller.file, todo.controller.edit, and todo.controller.options. Right-click the todo.controller.edit package and select New Action. Create the Add Task action, which is always enabled, and click Next.

Then select a category for the action. The categories represent semantic groupings of the actions. You can select a pre-existing category or create a new one. In our case, select the pre-existing Edit category. In addition, assign your action to the Edit menu bar and the Edit toolbar, and set the position where the action will be displayed. Drop-down menus show you possible locations for display. "HERE" identifies the location where the display of your action will be inserted. Don't forget to add a keyboard shortcut (Insert, as you'd use in the old to-do application). See Figure 14.

rcp-todo-f14

Figure 14

Name the class as shown in Figure 15, and don't forget to add an icon.

rcp-todo-f15

Figure 15

The AddTaskAction class is created (see Listing 5).

package todo.controller.edit;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import org.openide.awt.ActionRegistration;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.ActionID;
import org.openide.util.NbBundle.Messages;

@ActionID(category = "Edit",
id = "todo.controller.edit.AddTaskAction")
@ActionRegistration(iconBase = "todo/controller/edit/add_obj.gif",
displayName = "#CTL_AddTaskAction")
@ActionReferences({
    @ActionReference(path = "Menu/Edit", position = 10),
    @ActionReference(path = "Toolbars/Edit", position = 10),
    @ActionReference(path = "Shortcuts", name = "Insert")
})
@Messages("CTL_AddTaskAction=Add Task...")
public final class AddTaskAction implements ActionListener {

    public void actionPerformed(ActionEvent e) {
        // TODO implement action body
    }
}

Listing 5

Look how the input from the wizard is translated into Java annotations in NetBeans IDE 7.4. In versions prior to NetBeans IDE 7.0, this information was added in layer.xml. Modify the position to be 10 in both cases to avoid mix-ups with the next actions you create. Add 10 to the position of every new action—that is, for EditTaskAction set the position to 20, for DeleteTaskAction set it to 30, and so on. Leave the action body empty for the moment.

By executing the old to-do application, you might have noticed that while the Add Task action is always enabled, the other task actions are enabled only when you select one or more tasks from the table—in other words, they are context actions. You can accomplish the same functionality by using a conditionally enabled action. These actions operate on nodes. A node is the visual representation of a particular piece of data—a task, in our example. When you select a row from the table, you are actually selecting a task node. Context sensitivity is constructed from interfaces, which are called cookies. The node on which the action is to operate implements an interface specifying the method that the action should invoke. The action can specify a set of cookies, the presence of which in the active node (if the active node implements one of these interfaces) determines whether the action is enabled or not.

To create EditTaskAction, right-click the todo.controller.edit package and select New Action. Select Conditionally Enabled and User Selects One Node (see Figure 16). The cookie class is a Task. This means that whenever a task (row) is selected in the OutlineView, this action is enabled.

rcp-todo-f16

Figure 16

Click Next and complete the rest of steps as you did for AddTaskAction. If you completed the wizard correctly, you should see the output shown in Listing 6.

package todo.controller.edit;

import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

import org.openide.awt.ActionRegistration;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.ActionID;
import org.openide.util.NbBundle.Messages;
import todo.model.Task;

@ActionID(category = "Edit",
id = "todo.controller.edit.EditTaskAction")
@ActionRegistration(iconBase = "todo/controller/edit/configs.gif",
displayName = "#CTL_EditTaskAction")
@ActionReferences({
    @ActionReference(path = "Menu/Edit", position = 20),
    @ActionReference(path = "Toolbars/Edit", position = 20),
    @ActionReference(path = "Shortcuts", name = "O-ENTER")
})
@Messages("CTL_EditTaskAction=Edit Task...")
public final class EditTaskAction implements ActionListener {

    private final Task context;

    public EditTaskAction(Task context) {
        this.context = context;
    }

    public void actionPerformed(ActionEvent ev) {
        
    }
}

Listing 6

Shortcuts are defined as follows (for platform compatibility):

  • Alt: O-
  • Ctrl: D-
  • Shift: S-

Your compiler might complain, though, because it can't find a Task class. If this is true, you need to copy the Task class to your Model module from the old to-do application. Once you've done so, you need to expose this class to the other modules, as follows.

Right-click the Model module, select Properties -> API Versioning, and select the todo.model package from the Public packages to make it public. Clean and build the Model module. Then, add a dependency from Controller to Model. You do this by right-clicking Libraries under the Controller module and selecting the Add Module Dependency action. Search for Model and click OK. Now you may fix imports and have todo.model.Task added as an import to EditTaskAction.

Repeat these steps to create the rest of the actions, trying to make the RCP to-do application as similar as possible to the old to-do application. That means that EditTaskAction, DeleteTaskAction, and MarkAsCompletedTaskAction are conditionally enabled while the rest are always enabled. DeleteTaskAction and MarkAsCompletedTaskAction should be conditionally enabled, and you may select multiple nodes since they can be applied to many tasks at once. See Listing 7.

package todo.controller.edit;

import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.util.List;

import org.openide.awt.ActionRegistration;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.ActionID;
import org.openide.util.NbBundle.Messages;
import todo.model.Task;

@ActionID(category = "Edit",
id = "todo.controller.edit.MarkAsCompletedTaskAction")
@ActionRegistration(iconBase = "todo/controller/edit/complete_tsk.gif",
displayName = "#CTL_MarkAsCompletedTaskAction")
@ActionReferences({
    @ActionReference(path = "Menu/Edit", position = 40, separatorBefore = 35),
    @ActionReference(path = "Toolbars/Edit", position = 40),
    @ActionReference(path = "Shortcuts", name = "D-SPACE")
})
@Messages("CTL_MarkAsCompletedTaskAction=Mark as completed")
public final class MarkAsCompletedTaskAction implements ActionListener {

    private final List<Task> context;

    public MarkAsCompletedTaskAction(List<Task> context) {
        this.context = context;
    }

    public void actionPerformed(ActionEvent ev) {
        for (Task task : context) {
            
        }
    }
}

Listing 7

Also, make sure that you use the Options toolbar and menu (see Figure 17) for the actions that belong to them, that is, Show completed tasks, Sort by priority, Sort by due date, and Show Alerts.

The static visual prototype should look similar to Figure 18. If the order of the toolbars isn't correct, you can modify it either by right-clicking XML Layer and selecting Open or by right-clicking the Toolbars node and selecting Go to Declaration. The layer.xml file opens and you can locate the toolbars and change their Position attribute (a smaller value means that the toolbar is displayed first).

rcp-todo-f17

Figure 17

rcp-todo-f18

Figure 18

The task editing form shown in Figure 1 needs to be created, too. To save time and effort, simply copy the TaskDetailsDialog class from the old to-to application (if you haven't opened it in NetBeans IDE yet, now's your chance) and paste it inside todo.view in the View module. You'll notice some errors. Add a dependency from the View to the Model module for View to be able to access the Task class.

The other error can be resolved if you also copy the ActionSupport class from the old to-do application. You might encounter the error shown in Listing 8 during the build:

...\netbeans\harness\build.xml:174: Module org.jdesktop.layout excluded from 
the target platform
BUILD FAILED (total time: 5 seconds)

Listing 8

First, add a dependency to the deprecated org.jdesktop.layout module. Then, open the NetBeans Platform Config property file inside Important Files of the module suite TodoRCP and delete the line org.jdesktop.layout,\. Do another clean and build. The code should compile fine now.

To complete Step 1, you need to complete AddTaskAction to display TaskDetailsDialog. Simply copy the code of Listing 1 inside the actionPerformed() method of AddTaskAction, as shown in Listing 9.

TaskDetailsDialog taskDetailsDialog = new TaskDetailsDialog(null, true);
taskDetailsDialog.setNewTask(true);
taskDetailsDialog.setTask(new Task());
//        taskDetailsDialog.addActionListener(this);
taskDetailsDialog.setVisible(true);

Listing 9

EditTaskAction is also easy to write (see Listing 10).

public final class EditTaskAction implements ActionListener {

    private final Task context;

    public EditTaskAction(Task context) {
        this.context = context;
    }

    public void actionPerformed(ActionEvent ev) {
        TaskDetailsDialog taskDetailsDialog = new TaskDetailsDialog(null, true);
        taskDetailsDialog.setNewTask(false);
        taskDetailsDialog.setTask(context);
//        taskDetailsDialog.addActionListener(this);
        taskDetailsDialog.setVisible(true);
    }
}

Listing 10

You need to make the todo.view package of the View module public and add a dependency from Controller to View. Clean and build the application and execute it to make sure that the dialog box appears when you click the Add Task... action. What remains to be done is to replace the splash screen and the About box, and you're almost ready to show your prototype to your client. Right-click TodoRCP and select Branding. Here you can select a splash screen and the icon to display when the application is minimized.

Right-click the TodoRCP module suite, select Properties, and select the Installer category. Check the platforms that you wish to create installers for (such as Windows, Mac OSX, and Linux) and click OK. Right-click the TodoRCP module suite again and select Package as and select one of the available options, such as Installers or Zip distribution.

If everything ran smoothly, you created a new dist folder, which contains the deployed application. Run it by going inside dist/todorcp/bin and executing the executable file (depending on your platform, that could be todorcp.exe or something similar).

The prototype is almost the finished application from the UI design perspective, but in a real project, don't spend too much time perfecting its looks. Remember, the prototype is a tool for gathering and validating user requirements and for reducing the risk of missing important application functionality.

Step 2: Build a Dynamic Prototype of the Application

The second step—building the dynamic prototype—aims to implement as much user interaction as possible without using persistent storage or implementing complex business logic. Following the original article, you'll use two well-known design patterns in the to-do application: Data Access Object (DAO) and MVC. You'll also define a Value Object (VO) named Task for moving information between application tiers. Therefore, the view classes (such as TasksTopComponent and TaskDetailsDialog) will receive and return either Task objects or collections of Task objects. The controller classes will transfer those VOs from view classes to model classes and back.

The original article showed a Unified Modeling Language (UML) class diagram; you can compare it to the transformed UML diagram for the RCP to-do application, which is shown in Figure 19. Notice that the packages todo.model, todo.controller, and todo.view of the original to-do application have been transformed to modules for the RCP to-do application.

rcp-todo-f19

Figure 19

Here's the plan for building the dynamic prototype:

  1. Display the visual cues for late and completed tasks.
  2. Handle events:

    1. Handle action events to sort and filter the tasks list.
    2. Handle action events to create, edit, and remove tasks.
  3. Add an in-place property editor for dates.

Items 1 and 2a can be implemented and tested with a mock model object (TaskManager) that always returns the same task collection. Item 2b can be tested with a mock object that simply adds or removes objects from that collection.

Displaying Visual Cues

First, you'll want to display some data. First, wrap your model (Task) to a Node. Swing components follow the MVC design pattern, but you need a different model for each visual component—for example, a TableModel for a JTable, a ListModel for a JList, and so on. NetBeans attempts to create a true MVC design by creating a single model that can be used by all view components (such as OutlineView or BeanTreeView). This is done by wrapping the model to a node. The Node API provides several nodes that are shown in Figure 20, each one with a different purpose.

rcp-todo-f20

Figure 20

Next, you'll use BeanNode, which uses reflection to retrieve the attributes of the VOs. Create the following class inside the View module (and add a dependency on the Node API), as shown in Listing 11.

package todo.view;

import java.beans.IntrospectionException;
import org.openide.nodes.BeanNode;
import todo.model.Task;

class TaskNode extends BeanNode<Task> {

    public TaskNode(Task bean) throws IntrospectionException {
        super(bean);
    }   
}

Listing 11

To display tasks in OutlineView, you need a flat list of nodes. A flat list of nodes is a root node that provides leaf nodes only; that is, one-level-deep children only. Factories are used to create children (see Listing 12).

package todo.view;

import java.beans.IntrospectionException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.TimeZone;
import org.openide.nodes.ChildFactory;
import org.openide.nodes.Node;
import org.openide.util.Exceptions;
import todo.model.Task;

class TaskChildFactory extends ChildFactory<Task>{

    @Override
    protected boolean createKeys(final List<Task> toPopulate) {
        final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("Europe/Belgium"));
        cal.set(2012, Calendar.JULY, 2, 10, 00, 00);
        toPopulate.add(new Task(1, "Hotel Reservation", 1, cal.getTime(), true, 2));
        cal.set(2012, Calendar.JULY, 3, 16, 30, 00);
        toPopulate.add(new Task(2, "Review BOF-1", 1, cal.getTime(), true, 1));
        cal.set(2012, Calendar.JULY, 6, 12, 45, 00);
        toPopulate.add(new Task(3, "Reserve time for visit", 2, cal.getTime()));
        return true;
    }

    @Override
    protected Node createNodeForKey(final Task key) {
        TaskNode taskNode = null;
        try {
            taskNode = new TaskNode(key);
        } catch (IntrospectionException ex) {
            Exceptions.printStackTrace(ex);
        }
        return taskNode;
    }
}

Listing 12

OutlineView is populated with mock data, as shown in the method createKeys() in Listing 12. NetBeans IDE prompts you to create two new constructors in Task. Finally, TasksTopComponent needs to be modified, as shown in Listing 13.

public final class TasksTopComponent extends TopComponent implements
ExplorerManager.Provider {
    private final ExplorerManager em = new ExplorerManager();

    public TasksTopComponent() {
    ...
      em.setRootContext(new AbstractNode(Children.create(new TaskChildFactory(), true)));
  // asynchronously
    }
    ...
    @Override
    public ExplorerManager getExplorerManager() {
        return em;
    }

Listing 13

ExplorerManager, which is the controller of the explorer views, needs a root node element. Pass it an AbstractNode; its Children are derived from the TaskChildFactory.

With many fewer lines of code than in the original Swing to-do application, you now have a running prototype with populated data. Build and run it to see a view similar to that shown in Figure 21:

rcp-todo-f21

Figure 21

However, when you select one or more rows, the conditionally enabled actions aren't enabled accordingly. To fix this, you need to add the Task object to the TaskNode's lookup, and then you need to set the TopComponent's lookup to be that of its nodes. To do that, modify TaskNode as shown in Listing 14, which adds the task to the node's lookup.

class TaskNode extends BeanNode<Task> {

    public TaskNode(Task bean) throws IntrospectionException {
        super(bean, Children.LEAF, Lookups.singleton(bean));
    }   
}

Listing 14

The singleton lookup contains only one object, which in this case is your task. Set the TopComponent's lookup to be that of the node's by adding the following after the line where you set the root context for the ExplorerManager:

associateLookup (ExplorerUtils.createLookup (em, getActionMap()));

To customize the outline view so that it displays a collection of Task objects, change the background color of each row according to the task status: red for late tasks, yellow for tasks with an alert set, blue for completed tasks, and white otherwise. To do this, use the CustomOutlineCellRenderer from NetBeans RCP Recipes.

TasksTopComponent needs to be modified as shown in Listing 15:

public final class TasksTopComponent extends TopComponent implements 
ExplorerManager.Provider {
    ...

    public TasksTopComponent() {
    ...
       outlineView.getOutline().setDefaultRenderer(Node.Property.class, new 
       CustomOutlineCellRenderer() {

            @Override
            public Component getTableCellRendererComponent(final JTable table, 
                                                           final Object value, 
                                                           final boolean isSelected, 
                                                           final boolean hasFocus, 
                                                           final int row, 
                                                           final int column) {
              Component cell = super.getTableCellRendererComponent(table, value, 
isSelected, hasFocus, row, column);
              int modelRow = table.convertRowIndexToModel(row);
              Node node = EM.getRootContext().getChildren().getNodeAt(modelRow);
              if (node != null) {
                 Task task = node.getLookup().lookup(Task.class);
                 Decorator.decorate(task, cell);
              }
              return cell;
            }
        });
      ...
    }

Listing 15

The new utility class Decorator does the decoration (see Listing 16).

package todo.view;

import java.awt.Color;
import java.awt.Component;
import todo.model.Task;

class Decorator {

    static void decorate(final Task task, final Component cell) {
        if (task != null) {
            if (task.hasAlert()) {
                cell.setBackground(Color.yellow);
            } else if (task.isCompleted()) {
                cell.setBackground(Color.blue);
            } else if (task.isLate()) {
                cell.setBackground(Color.red);
            }
        }
    }
}

Listing 16

Handling Events

Now that you have a display of tasks ready, it's time to add some event handling. In the original to-do application, it was useful to separate GUI events into two mutually exclusive categories:

  • Internal events, which affect just the view itself
  • External events, which cause model methods to execute

Internal events include selection changes and clicks on Cancel buttons. In the original to-do application, these were handled by the view classes themselves and were not exposed as part of the view classes' public interfaces. For example, the selection of a task should enable the Edit task menu item and the Remove task menu item, and the corresponding toolbar buttons. Such events are now handled by conditionally enabled actions with cookies inside the Controller module, not in the View package anymore.

View classes should not handle the category of events that the author of the article of the original to-do application calls "external." These events should instead be forwarded to controller classes, which usually implement the workflow logic for a specific use case or a related set of use cases.

The original to-do application includes the todo.view.ActionSupport class, which simply keeps a list of ActionListeners and forwards ActionEvents to them. But ActionSupport is itself an ActionListener. This is done to avoid having lots of event-related methods, such as addNewTaskListener(), removeNewTaskListener(), addEditTaskListener(), removeEditTaskListener(), and so on. Instead, view classes generate only an ActionEvent. The ActionSupport classes capture ActionEvents from the view components and forward them to the controller, which registers itself as a view ActionListener.

All these classes aren't needed in the RCP to-do application because the RCP framework takes care of all this. What you need to do is simply complete the actionPerformed() methods of your actions. So your job is to transfer the logic from QueryEditTasks to your actions. Listing 17 shows how they map.

public final class MarkAsCompletedTaskAction implements ActionListener {

    private final List<Task> context;

    public MarkAsCompletedTaskAction(List<Task> context) {
        this.context = context;
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        for (Task task : context) {
            task.setCompleted(true);
        }
    }
}
public final class DeleteTaskAction implements ActionListener {

    private final List<Task> context;

    public DeleteTaskAction(List<Task> context) {
        this.context = context;
    }

    public void actionPerformed(ActionEvent ev) {
        for (Task task : context) {
            int response = JOptionPane.showConfirmDialog(null,
                        "Are you sure you want to remove task\n["
                        + task.getDescription() + "] ?",
                        "Remove Task",
                        JOptionPane.YES_NO_OPTION);
                            if (response == JOptionPane.YES_OPTION) {
//                    model.removeTask(task.getId());
                }
        }
    }
}

Listing 17

To make DeleteTaskAction functional, you need to copy todo.model package classes from the original to-do application to your Model todo.model package. However, the TaskManager contained in the original to-do application contains all the logic to save tasks in persistent storage, such as a relational database. Since this is left for later in Step 3 of this article, you won't use it now. The implementation of NewTaskListAction and OpenTaskListAction also comes in Step 3.

However, you will create a TaskManager that stores Tasks in memory. The TaskManager class is a DAO. Being the only DAO in the application, it contains many methods that would otherwise be in an abstract superclass. Its implementation is very simple, so there's lots of room for improvement. Start by creating the interface in the Model module that's based on the persistent TaskManager of the original article (see Listing 18).

package todo.model;

import java.util.List;

public interface TaskManagerInterface {
    void addTask(Task task) throws ValidationException;
    void updateTask(final Task task) throws ValidationException;
    void removeTask(final int id);
    List<Task> listAllTasks(boolean priorityOrDate);
    List<Task> listTasksWithAlert() throws ModelException;
    void markAsCompleted(final int id, final boolean completed);
}

Listing 18

Then TaskManager is implemented as a service, as shown in Listing 19.

package todo.model;

import java.util.*;
import org.openide.util.lookup.ServiceProvider;

@ServiceProvider(service = TaskManagerInterface.class)
public class TaskManager implements TaskManagerInterface {

    private final List<Task> tasks = new ArrayList<Task>();

    public TaskManager() {
        final GregorianCalendar cal = new GregorianCalendar(TimeZone.getTimeZone("Europe/Belgium"));
        cal.set(2012, Calendar.JULY, 2, 10, 00, 00);
        tasks.add(new Task(1, "Hotel Reservation", 1, cal.getTime(), true));
        cal.set(2012, Calendar.JULY, 6, 16, 30, 00);
        tasks.add(new Task(2, "Review BOF-1", 1, cal.getTime(), true));
        cal.set(2012, Calendar.JULY, 5, 12, 45, 00);
        tasks.add(new Task(3, "Reserve time for visit", 2, cal.getTime(), false));
    }

    @Override
    public List<Task> listAllTasks(final boolean priorityOrDate) {
        Collections.sort(tasks, priorityOrDate ? new PriorityComparator() : new DueDateComparator());
        return Collections.unmodifiableList(tasks);
    }

    @Override
    public List<Task> listTasksWithAlert() throws ModelException {
        final List<Task> tasksWithAlert = new ArrayList<Task>(tasks.size());
        for (Task task : tasks) {
            if (task.hasAlert()) {
                tasksWithAlert.add(task);
            }
        }
        return Collections.unmodifiableList(tasksWithAlert);
    }

    @Override
    public void addTask(final Task task)
 throws ValidationException {
        validate(task);
        tasks.add(task);
    }

    @Override
    public void updateTask(final Task task) throws ValidationException {
        validate(task);
        Task oldTask = findTask(task.getId());
        tasks.set(tasks.indexOf(oldTask), task);
    }

    @Override
    public void markAsCompleted(final int id, final boolean completed) {
        Task task = findTask(id);
        task.setCompleted(completed);
    }

    @Override
    public void removeTask(final int id) {
        tasks.remove(findTask(id));
    }

    private void validate(final Task task) throws ValidationException {
        if (task.getDescription().isEmpty()) {
            throw new ValidationException("Must provide a task description");
        }
    }

    private Task findTask(final int id) {
        for (Task task : tasks) {
            if (id == task.getId()) {
                return task;
            }
        }
        return null;
    }

    private static class PriorityComparator implements Comparator<Task> {

        @Override
        public int compare(final Task t1, final Task t2) {
            if (t1.getPriority() == t2.getPriority()) {
                return 0;
            } else if (t1.getPriority() > t2.getPriority()) {
                return 1;
            } else {
                return -1;
            }
        }
    }

    private static class DueDateComparator implements Comparator<Task> {

        @Override
        public int compare(final Task t1, final Task t2) {
            return t1.getDueDate().compareTo(t2.getDueDate());
        }
    }
}

Listing 19

Note that you moved the mock data from the TaskChildFactory.createKeys() method to the constructor of TaskManager. You added the class to the default lookup by annotating it like this:

@ServiceProvider(service = TaskManagerInterface.class)

You can also retrieve an implementation class from the default lookup.

TaskChildFactory.createKeys() now becomes what is shown in Listing 20:

@Override
protected boolean createKeys(final List<Task> toPopulate) {
    final TaskManagerInterface taskManager =
        Lookup.getDefault().lookup(TaskManagerInterface.class);
    toPopulate.addAll(taskManager.listAllTasks(true));
    return true;
}

Listing 20

DeleteTaskAction becomes what is shown in Listing 21:

public final class DeleteTaskAction implements ActionListener {

    private final List<Task> context;
    private final TaskManagerInterface taskManager;

    public DeleteTaskAction(List<Task> context) {
        this.context = context;
        this.taskManager = Lookup.getDefault().lookup(TaskManagerInterface.class);
    }

    @Override
    public void actionPerformed(ActionEvent ev) {
        for (Task task : context) {
            int response = JOptionPane.showConfirmDialog(null,
                    "Are you sure you want to remove task\n["
                    + task.getDescription() + "] ?",
                    "Remove Task",
                    JOptionPane.YES_NO_OPTION);
            if (response == JOptionPane.YES_OPTION) {
                taskManager.removeTask(task.getId());
            }
        }
    }
}

Listing 21

Finally, MarkAsCompletedTaskAction becomes what is shown in Listing 22:

public final class MarkAsCompletedTaskAction implements ActionListener {

    private final List<Task> context;
    private final TaskManagerInterface taskManager;

    public MarkAsCompletedTaskAction(List<Task> context) {
        this.context = context;
        taskManager = Lookup.getDefault().lookup(TaskManagerInterface.class);
    }

    public void actionPerformed(ActionEvent ev) {
        for (Task task : context) {
            taskManager.markAsCompleted(task.getId(), true);
        }
    }
}

Listing 22

ActionSupport, which you might have copied from the original to-do application to eliminate the errors of TaskDetailsDialog, isn't needed anymore, so remove it from the View module and tackle the errors. Initially, remove all statements from TaskDetailsDialog that refer to ActionSupport. Add a reference to TaskManager, as shown in Listing 23.

    private final TaskManagerInterface taskManager;

    public TaskDetailsDialog(java.awt.Frame parent, boolean modal) {
        super(parent, modal);
        initComponents();
        setLocationRelativeTo(parent);
        taskManager = Lookup.getDefault().lookup(TaskManagerInterface.class);
    }

Listing 23

Then add actions to the Remove and Save buttons, as shown in Listing 24:

 private void removeActionPerformed(java.awt.event.ActionEvent evt) {
   taskManager.removeTask(getTask().getId());
 }                                      

 private void saveActionPerformed(java.awt.event.ActionEvent evt) { 
   try {
      if (isNewTask()) {
        taskManager.addTask(getTask());
      } else {
        taskManager.updateTask(getTask());
      }
   } catch (ValidationException ex) {
      Exceptions.printStackTrace(ex);
   }
   cancel.doClick();
}         

Listing 24

Sorting behavior is delivered out of the box in OutlineViews just by clicking the specific header/field. Clicking once sorts the column in ascending order, clicking once more sorts in descending order, and clicking once more leaves the order as it was originally. However, if you want to implement SortByDateAction and SortByPriorityAction to see how sorting can be done programmatically, create a new Utilities class inside todo.view, and then copy the code from Listing 25 and paste into to the Utilities class.

/**
 * Sort the outline view {@code ov} on the given {@code field}.
 * @param ov outline view to sort
 * @param field to sort upon
 * @param ascending if {@code true} then the list is sorted in ascending order, 
 * if {@code false} in descending order.
 */
public static void sortBy(final OutlineView ov, final String field, final boolean ascending) {
  ETableColumnModel columnModel = (ETableColumnModel) ov.getOutline().getColumnModel();
  int columnCount = columnModel.getColumnCount();
  columnModel.clearSortedColumns();
  for (int i = 0; i < columnCount; i++) {
    ETableColumn column = (ETableColumn)columnModel.getColumn(i);
    if (column.getHeaderValue().equals(field)) {
      columnModel.setColumnSorted(column, ascending, 1);
    }
  }
  TableModel model = ov.getOutline().getModel();
  ov.getOutline().tableChanged(new TableModelEvent(model, 0, model.getRowCount()));
}

Listing 25

Since the Utilities class contains only static utility methods, add an empty private constructor to it to avoid initialization. Add the missing dependency to the ETable and Outline module. Add the following method to TasksTopComponent:

public void sortBy(final String field, final boolean ascending) {
   Utilities.sortBy((OutlineView)outlineView, field, ascending);
}

This step is to avoid adding a dependency to the Explorer & Property Sheet API in the Controller module. Because to call the original method in the Utilities class, you need to add a reference to OutlineView, which is contained in the Explorer & Property Sheet API in your Controller module. This workaround saves us this dependency, as shown in Listing 26.

public final class SortByPriorityAction implements ActionListener {

    public void actionPerformed(ActionEvent e) {
        TasksTopComponent tasksTopComponent = (TasksTopComponent)WindowManager.getDefault().findTopComponent(
"TasksTopComponent");
        tasksTopComponent.sortBy("Priority", true);
    }
}
public final class SortByDateAction implements ActionListener {

    public void actionPerformed(ActionEvent e) {
        TasksTopComponent tasksTopComponent = (TasksTopComponent)WindowManager.getDefault().findTopComponent(
"TasksTopComponent");
        tasksTopComponent.sortBy("Due Date", true);
    }
}

Listing 26

Regarding filtering, you can right-click a row and select Show only rows where and then select a criterion to filter. Remove the filter by following the same procedure and selecting No filter. However, your two actions, ShowAlertsAction and ShowCompletedTasksAction, can't be displayed by these out-of-the-box filters.

ShowAlertsAction displays only the tasks that have alerts—for example, alert==true. Start from the code generated by the wizard (see Listing 27).

package todo.controller.options;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JToggleButton;
import org.openide.awt.ActionRegistration;
import org.openide.awt.ActionReference;
import org.openide.awt.ActionReferences;
import org.openide.awt.ActionID;
import org.openide.util.NbBundle.Messages;

@ActionID(category = "Options",
id = "todo.controller.options.ShowAlertsAction")
@ActionRegistration(iconBase = "todo/controller/options/showwarn_tsk.gif",
displayName = "#CTL_ShowAlertsAction")
@ActionReferences({
    @ActionReference(path = "Menu/Options", position = 40, separatorBefore = 35),
    @ActionReference(path = "Toolbars/Options", position = 40),
    @ActionReference(path = "Shortcuts",
 name = "F9")
})
@Messages("CTL_ShowAlertsAction=Show alerts...")
public final class ShowAlertsAction implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
        // TODO implement action body
    }
}

Listing 27

First, you need to convert the button to a toggle button. However, JToggleButton doesn't really work with the NetBeans IDE toolbar, so you need to extend org.openide.util.actions.BooleanStateAction. Because BooleanStateAction implements java.awt.event.ActionListener, ActionListener can be deleted from your action (see Listing 28).

public final class ShowAlertsAction extends BooleanStateAction {

    @Override
    protected void initialize() {
        super.initialize();
        setBooleanState(false);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
    }

    @Override
    public String getName() {
        return "Show alerts...";
    }

    @Override
    public HelpCtx getHelpCtx() {
        return HelpCtx.DEFAULT_HELP;
    }

    @Override
    protected String iconResource() {
        return "todo/controller/options/showwarn_tsk.gif";
    }
}

Listing 28

Because, due to lazy initialization, BooleanStateAction doesn't recognize the values passed in the annotations, you need to override the iconResource() method. It's selected by default, so you need to set it to be unselected in the initialize() method.

The Outline class provides a method setQuickFilter(int col, Object filterObject), and filterObject can either be a value that matches one of the values of the column or a QuickFilter object (see Listing 29):

public interface QuickFilter {
    
    /** If the object is accepted its row is displayed by the table. */
    public boolean accept(Object aValue);
}

Listing 29

In this case, the accepted value is simply true, which means that you should just accept those rows that contain the value true. Listing 30 shows how the actionPerformed() method can be implemented:

super.actionPerformed(e);
TasksTopComponent tasksTopComponent = (TasksTopComponent) 
WindowManager.getDefault().findTopComponent("TasksTopComponent");
if (getBooleanState()) {
   tasksTopComponent.setQuickFilter(3, Boolean.TRUE);  // or a QuickFilter
} else {
   tasksTopComponent.unsetQuickFilter();
}

Listing 30

The setQuickFilter() wrapper method in TasksTopComponent wraps the same method of Outline (see Listing 31):

public void setQuickFilter(int column, Object filter) {
   outlineView.getOutline().setQuickFilter(column, filter);
}
    
public void unsetQuickFilter() {
   outlineView.getOutline().unsetQuickFilter();
}

Listing 31

Whether or not the toggle button is clicked is defined by getBooleanState(). Finally, super.actionPerformed(e); toggles selection and deselection of the toggle button, so you don't need to do it yourself.

ShowCompletedTasksAction is a more complex case because completed is not shown in the outline view as a column (only as a color). To implement it, you need a mechanism where you pass each one of the Tasks and you check whether completed == true.

However, creating this mechanism isn't as difficult as it might look. Start by transforming ShowCompletedTasksAction to a BooleanStateAction. If you noticed in the beginning of the article, you hid the first column of your OutlineView. This column (column 0) contains the node. So applying a filter to column 0 is like applying a filter to the node itself (see Listing 32):

super.actionPerformed(e);
TasksTopComponent tasksTopComponent = (TasksTopComponent) 
WindowManager.getDefault().findTopComponent("TasksTopComponent");
if (getBooleanState()) {
   tasksTopComponent.setQuickFilter(0, filter);  
} else {
   tasksTopComponent.unsetQuickFilter();
}

Listing 32

We now need to create the filter, as shown in Listing 33:

/** Show only tasks that have been completed. */
private final QuickFilter filter = new QuickFilter() {

    @Override
    public boolean accept(Object aValue) {
        if (aValue instanceof TaskNode) {
            TaskNode taskNode = (TaskNode) aValue;
            Task task = taskNode.getLookup().lookup(Task.class);
            return task.isCompleted();
        }
        return true;
    }
};

Listing 33

When you add this definition to the ShowCompletedTasksAction class, you need to add the Nodes and ETable and Outline dependencies to the Controller module. You could add this definition to a class in the View module (for example, in TasksTopComponent) and access it from there; however, you would still need a dependency to ETable and Outline since QuickFilter is defined there.

Adding an In-Place Property Editor for Dates

Note: The following functionality runs best with NetBeans IDE 7.4 or later, although with more effort, it can run on earlier versions.

Adding new tasks or editing tasks goes smoothly, except when you have to add or edit the due date. To provide a more user-friendly date input, follow the "NetBeans Property Editor Tutorial" and, most specifically, the section "Creating a Custom Inplace Editor." Use the date picker component of the SwingX library, which you can find inside the ide/modules/ext/ folder of your NetBeans IDE installation. However, don't add swingx.jar as a new library, as was done in the original article. You want to follow a different approach, because later on, you'll need hsqldb.jar for persisting the tasks to the database and maybe other to libraries.

Create a new module, Libraries, and wrap your various external libraries inside. The benefit is that you need to add only one new module to your suite and a single reference to it from the modules that need it. The drawback is that if you need only one JAR file, you need to have visibility to all other files that are wrapped inside the Libraries module.

  1. Right-click the Modules folder icon of the TodoRCP module suite and choose Add New. Type Libraries as the module name. Click Next. Type lib as the Code Base Name and then click Finish.
  2. Right-click the new module and select Properties -> Libraries -> Wrapped JARs. Click Add JAR and add ide/modules/ext/swingx.jar. Repeat the procedure to add hsqldb.jar, which you can download from the HyperSQL site.
  3. Select the API Versioning category from the left panel of the opened Project Properties -> Libraries dialog box and make the org.hsqldb and org.jdesktop.swingx packages public by selecting them.

After clicking OK, clean and build the Libraries module. Add a dependency to the Libraries module in the View module. Now you're ready to make use of the date picker, which involves implementing a couple of NetBeans IDE–specific interfaces:

  • ExPropertyEditor: A property editor interface through which the property sheet can pass an "environment" object (PropertyEnv) that gives the editor access to the Property object it is editing and more.
  • InplaceEditor.Factory: An interface for objects that own an InplaceEditor.
  • InplaceEditor: An interface that allows a custom component to be provided for display in the property sheet.

Create a new class DatePropertyEditor inside todo.view (see Listing 34):

public class DatePropertyEditor extends PropertyEditorSupport implements ExPropertyEditor,
 InplaceEditor.Factory {
private InplaceEditor ed;

 @Override
 public String getAsText() {
     Date d = (Date) getValue();
     if (d == null) {
         return "No Date Set";
     }
     return new SimpleDateFormat("dd/MM/yy HH:mm:ss").format(d);
 }

 @Override
 public void setAsText(String s) {
     try {
         setValue(new SimpleDateFormat("dd/MM/yy HH:mm:ss").parse(s));
     } catch (ParseException pe) {
         IllegalArgumentException iae = new IllegalArgumentException("Could not parse 
date");
         throw iae;
     }
 }

 @Override
 public void attachEnv(PropertyEnv env) {
     env.registerInplaceEditorFactory(this);
 }
 
 @Override
 public InplaceEditor getInplaceEditor() {
     if (ed == null) {
         ed = new Inplace();
     }
     return ed;
 }
 
 private static class Inplace implements InplaceEditor {
 
     private final JXDatePicker picker = new JXDatePicker();
     private PropertyEditor editor = null;
 
     @Override
     public void connect(PropertyEditor propertyEditor, PropertyEnv env) {
         editor = propertyEditor;
         reset();
     }
 
     @Override
     public JComponent getComponent() {
         return picker;
  
   }
 
     @Override
     public void clear() {
         //avoid memory leaks:
         editor = null;
         model = null;
     }
 
     @Override
     public Object getValue() {
         return picker.getDate();
     }
 
     @Override
     public void setValue(Object object) {
         picker.setDate((Date) object);
     }
 
     @Override
     public boolean supportsTextEntry() {
         return true;
     }
 
     @Override
     public void reset() {
         Date d = (Date) editor.getValue();
         if (d != null) {
             picker.setDate(d);
         }
     }
 
     @Override
     public KeyStroke[] getKeyStrokes() {
         return new KeyStroke[0];
     }
 
     @Override
     public PropertyEditor getPropertyEditor() {
         return editor;
     }
 
     @Override
     public PropertyModel getPropertyModel() {
         return model;
     }
     private PropertyModel model;
 
     @Override
     public void setPropertyModel(PropertyModel propertyModel) {
         this.model = propertyModel;
     }
 
     @Override
     public boolean isKnownComponent(Component component) {
         return component == picker || picker.isAncestorOf(component);
     }
 
     @Override
     public void addActionListener(ActionListener actionListener) {
         //do nothing - not needed for this component
     }
 
     @Override
     public void removeActionListener(ActionListener actionListener) {
         //do nothing - not needed for this component
     }
 }
}

Listing 34

The date format (dd/MM/yy HH:mm:ss) depends on your location, so adapt it accordingly.

The final thing that you need to do, which works only with NetBeans IDE 7.4 or later, is to add the following annotation to the definition of the class to register DatePropertyEditor globally—that is, as the default editor for all properties of the type java.util.Date throughout the system:

@PropertyEditorRegistration(targetType = Date.class)
public class DatePropertyEditor extends PropertyEditorSupport implements ExPropertyEditor,
 InplaceEditor.Factory {

Clean and build the View module and run the RCP to-do application again. A date picker now appears when you try to edit the due date field (see Figure 22).

rcp-todo-f22

Figure 22

Note: If you use NetBeans IDE 7.2 or earlier, you need to do more work to achieve this functionality. You'll have to override the createSheet() method of TaskNode and edit the properties, setting the following to the date property, too:

dateProp.setPropertyEditorClass(DatePropertyEditor.class); 

We need to add a date picker to the TaskDetailsDialog, too. To do this, add the visual components of swingx.jar to the Palette, as you did for the Explorer & Property Sheet API. Open the TaskDetailsDialog in Design view, if you haven't already done so. Right-click inside the Palette window and select Palette Manager. Create a new category by clicking New Category, and name the new category SwingX. Then, click Add from JAR, navigate to TodoRCP -> Libraries -> lib, select swingx.jar, and click Next. Select all available components and click Next. Select the category SwingX and click Finish. Click Close to close the Palette Manager. The new category is now shown in the Palette window.

Delete the Due date text field from the form, and then locate the JxDatePicker component in the Palette window. Drag it inside the form in the same place where the Due date text field was. Change to Source view, fix imports, and correct the errors.

In setTask():

  dueDate.setDate(task.getDueDate());

In getTask():

  task.setDueDate(dueDate.getDate());

Build the View module and run the application again. When you click the Add Task or Edit Task actions to display the Task Details dialog box, you can modify the due date by selecting it from the date picker. Cool! However, if you run the application and try to add a new task, your new task never appears in the OutlineView because OutlineView is never notified of changes to the model. You need to fix this.

First, modify TaskManagerInterface by adding two more methods to it:

void addPropertyChangeListener(PropertyChangeListener listener);
void removePropertyChangeListener(PropertyChangeListener listener);

Implement these methods in TaskManager, as shown in Listing 35:

@ServiceProvider(service = TaskManagerInterface.class)
public class TaskManager implements TaskManagerInterface {
    private final PropertyChangeSupport pcs = new PropertyChangeSupport(this);
    ...
    @Override
    public void addTask(final Task task) throws ValidationException {
        validate(task);
        tasks.add(task);
        pcs.firePropertyChange("ADDED", null, task);
    }

    @Override
    public void updateTask(final Task task) throws ValidationException {
        validate(task);
        Task oldTask = findTask(task.getId());
        tasks.set(tasks.indexOf(oldTask), task);
        pcs.firePropertyChange("UPDATED", oldTask, task);
    }

    @Override
    public void markAsCompleted(final int id, final boolean completed) {
        Task task = findTask(id);
        boolean oldValue = task.isCompleted();
        task.setCompleted(completed);
        pcs.firePropertyChange("COMPLETED", oldValue, completed);
    }

    @Override
    public void removeTask(final int id) {
        Task task = findTask(id);
        tasks.remove(task);
        pcs.firePropertyChange("DELETED", task, null);
    } 
    ...
    @Override
    public void addPropertyChangeListener(PropertyChangeListener listener) {
        pcs.addPropertyChangeListener(listener);
    }

    @Override
    public void removePropertyChangeListener(PropertyChangeListener listener) {
        pcs.removePropertyChangeListener(listener);
    }
}

Listing 35

Finally, TaskChildFactory needs to be notified (see Listing 36):

public class TaskChildFactory extends ChildFactory<Task> {

    private final TaskManagerInterface taskManager;
    private final transient PropertyChangeListener pcl = new PropertyChangeListener() {

        @Override
        public void propertyChange(final PropertyChangeEvent evt) {
            refresh(true);
        }
    };

    public TaskChildFactory() {
        taskManager = Lookup.getDefault().lookup(TaskManagerInterface.class);
        taskManager.addPropertyChangeListener(pcl);
    }
    ...
}

Listing 36

These changes should allow the OutlineView to be updated accordingly. However, you'll notice one more problem. When you edit a task, it isn't updated until you click another cell. To cope with this problem, you need to make some more changes. The first change is to your Task class (make it an observable to notify listeners of changes to its fields); second, add a firePropertyChange() to all setter methods that affect the outline view. An example is shown in Listing 37, but you need to do the same thing for the other setters, too.

public class Task implements Serializable {
    ...
    private final PropertyChangeSupport propertyChangeSupport = new 
PropertyChangeSupport(this);

     public void addPropertyChangeListener(final PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(propertyName, listener);
    }

    public void removePropertyChangeListener(final PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(listener);
    }

    ...
    public void setDueDate(Date dueDate) {

        Date oldValue = this.dueDate;

        this.dueDate = dueDate;

        propertyChangeSupport.firePropertyChange("DUE DATE CHANGED", oldValue, dueDate);

    }

    ...

}

Listing 37

Then, TaskNode must be notified of any changes (see Listing 38):

public class TaskNode extends BeanNode<Task> {

    private final transient PropertyChangeListener pcl = new PropertyChangeListener() {

        @Override
        public void propertyChange(final PropertyChangeEvent evt) {
            firePropertySetsChange(null, getPropertySets());
        }
    };

    public TaskNode(Task bean) throws IntrospectionException {
        super(bean, Children.LEAF, Lookups.singleton(bean));
        bean.addPropertyChangeListener(pcl);
    }
}

Listing 38

Now that you have fully functional view and model classes, it's time to replace the mock implementations of the model classes by real logic using persistent storage. In large application projects, you could have a team working on the UI—building the two prototypes in sequence as you did—and another team working on business and persistence logic, preferably using test-driven development (TDD). They can work in parallel and join up at the end, putting together functional view and controller implementations with functional model implementations.

Most of the work in this Step 2 was just coding. NetBeans IDE provides nice code editors and a good debugger that provide the usual benefits: code completion, Javadoc integration, and refactoring support. But NetBeans IDE can go beyond: it's easy to build in new plugin modules to package your project coding standards, such as project templates, controller class templates, and so on.

Step 3: Code the Persistence Logic

The RCP to-do application uses HyperSQL (hsqldb), an embedded Java database, which simplifies the deployment requirements for a typical desktop application. Because you're working with modules in RCP applications, adding the hsqldb.jar archive to a module’s libraries isn't straightforward. To allow for future libraries, you created a new module earlier called Libraries, which contains the libraries necessary for the application.

Inspecting the Database

When developing and debugging persistence code, developers usually need a way to tap into the database. Maybe they need to check the effect of an update or change some table definition. NetBeans IDE provides direct support for browsing any JDBC-compliant database and submitting SQL commands.

To connect to the database, switch to the Services tab or open it from the Window menu. Expand the Databases category and then expand the Drivers category. Open the Drivers folder. If the HSQLDB driver isn't there, right-click Drivers, select New Driver, and add the location of the hsqldb.jar archive. NetBeans IDE often sets the database driver class name by itself.

Now right-click the HSQLDB driver icon, and choose the Connect using menu item. Provide the parameters to connect to your local RCP to-do application's database, using the Figure 23 as a template.

rcp-todo-f23

Figure 23

The default database location is db/todo under the {user.home} folder, which is usually /home/<user> under Linux or C:\Users\<UserName> under Windows. Then you can open the connection and browse the database catalog for tables, indexes, and other database objects. Each item has a context menu for operations such as creating new tables, altering columns, and viewing data. Most operations have easy-to-use wizards.

The RCP to-do application uses HSQLDB in the standalone mode, which locks the database files for exclusive access so you can't use the NetBeans IDE's database console while the application is running. However, you can run HSQLDB in server mode accepting concurrent connections from multiple clients, allowing the inspection of a live task list database. Check the HSQLDB manual for instructions on how to start and connect to the database server.

To finish the application, you need to copy three more classes from the original to-do application: TaskManager, Parameters, and DatabaseException. TaskManager replaces your current class with some modifications—it must be a ServiceProvider and, as such, it must have a parameterless constructor (see Listing 39):

@ServiceProvider(service = TaskManagerInterface.class)
public class TaskManager implements TaskManagerInterface {

       public TaskManager() throws DatabaseException {
        this(new Parameters());
    }
    ...
}

Listing 39

You need to remove DatabaseException from the signature of the complaining methods (for an example, see Listing 40):

Override
public List<Task> listAllTasks(boolean priorityOrDate) {
    List<Task> allTasks = new ArrayList<Task>();
    try {
        allTasks = query(null, priorityOrDate
                  ? "priority, dueDate, description" : "dueDate, priority, description");
    } catch (DatabaseException e) {
        e.printStackTrace();
    }
    return allTasks;
}

Listing 40

You must also make TaskManager an observable by providing a PropertyChangeSupport, adding the appropriate firePropertyChange() commands to the end of the appropriate methods: addTask(), removeTask(), markAsCompleted(), and updateTask(). For an example, see Listing 41.

@Override
public void removeTask(int id) {
   try {
        update("DELETE FROM todo WHERE id = " + id);
   } catch (DatabaseException e) {
        e.printStackTrace();
   }
   pcs.firePropertyChange("DELETED", id, null);
}

Listing 41

Now, just add a dependency from the Model module to the Libraries module, and you're almost done! Run the application and verify that it works like it did before.

The final step is to add the two actions New Task List and Open Task List to the File menu, but this is left as an exercise for the reader.

A last thing to mention: you might notice that the Tasks window tab contains an x button, which you can click to close this window without any way to bring the window back. To disable this x button, add the following lines in the TasksTopCompnent constructor:

        putClientProperty(TopComponent.PROP_CLOSING_DISABLED, Boolean.TRUE);
    putClientProperty(TopComponent.PROP_UNDOCKING_DISABLED, Boolean.TRUE);

Conclusion

You've migrated the original to-do application successfully without much effort. Although simple in scope and involving only a few classes, the process explored here demonstrates many practices that could improve the quality your RCP desktop Java applications and your development speed.

You also practiced many features that NetBeans IDE provides to increase developer productivity. NetBeans IDE goes beyond visual development by supporting coding activities with specialized editors for Java, Ant, XML, and other languages, in addition to CVS, JUnit, and refactoring support and a database console.

See Also

About the Author

Ioannis (John) Kostaras is a software architect and has been a Java developer since JDK 1.0 was released. He has developed a number of standalone and web applications focusing on flexible object-oriented design and security. One such RCP application, written in NetBeans, was awarded the 2012 Duke's Choice Community Choice Award. He is also co-organizing the hottest Java conference on earth, JCrete.

Join the Conversation

Join the Java community conversation on Facebook, Twitter, and the Oracle Java Blog!