edu.makery.ch

Education in the Making.

JavaFX 2 Tutorial Part III - Interacting With the User

Topics in Part III

  • React to selection changes in the table.
  • Add functionality to the add, edit, and remove buttons.
  • Create a custom popup dialog to edit a person.
  • Validating user input.

Other Tutorial Parts

React to Table Selections

Obviousely, we haven’t used the right side of our application, yet. The idea is when the user selects a person in the table, the details about that person should be displayed on the right side.

First, let’s add a new method inside PersonOverviewController that helps us fill the labels with the data from a single Person.

Create a method called showPersonDetails(Person person). Go trough all the labels and set the text using setText(...) with details from the person. If null is passed as parameter, all labels should be cleared.

PersonOverviewController.java
1
2
3
4
5
6
7
8
9
10
11
/**
 * Fills all text fields to show details about the person.
 * If the specified person is null, all text fields are cleared.
 * 
 * @param person the person or null
 */
private void showPersonDetails(Person person) {

// use setText(...) on all labels with info from the person object
// use setText("") on all labels if the person is null
}

Convert the Birthday Date to a String

If you’ve implemented the method above, you will have noticed that we need a way to convert the Calendar from the birthday field to a String. In a Label we can only display Strings.

We will use the conversion from Calendar and String (and vice versa) in several places. It’s good practice to create a helper class with static methods for this. We’ll call it CalendarUtil and place it in a seperate package called ch.makery.address.util:

(CalendarUtil.java) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package ch.makery.address.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;

/**
 * Helper functions for handling dates.
 */
public class CalendarUtil {

  /**
  * Default date format in the form 2013-03-18.
  */
  private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd");

  /**
  * Returns the given date as a well formatted string. The above defined date
  * format is used.
  * 
  * @param calendar date to be returned as a string
  * @return formatted string
  */
  public static String format(Calendar calendar) {
      if (calendar == null) {
          return null;
      }
      return DATE_FORMAT.format(calendar.getTime());
  }

  /**
  * Converts a String in the format "yyyy-MM-dd" to a Calendar object.
  * 
  * Returns null if the String could not be converted.
  * 
  * @param dateString the date as String
  * @return the calendar object or null if it could not be converted
  */
  public static Calendar parse(String dateString) {
      Calendar result = Calendar.getInstance();
      try {
          result.setTime(DATE_FORMAT.parse(dateString));
          return result;
      } catch (ParseException e) {
          return null;
      }
  }
  
  /**
  * Checks the String whether it is a valid date.
  * 
  * @param dateString
  * @return true if the String is a valid date
  */
  public static boolean validString(String dateString) {
      try {
          DATE_FORMAT.parse(dateString);
          return true;
      } catch (ParseException e) {
          return false;
      }
  }
}

Note that you can change the format of the date by changing the constant DATE_FORMAT. For all possible formats see SimpleDateFormat in the Java API.

Listen for Table Selection Changes

To get informed when the user selects a person in the person table, we need to listen for changes.

If you’re not familiar with the concept of anonymous classes you might want to take a look at an explanation in German or English.

There is an interface in JavaFX called ChangeListener with one method called changed(...). We need an anonymous class that implements this interface and add it to our person table. That sounds quite complicated. I’ll explain it, but first let’s take a look at the new code, added to the initialize() method in PersonOverviewController:

PersonOverviewController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@FXML
private void initialize() {
  // Initialize the person table
  firstNameColumn.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
  lastNameColumn.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
  // Auto resize columns
  personTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

  // clear person
  showPersonDetails(null);

  // Listen for selection changes
  personTable.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Person>() {

    @Override
    public void changed(ObservableValue<? extends Person> observable,
        Person oldValue, Person newValue) {
      showPersonDetails(newValue);
    }
  });
}

In line 10 we reset the person details. If you’ve implemented showPersonDetails(...) correctly this should set an empty String to all text fields.

In line 13 we get the selectedItemProperty of the person table and add a listener to it. The new ChangeListener is of type Person since we have Person objects in the table. Now, whenever the user selects a person in the table, the method changed(...) is called. We take the newly selected person and pass it to the showPersonDetails(...) method.

Try to run your application at this point. Verify that when you select a person in the table, details about that person are displayed on the right.

If something doesn’t work, you can compare your PersonOverviewController class with PersonOverviewController.java.

The Delete Button

Our user interface already contains a delete button, but without any functionality. We can select the action for a button inside the Scene Builder. Any method inside our controller that is annotated with @FXML (or is public) is accessible by the Scene Builder. Thus, let’s first create the delete method at the end of our PersonOverviewController class:

PersonOverviewController.java
1
2
3
4
5
6
7
8
/**
 * Called when the user clicks on the delete button.
 */
@FXML
private void handleDeletePerson() {
  int selectedIndex = personTable.getSelectionModel().getSelectedIndex();
  personTable.getItems().remove(selectedIndex);
}

Now, open the PersonOverview.fxml file in SceneBuilder. Select the Delete button, open the Code view and choose #handleDeletePerson in the dropdown of On Action.

