Expressing the UI for Enterprise Applications with JavaFX 2.0 FXML

By James L. Weaver

Leverage the power of the FX Markup Language to define the UI for enterprise applications.

Published June 2012

Downloads:

Download: Java FX

Download: NetBeans IDE

Download: Sample Project (Zip)
 

JavaFX 2.0 is an API and runtime for creating Rich Internet Applications (RIAs). JavaFX was introduced in 2007, and version 2.0 was released in October 2011. One of the advantages of JavaFX 2.0 is that the code can be written in the Java language using mature and familiar tools. This article focuses on using the capabilities of FX Markup Language (FXML), a facility that comes with JavaFX 2.0, to quickly define the user interface for enterprise applications.

The purpose of FXML is to enable the expression of the UI using XML. Classes that contain FXML functionality are located in the javafx.fxml package, and they include FXMLLoader,JavaFXBuilderFactory, and an interface named Initializable. This article contains an example of using capabilities of FXML and JavaFX 2.0 to create an enterprise application.

Overview of the SearchDemoFXML Application

To help you learn how to leverage FXML in JavaFX 2.0 applications, an example application named SearchDemoFXML will be employed. As shown in Figure 1, this application contains the following:

  • A TextField and Button for searching media located in iTunes
  • A TableView for displaying the results of the search
  • An ImageView for viewing the cover of the selected album
  • A Button that opens a browser tab for previewing an audio/video clip from the selected title

The SearchDemoFXML project, which you'll download in the next section, contains the code for this application, portions of which we’ll highlight during the course of this article.

Figure 1: Search Demo Startup

Figure 1: Screen Capture of the SearchDemoFXML Application

When you enter a song title, an album title, or an artist’s name in the text field and click the button, a message appears in the upper right corner of the UI indicating that a search is in progress, as shown in Figure 2. In addition, the button’s appearance changes to indicate that you can cancel the search by clicking the button, and the text field is disabled.

Figure 2: Screen Capture of SearchDemoFXML During a Search

Figure 2: Screen Capture of SearchDemoFXML During a Search

Clicking the Preview button opens up a new tab in the default browser with a video or audio clip, as shown in Figure 3:

Figure 3: Browser Tab with a Video Clip

Figure 3: Browser Tab with a Video Clip

Obtaining and Running the SearchDemoFXML Project

  • Download the NetBeans project file, which includes the SearchDemoFXML project.
  • Expand the project into a directory of your choice.
  • Start NetBeans, and select File -> Open Project.
  • From the Open Project dialog box, navigate to your chosen directory and open the SearchDemoFXML project, as shown in Figure 4. If you receive a message stating that the jfxrt.jar file can't be found, click the Resolve button and navigate to the rt/lib folder subordinate to where you installed the JavaFX 2.0 SDK.

Note: You can obtain the NetBeans IDE from the NetBeans site.

Figure 4: Opening the SearchDemoFXML Project in NetBeans

Figure 4: Opening the SearchDemoFXML Project in NetBeans

  • To run the application, click the Run Project icon on the toolbar, or press the F6 key. The Run Project icon looks like a Play button on a media (for example, DVD) player, as shown in Figure 5.

Figure 5: Running the SearchDemoFXML Program in NetBeans

Figure 5: Running the SearchDemoFXML Program in NetBeans

The SearchDemoFXML application should appear in a window, as shown previously in Figure 1. Go ahead and play with the application, searching for song titles, album titles, and artists. We can analyze the application and walk through the FXML-related code when you get back.

Analyzing the SearchDemoFXML Application

Before diving deeply into the code, let’s analyze the pieces shown in Figure 6, which comprise the SearchDemoFXML application.

Figure 6: Diagram of the SearchDemoFXML Application

Figure 6: Diagram of the SearchDemoFXML Application

Starting from the top left of Figure 6, SearchDemo.java is the main module of our JavaFX application, extending Application and containing main() and start() methods.  Its role in the SearchDemoFXML application is to create a scene and populate it with the scene graph obtained from the search_demo.fxml file via the FXMLLoader.load() method. This produces the UI shown in the lower left of Figure 6.

The search_demo.fxml file in the top middle of Figure 6 contains an XML representation of the scene graph. It also specifies that the SearchDemoController.java file is a controller class, and it delegates events that occur to UI components to handler methods in the controller.

