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
1234567891011
/** * 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 */privatevoidshowPersonDetails(Personperson){// 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:
packagech.makery.address.util;importjava.text.ParseException;importjava.text.SimpleDateFormat;importjava.util.Calendar;/** * Helper functions for handling dates. */publicclassCalendarUtil{/** * Default date format in the form 2013-03-18. */privatestaticfinalSimpleDateFormatDATE_FORMAT=newSimpleDateFormat("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 */publicstaticStringformat(Calendarcalendar){if(calendar==null){returnnull;}returnDATE_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 */publicstaticCalendarparse(StringdateString){Calendarresult=Calendar.getInstance();try{result.setTime(DATE_FORMAT.parse(dateString));returnresult;}catch(ParseExceptione){returnnull;}}/** * Checks the String whether it is a valid date. * * @param dateString * @return true if the String is a valid date */publicstaticbooleanvalidString(StringdateString){try{DATE_FORMAT.parse(dateString);returntrue;}catch(ParseExceptione){returnfalse;}}}
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
123456789101112131415161718192021
@FXMLprivatevoidinitialize(){// Initialize the person tablefirstNameColumn.setCellValueFactory(newPropertyValueFactory<Person,String>("firstName"));lastNameColumn.setCellValueFactory(newPropertyValueFactory<Person,String>("lastName"));// Auto resize columnspersonTable.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);// clear personshowPersonDetails(null);// Listen for selection changespersonTable.getSelectionModel().selectedItemProperty().addListener(newChangeListener<Person>(){@Overridepublicvoidchanged(ObservableValue<?extendsPerson>observable,PersonoldValue,PersonnewValue){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.
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
12345678
/** * Called when the user clicks on the delete button. */@FXMLprivatevoidhandleDeletePerson(){intselectedIndex=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:
Download the newest javafx-dialogs-x.x.x.jar file from my GitHub Page.
Create a lib subfolder in the project and add the jar file to this folder.
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
123456789101112131415
/** * Called when the user clicks on the delete button. */@FXMLprivatevoidhandleDeletePerson(){intselectedIndex=personTable.getSelectionModel().getSelectedIndex();if(selectedIndex>=0){personTable.getItems().remove(selectedIndex);}else{// Nothing selectedDialogs.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.
Create a new fxml file called PersonEditDialog.fxml inside the view package.
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.
packagech.makery.address;importjavafx.fxml.FXML;importjavafx.scene.control.Dialogs;importjavafx.scene.control.TextField;importjavafx.stage.Stage;importch.makery.address.model.Person;importch.makery.address.util.CalendarUtil;/** * Dialog to edit details of a person. * * @author Marco Jakob */publicclassPersonEditDialogController{@FXMLprivateTextFieldfirstNameField;@FXMLprivateTextFieldlastNameField;@FXMLprivateTextFieldstreetField;@FXMLprivateTextFieldpostalCodeField;@FXMLprivateTextFieldcityField;@FXMLprivateTextFieldbirthdayField;privateStagedialogStage;privatePersonperson;privatebooleanokClicked=false;/** * Initializes the controller class. This method is automatically called * after the fxml file has been loaded. */@FXMLprivatevoidinitialize(){}/** * Sets the stage of this dialog. * @param dialogStage */publicvoidsetDialogStage(StagedialogStage){this.dialogStage=dialogStage;}/** * Sets the person to be edited in the dialog. * * @param person */publicvoidsetPerson(Personperson){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 */publicbooleanisOkClicked(){returnokClicked;}/** * Called when the user clicks ok. */@FXMLprivatevoidhandleOk(){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. */@FXMLprivatevoidhandleCancel(){dialogStage.close();}/** * Validates the user input in the text fields. * * @return true if the input is valid */privatebooleanisInputValid(){StringerrorMessage="";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 inttry{Integer.parseInt(postalCodeField.getText());}catch(NumberFormatExceptione){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){returntrue;}else{// Show the error messageDialogs.showErrorDialog(dialogStage,errorMessage,"Please correct invalid fields","Invalid Fields");returnfalse;}}}
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:
/** * 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. */publicbooleanshowPersonEditDialog(Personperson){try{// Load the fxml file and create a new stage for the popupFXMLLoaderloader=newFXMLLoader(MainApp.class.getResource("view/PersonEditDialog.fxml"));AnchorPanepage=(AnchorPane)loader.load();StagedialogStage=newStage();dialogStage.setTitle("Edit Person");dialogStage.initModality(Modality.WINDOW_MODAL);dialogStage.initOwner(primaryStage);Scenescene=newScene(page);dialogStage.setScene(scene);// Set the person into the controllerPersonEditDialogControllercontroller=loader.getController();controller.setDialogStage(dialogStage);controller.setPerson(person);// Show the dialog and wait until the user closes itdialogStage.showAndWait();returncontroller.isOkClicked();}catch(IOExceptione){// Exception gets thrown if the fxml file could not be loadede.printStackTrace();returnfalse;}}
Add the following methods to the PersonOverviewController:
/** * Called when the user clicks the new button. * Opens a dialog to edit details for a new person. */@FXMLprivatevoidhandleNewPerson(){PersontempPerson=newPerson();booleanokClicked=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. */@FXMLprivatevoidhandleEditPerson(){PersonselectedPerson=personTable.getSelectionModel().getSelectedItem();if(selectedPerson!=null){booleanokClicked=mainApp.showPersonEditDialog(selectedPerson);if(okClicked){refreshPersonTable();showPersonDetails(selectedPerson);}}else{// Nothing selectedDialogs.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) */privatevoidrefreshPersonTable(){intselectedIndex=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.