TodoFX

By John Kostaras


In the TodoRCP article, which has also been published in Java Magazine, we ported a Java Swing application to its equivalent NetBeans Rich Client Platform (RCP).

An updated article (TodoRCP2) ported the TodoRCP application to Java 8 and NetBeans 8.

In this article we shall create a JavaFX version of the original Java Swing application, using NetBeans and SceneBuilder.

Contents

Introduction

To refresh your memory, the original Todo PIM application was written in Swing, and was the application described in the article "A complete App using NetBeans 5" by Fernando Lozano, in the first issue of NetBeans magazine. Please refer here for a short description of the requirements and the steps we followed in the previous articles to develop the PIM application. The same steps will be followed to build the JavaFX version of it.

Prerequisites and Setup

SceneBuilder allows you to build JavaFX applications graphically. Gluon provides binaries of the Oracle SceneBuilder. For the needs of this tutorial we will need some of the widgets that exist in Gluon SceneBuilder but not in Oracle SceneBuilder 2.0. We shall see how to setup NetBeans to use SceneBuilder shortly. SceneBuilder runs externally to NetBeans. Sven Reimer's Monet plugin is an attempt to run SceneBuilder from within NetBeans to build the GUI of JavaFX applications the same way as when you use Matisse to build the GUI of your Swing applications.

To integrate SceneBuilder with NetBeans follow these steps:

  1. In NetBeans, navigate to Tools --> Options --> Java --> JavaFX (Windows/Linux) or to NetBeans --> Preferences --> Java --> JavaFX (MacOSX) and click Activate if JavaFX support is not yet activated.
  2. After activation is finished, set the Scene Builder Home to be the Gluon directory. Some Windows installers install SceneBuilder without asking you for a directory. SceneBuilder can be found in C:\Users\<YourUser>\AppData\Local\SceneBuilder.
  3. Click on OK and you are ready to start.

Note! If NetBeans complains with the message "Selected location ... does not represent a valid Java FX Scene Builder installation", then do the following workaround:

  1. Navigate to the directory where SceneBuilder was installed
  2. Enter app folder
  3. Make a duplicate of SceneBuilder.cfg to SceneBuilder.properties in the same folder.

There are two ways to develop a JavaFX application. Either programmatically, using Java (JavaFX Application), or declaratively, declaring the GUI in a special XML format, called FXML (JavaFX FXML Application). We shall choose the second way here, which has also the advantage that you can use the SceneBuilder to graphically design the GUI.

Step 1 - Designing the tasks list window

Let's get started. Click on the New Project toolbar button and select the JavaFX FXML Application in the JavaFX category. Click Next.

"Figure 1 - Create a New JavaFX Project"

Use "TodoFX" as the project name and choose a suitable project location (anywhere in your hard disk). Modify the rest of the dialog box entries as shown in the following figure. Then click Finish.

"Figure 2 - Create a New JavaFX Project"

The "TodoFX" project's layout is shown in the following figure:

"Figure 3 - TodoFX project layout"

You can easily identify Main, the .fxml file which represents the main view (the tasks list in our case) and the respective TaskMainController. NetBeans saves you development steps by creating all these for you. You don't need to create the view and the controller separately; they are created in a single step! Run the project; you should see a similar application like in the figure:

"Figure 4 - TodoFX application first run"

NetBeans has created a full blown sample JavaFX application for you. Double click on TaskMain.fxml; the file will open inside Scenebuilder:

"Figure 5 - TaskMain.fxml inside Scenebuilder"

With Scenebuilder you can quickly design your GUI by using simple drag & drop of widgets, in a similar way that Matisse allows you to build Swing GUIs.

We shall see how to build our task list window in SceneBuilder shortly.

As in the original article, we shall follow good design principles and create todofx.model and todofx.viewcontroller packages, following not the Model-View-Controller (MVC) architectural pattern but the Model-View-ViewModel one. Move TaskMainController.java and TaskMain.fxml inside the new todofx.viewcontroller package. (You may also choose to create separate todofx.view and todofx.controller packages instead, for your TaskMain.fxml and TaskMainController.java respectively). You can always refer to the controller from the view (.fxml) as we shall see shortly.

  1. Close SceneBuilder
  2. Right-click on todofx folder and select New Java Package
  3. Enter model and select Finish.
  4. Repeat steps 2 and 3 to create the viewcontroller package
  5. Click on TaskMainController.java to select it and drag it on viewcontroller package. Click Refactor.
  6. Repeat previous step for TaskMain.fxml

The result should look like the following figure:

"Figure 6 - TodoFX new project layout"

  1. Open TaskMain.fxml in SceneBuilder again.
  2. In the lower left (Document area) of the SceneBuilder window, click on Controller.
  3. Make sure that the Controller class points to the correct controller (see following figure) - the refactoring didn't do the change.
  4. Open Main.java and modify the path to viewcontroller/TaskMain.fxml
  5. Save your changes
  6. Run the application again to make sure that nothing was broken.

"Figure 7 - Link view with the controller"

Now it is time to design our tasks list window.

  1. In Document area, click on Hierarchy.
  2. Select AnchorPane and delete it from the popup menu (right-click)

JavaFX, like Swing, comes with a number of layout managers. Here is a comparison between JavaFX and Swing layout managers.

From the various layout managers, it seems that the VBox satisfied our needs (of having a menu, a toolbar, a table and a status bar).

  1. On the upper left (Library) area, select the VBox from the list of Containers and drag it in the main area or inside the Document/Hierarchy.
  2. Adjust its preferred size in Inspector/Layout.
  3. Add on the VBox a MenuBar (Controls), a ToolBar (Containers), a TableView (Controls) and a Label (Controls).
  4. Select the TableView and click on Layout.
  5. Change Vgrow to ALWAYS.

"Figure 8 - SceneBuilder initial design of task list window"

Select Preview --> Show Preview in Window in order to see how the window looks like. With the last change we made, resizing the window causes the table to be resized, too.

Time to make our design look like the Swing Todo application.

  1. Drag Menu Items, CheckMenuItems and SeparatorMenuItems on MenuBar and rename them appropriately. (Menu Edit --> Duplicate or Ctrl+D can be useful.)
  2. Drag Buttons and CheckButtons on the Toolbar.
  3. Drag TableColumns onto the TableView and rename them appropriately.
  4. Select the TableView and choose constrained-resize for the Column Resize Policy (under Properties). This ensures that the colums will always take up all available space.

To create buttons with only image icons for the toolbar, we need to do some tricks:

  1. Copy the icons folder of the original Todo application inside TodoFX/resources/icons.
  2. Right-click on TodoFX project and select Properties.
  3. Select category Sources and under Source Package Folders click on Add Folder.
  4. Add the resources folder
  5. Drag ImageView widgets and drop them inside the Buttons.
  6. Select an ImageView and set its Image property (see following figure). Click on the small star icon next to the ... button, and select Switch to classpath relative path. Then click on the ... button and select the respective icon (e.g. icons/add_obj.gif).
  7. To verify that you have selected the correct path, in NetBeans, right-click on the TaskMain.fxml in NetBeans and select Edit. Verify e.g. that <Image url="@/icons/add_obj.gif"> for the specific button.

"Figure 9 - Add resources to the classpath"

The main window of our static prototype should look like the following figure. Don't worry that the icons do not appear in SceneBuilder.

"Figure 10 - SceneBuilder final design of task list window"

Run the application again. You should see our prototype running. And we haven't written a single line of code yet! But don't worry, I promise you will write a lot of code in Step 2.

"Figure 11 - Prototype application window"

Let's design the Task Details dialog box, too.

  1. Right-click on the viewcontroller package and select New --> Other --> JavaFX --> Empty FXML as shown below. Click Next.
  2. Provide TaskDetailsDialog as the FXML Name. Click Next.
  3. Check Use Java Controller and accept the default. Click Next.
  4. Click Finish.