The SearchDemoController controller class in the bottom middle of Figure 6 holds references to UI components expressed in search_demo.fxml and handles events that occur in them.

Turning to the right side of Figure 6, the SearchDemoController invokes methods of the RestFX classes to query the iTunes services' REST interface. RestFX is a library external to JavaFX 2.0 that is leveraged here for communication with the iTunes REST endpoints and parses its JSON responses. The "See Also" section at the end of this article has a link to the REST/FX project.

Bootstrapping an FXML Application

As pointed out in the previous section, SearchDemo.java is the main module of our JavaFX application, as shown in Listing 1:

package demos.search;

import java.util.ResourceBundle;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.SceneBuilder;
import javafx.stage.Stage;

public class SearchDemo extends Application {
  @Override
  public void start(Stage primaryStage) throws Exception {
    Parent root = FXMLLoader.load(getClass().getResource("search_demo.fxml"),
                       ResourceBundle.getBundle("demos/search/search_demo"));

    primaryStage.setTitle("Search Demo");
    primaryStage.setWidth(650);
    primaryStage.setHeight(500);
    primaryStage.setScene(
      SceneBuilder.create()
        .root(root)
        .build()
    );
    primaryStage.show();
  }

  public static void main(String[] args) {
    launch(args);
  }
}

Listing 1: SearchDemo.java

The FXMLLoader.load() invocation shown in Listing 1 accepts two arguments:

  • java.net.URL, which represents the FXML document (in this case, search_demo.fxml)
  • ResourceBundle, which contains strings used in our application

Note that the FXMLLoader.load() method has other signatures, including those that don’t specify a ResourceBundle.

After the FXML document is loaded, the scene graph is instantiated and assigned to the root property of the scene, which is associated with the stage and made visible with its show() method.

Expressing the UI with FXML

As shown in Figure 6, the search_demo.fxml file contains an XML representation of the scene graph. Let’s take a closer look at this file, which is shown in Listing 2:

<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.image.*?>
<?import demos.search.*?>

<BorderPane fx:controller="demos.search.SearchDemoController" 
            style="-fx-padding: 6 6 6 6" 
            xmlns:fx="http://javafx.com/fxml">
  <top>
    <BorderPane>
      <left>
        <HBox spacing="6" style="-fx-padding: 0 0 6 0">
          <children>
            <TextField fx:id="searchTermTextField" prefColumnCount="18"
                       onAction="#handleSearchAction"/>
            <Button fx:id="searchButton" disable="false" 
                    onAction="#handleSearchAction"/>
          </children>
        </HBox>
      </left>
      <right>
        <Label fx:id="statusLabel"/>
      </right>
    </BorderPane>
</top>

  <center>
    <BorderPane>
      <center>
        <TableView fx:id="resultsTableView">
          <fx:define>
             <ResultCellValueFactory fx:id="resultCellValueFactory"/>
          </fx:define>
            <columns>
              <ResultTableColumn 
                  key="itemName" text="%name" prefWidth="170" 
                  cellValueFactory="$resultCellValueFactory"/>
              <ResultTableColumn 
                  key="itemParentName" text="%album" prefWidth="170" 
                  cellValueFactory="$resultCellValueFactory"/>
              <ResultTableColumn 
                  key="artistName" text="%artist" prefWidth="170" 
                  cellValueFactory="$resultCellValueFactory"/>
            </columns>
        </TableView>
      </center>
      <right>
        <VBox alignment="topCenter" spacing="6" style="-fx-padding: 0 0 0 6">
          <children>
            <StackPane prefWidth="120" prefHeight="120" 
                style="-fx-border-color: #929292; -fx-border-width: 1px">
              <children>
                  <ImageView fx:id="artworkImageView"/>
              </children>
            </StackPane>
            <Button fx:id="previewButton" text="%preview" 
                    onAction="#handlePreviewAction"/>
          </children>
        </VBox>
      </right>
    </BorderPane>
  </center>
</BorderPane>

Listing 2: search_demo.fxml

Understanding the Structure of our FXML Document

As shown in Listing 2, our FXML document consists of some import processing instructions and instance declarations. The import processing instructions are analogous to Java import statements, which prevent the need for class names to be qualified by package names.

The instance declarations express the scene graph hierarchically, including nodes such as layout containers, shapes, and UI controls. Note that the root node (in this case, a BorderPane) contains an attribute named fx:controller. This specifies that a class named demos.search.SearchDemoController will serve as the controller for this FXML document. We’ll take a closer look at the controller a bit later.