Scene Builder Problem (fixed in Scene Builder 1.1 beta 17 and above!): In my version of Scene Builder (1.1 beta_11) the methods did not appear. I had to go to the root AnchorPane (in Hierarchy view), delete the controller class, hit enter and add the controller class again. Now, the methods appear in the dropdown. Hope this bug will be corrected soon.

Error Handling

If you run the application at this point, you should be able to delete selected persons from the table. But what happenes, if you click the delete button if no person is selected in the table?

There will be an ArrayIndexOutOfBoundsException because it could not remove a person item at index -1. The index -1 was returned by getSelectedIndex() which means that there was no selection.

To ignore such an error is not very nice, of course. We should let the user know that he/she must select a person before deleting. Even better would be if we disabled the button so that the user doesn’t even have the chance to do something wrong. I’ll show how to do the first approach here.

We’ll add a popup dialog to inform the user. You’ll need to add a library for the Dialogs:

  1. Download the newest javafx-dialogs-x.x.x.jar file from my GitHub Page.
  2. Create a lib subfolder in the project and add the jar file to this folder.
  3. Add the jar file to the project’s classpath: In Eclipse right-click on the jar file | Build Path | Add to Build Path.

With some changes made to the handleDeletePerson() method, we can show a popup dialog whenever the user pushes the delete button while no person is selected in the table:

PersonOverviewController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
 * Called when the user clicks on the delete button.
 */
@FXML
private void handleDeletePerson() {
  int selectedIndex = personTable.getSelectionModel().getSelectedIndex();
  if (selectedIndex >= 0) {
    personTable.getItems().remove(selectedIndex);
  } else {
    // Nothing selected
    Dialogs.showWarningDialog(mainApp.getPrimaryStage(),
        "Please select a person in the table.",
        "No Person Selected", "No Selection");
  }
}

The New and Edit Buttons

The new and edit buttons are a bit more work: We’ll need a new custom dialog (a.k.a stage) with a form to ask the user for details about the person.

  1. Create a new fxml file called PersonEditDialog.fxml inside the view package.
  2. Use a GridPane, Labels, TextBoxes and Buttons to create a Dialog like the following:

    If you don’t to do the work, you can download this PersonEditDialog.fxml.

  3. Create the controller PersonEditDialogController:

(PersonEditDialogController.java) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package ch.makery.address;

import javafx.fxml.FXML;
import javafx.scene.control.Dialogs;
import javafx.scene.control.TextField;
import javafx.stage.Stage;
import ch.makery.address.model.Person;
import ch.makery.address.util.CalendarUtil;

/**
 * Dialog to edit details of a person.
 * 
 * @author Marco Jakob
 */
public class PersonEditDialogController {

  @FXML
  private TextField firstNameField;
  @FXML
  private TextField lastNameField;
  @FXML
  private TextField streetField;
  @FXML
  private TextField postalCodeField;
  @FXML
  private TextField cityField;
  @FXML
  private TextField birthdayField;
  
  
  private Stage dialogStage;
  private Person person;
  private boolean okClicked = false;
  
  /**
  * Initializes the controller class. This method is automatically called
  * after the fxml file has been loaded.
  */
  @FXML
  private void initialize() {
      
  }
  
  /**
  * Sets the stage of this dialog.
  * @param dialogStage
  */
  public void setDialogStage(Stage dialogStage) {
      this.dialogStage = dialogStage;
  }
  
  /**
  * Sets the person to be edited in the dialog.
  * 
  * @param person
  */
  public void setPerson(Person person) {
      this.person = person;
      
      firstNameField.setText(person.getFirstName());
      lastNameField.setText(person.getLastName());
      streetField.setText(person.getStreet());
      postalCodeField.setText(Integer.toString(person.getPostalCode()));
      cityField.setText(person.getCity());
      birthdayField.setText(CalendarUtil.format(person.getBirthday()));
      birthdayField.setPromptText("yyyy-mm-dd");
  }
  
  /**
  * Returns true if the user clicked OK, false otherwise.
  * @return
  */
  public boolean isOkClicked() {
      return okClicked;
  }
  
  /**
  * Called when the user clicks ok.
  */
  @FXML
  private void handleOk() {
      if (isInputValid()) {
          person.setFirstName(firstNameField.getText());
          person.setLastName(lastNameField.getText());
          person.setStreet(streetField.getText());
          person.setPostalCode(Integer.parseInt(postalCodeField.getText()));
          person.setCity(cityField.getText());
          person.setBirthday(CalendarUtil.parse(birthdayField.getText()));
          
          okClicked = true;
          dialogStage.close();
      }
  }
  
  /**
  * Called when the user clicks cancel.
  */
  @FXML
  private void handleCancel() {
      dialogStage.close();
  }
  