"Figure 12 - Create new FXML file"

Double click on TaskDetailsDialog.fxml to open it in SceneBuilder. Build the dialog box as shown in the following figure:

"Figure 13 - TaskDetailsDialog.fxml in SceneBuilder"

Even though we didn't do the connection between the main task window and the task details dialog box, you can show both to your customer explaining how they are connected. If the customer is satisfied with the prototype, you can continue to the next step.

Step 2 - Dynamic Prototype

The second step – building the "dynamic prototype" – aims to implement as much user interaction as possible without using a persistent storage or implementing complex business logic. Following the original article [1], we’ll use two well-known design patterns in the TodoFX application: DAO (Data Access Object) and the MVC (Model-View Controller). We’ll also define a VO (Value Object) named Task for moving information between application tiers. Therefore the view classes (such as the TaskMain 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. See Figure 20 on page 14 of the original article [1] for the UML class diagram. Notice that the packages todo.model, todo.controller, todo.view of the original todo application have been transformed to the packages todofx.model and todofx.viewcontroller in TodoFX.

"Figure 14 - The TodoFX UML class diagram"

But before we continue, we need to explain how JavaFX works. JavaFX is designed differently than Swing. Open Main.java:

package todofx;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {
    
    @Override
    public void start(Stage stage) throws Exception {
        Parent root = FXMLLoader.load(getClass().getResource("viewcontroller/TaskMain.fxml"));
        
        Scene scene = new Scene(root);
        
        stage.setScene(scene);
        stage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }
    
}

NetBeans has created the basic structure of a JavaFX application. Main extends from Application and the main() method calls launch(). The most important method is start() which accepts a Stage. It is automatically called when the application is launched from within the main method.

Like a theatrical play, the Stage is our main container, similar to a JFrame or Window in Swing. Inside the Stage, a Scene can be added, which can be switched by another Scene. A Scene is a container of UI elements like Buttons, AnchorPanes etc.

"Figure 14a - Stage, Scene and UI Elements"

Image Source: http://www.oracle.com/

The first line of start() method retrieves the root of TaskMain.fxml, which is the VBox. This is added in a Scene which in turn is added to the Stage.

The plan for building the second prototype follows the same steps as of the original Todo application:

  1. Display the visual cues for late and completed tasks;
  2. Handle action events to sort and filter the tasks list;
  3. Handle action events to create, edit and remove tasks.

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

But before we do this, let's display some data. Copy the Task and TaskManager classes from the Todo application to the TodoFX application (todofx.model).

To fully take advantage of JavaFX, we need to use Properties for the attributes of the Task class. Properties are basically wrapper objects for JavaFX-based object attributes such as String or Integer. A Property allows us to be automatically notified when the wrapped value of an object has changed or is flagged as invalid. This helps us keep the view in sync with the data. To learn more about Properties read Using JavaFX Properties and Binding.

Don't forget to get rid of java.util.Date and use the Java 8 java.time.LocalDate instead, as described in TodoRCP2 article.

Since Task objects will be stored in TaskManager in a list, they need to implement equals() and hashCode() methods. This is done easily with NetBeans.

  1. Right-click inside Task class and select Insert code --> equals() and hashCode().
  2. Select only the id attribute for both methods. We assume that two tasks with the same id, refer to the same task, thus they are equal.
  3. Click on Generate.

Repeat the above steps to generate a toString() method, selecting all fields this time.

You may be tempted to transform the Task class like the one shown below, following e.g. [5]:

package todofx.model;

import java.time.LocalDate;
import java.util.Objects;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;

/**
 * Task
 *
 * @author ikost
 */
public class Task {

    private final IntegerProperty id;
    private final StringProperty description;
    private final IntegerProperty priority;
    private final ObjectProperty<LocalDate> dueDate;
    private final BooleanProperty alert;
    private final IntegerProperty daysBefore;
    private final StringProperty obs;
    private final BooleanProperty completed;

    public Task() {
        this(-1, "", 0, LocalDate.now());
    }

    public Task(int id, String descr, int prio, LocalDate due) {
        this(id, descr, prio, due, false, 0, "");
    }

    public Task(int id, String descr, int prio, LocalDate due, boolean alert, int daysBefore) {
        this(id, descr, prio, due, alert, daysBefore, "");
    }

    public Task(int id, String descr, int prio, LocalDate due, 
                boolean alert, int daysBefore, String obs) {
        this.id = new SimpleIntegerProperty(id);
        this.description = new SimpleStringProperty(descr);
        this.priority = new SimpleIntegerProperty(prio);
        this.dueDate = new SimpleObjectProperty<>(due);
        this.alert = new SimpleBooleanProperty(alert);
        this.daysBefore = new SimpleIntegerProperty(daysBefore);
        this.obs = new SimpleStringProperty(obs);
        this.completed = new SimpleBooleanProperty(false);
    }

    public boolean isLate() {
        LocalDate dateDue = getDueDate();
        return (dateDue == null) ? false : dateDue.compareTo(LocalDate.now()) < 0;
    }

    public boolean hasAlert() {
        LocalDate dateDue = getDueDate();
        if (!getAlert() || dateDue == null) {
            return false;
        } else {
            int dias = dueDate.get().getDayOfYear() - LocalDate.now().getDayOfYear();
            return dias <= getDaysBefore();
        }
    }

    public boolean isCompleted() {
        return completed.get();
    }

    public void setCompleted(boolean completed) {
        this.completed.set(completed);
    }

    public String getDescription() {
        return description.get();
    }

    public void setDescription(String description) {
        this.description.set(description);
    }

    public int getPriority() {
        return priority.get();
    }

    public void setPriority(int priority) {
        this.priority.set(priority);
    }

    public LocalDate getDueDate() {
        return dueDate.get();
    }

    public void setDueDate(LocalDate dueDate) {
        this.dueDate.set(dueDate);
    }

    public boolean getAlert() {
        return alert.get();
    }

    public void setAlert(boolean alert) {
        this.alert.set(alert);
    }

    public int getDaysBefore() {
        return daysBefore.get();
    }

    public void setDaysBefore(int daysBefore) {
        this.daysBefore.set(daysBefore);
    }

    public String getObs() {
        return obs.get();
    }

    public void setObs(String obs) {
        this.obs.set(obs);
    }

    public int getId() {
        return id.get();
    }

    public void setId(int id) {
        this.id.set(id);
    }

    public IntegerProperty getPriorityProperty() {
        return priority;
    }

    public StringProperty getDescriptionProperty() {
        return description;
    }

    public BooleanProperty getAlertProperty() {
        return alert;
    }

    public ObjectProperty<LocalDate> getDueDateProperty() {
        return dueDate;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 59 * hash + Objects.hashCode(this.id);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Task other = (Task) obj;
        if (!Objects.equals(this.id, other.id)) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Task{" + "id=" + id + ", description=" + description 
             + ", priority=" + priority + ", dueDate=" + dueDate 
             + ", alert=" + alert + ", daysBefore=" + daysBefore 
             + ", obs=" + obs + ", completed=" + completed + '}';
    }

}

However, this might be a bad idea because you bind your model (Task) to presentation details of the View. It would be wise to keep Task as a pure POJO, and use a wrapper class (TaskWrapper) to handle the View details. If in the future you decide to abandon JavaFX and use another presentation layer (e.g. web or DukeScript) you won't need to change the Task again.

package todofx.model;

import java.time.LocalDate;
import java.util.Objects;

/**
 * Task
 *
 * @author ikost
 */
public class Task {

    private int id;
    private String description;
    private int priority;
    private LocalDate dueDate;
    private boolean alert;
    private int daysBefore;
    private String obs;
    private boolean completed;

    public Task() {
        this(-1, "", 0, LocalDate.now());
    }

    public Task(int id, String descr, int prio, LocalDate due) {
        this(id, descr, prio, due, false, 0, "");
    }

    public Task(int id, String descr, int prio, LocalDate due, boolean alert, int daysBefore) {
        this(id, descr, prio, due, alert, daysBefore, "");
    }

    public Task(int id, String descr, int prio, LocalDate due, 
                boolean alert, int daysBefore, String obs) {
        this.id = id;
        this.description = descr;
        this.priority = prio;
        this.dueDate = due;
        this.alert = alert;
        this.daysBefore = daysBefore;
        this.obs = obs;
        this.completed = false;
    }

    public boolean isLate() {
        LocalDate dateDue = getDueDate();
        return (dateDue == null) ? false : dateDue.compareTo(LocalDate.now()) < 0;
    }

    public boolean hasAlert() {
        LocalDate dateDue = getDueDate();
        if (!getAlert() || dateDue == null) {
            return false;
        } else {
            int dias = dueDate.getDayOfYear() - LocalDate.now().getDayOfYear();
            return dias <= getDaysBefore();
        }
    }

    public boolean isCompleted() {
        return completed;
    }

    public void setCompleted(boolean completed) {
        this.completed = completed;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public int getPriority() {
        return priority;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    public LocalDate getDueDate() {
        return dueDate;
    }

    public void setDueDate(LocalDate dueDate) {
        this.dueDate = dueDate;
    }

    public boolean getAlert() {
        return alert;
    }

    public void setAlert(boolean alert) {
        this.alert = alert;
    }

    public int getDaysBefore() {
        return daysBefore;
    }

    public void setDaysBefore(int daysBefore) {
        this.daysBefore = daysBefore;
    }

    public String getObs() {
        return obs;
    }

    public void setObs(String obs) {
        this.obs = obs;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 59 * hash + Objects.hashCode(this.id);
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final Task other = (Task) obj;
        if (!Objects.equals(this.id, other.id)) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "Task{" + "id=" + id + ", description=" + description
                + ", priority=" + priority + ", dueDate=" + dueDate
                + ", alert=" + alert + ", daysBefore=" + daysBefore
                + ", obs=" + obs + ", completed=" + completed + '}';
    }

}

Instead, we shall create a TaskWrapper, inside viewcontroller that will contain the JavaFX properties:

package todofx.viewcontroller;

import java.time.LocalDate;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import todofx.model.Task;

/**
 * A JavaFX wrapper for Task
 *
 * @author ikost
 */
public class TaskWrapper {

    private final IntegerProperty idProperty;
    private final StringProperty descriptionProperty;
    private final IntegerProperty priorityProperty;
    private final ObjectProperty<LocalDate> dueDateProperty;
    private final BooleanProperty alertProperty;
    private final IntegerProperty daysBeforeProperty;
    private final StringProperty obsProperty;
    private final BooleanProperty completedProperty;
    private final Task task;

    public TaskWrapper(Task task) {
        this.task = task;
        this.idProperty = new SimpleIntegerProperty(task.getId());
        this.descriptionProperty = new SimpleStringProperty(task.getDescription());
        this.descriptionProperty.addListener(      
            (ObservableValue<? extends String> observable, String oldValue, String newValue)
                -> task.setDescription(newValue));
        this.priorityProperty = new SimpleIntegerProperty(task.getPriority());
        this.priorityProperty.addListener(    
            (ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
                -> task.setPriority(newValue.intValue())
        );
        this.dueDateProperty = new SimpleObjectProperty<>(task.getDueDate());
        this.dueDateProperty.addListener(
            (ObservableValue<? extends LocalDate> observable, LocalDate oldValue, LocalDate newValue)
                -> task.setDueDate(newValue)
        );
        this.alertProperty = new SimpleBooleanProperty(task.getAlert());
        this.alertProperty.addListener(
            (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
                -> task.setAlert(newValue)
        );
        this.daysBeforeProperty = new SimpleIntegerProperty(task.getDaysBefore());
        this.daysBeforeProperty.addListener(
            (ObservableValue<? extends Number> observable, Number oldValue, Number newValue)
                -> task.setDaysBefore(newValue.intValue())
        );
        this.obsProperty = new SimpleStringProperty(task.getObs());
        this.obsProperty.addListener(
            (ObservableValue<? extends String> observable, String oldValue, String newValue)
                -> task.setObs(newValue)
        );
        this.completedProperty = new SimpleBooleanProperty(task.isCompleted());
        this.completedProperty.addListener(
            (ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue)
                -> task.setCompleted(newValue)
        );
    }

    public Task getTask() {
        return task;
    }

    public IntegerProperty getPriorityProperty() {
        return priorityProperty;
    }

    public StringProperty getDescriptionProperty() {
        return descriptionProperty;
    }

    public BooleanProperty getAlertProperty() {
        return alertProperty;
    }

    public ObjectProperty<LocalDate> getDueDateProperty() {
        return dueDateProperty;
    }

    public int getId() {
        return idProperty.get();
    }

    public String getDescription() {
        return descriptionProperty.get();
    }

    public void setDescription(String description) {
        this.descriptionProperty.set(description);
    }

    public int getPriority() {
        return priorityProperty.get();
    }

    public void setPriority(int priority) {
        this.priorityProperty.set(priority);
    }

    public LocalDate getDueDate() {
        return dueDateProperty.get();
    }

    public void setDueDate(LocalDate dueDate) {
        this.dueDateProperty.set(dueDate);
    }

    public boolean getAlert() {
        return alertProperty.get();
    }

    public void setAlert(boolean alert) {
        this.alertProperty.set(alert);
    }

    public int getDaysBefore() {
        return daysBeforeProperty.get();
    }

    public void setDaysBefore(int daysBefore) {
        this.daysBeforeProperty.set(daysBefore);
    }

    public void setObs(String obs) {
        this.obsProperty.set(obs);
    }

    public String getObs() {
        return obsProperty.get();
    }

    public void setCompleted(boolean b) {
        this.completedProperty.set(b);
    }

    public boolean isCompleted() {
        return completedProperty.get();
    }

    public boolean isLate() {
        return getTask().isLate();
    }

    public boolean hasAlert() {
        return getTask().hasAlert();
    }

    @Override
    public boolean equals(Object obj) {
        return getTask().equals(obj);
    }

    @Override
    public int hashCode() {
        return getTask().hashCode();
    }

    @Override
    public String toString() {
        return getTask().toString();
    }
}

We mainly wrap a Task object with JavaFX properties. In the constructor we make sure to update the Task attributes when the respective JavaFX properties change. This class will be used for the presentation layer.

Apply the modifications of TodoRCP2 article to make TaskManager Java 8 compatible. An automated way to increment Task ids when new tasks are inserted was added. Finally, it makes sense to make TaskManager a Singleton. Remember that for this step, TaskManager is just a mockup that stores tasks in memory only.

package todofx.model;

import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import static java.util.stream.Collectors.toList;

public final class TaskManager {

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

    private TaskManager() {
        tasks.add(new Task(getNextIndex(), "Hotel Reservation", 1, LocalDate.of(2016, 7, 2), true, 2));
        tasks.add(new Task(getNextIndex(), "Review BOF-1", 1, LocalDate.of(2016, 7, 9), true, 1));
        tasks.add(new Task(getNextIndex(), "Reserve time for visit", 2, LocalDate.of(2016, 6, 15)));
    }

    private int getNextIndex() {
        return INDEX.incrementAndGet();
    }

    private final static class SingletonHolder {

        private final static TaskManager INSTANCE = new TaskManager();
    }

    public static TaskManager getInstance() {
        return SingletonHolder.INSTANCE;
    }

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

    public List<Task> listTasksWithAlert() {
        return tasks.stream().filter(Task::hasAlert).collect(toList());
    }

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

    public void updateTask(final Task task) throws ValidationException {
        validate(task);
        Task oldTask = findTask(task.getId()).get();
        if (oldTask != null) {
            tasks.set(tasks.indexOf(oldTask), task);
        }
    }

    public void markAsCompleted(final int id, final boolean completed) {
        Task task = findTask(id).get();
        if (task != null) {
            task.setCompleted(completed);
        }
    }

    public void removeTask(final int id) {
        tasks.removeIf(task -> id == task.getId());
    }

    private boolean isEmpty(final String str) {
        return str == null || str.trim().isEmpty();
    }

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

    private Optional<Task> findTask(final int id) {
        return tasks.stream().filter(task -> id == task.getId()).findFirst();
    }

    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());
        }
    }
}

JavaFX offers another useful class, ObservableList, which allows to be notified when one of its elements has changed. Since we don't want to pollute our TaskManager with details of the view, we create a wrapper class inside viewcontroller which will be used by the views:

package todofx.viewcontroller;

import java.util.List;
import static java.util.stream.Collectors.toList;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import todofx.model.Task;

/**
 * 
 * @author ikost
 */
public final class TaskListWrapper {

    /**
     * @param allTasks a list of tasks
     * @return an ObservableList of TaskWrappers
     */
    public static final ObservableList<TaskWrapper> wrap(List<Task> allTasks) {
        return FXCollections.observableArrayList(
           allTasks.stream().map(task -> new TaskWrapper(task)).collect(toList()));
    }
}

Now let's finally insert some data into our table. In order to access the widgets in the view (.fxml), some variables need to be added to the controller that can be referenced from the view. A special @FXML annotation is used in the controller that allows the view to get access to private fields and private methods of the controller. After we have everything set up in the fxml file, the application will automatically fill the variables when the fxml file is loaded.

public class TaskMainController implements Initializable {

    @FXML
    private TableView<TaskWrapper> taskTable;
    @FXML
    private TableColumn<TaskWrapper, Integer> priorityColumn;
    @FXML
    private TableColumn<TaskWrapper, String> descriptionColumn;
    @FXML
    private TableColumn<TaskWrapper, Boolean> alertColumn;
    @FXML
    private TableColumn<TaskWrapper, LocalDate> dueDateColumn;

    @Override
    public void initialize(URL url, ResourceBundle rb) {
        // Add observable list data to the table    
        taskTable.setItems(TaskListWrapper.wrap(
            TaskManager.getInstance().listAllTasks(true)));
        taskTable.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
        priorityColumn.setCellValueFactory(cellData -> 
           cellData.getValue().getPriorityProperty().asObject());
        descriptionColumn.setCellValueFactory(cellData -> 
           cellData.getValue().getDescriptionProperty());
        alertColumn.setCellValueFactory(cellData -> 
           cellData.getValue().getAlertProperty());
        dueDateColumn.setCellValueFactory(cellData -> 
           cellData.getValue().getDueDateProperty());
    }
}

IntegerProperties need special attention (getPriorityProperty().asObject()) due to a bad design decision (see this discussion).

We now need to link the TaskMain.fxmls' widgets to the TaskMainController's fields.

  1. Open the TaskMain.fxml in SceneBuilder.
  2. Select the TableView
  3. In the Inspector area, click on Code and set the fx:id to be taskTable.
  4. Repeat the above steps for the four TableColumns.

Run the application again. You should be able to see 3 tasks in the TableView.

Verify that TaskMain.fxml's controller class is todofx.viewcontroller.TaskMainController (hint! Document area, Controller tab).

"Figure 15 - TodoFX application with sample data"

Nice, but we would like to format the date to e.g. dd/MM/yyyy. Create the following class in todofx.util:

package todofx.util;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;

/**
 * Helper functions for handling dates.
 *
 * @author Marco Jakob
 */
public class DateUtil {

    /** The date pattern that is used for conversion. Change as you wish. */
    public static final String DATE_PATTERN = "dd/MM/yyyy";

    /** The date formatter. */
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern(DATE_PATTERN);

    /**
     * Returns the given date as a well formatted String. The above defined
     * {@link DateUtil#DATE_PATTERN} is used.
     *
     * @param date the date to be returned as a string
     * @return formatted string
     */
    public static String format(LocalDate date) {
        if (date == null) {
            return null;
        }
        return DATE_FORMATTER.format(date);
    }

    /**
     * Converts a String in the format of the defined
     * {@link DateUtil#DATE_PATTERN} to a {@link LocalDate} object.
     *
     * Returns null if the String could not be converted.
     *
     * @param dateString the date as String
     * @return the date object or null if it could not be converted
     */
    public static LocalDate parse(String dateString) {
        try {
            return DATE_FORMATTER.parse(dateString, LocalDate::from);
        } catch (DateTimeParseException e) {
            return null;
        }
    }

    /**
     * Checks the String whether it is a valid date.
     *
     * @param dateString
     * @return {@code true} if {@code dateString} is a valid date
     */
    public static boolean validDate(String dateString) {
        // Try to parse the String.
        return DateUtil.parse(dateString) != null;
    }
}

Then add one more line inside TaskMainController's initialize() method:

        dueDateColumn.setCellFactory(column -> {
            return new TableCell<TaskWrapper, LocalDate>() {
                @Override
                protected void updateItem(LocalDate item, boolean empty) {
                    super.updateItem(item, empty);
                    if (item == null || empty) {
                        setText(null);
                        setStyle("");
                    } else {
                        setText(DateUtil.format(item));
                    }
                }
            };
        });

Finally, we would like to render rows with different color depending on whether a task is completed, late, of if it has alerts (still inside initialize() method):

        final String defaultStyle = taskTable.getStyle();
        taskTable.setRowFactory(tableView -> {
            return new TableRow<TaskWrapper>() {
                @Override
                protected void updateItem(TaskWrapper item, boolean empty) {
                    super.updateItem(item, empty);
                    if (!empty) {
                        if (item.isCompleted()) {
                            setStyle("-fx-background-color: #95caff;");
                        } else if (item.isLate()) {
                            setStyle("-fx-background-color: #f06f06;");
                        } else if (item.hasAlert()) {
                            setStyle("-fx-background-color: #ffff00;");
                         } else {
                            setStyle(defaultStyle);
                        }
                    } else {
                        setStyle(defaultStyle);
                    }
                }
            };
        });

(Note that setTextFill() doesn't seem to work here.)

In JavaFX you can change the look & feel of your user interface using Cascading Style Sheets (CSS). The default CSS file in JavaFX 8 is called modena.css. This css file can be found in the JavaFX jar file jfxrt.jar located in your Java folder under jdk1.8.x/jre/lib/ext/jfxrt.jar. The modena.css can be found under com/sun/javafx/scene/control/skin/modena/.

This default style sheet is always applied to a JavaFX application. By adding a custom style sheet we can override the default styles of the modena.css.

Once you create your stylesheet, you can add it to your application from SceneBuilder by clicking on the Stylesheets button under Properties:

"Figure 16 - Set stylesheet in SceneBuilder"

For more JavaFX specific information about CSS have a look at:

Let's now see how we can add action functionality to our buttons and menu items. Add the following method in TaskMainController:

    /**
     * Called when the user clicks on Show Alerts.
     */
    @FXML
    private void handleShowAlerts() {
        List<Task> tasksWithAlert = TaskManager.getInstance().listTasksWithAlert();
        tasksWithAlert.stream().forEach(t -> {
            Alert alert = new Alert(Alert.AlertType.INFORMATION);
            alert.setTitle("Alert");
            alert.setHeaderText("Task: " + t.getDescription());
            alert.setContentText("expired on " + DateUtil.format(t.getDueDate()));
            alert.showAndWait();
        });
        if (tasksWithAlert.isEmpty()) {
            lblMessage.setText("There are no tasks with alerts for today.");
        } else {
            lblMessage.setText("There are " + tasksWithAlert.size() + " task(s) with alerts for today.");
        }
    }

and the following field:

@FXML
private Label lblMessage;

When there are tasks with alerts (e.g. tasks that expire), then dialog boxes are displayed to notify the user and the status bar displays a relevant message. Add a call to this method at the end of initialize().

Finally, we need to link this method to the TaskMain.fxml. In SceneBuilder:

  1. Select the last button of the toolbar (Show Alerts)
  2. Click on Inspector --> Code and fill in the On Action property as shown in the following figure.
  3. Repeat the above step for the Show Alerts menu item.
  4. Don't forget to link lblMessage to the status bar label.

"Figure 17 - Set action in SceneBuilder"

Let's add the rest of the functionality. Add the following fields:

    @FXML
    private ToggleButton chkShowCompletedTasks;
    @FXML
    private CheckMenuItem chkMnuShowCompletedTasks;
    @FXML
    private ToggleButton chkSortBy;
    @FXML
    private Button btnMarkAsCompleted;
    @FXML
    private ToggleGroup toggleGroup;

and link them to their respective buttons/menu items in the SceneBuilder. Add the following handler methods:

    @FXML
    private void handleAbout() {
        Alert alert = new Alert(Alert.AlertType.INFORMATION);
        alert.setTitle("About TodoFX");
        alert.setHeaderText("TodoFX - Task List\nRelease 1.0\n\n"
                + "                http://wiki.netbeans.org/TodoFX");
        alert.setContentText("Author: Ioannis Kostaras");
        alert.showAndWait();
    }

    @FXML
    private void handleRemoveTask() {
        ObservableList<TaskWrapper> selectedTasks = taskTable.getSelectionModel().getSelectedItems();
        selectedTasks.stream().forEach(task -> {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Remove Task");
            alert.setHeaderText("Are you sure you want to remove task? ");
            alert.setContentText(task.getDescription());
            Optional<ButtonType> result = alert.showAndWait();
            if (result.get() == ButtonType.OK) {
                taskTable.getItems().remove(task);
                TaskManager.getInstance().removeTask(task.getId());
            }
        });
    }

    /**
     * Called when the user clicks Show Completed Tasks.
     */
    private void showCompletedTasks(boolean show) {
        if (show) {
            taskTable.setItems(TaskListWrapper.wrap(
                 TaskManager.getInstance().listAllTasks(true)
                     .filtered(t -> t.isCompleted())));
        } else {
            taskTable.setItems(TaskListWrapper.wrap(
                 TaskManager.getInstance().listAllTasks(true)));
        }
    }

    /**
     * Called when the user clicks on Show Completed Tasks check button.
     */
    @FXML
    private void handleButtonShowCompletedTasks() {
        showCompletedTasks(chkShowCompletedTasks.isSelected());
        chkMnuShowCompletedTasks.setSelected(chkShowCompletedTasks.isSelected());
    }

    /**
     * Called when the user clicks Show Completed Tasks check menu item.
     */
    @FXML
    private void handleMenuItemShowCompletedTasks() {
        showCompletedTasks(chkMnuShowCompletedTasks.isSelected());
        chkShowCompletedTasks.setSelected(chkMnuShowCompletedTasks.isSelected());
    }

    /**
     * Called when the user clicks Sort By Priority/Due Date check button.
     */
    @FXML
    private void handleSortBy() {
        sortBy(chkSortBy.isSelected());
    }

    /**
     * @param byPriority if {@code true} sort tasks by priority, otherwise by
     * due date
     */
    private void sortBy(boolean byPriority) {
        taskTable.setItems(TaskListWrapper.wrap  
            (TaskManager.getInstance().listAllTasks(byPriority)));
        chkSortBy.setSelected(byPriority);
    }

    /**
     * Called when the user clicks Sort By Priority menu item.
     */
    @FXML
    private void handleSortByPriority() {
        sortBy(true);
    }

    /**
     * Called when the user clicks Sort By Due Date menu item.
     */
    @FXML
    private void handleSortByDueDate() {
        sortBy(false);
    }

    /**
     * Called when the user clicks Mark As Completed.
     */
    @FXML
    private void handleMarkAsCompleted() {
        ObservableList<TaskWrapper> selectedTasks = taskTable.getSelectionModel().getSelectedItems();
        selectedTasks.forEach(selectedTask -> {
            if (selectedTask != null) {
                selectedTask.setCompleted(true);
                TaskManager.getInstance().markAsCompleted(selectedTask.getId(), true);
            } else {
                Alert alert = new Alert(Alert.AlertType.WARNING);
                alert.setTitle("Warning");
                alert.setHeaderText("No Task Selected");
                alert.setContentText("Select a task first");
                alert.showAndWait();
            }
        });
        taskTable.refresh();
    }

    /**
     * Closes the application.
     */
    @FXML
    private void handleExit() {
        System.exit(0);
    }

and link them to their respective buttons/menu items, too.

The last part is the functionality of Add Task and Edit Task buttons and menu items. TaskMainController need to somehow know about TaskDetailsDialogController. The solution shown below is a dirty and easy one that passes the Main class to the TaskMainController. We could have used a more loosely coupled solution (using NetBeans RCP lookups for example). Don't forget to link them to the respective buttons/menu items.

    private Main mainApp;
    public void setMainApp(Main mainApp) {
        this.mainApp = mainApp;
    }

    @FXML
    private void handleNewTask() {
        Task tempTask = new Task();
        boolean saveClicked = mainApp.showTaskDetailsDialog(new TaskWrapper(tempTask));
        if (saveClicked) {
            try {
                TaskManager.getInstance().addTask(tempTask);
            } catch (ValidationException ex) {
                Logger.getLogger(TaskMainController.class.getName())
                      .log(Level.SEVERE, "Description is empty!", ex);
            }
        }
    }

    @FXML
    private void handleEditTask() {
        TaskWrapper selectedTask = taskTable.getSelectionModel().getSelectedItem();
        if (selectedTask != null) {
            boolean saveClicked = mainApp.showTaskDetailsDialog(selectedTask);
        } else {
            // Nothing selected.
            Alert alert = new Alert(Alert.AlertType.WARNING);
            alert.setTitle("No Selection");
            alert.setHeaderText("No Task Selected");
            alert.setContentText("Please select a task in the table.");

            alert.showAndWait();
        }
    }    

Main needs to be modified accordingly:

    private Stage primaryStage;

    @Override
    public void start(Stage stage) throws Exception {
        primaryStage = stage;
        // Load the fxml file and create a new stage for the main window.
        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("viewcontroller/TaskMain.fxml"));
        Parent root = (VBox) loader.load();

        TaskMainController controller = loader.getController();
        controller.setMainApp(this);

        Scene scene = new Scene(root);

        primaryStage.setScene(scene);
        primaryStage.setTitle("TodoFX");
        primaryStage.show();
    }

    /**
     * Opens a dialog to edit details for the specified task. If the user clicks
     * OK, the changes are saved into the provided Task object and {@code true}
     * is returned.
     *
     * @param task the task object to be edited
     * @return {@code true} if the user clicked OK, {@code false} otherwise.
     */
    public boolean showTaskDetailsDialog(TaskWrapper task) {
        try {
            // Load the fxml file and create a new stage for the popup dialog.
            FXMLLoader loader = new FXMLLoader();
            loader.setLocation(Main.class.getResource("viewcontroller/TaskDetailsDialog.fxml"));
            Parent page = (VBox) loader.load();

            // Create the dialog Stage.
            Stage dialogStage = new Stage();
            String title = task.getId() == -1 ? "New Task" : "Edit Task";
            dialogStage.setTitle(title);
            dialogStage.getIcons().add(new Image("file:resources/icons/problems_view.gif"));
            dialogStage.initModality(Modality.WINDOW_MODAL);
            Scene scene = new Scene(page);
            dialogStage.setScene(scene);

            // Set the task into the controller.
            TaskDetailsDialogController controller = loader.getController();
            controller.setDialogStage(dialogStage);
            controller.setNewTask(task.getId() == -1);
            controller.setTask(task);

            // Show the dialog and wait until the user closes it
            dialogStage.showAndWait();

            return controller.isSaveClicked();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
    }

Add the following code segment inside initialize(), too:

  taskTable.setOnMousePressed(event -> {
     if (event.isPrimaryButtonDown() && event.getClickCount() == 2
           && taskTable.getSelectionModel().getSelectedIndex() > -1) {
               handleEditTask();
     }
  });

This allows the user to open the TaskDetailsDialog window on double click.

And, of course, it is about time to build the TaskDetailsDialogController:

package todofx.viewcontroller;

import java.net.URL;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import todofx.model.Task;
import todofx.util.DateUtil;

/**
 *
 * @author ikost
 */
public class TaskDetailsDialogController implements Initializable {

    @FXML
    private Label lblHeader;
    @FXML
    private TextField txtDescription;
    @FXML
    private Spinner spPriority;
    @FXML
    private DatePicker dtDueDate;
    @FXML
    private CheckBox chkAlert;
    @FXML
    private Spinner spDaysBefore;
    @FXML
    private Label lblAlert;
    @FXML
    private TextArea txtaObs;
    @FXML
    private CheckBox chkCompleted;
    @FXML
    private Button btnRemoved;
    private TaskWrapper task;
    private Stage dialogStage;
    private boolean isNewTask;
    private boolean saveClicked = false;
    private final SpinnerValueFactory svfPriority = 
            new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 10);
    private final SpinnerValueFactory svfDaysBefore = 
            new SpinnerValueFactory.IntegerSpinnerValueFactory(0, 30);

    /**
     * Initializes the controller class.
     */
    @Override
    public void initialize(URL url, ResourceBundle rb) {
        spPriority.setValueFactory(svfPriority);
        spDaysBefore.setValueFactory(svfDaysBefore);
        chkAlert.setOnAction(event -> {
            spDaysBefore.setDisable(!chkAlert.isSelected());
            lblAlert.setDisable(!chkAlert.isSelected());
        });
        dtDueDate.setPromptText(DateUtil.DATE_PATTERN);
        dtDueDate.setConverter(new StringConverter<LocalDate>() {
            DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(DateUtil.DATE_PATTERN);

            @Override
            public String toString(LocalDate date) {
                return date != null ? dateFormatter.format(date) : "";
            }

            @Override
            public LocalDate fromString(String string) {
                return string == null || string.isEmpty() ? 
                         null : LocalDate.parse(string, dateFormatter);
            }
        });
    }

    /**
     * Sets the stage of this dialog.
     *
     * @param dialogStage
     */
    public void setDialogStage(Stage dialogStage) {
        this.dialogStage = dialogStage;
    }

    public void setNewTask(boolean newTask) {
        this.isNewTask = newTask;
        btnRemoved.setDisable(newTask);
        if (isNewTask()) {
            setMessage("Fill in the new task information", false);
        } else {
            setMessage("Changing task information", false);
        }
    }

    public boolean isNewTask() {
        return isNewTask;
    }

    public void setMessage(String msg, boolean isError) {
        lblHeader.setText(msg);
        if (isError) {
            lblHeader.setTextFill(Color.RED);
        } else {
            lblHeader.setTextFill(Color.BLUE);
        }
    }

    public void setTask(TaskWrapper task) {
        this.task = task;
        txtDescription.setText(task.getDescription());
        svfPriority.setValue(task.getPriority());
        chkCompleted.setSelected(task.isCompleted());
        dtDueDate.setValue(task.getDueDate());
        chkAlert.setSelected(task.getAlert());
        spDaysBefore.setDisable(!task.getAlert());
        lblAlert.setDisable(!task.getAlert());
        svfDaysBefore.setValue(task.getDaysBefore());
        txtaObs.setText(task.getObs());
        btnRemoved.setDisable(task.getId() == -1);
    }

    public Task getTask() {
        task.setDescription(txtDescription.getText());
        task.setPriority(((Number) spPriority.getValue()).intValue());
        task.setCompleted(chkCompleted.isSelected());
        task.setDueDate(dtDueDate.getValue());
        task.setAlert(chkAlert.isSelected());
        task.setDaysBefore(((Number) spDaysBefore.getValue()).intValue());
        task.setObs(txtaObs.getText());
        return task;
    }

    /**
     * @return {@code true} if the user clicked Save, {@code false} otherwise.
     */
    public boolean isSaveClicked() {
        return saveClicked;
    }

}

Add the following handling methods and don't forget to link them to their respective buttons:

    /**
     * Called when the user clicks Cancel.
     */
    @FXML
    private void handleCancel() {
        dialogStage.close();
    }

    /**
     * Called when the user clicks Remove.
     */
    @FXML
    private void handleRemove() {
        task = getTask();
        saveClicked = false;
        Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
        alert.initOwner(dialogStage);
        alert.setTitle("Remove Task");
        alert.setHeaderText("Are you sure you want to remove task? ");
        alert.setContentText(task.getDescription());
        Optional<ButtonType> result = alert.showAndWait();
        if (result.get() == ButtonType.OK) {
            TaskManager.getInstance().removeTask(task.getId());
        }
        dialogStage.close();
    }

    /**
     * Called when the user clicks Save.
     */
    @FXML
    private void handleSave() {
        if (isInputValid()) {
            task = getTask();
            saveClicked = true;
            dialogStage.close();
        }
    }

    /**
     * Validates the user input in the text fields.
     *
     * @return {@code true} if the input is valid
     */
    private boolean isInputValid() {
        String errorMessage = "";

        if (txtDescription.getText() == null || txtDescription.getText().isEmpty()) {
            errorMessage += "No valid description!\n";
        }

        if (dtDueDate.getValue() == null) {
            errorMessage += "No valid due date!\n";
        }

        if (errorMessage.length() == 0) {
            return true;
        } else {
            // Show the error message.
            Alert alert = new Alert(AlertType.ERROR);
            alert.initOwner(dialogStage);
            alert.setTitle("Invalid Fields");
            alert.setHeaderText("Please correct invalid fields");
            alert.setContentText(errorMessage);

            alert.showAndWait();

            return false;
        }
    }

Done!

End of Step 2

Now that we have fully functional view and model classes, it’s time to start replacing 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 we did, and another team working on business and persistence logic, preferably using TDD. They can work in parallel and join at the end, putting together functional view and controller implementations with functional model implementations. Most of the work in this step was just coding. NetBeans provides nice code editors and a good debugger that eases the task providing the usual benefits: code-completion, JavaDoc integration and refactoring support. But it can go beyond: it’s very easy to build in NetBeans new plug-in modules to package your project coding standards, such as project templates, controller class templates and so on.

Step 3 - Model classes and database

The Todo application uses HSQLDB, an embedded Java database. This allows the application to meet the ease of deployment requirements for a typical desktop application.

Follow the steps in the original article's Step 3 in order to setup HSQLDB with NetBeans. Add hsqldb.jar to your project's libraries (lib) directory:

  1. Right-click on Libraries
  2. Add JAR/Folder
  3. Navigate to the folder where you downloaded HSQLDB and inside lib
  4. Choose hsqldb.jar and choose Copy to Libraries Folder
  5. Click Open

To finish the application, you need to copy five more classes from the original to-do application to todofx.model package: TaskManager (as TaskManagerDB), Parameters, ModelException, ValidationException and DatabaseException. TaskManagerDB replaces TaskManager with some modifications — make it a singleton and return ObservableList<Task> instead of List<Task>:

package todofx.model;

import java.sql.Connection;
import java.sql.Date;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

public final class TaskManagerDB {

    private final Parameters params;
    private Connection con;
    private Statement stmt;
    private ResultSet rs;

    private TaskManagerDB(Parameters params) {
        this.params = params;
        try {
            connect();
        } catch (DatabaseException ex) {
            
Logger.getLogger(TaskManagerDB.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private final static class SingletonHolder {

        private final static TaskManagerDB INSTANCE = new TaskManagerDB(new Parameters());
    }

    public static TaskManagerDB getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public Parameters getParams() {
        return params;
    }

    public void reconnect(String database) throws DatabaseException {
        disconnect();
        params.setDatabase(database);
        connect();
    }

    private void connect() throws DatabaseException {
        try {
            Class.forName(params.getJdbcDriver());
            con = DriverManager.getConnection(params.getJdbcUrl(), "sa", "");
            if (!checkTables()) {
                createTables();
            }
        } catch (DatabaseException e) {
            throw new DatabaseException("Cannot initialize the database tables", e.getCause());
        } catch (ClassNotFoundException e) {
            throw new DatabaseException("Cannot load the database driver", e);
        } catch (SQLException e) {
            throw new DatabaseException("Cannot open the database", e);
        }
    }

    private boolean checkTables() {
        try {
            String sql = "SELECT COUNT(*) FROM todo";
            stmt = con.createStatement();
            rs = stmt.executeQuery(sql);
            return true;
        } catch (SQLException e) {
            return false;
        } finally {
            cleanUp();
        }
    }

    private void createTables() throws DatabaseException {
        update("CREATE TABLE todo ("
                + "id IDENTITY, "
                + "description VARCHAR(100), "
                + "priority INTEGER, "
                + "completed BOOLEAN, "
                + "dueDate DATE, "
                + "alert BOOLEAN, "
                + "daysBefore INTEGER, "
                + "obs VARCHAR(250) "
                + ")");
    }

    public void disconnect() {
        try {
            if (con != null) {
                con.close();
            }
            con = null;
        } catch (SQLException e) {
            // ignores the exception
        }
    }

    private void cleanUp() {
        try {
            if (rs != null) {
                rs.close();
            }
            rs = null;
            if (stmt != null) {
                stmt.close();
            }
            stmt = null;
        } catch (SQLException e) {
            // ignores the exception
        }
    }

    private void update(String sql) throws DatabaseException {
        try {
            stmt = con.createStatement();
            stmt.executeUpdate(sql);
        } catch (SQLException e) {
            throw new DatabaseException("Cannot modify the database", e);
        } finally {
            cleanUp();
        }
    }

    private PreparedStatement prepare(String sql) throws SQLException {
        try {
            PreparedStatement pst = con.prepareStatement(sql);
            stmt = pst;
            return pst;
        } finally {
            cleanUp();
        }
    }

    private List<Task> query(String where, String orderBy) throws DatabaseException {
        List<Task> result = new ArrayList<>();
        try {
            String sql = "SELECT id, description, priority, completed, "
                    + "dueDate, alert, daysBefore, obs FROM todo ";
            if (where != null) {
                sql += "WHERE " + where + " ";
            }
            if (orderBy != null) {
                sql += "ORDER BY " + orderBy;
            }
            stmt = con.createStatement();
            rs = stmt.executeQuery(sql);
            while (rs.next()) {
                Task task = new Task();
                task.setId(rs.getInt(1));
                task.setDescription(rs.getString(2));
                task.setPriority(rs.getInt(3));
                task.setCompleted(rs.getBoolean(4));
                Date date = rs.getDate(5);
                task.setDueDate(date == null ? LocalDate.now() : date.toLocalDate());
                task.setAlert(rs.getBoolean(6));
                task.setDaysBefore(rs.getInt(7));
                task.setObs(rs.getString(8));
                result.add(task);
            }
        } catch (SQLException e) {
            throw new DatabaseException("Cannot fetch from database", e);
        } finally {
            cleanUp();
        }
        return result;
    }

    private void modify(String sql, Task task) throws DatabaseException {
        try {
            PreparedStatement pst = con.prepareStatement(sql);
            stmt = pst;
            pst.setString(1, task.getDescription());
            pst.setInt(2, task.getPriority());
            pst.setBoolean(3, task.isCompleted());
            if (task.getDueDate() == null) {
                pst.setDate(4, null);
            } else {
                pst.setDate(4, Date.valueOf(task.getDueDate()));
            }
            pst.setBoolean(5, task.getAlert());
            pst.setInt(6, task.getDaysBefore());
            pst.setString(7, task.getObs());
            pst.executeUpdate();
        } catch (SQLException e) {
            throw new DatabaseException("Cannot update the database", e);
        } finally {
            cleanUp();
        }
    }

    public List<Task> listAllTasks(boolean priorityOrDate) throws DatabaseException {
        return query(null, priorityOrDate
                ? "priority, dueDate, description" : "dueDate, priority, description");
    }

    public List<Task> listTasksWithAlert() throws ModelException {
        return query("alert = true AND "
                // BUG FIX to work with HSQLDB 2.3.3
                + "datediff('dd', CURRENT_TIMESTAMP, CAST(dueDate AS TIMESTAMP)) <= daysBefore", 
                //+ "datediff('dd', curtime(), dueDate) <= daysBefore", 
                "dueDate, priority, description");
    }

    public void addTask(Task task) throws ValidationException, DatabaseException {
        validate(task);
        String sql = "INSERT INTO todo ("
                + "description, priority, completed, dueDate, alert,"
                + "daysBefore, obs) VALUES (?, ?, ?, ?, ?, ?, ?)";
        modify(sql, task);
    }

    public void updateTask(Task task) throws ValidationException, DatabaseException {
        validate(task);
        String sql = "UPDATE todo SET "
                + "description = ?, priority = ?, completed = ?, dueDate = ?, "
                + "alert = ?, daysBefore = ?, obs = ? "
                + "WHERE id = " + task.getId();
        modify(sql, task);
    }

    public void markAsCompleted(int id, boolean completed) throws DatabaseException {
        update("UPDATE todo SET completed = " + completed + " "
                + "WHERE id = " + id);
    }

    public void removeTask(int id) throws DatabaseException {
        update("DELETE FROM todo WHERE id = " + id);
    }

    private boolean isEmpty(final String str) {
        return str == null || str.trim().isEmpty();
    }

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

TaskManager is used by TaskDetailsDialogController and TaskMainController. In TaskDetailsDialogController it is used only in handleRemove(), so replace it with the following lines:

  try {
      TaskManagerDB.getInstance().removeTask(task.getId());
  } catch (DatabaseException ex) {   
      Logger.getLogger(TaskDetailsDialogController.class.getName())
            .log(Level.SEVERE, null, ex);
  }

However, TaskMainController requires more changes. Inside initialize(), first line becomes:

  try {
    // Add observable list data to the table
    taskTable.setItems(TaskListWrapper.wrap(
        TaskManagerDB.getInstance().listAllTasks(true)));
  } catch (DatabaseException ex) {
           Logger.getLogger(TaskMainController.class.getName())
                 .log(Level.SEVERE, null, ex);
  }

and then:

    @FXML
    private void handleShowAlerts() {
        List<Task> tasksWithAlert = new ArrayList<>();
        try {
            tasksWithAlert = TaskManagerDB.getInstance().listTasksWithAlert();
        } catch (ModelException ex) {
            Logger.getLogger(TaskMainController.class.getName())
                  .log(Level.SEVERE, null, ex);
        }
...
    @FXML
    private void handleNewTask() {
        Task tempTask = new Task();
        boolean saveClicked = mainApp.showTaskDetailsDialog(new TaskWrapper(tempTask));
        if (saveClicked) {
            try {
                TaskManagerDB.getInstance().addTask(tempTask);
            } catch (DatabaseException | ValidationException ex) {
                Logger.getLogger(TaskMainController.class.getName())
                      .log(Level.SEVERE, null, ex);
            }
            try {
                // Add observable list data to the table
                taskTable.setItems(TaskListWrapper.wrap(
                    TaskManagerDB.getInstance().listAllTasks(true)));
            } catch (DatabaseException ex) {
                Logger.getLogger(TaskMainController.class.getName())
                      .log(Level.SEVERE, null, ex);
            }
        }
    }

    @FXML
    private void handleEditTask() {
        TaskWrapper selectedTask = taskTable.getSelectionModel().getSelectedItem();
        if (selectedTask != null) {
            boolean saveClicked = mainApp.showTaskDetailsDialog(selectedTask);
            if (saveClicked) {
                try {
                    TaskManagerDB.getInstance().updateTask(selectedTask.getTask());
                } catch (DatabaseException | ValidationException ex) {
                    Logger.getLogger(TaskMainController.class.getName())
                          .log(Level.SEVERE, null, ex);
                }
                taskTable.refresh();  
            }
        } else {
...
    @FXML
    private void handleRemoveTask() {
        ObservableList<TaskWrapper> selectedTasks = taskTable.getSelectionModel().getSelectedItems();
        selectedTasks.stream().forEach(task -> {
            Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
            alert.setTitle("Remove Task");
            alert.setHeaderText("Are you sure you want to remove task? ");
            alert.setContentText(task.getDescription());
            Optional<ButtonType> result = alert.showAndWait();
            if (result.get() == ButtonType.OK) {
                taskTable.getItems().remove(task);
                try {
                    TaskManagerDB.getInstance().removeTask(task.getId());
                    // refresh
                    taskTable.setItems(TaskListWrapper.wrap(
                        TaskManagerDB.getInstance().listAllTasks(true)));
                } catch (DatabaseException ex) {
                    Logger.getLogger(TaskMainController.class.getName())
                          .log(Level.SEVERE, null, ex);
                }
            }
        });
    }

    private void showCompletedTasks(boolean show) {
        if (show) {
            try {
                taskTable.setItems(TaskListWrapper.wrap(
                    TaskManagerDB.getInstance().listAllTasks(true)
                                 .filtered(t -> t.isCompleted())));
            } catch (DatabaseException ex) {
                Logger.getLogger(TaskMainController.class.getName())
                      .log(Level.SEVERE, null, ex);
            }
        } else {
            try {
                taskTable.setItems(TaskListWrapper.wrap(
                    TaskManagerDB.getInstance().listAllTasks(true)));
            } catch (DatabaseException ex) {
                Logger.getLogger(TaskMainController.class.getName())
                      .log(Level.SEVERE, null, ex);
            }
        }
    }
...
    private void sortBy(boolean byPriority) {
        try {
            taskTable.setItems(TaskListWrapper.wrap(
                TaskManagerDB.getInstance().listAllTasks(byPriority)));
        } catch (DatabaseException ex) {
            Logger.getLogger(TaskMainController.class.getName())
                  .log(Level.SEVERE, null, ex);
        }
        chkSortBy.setSelected(byPriority);
    }
...
    private void handleMarkAsCompleted() {
        ObservableList<TaskWrapper> selectedTasks = taskTable.getSelectionModel().getSelectedItems();
        selectedTasks.forEach(selectedTask -> {
            if (selectedTask != null) {
                selectedTask.setCompleted(true);
                try {
                    TaskManagerDB.getInstance().markAsCompleted(selectedTask.getId(), true);
                } catch (DatabaseException ex) {
                  Logger.getLogger(TaskMainController.class.getName())
                        .log(Level.SEVERE, null, ex);
                }
            } else {
...

We are still missing the two menu items New Task List and Open Task List under File menu (ported from the Todo Swing application). This is how they look like in JavaFX:

    /**
     * Called when the user clicks on New Task List.
     */
    @FXML
    private void handleNewTaskList() {
        FileChooser dlg = new FileChooser();
        dlg.setTitle("New Task List");
        dlg.getExtensionFilters().addAll(
                new ExtensionFilter("Task Lists - HSQLDB Databases (*.script)", "*.script"),
                new ExtensionFilter("All Files", "*.*")
        );
        File dir = new File(TaskManagerDB.getInstance().getParams().getDatabase()).getParentFile();
        dlg.setInitialDirectory(dir);
        File selectedFile = dlg.showSaveDialog(mainApp.getPrimaryStage());
        if (selectedFile != null) {
            try {
                String arq = createOpenDatabase(selectedFile);
                setStatus("Task list created: " + arq, false);
            } catch (DatabaseException e) {
                setStatus("Cannot create task list", true);
            }
        }
    }

    /**
     * Called when the user clicks on Open Task List.
     */
    @FXML
    private void handleOpenTaskList() {
        FileChooser dlg = new FileChooser();
        dlg.setTitle("Open Task List");
        dlg.getExtensionFilters().addAll(
                new ExtensionFilter("Task Lists - HSQLDB Databases (*.script)", "*.script"),
                new ExtensionFilter("All Files", "*.*")
        );
        File dir = new File(TaskManagerDB.getInstance().getParams().getDatabase()).getParentFile();
        dlg.setInitialDirectory(dir);
        File selectedFile = dlg.showOpenDialog(mainApp.getPrimaryStage());
        if (selectedFile != null) {
            try {
                String arq = createOpenDatabase(selectedFile);
                setStatus("Task List opened: " + arq, false);
            } catch (DatabaseException e) {
                setStatus("Cannot open task list", true);
            }
        }
    }

    private String createOpenDatabase(File databaseFile) throws DatabaseException {
        String fileName = databaseFile.getAbsolutePath();
        if (fileName.startsWith("file:")) {
            fileName = fileName.substring(5);
        }
        if (fileName.endsWith(".script")) {
            fileName = fileName.substring(0, fileName.length() - 7);
        }
        TaskManagerDB.getInstance().reconnect(fileName);
        taskTable.setItems(TaskManagerDB.getInstance().listAllTasks(true));
        return fileName;
    }

Main needs to be slightly modified:

    private Stage primaryStage;

    @Override
    public void start(Stage stage) throws Exception {
        primaryStage = stage;
        ...

    public Stage getPrimaryStage() {
        return primaryStage;
    }

Don't forget to link the two menu items with the above handler methods in SceneBuilder, and you are done!

Congratulations! You have created a fully blown JavaFX application where you can store your tasks and deadlines. In TodoFX2 article we shall show how to further modernize this application and add new functionality.

References

  1. Lozano F. (2006), "A complete App using NetBeans 5", NetBeans Magazine, Issue 1, May, http://netbeans.org/download/magazine/01/nb01_completeapp.pdf
  2. Anderson G., Anderson P. (2014), JavaFX Rich Client Programming on the Netbeans Platform, Addison-Wesley.
  3. Dea C. et al. (2014), JavaFX 8 Introduction by Example, 2nd Ed., APress.
  4. Duodu E. (2015), "How to Create a JavaFX GUI using Scene Builder in NetBeans", IDR Solutions.
  5. Jacob M. (2014), "JavaFX 8 Tutorial", code.makery.
  6. Sharan K. (2014), Learn JavaFX 8, APress.
  7. Tamam M. (2015), JavaFX Essentials, APress.
  8. Vos J. et al. (2014), Pro JavaFX 8, APress.
Not logged in. Log in, Register

By use of this website, you agree to the NetBeans Policies and Terms of Use. © 2012, Oracle Corporation and/or its affiliates. Sponsored by Oracle logo