Using Instance Declarations

An instance declaration is characterized by an element that begins with an uppercase letter, optional attributes that begin with lowercase letters, and optional attributes represented as nested elements beginning with lowercase letters. For example, the snippet below from Listing 2 causes an HBox to be instantiated with its spacing property set to 6 and its style property set to -fx-padding: 0 0 6 0. In addition, a couple of children (a TextField and a Button) will be instantiated and contained by the HBox.

        <HBox spacing="6" style="-fx-padding: 0 0 6 0">
          <children>
            <TextField fx:id="searchTermTextField" prefColumnCount="18"
                       onAction="#handleSearchAction"/>
            <Button fx:id="searchButton" disable="false" 
                    onAction="#handleSearchAction"/>
          </children>
        </HBox>

Note that any class that adheres to JavaBean conventions (for example, a no-argument constructor and get/set methods) may be used in an instance declaration.

Mapping Between the FXML Document and the Controller

Taking another look at the previous snippet from Listing 2, notice that the Button instance declaration contains the fx:id and onAction attributes. The fx:id attribute is assigned the "searchButton" string, which maps to the searchButton instance variable in the SearchDemoController class. 

The onAction attribute is assigned the "#handleSearchAction" string, which maps to the handleSearchAction() method in the SearchDemoController class.  

The searchButton instance variable and the handleSearchAction() method are shown in the following snippet from Listing 4 (which you’ll see in the "Defining the Controller" section in a moment).

public class SearchDemoController implements Initializable {

  @FXML private Button searchButton;

  ...

  @FXML

  protected void handleSearchAction(ActionEvent event) {...}

  ...

}

This mapping enables the controller to manage the state of the instances expressed in the FXML document, as well as to handle events that occur in them.

Mapping Attributes to Resource Names in a ResourceBundle

The code in Listing 3, which is another snippet from Listing 2, demonstrates how to map the value of an attribute in an instance declaration to a resource name in a ResourceBundle. When the value assigned to an attribute begins with a percent character (%) and the FXMLLoader.load() invocation is supplied with a ResourceBundle, the locale-specific values will be used in place of the resource names.

        <TableView fx:id="resultsTableView">
          <fx:define>
             <ResultCellValueFactory fx:id="resultCellValueFactory"/>
          </fx:define>
            <columns>
              <ResultTableColumn 
                  key="itemName" text="%name" prefWidth="170" 
                  cellValueFactory="$resultCellValueFactory"/>
              <ResultTableColumn 
                  key="itemParentName" text="%album" prefWidth="170" 
                  cellValueFactory="$resultCellValueFactory"/>
              <ResultTableColumn 
                  key="artistName" text="%artist" prefWidth="170" 
                  cellValueFactory="$resultCellValueFactory"/>
            </columns>
        </TableView>

Listing 3: Example of Mapping Attributes to Resource Names

In our example, the TableView column headers are retrieved at startup from the resource names name, album, and artist located in the search_demo.properties file.

Creating an Object Outside of the Scene Graph

Taking another look at the snippet in Listing 3, you’ll see an fx:define element. This element is being used to create a ResultCellValueFactory object and to define a variable named resultCellValueFactory that refers to it. This object is then used in each of the ResultTableColumn instantiations, with the $ signifying that this is a variable reference. 

A classic use of the fx:define element is to create a ToggleGroup, which isn’t a scene graph node, and to supply that ToggleGroup object to multiple radio buttons for mutually exclusive selection behavior.

Let’s turn our attention away from the FXML document and toward the controller.

Defining the Controller

As you saw earlier, instances declared in an FXML document can be mapped to variables in the controller, and events that occur in those instances can be mapped to handlers in the controller.  Go ahead and peruse Listing 4, which contains the code for our controller, and we’ll discuss more relevant FXML concepts afterward.

package demos.search;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import restfx.web.GetQuery;
import restfx.web.Query;
import restfx.web.QueryListener;

import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Button;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Label;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

public class SearchDemoController implements Initializable {
  @FXML private TextField searchTermTextField;
  @FXML private Button searchButton;
  @FXML private Label statusLabel;
  @FXML private TableView<Map<String, Object>> resultsTableView;
  @FXML private ImageView artworkImageView;
  @FXML private Button previewButton;