  /**
  * Validates the user input in the text fields.
  * 
  * @return true if the input is valid
  */
  private boolean isInputValid() {
      String errorMessage = "";

      if (firstNameField.getText() == null || firstNameField.getText().length() == 0) {
          errorMessage += "No valid first name!\n";
      }
      if (lastNameField.getText() == null || lastNameField.getText().length() == 0) {
          errorMessage += "No valid last name!\n";
      }
      if (streetField.getText() == null || streetField.getText().length() == 0) {
          errorMessage += "No valid street!\n";
      }
      
      if (postalCodeField.getText() == null || postalCodeField.getText().length() == 0) {
          errorMessage += "No valid postal code!\n";
      } else {
          // try to parse the postal code into an int
          try {
              Integer.parseInt(postalCodeField.getText());
          } catch (NumberFormatException e) {
              errorMessage += "No valid postal code (must be an integer)!\n";
          }
      }
      
      if (cityField.getText() == null || cityField.getText().length() == 0) {
          errorMessage += "No valid city!\n";
      }
      
      if (birthdayField.getText() == null || birthdayField.getText().length() == 0) {
          errorMessage += "No valid birthday!\n";
      } else {
          if (!CalendarUtil.validString(birthdayField.getText())) {
              errorMessage += "No valid birthday. Use the format yyyy-mm-dd!\n";
          }
      }
      
      if (errorMessage.length() == 0) {
          return true;
      } else {
          // Show the error message
          Dialogs.showErrorDialog(dialogStage, errorMessage,
                  "Please correct invalid fields", "Invalid Fields");
          return false;
      }
  }
}
  • A few things to note about this controller:
    • The setPerson(...) method can be called from another class to set the person that is to be edited.
    • When the user clicks the OK butten, the handleOk() method is called. First, some validation is done by calling the isInputValid() method. Only if validation was successful, the person object is filled with the data that the user entered. Those changes will directly be applied to the person object that was passed to setPerson(...)!
    • The boolean okClicked is used so that the caller can determine whether the user clicked the OK or Cancel button.

Opening the Dialog

Add a method to load and display the edit person dialog inside our MainApp:

MainApp.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
 * Opens a dialog to edit details for the specified person. If the user
 * clicks OK, the changes are saved into the provided person object and
 * true is returned.
 * 
 * @param person the person object to be edited
 * @return true if the user clicked OK, false otherwise.
 */
public boolean showPersonEditDialog(Person person) {
  try {
    // Load the fxml file and create a new stage for the popup
    FXMLLoader loader = new FXMLLoader(MainApp.class.getResource("view/PersonEditDialog.fxml"));
    AnchorPane page = (AnchorPane) loader.load();
    Stage dialogStage = new Stage();
    dialogStage.setTitle("Edit Person");
    dialogStage.initModality(Modality.WINDOW_MODAL);
    dialogStage.initOwner(primaryStage);
    Scene scene = new Scene(page);
    dialogStage.setScene(scene);

    // Set the person into the controller
    PersonEditDialogController controller = loader.getController();
    controller.setDialogStage(dialogStage);
    controller.setPerson(person);

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

    return controller.isOkClicked();

  } catch (IOException e) {
    // Exception gets thrown if the fxml file could not be loaded
    e.printStackTrace();
    return false;
  }
}

Add the following methods to the PersonOverviewController:

PersonOverviewController.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
 * Called when the user clicks the new button.
 * Opens a dialog to edit details for a new person.
 */
@FXML
private void handleNewPerson() {
  Person tempPerson = new Person();
  boolean okClicked = mainApp.showPersonEditDialog(tempPerson);
  if (okClicked) {
    mainApp.getPersonData().add(tempPerson);
  }
}

/**
 * Called when the user clicks the edit button.
 * Opens a dialog to edit details for the selected person.
 */
@FXML
private void handleEditPerson() {
  Person selectedPerson = personTable.getSelectionModel().getSelectedItem();
  if (selectedPerson != null) {
    boolean okClicked = mainApp.showPersonEditDialog(selectedPerson);
    if (okClicked) {
      refreshPersonTable();
      showPersonDetails(selectedPerson);
    }

  } else {
    // Nothing selected
    Dialogs.showWarningDialog(mainApp.getPrimaryStage(),
        "Please select a person in the table.",
        "No Person Selected", "No Selection");
  }
}

/**
 * Refreshes the table. This is only necessary if an item that is already in
 * the table is changed. New and deleted items are refreshed automatically.
 * 
 * This is a workaround because otherwise we would need to use property
 * bindings in the model class and add a *property() method for each
 * property. Maybe this will not be necessary in future versions of JavaFX
 * (see http://javafx-jira.kenai.com/browse/RT-22599)
 */
private void refreshPersonTable() {
  int selectedIndex = personTable.getSelectionModel().getSelectedIndex();
  personTable.setItems(null);
  personTable.layout();
  personTable.setItems(mainApp.getPersonData());
  // Must set the selected index again (see http://javafx-jira.kenai.com/browse/RT-26291)
  personTable.getSelectionModel().select(selectedIndex);
}

Open the PersonOverview.fxml file in Scene Builder. Choose the corresponding methods in On Action for the new and edit buttons.

Done!

You should have a working Address Application now. The application is able to add, edit, and delete persons. There is even some validation for the text fields to avoid bad user entries.

I hope the concepts and structure of this application will get you started with writing your own JavaFX application! Have fun and stay tuned for possible future tutorials.


What’s Next?

In Tutorial Part IV we will add some CSS styling.

Download

Source of Tutorial Part III as Eclipse Project.

Comments