  private ResourceBundle resources = null;
  private GetQuery getQuery = null;
  public static final String QUERY_HOSTNAME = 
                             "ax.phobos.apple.com.edgesuite.net";
  public static final String BASE_QUERY_PATH = 
                             "/WebObjects/MZStoreServices.woa/wa/itmsSearch";
  public static final String MEDIA = "music";
  public static final int LIMIT = 100;

  public static final ImageView SEARCH_IMAGE_VIEW;
  public static final ImageView CANCEL_IMAGE_VIEW;

  static {
    SEARCH_IMAGE_VIEW = new ImageView(new Image(SearchDemo.class
                                 .getResourceAsStream("magnifier.png")));
    CANCEL_IMAGE_VIEW = new ImageView(new Image(SearchDemo.class
                                 .getResourceAsStream("bullet_cross.png")));
  }

  @Override
  @SuppressWarnings("rawtypes")
  public void initialize(URL location, ResourceBundle resources) {
    this.resources = resources;

    // Initialize the search button content
    searchButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    searchButton.setGraphic(SEARCH_IMAGE_VIEW);

    // Add a selection change listener to the table view
    resultsTableView.getSelectionModel().getSelectedCells()
                    .addListener(new ListChangeListener<TablePosition>() {
      @Override
      public void onChanged(Change<? extends TablePosition> change) {
        while (change.next()) {
          if (change.wasAdded()) {
            updateArtwork(change.getAddedSubList().get(0).getRow());
          }
        }
      }
    });

    // Do an example initial search so that the table is populated on startup
    searchTermTextField.setText("Cheap Trick");
    handleSearchAction(null);
  }

  @FXML
  protected void handleSearchAction(ActionEvent event) {
    if (getQuery == null) {
      String searchTerms = searchTermTextField.getText();

      if (searchTerms.length() > 0) {
        getQuery = new GetQuery(QUERY_HOSTNAME, BASE_QUERY_PATH);
        getQuery.getParameters().put("term", searchTerms);
        getQuery.getParameters().put("media", MEDIA);
        getQuery.getParameters().put("limit", Integer.toString(LIMIT));
        getQuery.getParameters().put("output", "json");

        System.out.println(getQuery.getLocation());

        statusLabel.setText(resources.getString("searching"));
        updateActivityState();

        getQuery.execute(new QueryListener<Object>() {
          @Override
          @SuppressWarnings("unchecked")
          public void queryExecuted(Query<Object> task) {
            if (task == getQuery) {
              if (task.isCancelled()) {
                statusLabel.setText(resources.getString("cancelled"));
                searchTermTextField.requestFocus();
              } 
              else {
                Throwable exception = task.getException();
                if (exception == null) {
                  Map<String, Object> value = 
                                      (Map<String, Object>)task.getValue();
                  List<Object> results = (List<Object>)value.get("results");

                  // Update the table data
                  ObservableList<?> items = 
                                     FXCollections.observableList(results);
                  resultsTableView.setItems(
                            (ObservableList<Map<String, Object>>)items);
                  statusLabel.setText(String.format(resources
                           .getString("resultCountFormat"), results.size()));

                  if (results.size() > 0) {
                    resultsTableView.getSelectionModel().select(0);
                    resultsTableView.requestFocus();
                  } 
                  else {
                    searchTermTextField.requestFocus();
                  }
                } 
                else {
                  statusLabel.setText(exception.getMessage());
                  searchTermTextField.requestFocus();
                }
              }

              getQuery = null;
              searchButton.setDisable(false);

              updateActivityState();
            }
          }
        });
      }
    } 
    else {
      getQuery.cancel(true);

      searchButton.setDisable(true);
      statusLabel.setText(resources.getString("aborting"));
    }
  }
  @FXML
  protected void handlePreviewAction(ActionEvent event) {
    Map<String, Object> selectedResult = 
                      resultsTableView.getSelectionModel().getSelectedItem();

    URL url;
    try {
      url = new URL((String)selectedResult.get("previewUrl"));
    } 
    catch (MalformedURLException exception) {
      throw new RuntimeException(exception);
    }

    try {
      java.awt.Desktop.getDesktop().browse(url.toURI());
    } 
    catch (URISyntaxException exception) {
      throw new RuntimeException(exception);
    } 
    catch (IOException exception) {
      throw new RuntimeException(exception);
    }
  }

  private void updateActivityState() {
    boolean active = (getQuery != null);
    searchTermTextField.setDisable(active);
    searchButton.setGraphic(active ? CANCEL_IMAGE_VIEW : SEARCH_IMAGE_VIEW);
  }

  private void updateArtwork(int index) {
    Map<String, Object> result = resultsTableView.getItems().get(index);
    String artworkURL;
    if (result == null) {
      artworkURL = null;
      previewButton.setDisable(true);
    } 
    else {
      artworkURL = (String)result.get("artworkUrl100");
      System.out.println(result.get("itemName"));
      previewButton.setDisable(false);
    }
    artworkImageView.setImage(artworkURL == null ? 
                              null : new Image(artworkURL));
  }
}

Listing 4: SearchDemoController.java

Initializing the Controller

As shown in Figure 1, when the SearchDemoFXML program starts, the table is initially populated from a search for songs by the rock band Cheap Trick. This is accomplished in the initialize() method of the controller shown in Listing 4, specified by the optional Initializable interface. When the controller implements Initializable, the initialize() method is invoked after instantiation and supplied with the URL of the FXML document and a reference to a ResourceBundle , if a reference was loaded by FXMLLoader.load().

As shown in Listing 5 (which is a snippet from Listing 4), other initialization that is performed by our SearchDemoController includes the following:

  • Assigning the supplied ResourceBundle reference to an instance variable
  • Modifying the appearance of the Button referenced by searchButton
  • Adding a change listener to the table so that the artwork image can be updated when the selected row in the table changes
  public void initialize(URL location, ResourceBundle resources) {
    this.resources = resources;

    // Initialize the search button content
    searchButton.setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
    searchButton.setGraphic(SEARCH_IMAGE_VIEW);

    // Add a selection change listener to the table view
    resultsTableView.getSelectionModel().getSelectedCells()
                    .addListener(new ListChangeListener<TablePosition>() {
      @Override
      public void onChanged(Change<? extends TablePosition> change) {
        while (change.next()) {
          if (change.wasAdded()) {
            updateArtwork(change.getAddedSubList().get(0).getRow());
          }
        }
      }
    });

Listing 5: Initialization Performed by SearchDemoController.java

Handling Events

In the "Mapping Between the FXML Document and the Controller" section, we saw how to map instances and event handling between the FXML document and the controller. Listing 6 (which is another snippet from Listing 4) contains some portions of the event handler in our controller for when the user clicks the Search button.

  @FXML
protected void handleSearchAction(ActionEvent event) {
    ...
      String searchTerms = searchTermTextField.getText();
      ...
      if (searchTerms.length() > 0) {
        statusLabel.setText(resources.getString("searching"));
                  ...
                  resultsTableView.setItems(
                           (ObservableList<Map<String, Object>>)items);
                  statusLabel.setText(String.format(resources
                          .getString("resultCountFormat"), results.size()));

                  if (results.size() > 0) {
                    resultsTableView.getSelectionModel().select(0);
                    resultsTableView.requestFocus();
                  } 
                  else {
                    searchTermTextField.requestFocus();
                  }

Listing 6: Portions of the Event Handler from SearchDemoController.java

The code in Listing 6 demonstrates interaction by the controller with nodes in the scene graph expressed by the FXML document. For example, the search terms are obtained from the searchTermTextField, and the statusLabel text is set to the locale-specific value for the “searching” resource name. In addition, the table is populated with search results, its first row is selected, and keyboard focus is requested.

Conclusion

The facility known as FXML, which comes with JavaFX 2.0, provides the ability to express a UI using XML. The javafx.fxml package contains the classes that provide FXML functionality. When a JavaFX application that employs FXML starts up, it uses the FXMLLoader.load() method to load an FXML document and create a scene graph represented by the document. The FXML document specifies a controller that has instance variables and handler methods that map to objects and events expressed in the document.

See Also

About the Author

James L. (Jim) Weaver is a Java and JavaFX developer, author, and speaker with a passion for helping rich-client Java and JavaFX become preferred technologies for new application development. Books that Jim has authored include Inside Java, Beginning J2EE, and Pro JavaFX 2.  His professional background includes 15 years as a systems architect at EDS, and the same number of years as an independent developer. As an Oracle Java Evangelist, Jim speaks internationally at software technology conferences, including the JavaOne conferences in San Francisco and São Paulo. Jim blogs at http://javafxpert.com, tweets @javafxpert, and may be reached at james.weaver AT oracle.com.