Chapter     3

Lambdas and Properties

In this chapter you will learn about the new language feature introduced in Java 8 called lambda expressions, or lambdas, and I will also discuss JavaFX properties and binding APIs. The goal of this chapter is to demonstrate how lambdas and properties will be used in the context of JavaFX GUI applications. Having said this, I will mainly concentrate on the common features that will be used in most of the examples in this book, and will not detail every lambda and properties feature.

To get a better idea of Java’s lambda roadmap, please visit the following article by Brian Goetz, who is Oracle’s Java Language Architect:

http://cr.openjdk.java.net/~briangoetz/lambda/lambda-state-final.html

The term lambda derives from what is known in mathematics as lambda calculus. The general concepts from that field are used to derive the specific behaviors that have been added to Java, which are described in this chapter. Figure 3-1 shows the Greek letter lambda, which you sometimes see in connection with the topic.

9781430264606_Fig03-01.jpg

Figure 3-1. The Greek Symbol for Lambda

Lambda

Java Lambda is based on JSR (Java Specification Request) 335, “Lambda Expressions for the Java™ Programming Language.” The feature was appropriately named after lambda calculus, the formal system in mathematical logic and computer science to express computations. JSR 335, better known as Project Lambda, comprised many features, such as expressing parallel calculations on streams (the Stream API).

A primary goal of lambdas is to help address the lack in the Java language of a good way to express functional programming concepts. Languages that support functional programming concepts have the ability to create anonymous (unnamed) functions, similar to creating objects instead of methods in Java. These function objects are commonly known as closures. Some common languages that support closures or lambdas are Common Lisp, Clojure, Scheme, Scala, Groovy, Python, Ruby, and JavaScript.

Some will argue about what a closure is and what it is not; however the main idea is that languages that support functional programming with closure syntax are able to create anonymous functions as first-class citizens. In other words, functions or closures can be treated like objects, so that they can be assigned to variables and passed into other functions. In order to mimic closures in the previous Java world (Java 1.1 to Java 7), you would typically use anonymous inner classes to encapsulate functions (behavior) as objects. Listing 3-1 shows an example of an anonymous inner class that defines handler code for a JavaFX button when pressed.

Listing 3-1. A Button’s OnAction Set with an Anonymous Inner Class

Button btn = new Button();
btn.setOnAction(new EventHandler<ActionEvent>() {     public void handle(ActionEvent event) {        System.out.println("Hello World");     }
});

You will notice that this code looks very verbose just to wire up a button. Buried deep in an anonymous inner class is a single line to output text. Wouldn’t it be nice to be able to express a block of code containing the behavior you want without the need of so much boilerplate code? Look no further. Java 8 solves this issue with the use of lambda expressions (closures). To see how it looks with Java 8 lambda expressions, let’s rewrite the button handler code just shown. Listing 3-2 is our button’s EventHandler code rewritten as a Java 8 lambda expression.

Listing 3-2. A Button’s OnAction Set with a Lambda Expression

btn.setOnAction(event -> System.out.println("Hello World") );

Using lambda expressions not only makes code concise and easy to read, but the code is also likely to perform better. Actually, under the hood the compiler is capable of optimizing code and able to reduce its footprint.

Lambda Expressions

As discussed before, lambda expressions are Java 8’s version of closures. In the same way other languages create closures syntactically, you will also be able to reference them and pass them into other methods. In order to achieve this ability you will later learn about functional interfaces. But for now, let’s look at the basic syntax for lambda expressions.

Syntax

There are two ways to specify lambda expressions. The following simple examples illustrate the general forms:

(param1, param2, ...) -> expression;

(param1, param2, ...) -> {  /* code statements */ };

Lambda expressions begin with a list of parameters surrounded by parentheses, the arrow symbol -> (a hyphen and a greater-than sign), and an expression body. Similar to Java methods, the parentheses are for parameters that are passed into an expression. When the expression doesn’t accept parameters (and is said to be empty), the parentheses still are required. Separating the parameter list and the expression body is the arrow symbol. The expression body or code block may or may not be surrounded by curly braces. When the expression body doesn’t have surrounding curly braces, it must consist of only one statement. When an expression is a one-line statement, it is evaluated and returned to the caller implicitly. Just remember that if the method requires a return type and your code block has curly braces, you must have a return statement. Listing 3-3 shows two equivalent lambda expressions, with and without curly braces.

Listing 3-3. One Statement with an Explicit Return, and the Other with an Implicit Return

x -> { return x * x; }   // must explicitly return result
x -> x * x;              // evaluates and implicitly returns result

Another thing to note is that parameters can be optionally typed. The compiler will infer the type of the parameters depending on the context. For example, when you set a JavaFX button by using the setOnAction() method, the compiler can figure out the lambda parameters by matching the method’s argument types (its signature) and its return type. Sometimes for readability you can optionally specify the type of each parameter. If you specify the type, you will be required to surround the list of parameters in parentheses. The following line specifies the type (ActionEvent) of the parameter for the lambda expression for a JavaFX button’s setOnAction() method.

btn.setOnAction( (ActionEvent event) -> System.out.println("Hello World") );

You will also notice that it is okay to leave off the semicolon for a one line statement that is inside methods, such as the setOnAction() method here. One last piece of syntactic sugar is that if you have one parameter to be passed into your lambda, the parentheses can be omitted. Following is a lambda expression that has one parameter (event) without parentheses.

btn.setOnAction( event -> System.out.println("Hello World") );

As a recap of the syntax variations just discussed, the statements in Listing 3-4 are all equivalent lambda expressions.

Listing 3-4. Three Syntactically Equivalent Lambda Expressions to Set Action Code on a JavaFX Button

btn.setOnAction( (ActionEvent event) -> {System.out.println("Hello World"); } );
btn.setOnAction( (event) -> System.out.println("Hello World") );
btn.setOnAction( event -> System.out.println("Hello World") );

Variable Capture

Typically in functional programming languages, the use of closures (lambda expressions) allows you to use variables outside the closure’s scope. These variables are often described as nonlocal or capture variables because they aren’t locally scoped to the lambda expression or method. When these nonlocal variables are used in lambda expressions, they need to be immutable. This is a key principle in the functional programming world. New to the Java 8 compiler is the ability to reference variables outside the scope of an anonymous inner class or lambda expression.

When using lambda expressions you can optionally reference variables scoped outside the closure function. Typically, prior to Java 8 these nonlocal variables needed to be final (immutable). This immutable state enables the compiler to better predict program behavior and thus capable of optimizing code. In Java Swing development, this occurred often when variables of the enclosing scope needed to be used inside the scope of an anonymous inner class method (local variable scope). In order to abide by the compiler rules you had to declare a variable as final.

The Java 8 compiler has a new capability that will infer the intent of nonlocal variables passed into anonymous inner class methods and lambda expressions to then be converted to final (immutable). To see how we old-school Java Swing developers (prior to Java 8) used to receive compile-time errors, please look at Listing 3-5. The scenario is a button that will modify the text of a Swing JLabel component after the button is pressed. Prior to Java 8, the code would not compile correctly, because the label variable needed to be declared final.

Listing 3-5. Code That Would Receive a Compiler Error Prior to JDK 8

// Prior to Java 8
JLabel label = new JLabel("Press Me"); // nonlocal variable to actionPerformed()
btn.addActionListener(new ActionListener() {
     public void actionPerformed(ActionEvent e) {
        label.setText("Pressed"); // !Compile Error! Variable label must be final.
     }
});

New to Java 8 is the ability to convert nonlocal variables to be final. In Listing 3-6 you’ll notice that the code is identical except for the comments. Starting with Java 8, you can now make use of the variable capture capability with anonymous inner classes and lambda expressions.

Listing 3-6. Code That Compiles Successfully in JDK 8

// New in Java 8
JLabel label = new JLabel("Press Me"); // nonlocal variable
// compiler implicitly converts label as a final (immutable) variable
btn.addActionListener(new ActionListener() {     public void actionPerformed(ActionEvent e) {        label.setText("Pressed"); // Legal. Variable label is converted to be final     }
});

Both of the code examples shown in Listing 3-6 are identical. The difference comes when using a JDK 8 compiler. When using the Java 8 compiler, the capture variable will implicitly become final (immutable), and that will occur without triggering any compilation error.

Functional Interfaces

Now that you know how to create lambda expressions, you may be wondering what exactly a lambda expression’s type is. Another way to think about this is to ask questions such as the following:

  • How can I assign a lambda expression to a variable?
  • How can I create a method with parameters that accepts lambda expressions?

To answer these questions you will be looking at examples of Java 8’s functional interfaces. Before we get into the details, however, I’ll give you some context about functional programming languages and how they differ from imperative languages.

Functional vs. Imperative Languages

The advent of cloud computing has helped to popularize many functional programming languages. It became apparent there was a paradigm shift in problem-solving that involves extremely large datasets. A typical use case when applying functional programming techniques is the ability to iterate over datasets while performing computations in a distributed fashion so that load can be shared among nodes or CPU cores. In contrast, imperative programming languages gather data to then be passed into a tight for-loop to be processed. Because of how data and code are coupled, this puts a lot of the burden on one thread (core) to process so much data. The problem needs to be decomposed to allow other threads (cores) to participate in the computation, which then becomes distributed.

One of the advantages of functional programming is the ability to express functionality in a syntactically concise manner, but more important is the ability to pass functionality (lambda expressions) to methods. Being able to pass lambda expressions to methods often fosters the concept of lazy evaluation. This behavior is the same as function callback behavior (asynchronous message passing), where invocations are deferred (and thus “lazy”) until a later time. The opposite of lazy evaluation is eager evaluation. Using lazy evaluations will often increase performance by avoiding unnecessary calculations.

Another important topic to discuss is how functional interfaces are used as closures in the Java language. Instead of implementing functions as first-class types in Java, the designers/architects of the Java Language cleverly defined the notion of functional interfaces as closures. Simply put, a functional interface is basically a single abstract method (SAM). The idea of functional interfaces has been around for a very long time. For instance, those who have worked with Java threads will recall using the Runnable interface, where there is a single run() method with a void return type. The single abstract method pattern is an integral part of Java 8’s lambda expressions. As an example of a functional interface, I created an interface called MyEquation with a single abstract compute() method. Once it is created, you can declare and assign variables with a lambda expression. Listing 3-7 is a functional interface that has a single abstract compute() method.

Listing 3-7. A Declared Functional Interface with a Single Abstract Method, compute()

// functional interface
interface MyEquation {
   double compute(double val1, double val2);
}

After creating a functional interface, you can declare a variable to be assigned with a lambda expression. Listing 3-8 demonstrates the assignment of lambda expressions to functional interface variables.

Listing 3-8. Two Variables of a Functional Interface, Assigned with Lambda Expressions

MyEquation area = (height, width) -> height * width;
MyEquation perimeter = (height, width) -> 2*height + 2*width;

System.out.println("Area = " + area.compute(3, 4));
System.out.println("Perimeter = " + perimeter.compute(3, 4));

The following is the output of Listing 3-8:

Area = 12.0
Perimeter = 14.0

Aggregate Operations

Another powerful use of Java 8’s lambda expressions is working with collections. Java 8 introduces the new Stream API (java.util.stream.*), which allows you to process elements from a given source. A source can be a reference to data structures such as collections or IO channels. According to Oracle’s Java 8 documentation (Javadoc) on the Stream API, “a stream is not a data structure that stores elements; instead, they carry values from a source (which could be a data structure, a generator, an IO channel, etc.) through a pipeline of computational operations.”

A pipeline is a sequence of operations (lambda expressions/functional interfaces) that can process or interrogate each element in a stream. Such operations allow you to perform aggregate tasks. Aggregate operations are similar to the way spreadsheets can execute some computation over a series of cells, such as formatting, averaging, or summing up values. To begin using aggregate operations on collections, you will first invoke the default stream() method on the java.util.Collection interface. You’ll learn more about default methods shortly, but for now all collections (Collection) have a method stream() that returns a java.util.stream.Stream instance. Listing 3-9 is a snippet of code that obtains a stream of elements from a source collection (List<Integer>).

Listing 3-9. A Stream Obtained by Invoking the stream() Method from a Collection

List<Integer> values = Arrays.asList(23, 84, 74, 85, 54, 60);
Stream<Integer> stream = values.stream();

The common built-in aggregate operations are filter, map, and forEach. A filter allows you to pass in an expression to filter elements and returns a new Stream containing the selected items. The map operation converts (or maps) each element to another type and returns a new Stream containing items of the mapped type. For instance, you may want to map Integer values to String values of a stream. A forEach operation allows you to pass in a lambda expression to process each element in the stream.

A typical use case to exercise the three common aggregate operations would be this: given a collection of integers, filter on values greater than a threshold value (a nonlocal variable), convert filtered items to hex values, and print each hex value. Listing 3-10 implements the use case, which exercises various aggregate operations.

Listing 3-10. The Use of Aggregate Operations Over a Collection of Integers

// create a list of values
List<Integer> values = Arrays.asList(23, 84, 74, 85, 54, 60);
System.out.println("values: " + values.toString());
 
// nonlocal variable to be used in lambda expression.
int threshold = 54;
System.out.println("Values greater than " + threshold + " converted to hex:");
// using aggregate functions filter() and forEach()
values.stream()
      .filter(val -> val > threshold) // Predicate functional interface
      .sorted()
      .map(dec -> Integer.toHexString(dec).toUpperCase() ) // Consumer functional interface
      .forEach(val -> System.out.println(val)); // each output values.

Following is the output from Listing 3-10:

values: [23, 84, 74, 85, 54, 60]
Values greater than 54 converted to hex:
3C
4A
54
55

In Listing 3-10 the code begins by creating a list of integers as a collection. Next, it outputs the original list of elements (unmodified). Then the code declares a nonlocal variable to the lambda expression called threshold, to be used in a filter expression. Next, we obtain a stream from the values collection (via stream() method) to perform aggregate operations.

The first operation is a filter that receives a lambda expression to filter elements (integers) greater than 54 (val > threshold). The next operation is the sorted() method, which sorts elements that were returned from the filter method stream. Continuing the method chaining, notice the map operation. The map() method operation is able to map each element in the stream from one data type to another.

In this scenario the stream elements are Integer objects, which are mapped to String objects. The map() method returns another stream, containing elements of type Stream<String>. In this example the string elements are hexadecimal values. Finally, the forEach() operation iterates over each element in the stream of string (hex) elements to be printed.

You’ll notice that as each aggregate function is called, the return type is a Stream object, which allows you to method-chain operations in a way very similar to the builder pattern. To clarify, the idea of method chaining actually comes from the fluent interface pattern for API design.

Default Methods

Default methods are a new way to add default implementation methods to Java interfaces. You’re probably scratching your head and thinking out loud, “Java interfaces can have implementations?!” Yes, Java 8 now has support for the concept, called virtual extension methods, better known as defender methods. Default methods have the effect of adding (extending) new behavior to interfaces without breaking compatibility. Default methods are not abstract methods but methods having implementation (code).

For example, adding new methods to Java interfaces can affect all implementation classes. Because of the strict contract of Java interfaces, the compiler forces the implementer to implement abstract methods, but classes that implement interfaces with default methods do not force the developer to implement those default methods. Rather, derived classes will acquire the behavior of the default method implementation.

An Example Case: Cats Large and Small

It might not be obvious, but the method stream() on the Collection interface used earlier for aggregate operations isn’t an abstract method, but actually a Java 8 default method. Listing 3-11 is the default method stream() in the java.util.Collection interface from the Java 8 source code.

Listing 3-11. The Default Method stream() of Java 8’s Collection Interface

defaultStream<E> stream() {
   return StreamSupport.stream(spliterator, false);
}

For this example I created a domain model (consisting of classes and interfaces) about cats (grrr, meow). The cat kind or species can easily demonstrate default behavior that is found in some cat kinds and not others. For instance, the great cats such as lions, tigers, and jaguars are able to roar, and smaller cats can purr. Also, a house (domestic) cat is able to meow. To further describe these behavioral traits, let’s look at Figure 3-2, a class diagram depicting default behavior between cat kinds.

9781430264606_Fig03-02.jpg

Figure 3-2. Cat domain model represented in a UML class diagram

Looking at the Tiger and Lion classes, you’ll notice they each implement both Roarable and Cat interfaces. Both tigers and lions have the same behavior (can roar) via the default roar() method from the Roarable interface. Keep in mind that the Roarable interface doesn’t have any abstract methods, but rather a default method roar(), which both Lion and Tiger acquire without implementing it.

In contrast, if the Roarable interface had roar() as an abstract method (a pure virtual function), you would have to implement roar() in both Tiger and Lion’s concrete classes. With both having the same functionality in two places, later down the road things could become a major maintenance nightmare. That’s why default methods allow you to add behavior without having to modify derived classes.

Code for the Example

To see the class diagram in Figure 3-2 implemented as code, refer to Listings 3-12 through 3-19. To implement the class diagram I will briefly outline the three parts of the code example. First is the set of cat interfaces with default method behaviors. Second are the concrete cat classes such as Lion, Cheetah and HouseCat. Last is the main example application, which demonstrates default methods in action.

In Listings 3-12 through 3-15 are the UML interfaces Roarable, Purrable, Meowler and Cat, each containing a default method.

Listing 3-12. The Roarable Interface with a Default Method roar()

public interface Roarable {
   default void roar() {
       System.out.println("Roar!!");
   }
}

Listing 3-13. The Purrable Interface with a Default Method purr()

public interface Purrable {
    default void purr() {
        System.out.println("Purrrrrrr...");
    }
}

Listing 3-14. The Meowler Interface with a Default Method meow()

public interface Meowler {
   default void meow() {
       System.out.println("MeeeeOww!");
   }
}

Listing 3-15. The Main Cat Interface with Both Abstract and Default Methods

/**
 * This represents an abstract Cat containing default methods common to all cats.
 * @author carldea
 */
public interface Cat {
    String getCatKind();
    String getFurDescription();
    
    default void growl() {
        System.out.println("Grrrrowl!!");
    }
    default void walk() {
        System.out.println(getCatKind() + " is walking.");
    }
    default void eat() {
        System.out.println(getCatKind() + " is eating.");
    }
    default void sleep() {
        System.out.println(getCatKind() + " is sleeping.");
    }
}

Shown in Listings 3-16 through 3-18 are the concrete implementation classes Lion, Cheetah, and HouseCat, which utilize the interfaces just shown.

Listing 3-16. The Lion Class Implementing the Cat and Roarable Interfaces

public class Lion implements Cat, Roarable {
   @Override
   public String getCatKind() {
       return getClass().getSimpleName();
   }
   @Override
   public String getFurDescription() {
       return "gold-brown";
   }
}

Listing 3-17. The Cheetah Class Implementing the Cat and Purrable Interfaces

public class Cheetah  implements Cat, Purrable {
    @Override
    public String getCatKind() {
        return getClass().getSimpleName();
    }
    @Override
    public String getFurDescription() {
        return "spotted";
    }
}

Listing 3-18. The HouseCat Class Implementing the Cat, Purrable, and Meowler Interfaces

public class HouseCat implements Cat, Purrable, Meowler {
   @Override
   public String getCatKind() {
       return "Domestic Cat";
   }
   @Override
   public String getFurDescription() {
       return "mixed brown and white";
   }
}

Listing 3-19 is the main application, which invokes all of the methods of cat instances, demonstrating their behaviors.

Listing 3-19. The Main Application File (Mixins.java) That Executes the Code Example

/**Demonstrates default methods in Java 8.
 * Mixins.java
 *
 * @author carldea
 */
public class Mixins {
    public static void main(String[] args) {
        Tiger bigCat = new Tiger();
        Cheetah mediumCat = new Cheetah();
        HouseCat smallCat = new HouseCat();
        
        System.out.printf("%s with %s fur. ", bigCat.getCatKind(),
           bigCat.getFurDescription());
        bigCat.eat();
        bigCat.sleep();
        bigCat.walk();
        bigCat.roar();
        bigCat.growl();
        System.out.println("------------------");
        System.out.printf("%s with %s fur. ", mediumCat.getCatKind(),
           mediumCat.getFurDescription());
        mediumCat.eat();
        mediumCat.sleep();
        mediumCat.walk();
        mediumCat.growl();
        mediumCat.purr();
        System.out.println("------------------");
        System.out.printf("%s with %s fur. ", smallCat.getCatKind(),
            smallCat.getFurDescription());
        smallCat.eat();
        smallCat.sleep();
        smallCat.walk();
        smallCat.growl();
        smallCat.purr();
        smallCat.meow();
        System.out.println("------------------");
        
    }
}

Following is the output from Listing 3-19, which demonstrates default method invocations:

run:
Tiger with striped fur.
Tiger is eating.
Tiger is sleeping.
Tiger is walking.
Roar!!
Grrrrowl!!
------------------
Cheetah with spotted fur.
Cheetah is eating.
Cheetah is sleeping.
Cheetah is walking.
Grrrrowl!!
Purrrrrrr...
------------------
Domestic Cat with mixed brown and white fur.
Domestic Cat is eating.
Domestic Cat is sleeping.
Domestic Cat is walking.
Grrrrowl!!
Purrrrrrr...
MeeeeOww!
------------------
BUILD SUCCESSFUL (total time: 0 seconds)

Explanation of the Code

Figure 3-2, shown earlier, depicts a UML class diagram of a cat domain model. In the top layer, you’ll notice the interfaces Roarable, Cat, Purrable, and Meowler. These interfaces provide behavior for any derived class that wants to pick and choose (mixin) common behaviors.

The concrete implementation classes are Lion, Tiger, Cheetah, and HouseCat. You will notice that the main interface Cat contains abstract methods getCatKind(), getFurDescription(), and default methods walk(), eat(), and sleep(). As usual, the Java compiler will enforce classes implementing the Cat interface to implement the abstract methods getCatKind() and getFurDescription().

The default methods on the Cat interface have implementation code that serves as functionality for all cats. These default methods will provide behavior to the Cat interface while also centralizing functionality (implementation) for all derived classes. I created empty (marker) interfaces Roarable, Purrable, and Meowler, which contain respective default methods roar(), purr() and meow(). These interfaces are similar to the idea of a mixin, which in object-oriented languages allows developers to extend or add default behavior to any derived class or interface.

Listing 3-20 is the main class that begins by creating three cat objects: Tiger, Cheetah, and HouseCat.

Listing 3-20. Creating Three Types of Cat Instances

Tiger bigCat = new Tiger();
Cheetah mediumCat = new Cheetah();
HouseCat smallCat = new HouseCat();

After creating the cat objects, the code begins by exercising the default and abstract methods. It invokes all of the methods on each cat object. Listing 3-21 shows a cat of type Tiger with all of its methods being invoked.

Listing 3-21. All Methods Invoked on a Tiger Instance

System.out.printf("%s with %s fur.
", bigCat.getCatKind(), bigCat.getFurDescription());
bigCat.eat();
bigCat.sleep();
bigCat.walk();
bigCat.roar();
bigCat.growl();
System.out.println("------------------");

Since the rest of the cat kinds have similar tasks, I won’t discuss them any further as I believe you have a handle on things. If you’ve gotten this far, you’re a real trooper. Whenever I learn a new skill or concept, I’m always reminded of the programming proverb, “If all you have is a hammer, everything looks like a nail”. Getting to learn new language features can often be difficult, especially when you’ve solved problems for so long without using these new and powerful concepts in Java.

I’ve only scratched the surface, so I encourage you to dig deeper into the features of Java 8. Now that you’ve been able to equip yourself with new-found powers of lambdas, let’s get back on track with JavaFX.

Properties and Binding

Properties are basically wrapper objects for JavaFX-based object attributes such as String or Integer. Properties allow developers to add listener code to respond when the wrapped value of an object has changed or is flagged as invalid. Also, property objects can be bound to one another. Binding behavior allows properties to update or synchronize their values based on a changed value from another property.

UI Patterns

Before discussing JavaFX’s properties and bindings APIs, I would like to share with you a little bit about UI patterns. When developing GUI applications you will inevitably encounter UI architectural framework concepts such as model view controller (MVC), model view presenter (MVP), or model view view-model (MVVM).

Depending on who you talk to, you might get different explanations; however, these concepts all address the issue of how best to handle synchronization between the model and view. What this means is that when a user interacts (input) with the UI (view) the underlying backend data store (the model) is automatically updated, and vice-versa.

Without trying to oversimplify the concepts, I will refer you to the actual UI patterns that are involved. The main UI patterns involved are the Supervising Controller, Presentation Model, and Mediator. If you are interested in understanding more about UI patterns, please read “GUI Architectures” by Martin Fowler at: http://martinfowler.com/eaaDev/uiArchs.html.

Because these patterns have been heavily discussed over the years, the JavaFX team has designed and implemented APIs to overcome problems that arose with these UI scenarios. In this section you will be learning how to use JavaFX properties and bindings to synchronize between your GUI and your data objects. Of course, I can’t possibly cover all of the use-case scenarios involving the UI patterns just mentioned, but hopefully I can give you the basics to get you on the right path.

Properties

Before the JavaFX properties API, Java Swing developers adhered to the JavaBean convention (specification), which specifies that objects will contain privately scoped instance variables which have associated getter (accessor) and setter (mutator) methods. For instance, a User class might have a private instance variable password of type String. The associated getter and setter would be the getPassword() and setPassword() methods, respectively. Listing 3-22 shows a User class that follows the older JavaBean convention.

Listing 3-22. A Simple JavaBean Class with Getter and Setter Methods

public class User {
   private String password;
   public String getPassword() {
      return password;
   }
   public void setPassword(String password) {
      this.password = password;
   }
}

As a developer who follows the JavaBean convention, you will quickly see the naming convention for the property, which is based on the getter and setter method names and not the private variable name. In other words, if the getter method were named getPwd(), the property would be named pwd and would have nothing to do with the attribute named password.

The JavaBeans specification also provided an API that has the concept of property change support (java.beans.PropertyChangeSupport), which allowed the developer to add handler (listener) code when the property changed. At the time, this solved only part of the issue of property change support. The JavaBeans API started showing its age when it came to binding variables and working with the Collections API. Although the JavaBeans API became a standard way to build domain objects, it still lacked robust ways to synchronize the domain models and views within a GUI application as described earlier.

Through the years the JavaBean specification and API gave rise to many third-party tools and frameworks to ease the developer experience. However, wiring up GUI components to JavaBeans using unidirectional or bidirectional binding still got pretty complicated. At times, developers would have resources that were not properly released, which led to object leaks. Developers quickly realized they needed a better way to bind and unbind GUI controls to be wired up to properties.

Types of JavaFX Properties

Let’s fast forward to JavaFX’s Properties API to see how it handles the common issues. We’ll first discuss the different types of JavaFX-based properties. There are two types to be concerned about:

  • Read/Writable
  • Read-Only

In short, JavaFX’s properties are wrapper objects holding actual values while providing change support, invalidation support, and binding capabilities. I will address binding later, but for now let’s examine commonly used property classes.

Properties are wrapper objects that have the ability to make values accessible as read/writable or read-only. All wrapper property classes are located in the javafx.beans.property.* package namespace. Listed here are commonly used property classes. To see all of the property classes, please refer to the documentation in Javadoc.

  • javafx.beans.property.SimpleBooleanProperty
  • javafx.beans.property.ReadOnlyBooleanWrapper
  • javafx.beans.property.SimpleIntegerProperty
  • javafx.beans.property.ReadOnlyIntegerWrapper
  • javafx.beans.property.SimpleDoubleProperty
  • javafx.beans.property.ReadOnlyDoubleWrapper
  • javafx.beans.property.SimpleStringProperty
  • javafx.beans.property.ReadOnlyStringWrapper

The properties that have a prefix of Simple and a suffix of Property are the read/writable property classes, and the classes with a prefix of ReadOnly and a suffix of Wrapper are the read-only properties. Later, you will see how to create a JavaFX bean using these commonly used properties, but for now let’s examine read/writable properties.

Read/Writable Properties

Read/writable properties are, as the name suggests, property values that can be both read and modified. As an example, let’s look at JavaFX string properties. To create a string property that is capable of both readable and writable access to the wrapped value, you will use the javafx.beans.property.SimpleStringProperty class. Listing 3-23 is a code snippet that demonstrates an instance of a SimpleStringProperty class and modifies the property via the set() method.

Listing 3-23. Creating a StringProperty Instance

StringProperty password = new SimpleStringProperty("password1");
password.set("1234");
System.out.println("Modified StringProperty " + password.get() ); // 1234

Here a declared variable password of type StringProperty is assigned to an instance of a SimpleStringProperty class. It’s always a good idea when declaring variables to be more abstract in object-oriented languages. Thus, referencing a StringProperty exposes fewer methods of the implementation class (SimpleStringProperty). Also notice that the actual value is the string “password1”, which is passed into the constructor of the SimpleStringProperty class. You will later discover other convenient constructor methods when working with JavaBeans and Property objects.

In the case of reading the value back, you would invoke the get() method (or getValue()), which returns the actual wrapped value(String) to the caller. To modify the value you simply call the set() method (or setValue()) by passing in a string.

Read-Only Properties

To make a property read-only you would use the wrapper classes that are prefixed with ReadOnly from the javafx.beans.property.* package. To create a property to be read-only you will need to take two steps. First is to instantiate a read-only wrapper class and invoke the method getReadOnlyProperty() to return a true read-only property object. Listing 3-24 creates a read-only string property.

Listing 3-24. Creating a Read-Only String Property

ReadOnlyStringWrapper userName = new ReadOnlyStringWrapper("jamestkirk");
ReadOnlyStringProperty readOnlyUserName = userName.getReadOnlyProperty();

This code snippet actually takes two steps to obtain a read-only string property. You will notice the call to getReadOnlyProperty(), which returns a read-only copy (synchronized) of the property. According to the Javadoc API, the ReadOnlyStringWrapper class “creates two properties that are synchronized. One property is read-only and can be passed to external users. The other property is read- and writable and should be used internally only.” Knowing that the other property can be read and written could allow a malicious developer to cast the object to type StringProperty, which then could be modified at will. From a security perspective you should be aware of the proper steps to create a true read-only property.

JavaFX JavaBean

Now that you’ve seen the JavaBean specification’s approach to creating domain objects, I will rewrite the earlier JavaBean User class to use JavaFX properties instead. In the process of rewriting the bean I also wanted to add an additional read-only property called userName to demonstrate the read-only property behavior. Listing 3-25 shows the User class rewritten to use JavaFX properties.

Listing 3-25. The File User.java Rewritten as a JavaFX Bean

import javafx.beans.property.*;
public class User {    private final static String USERNAME_PROP_NAME = "userName";    private final ReadOnlyStringWrapper userName;    private final static String PASSWORD_PROP_NAME = "password";    private StringProperty password;    public User() {        userName = new ReadOnlyStringWrapper(this,
USERNAME_PROP_NAME, System.getProperty("user.name"));        password = new SimpleStringProperty(this, PASSWORD_PROP_NAME, "");    }        public final String getUserName() {        return userName.get();    }    public ReadOnlyStringProperty userNameProperty() {        return userName.getReadOnlyProperty();    }        public final String getPassword() {        return password.get();    }    public final void setPassword(String password) {        this.password.set(password);    }    public StringProperty passwordProperty() {        return password;    }
}

You may notice some obvious differences in the way I instantiated the ReadOnlyStringWrapper and SimpleStringProperty classes. Similar to the JavaBean property change support, JavaFX properties have constructors that will allow you to specify the bean itself, its property name, and its value. As a simple example, Listing 3-26 shows the instantiations of a read-only and a read-writable property using the JavaFX property change support-based constructors. The userName variable is assigned a read-only property, and the password variable is assigned a read/writable property.

Listing 3-26. Instantiating a Read-Only and a Read-Writable Property

userName = new ReadOnlyStringWrapper(this, USERNAME_PROP_NAME, System.getProperty("user.name"));
password = new SimpleStringProperty(this, PASSWORD_PROP_NAME, "");

It is good practice to use the property change support-based constructors for many reasons, such as third-party tooling and testing. But most of all, when dealing with property change support, you will need access to the bean and its other properties.

One last thing to mention is that in Listing 3-25 you’ll also notice the getter and setter methods are final. Making the getter and setter final prevents any derived classes from overriding and possibly changing the underlying property.

Property Change Support

Property change support is the ability to add handler code that will respond when a property changes. JavaFX property objects contain an addListener() method. This method will accept two types of functional interfaces, ChangeListener and InvalidationListener. Recall that functional interfaces are single abstract methods which are expressed using the Java lambda syntax. All JavaFX properties are descendants of the ObservableValue and Observable interfaces (method overloading), which provide the addListener() methods for ChangeListener and InvalidationListener, respectively. One last thing to point out is that it is important to clean up listeners by removing them. To remove them you will invoke the removeListener() method by passing into it a referenced (named) listener as opposed to an anonymous inner class or anonymous lambda expression.

Listing 3-27 shows how to create a ChangeListener to be registered with a property. As the property’s value changes, the change() method will be invoked. I’ve provided the implementations for both the anonymous inner class and lambda expression-style syntax for you to compare.

Listing 3-27. Adding a ChangeListener Instance to Respond When the Property Value Changes

SimpleIntegerProperty xProperty = new SimpleIntegerProperty(0);
// Adding a change listener (anonymous inner class)
xProperty.addListener(new ChangeListener<Number>(){      @Override      public void changed(ObservableValue<? extends Number> ov, Number oldVal, Number newVal) {         // code goes here      }
});
// Adding a change listener (lambda expression)
xProperty.addListener((ObservableValue<? extends Number> ov, Number oldVal, Number newVal) -> {   // code goes here
});

Listing 3-28 shows how to create an InvalidationListener to be registered with a property. As the property’s value changes, the invalidated () method will be invoked. I’ve provided the implementations for both the anonymous inner class and lambda expression-style syntax for you to compare.

Listing 3-28. Adding an InvalidationListener Instance to Respond When the Property Value Changes

// Adding a invalidation listener (anonymous inner class)
xProperty.addListener(new InvalidationListener() {
   @Override
   public void invalidated(Observable o) {
      // code goes here
   }
});
 
// Adding a invalidation listener (lambda expression)
xProperty.addListener((Observable o) -> {
   // code goes here
});

Often developers become confused about the difference between a ChangeListener and an InvalidationListener. In Listing 3-28 you’ll notice that using a ChangeListener you will get the Observable (ObservableValue), the old value, and the new value, while using the InvalidationListener only gets the Observable object (property).

In the Javadoc documentation, the difference between a ChangeListener and InvalidationListener is described as follows:

“A change event indicates that the value has changed. An invalidation event is generated, if the current value is not valid anymore. This distinction becomes important, if the ObservableValue supports lazy evaluation, because for a lazily evaluated value one does not know if an invalid value really has changed until it is recomputed. For this reason, generating change events requires eager evaluation while invalidation events can be generated for eager and lazy implementations.”

The InvalidationListener provides a way to mark values as invalid but does not recompute the value until it is needed. This is often used in UI layouts or custom controls, where you can avoid unnecessary computations when nodes don’t need to be redrawn/repositioned during a layout request or draw cycle. When using the ChangeListener, you normally want eager evaluation such as the validation of properties on a form-type application. That doesn’t mean you can’t use InvalidationListeners for validation of properties; it just depends on your performance requirements and exactly when you need the new value to be recomputed (evaluated). When you access the observable value, it causes the InvalidationListener to be eager.

Binding

Simply put, binding has the idea of at least two values (properties) being synchronized. This means that when a dependent variable changes, the other variable changes. JavaFX provides many binding options that enable the developer to synchronize between properties in domain objects and GUI controls. In this section you will learn about three binding strategies when binding property objects.

In this section you will be focusing on the following JavaFX API packages:

  • javafx.beans.binding.*
  • javafx.beans.property.*

Binding of properties is quite easy to do. The only requirement is that the property invoking the bind must be a read/writeable property. To bind a property to another property, you will invoke the bind() method. This method will bind in one direction (unidirectional). For instance, when property A binds to property B the change in property B will update property A, but not the other way. If A is bound to B you can’t update A, as you’ll get a RuntimeException: A bound value cannot be set.

The following are three additional binding strategies to consider using in JavaFX’s Properties API:

  • Bidirectional binding on a Java Bean
  • High-level binding using the Fluent API
  • Low-level binding using javafx.beans.binding.* binding objects

Bidirectional Binding

Bidirectional binding allows you to bind properties with the same type allowing changes on either end while keeping a value synchronized. When binding bi-directionally, it’s required that both properties must be read/writable. It’s pretty easy to bind two properties bidirectionally through the use of the bindBidirectional() method. To demonstrate, I’ve created a simple example. Listing 3-29 is a Contact bean having a property firstName that is bound bidirectionally to a local variable fname of type StringProperty.

Listing 3-29. Bidirectional Binding between the firstName Property and a Regular String Property Variable

Contact contact = new Contact("John", "Doe");
StringProperty fname = new SimpleStringProperty();
fname.bindBidirectional(contact.firstNameProperty());

contact.firstNameProperty().set("Play");
fname.set("Jane");

System.out.println("contact.firstNameProperty = " + contact.firstNameProperty().get() );
System.out.println("fname = " + fname.get() );

The output of this code snippet is as follows:

contact.firstNameProperty = Jane
fname = Jane

High-level Binding

Introduced in JavaFX 2.0 is a fluent interface API to bind properties. The fluent APIs are methods that allow you to perform operations on properties using English-like method names. For example, if you have a numeric property, there would be methods like multiply(), divide(), subtract(), and so on. Another example would be a string property having methods like isEqualTo(), isNotEqualTo(), concat(), and similar. As an example, Listing 3-30 shows how to create a property that represents the formula for the area of a rectangle.

Listing 3-30. Creating a Property (area) Using the High-Level Binding Strategy for Calculating the Area of a Rectangle

// Area = width * height
IntegerProperty width = new SimpleIntegerProperty(10);
IntegerProperty height = new SimpleIntegerProperty(10);

NumberBinding area = width.multiply(height);

This code demonstrates high-level binding by using the fluent API from the javafx.beans.binding.IntegerExpression parent interface. This example binds by using the multiply() method, which returns a NumberBinding containing the computed value. What’s nice is that the binding is lazy-evaluated, which means the computation (multiplying) doesn’t occur unless you invoke the property’s (area) value via the get() (or getValue())method. For all available fluent interfaces, please see the Javadoc for javafx.beans.binding.* packages.

Low-Level Binding

When using low-level binding, you would use a derived NumberBinding class, such as a DoubleBinding class for values of type Double. With a DoubleBinding class you will override its computeValue() method so that you can use the familiar operators such as * and to formulate complex math equations. The difference between high-and low-level binding is that high-level uses methods such as multiply(), subtract() instead of the operators * and . Listing 3-31 shows how to create a low-level binding for the formula for the volume of a sphere.

Listing 3-31. A Property Binding (volumnOfSphere) Using the Low-Level Binding Strategy

DoubleProperty radius = new SimpleDoubleProperty(2);
DoubleBinding volumeOfSphere = new DoubleBinding() {
   {
      super.bind(radius); // initial bind
   }
 
   @Override
   protected double computeValue() {
        // Math.pow() (power) cubes the radius
      return (4 / 3 * Math.PI * Math.pow(radius.get(), 3));
   }
};

If you’ve gotten this far, you are definitely ready to begin building GUIs. So far you’ve had a chance to learn about lambda expressions, properties, and bindings. Next, I want to close out this chapter with an example showing how to build a snazzy-looking logon dialog window that makes use of shapes, lambda expressions, properties, and bindings (with a hint of UI controls).

A Logon Dialog Example

Logon dialogs are typical of any application when authenticating users. In this example I created a simple form application that will involve all of the skills you’ve learned from Chapter 2 and in this chapter thus far. Before getting deep into the code, which you’ll see in Listing 3-32, let’s look at the logon dialog. Shown in Figure 3-3 is the initial display of the logon dialog window the code will create.

9781430264606_Fig03-03.jpg

Figure 3-3. Initial display of the logon dialog window

Following are the instructions for interacting with the application.

  1. The user will have three attempts to enter the correct password. Figure 3-4 shows the user making the first attempt to log on. (Note: The password is “password1.”)

    9781430264606_Fig03-04.jpg

    Figure 3-4. The user is entering the password into the logon dialog window

  2. As the user enters the password, they have an opportunity to hit the Enter key. If the password is incorrect, a red X appears to the right of the password text field. Figure 3-5 shows an invalid logon.

    9781430264606_Fig03-05.jpg

    Figure 3-5. The user has pressed Enter with an invalid password

  3. If the user types the correct password in real time a green check appears. After the green check appears, the user may hit the Enter key to be granted access. Figure 3-6 shows the user entering a valid password.

9781430264606_Fig03-06.jpg

Figure 3-6. The user has entered a valid password

Login Dialog Source Code

Listing 3-32 is the source code for the JavaFX example logon dialog application.

Listing 3-32. The JavaFX Example Logon Dialog Application

package jfx8ibe;

import javafx.application.*;
import javafx.beans.property.*;
import javafx.geometry.Pos;
import javafx.scene.*;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.*;
import javafx.scene.text.*;
import javafx.stage.*;

/**
 * A login form to demonstrate lambdas, properties, and bindings.
 * @author cdea
 */
public class FormValidation extends Application {
      private final static String MY_PASS = "password1";    private final static BooleanProperty GRANTED_ACCESS = new SimpleBooleanProperty(false);    private final static int MAX_ATTEMPTS = 3;    private final IntegerProperty ATTEMPTS = new SimpleIntegerProperty(0);
   @Override    public void start(Stage primaryStage) {        // create a model representing a user        User user = new User();
       // create a transparent stage        primaryStage.initStyle(StageStyle.TRANSPARENT);
       Group root = new Group();        Scene scene = new Scene(root, 320, 112, Color.rgb(0, 0, 0, 0));        primaryStage.setScene(scene);
       // all text, borders, svg paths will use white        Color foregroundColor = Color.rgb(255, 255, 255, .9);
       // rounded rectangular background        Rectangle background = new Rectangle(320, 112);        background.setX(0);        background.setY(0);        background.setArcHeight(15);        background.setArcWidth(15);        background.setFill(Color.rgb(0, 0, 0, .55));        background.setStrokeWidth(1.5);        background.setStroke(foregroundColor);
       // a read only field holding the user name.        Text userName = new Text();        userName.setFont(Font.font("SanSerif", FontWeight.BOLD, 30));        userName.setFill(foregroundColor);        userName.setSmooth(true);        userName.textProperty()                .bind(user.userNameProperty());
       // wrap text node        HBox userNameCell = new HBox();        userNameCell.prefWidthProperty()                    .bind(primaryStage.widthProperty()                                      .subtract(45));        userNameCell.getChildren().add(userName);
       // pad lock        SVGPath padLock = new SVGPath();        padLock.setFill(foregroundColor);        padLock.setContent("M24.875,15.334v-4.876c0-4.894-3.981-8.875-8.875-8.875s-8.875,3.981-8.875,8.875v4.876H5.042v15.083h21.916V15.334H24.875zM10.625,10.458c0-2.964,2.411-5.375,5.375-5.375s5.375,2.411,5.375,5.375v4.876h-10.75V10.458zM18.272,26.956h-4.545l1.222-3.667c-0.782-0.389-1.324-1.188-1.324-2.119c0-1.312,1.063-2.375,2.375-2.375s2.375,1.062,2.375,2.375c0,0.932-0.542,1.73-1.324,2.119L18.272,26.956z");
       // first row        HBox row1 = new HBox();        row1.getChildren()            .addAll(userNameCell, padLock);
       // password text field        PasswordField passwordField = new PasswordField();        passwordField.setFont(Font.font("SanSerif", 20));        passwordField.setPromptText("Password");        passwordField.setStyle("-fx-text-fill:black; "                + "-fx-prompt-text-fill:gray; "                + "-fx-highlight-text-fill:black; "                + "-fx-highlight-fill: gray; "                + "-fx-background-color: rgba(255, 255, 255, .80); ");        passwordField.prefWidthProperty()                     .bind(primaryStage.widthProperty()                                       .subtract(55));        user.passwordProperty()            .bind(passwordField.textProperty());
       // error icon        SVGPath deniedIcon = new SVGPath();        deniedIcon.setFill(Color.rgb(255, 0, 0, .9));        deniedIcon.setStroke(Color.WHITE);//        deniedIcon.setContent("M24.778,21.419 19.276,15.917 24.777,10.415 21.949,7.585 16.447,13.087 10.945,7.585 8.117,10.415 13.618,15.917 8.116,21.419 10.946,24.248 16.447,18.746 21.948,24.248z");        deniedIcon.setVisible(false);
       SVGPath grantedIcon = new SVGPath();        grantedIcon.setFill(Color.rgb(0, 255, 0, .9));        grantedIcon.setStroke(Color.WHITE);//        grantedIcon.setContent("M2.379,14.729 5.208,11.899 12.958,19.648 25.877,6.733 28.707,9.561 12.958,25.308z");        grantedIcon.setVisible(false);
       StackPane accessIndicator = new StackPane();        accessIndicator.getChildren()                       .addAll(deniedIcon, grantedIcon);        accessIndicator.setAlignment(Pos.CENTER_RIGHT);
       grantedIcon.visibleProperty().bind(GRANTED_ACCESS);
       // second row        HBox row2 = new HBox(3);        row2.getChildren()            .addAll(passwordField, accessIndicator);        HBox.setHgrow(accessIndicator, Priority.ALWAYS);
       // user hits the enter key        passwordField.setOnAction(actionEvent -> {            if (GRANTED_ACCESS.get()) {                System.out.printf("User %s is granted access. ",                        user.getUserName());                System.out.printf("User %s entered the password: %s ",                        user.getUserName(), user.getPassword());                Platform.exit();            } else {                deniedIcon.setVisible(true);            }            ATTEMPTS.set(ATTEMPTS.add(1).get());            System.out.println("Attempts: " + ATTEMPTS.get());        });
       // listener when the user types into the password field        passwordField.textProperty().addListener((obs, ov, nv) -> {            boolean granted = passwordField.getText()                                           .equals(MY_PASS);            GRANTED_ACCESS.set(granted);            if (granted) {                deniedIcon.setVisible(false);            }        });
       // listener on number of attempts        ATTEMPTS.addListener((obs, ov, nv) -> {            if (MAX_ATTEMPTS == nv.intValue()) {                // failed attemps                System.out.printf("User %s is denied access. ",                        user.getUserName());                Platform.exit();            }        });
       VBox formLayout = new VBox(4);        formLayout.getChildren().addAll(row1, row2);        formLayout.setLayoutX(12);        formLayout.setLayoutY(12);
       root.getChildren().addAll(background, formLayout);
       primaryStage.show();    }    public static void main(String[] args) {        launch(args);    }
}

Explanation of the Code

The JavaFX application in Listing 3-32 simulates a logon dialog window. The code mainly demonstrates binding JavaFX JavaBeans properties with UI controls by reusing the User class that was shown earlier, in Listing 3-25.

Let’s begin by describing the class variables. The variable MY_PASS contains a hardcoded string of the password of “password1”. Next declared is the GRANTED_ACCESS variable, of type SimpleBooleanProperty class. This variable will be later bound from the green check (grantedIcon) node’s visible property. Basically, as the user enters the correct keystrokes for the password, the GRANTED_ACCESS become true, which will internally set the green check (SVGPath) node’s visible property to true, thus displaying the icon in the scene graph. When the password is incorrect, the visible property becomes false, which hides the green check node (SVGPath). The constant variable MAX_ATTEMPTS is the maximum number of attempts the user can try to log in. Last, the ATTEMPTS variable of type IntegerProperty holds the current number of attempts the user has tried to log into the dialog by hitting the Enter key.

In the start() method, a User object is created to be used as the domain object (model) to be synchronized with the GUI form. Next, the stage is set to be transparent by invoking the Stage.initStyle(StageStyle.TRANSPARENT); method. This will allow us to create translucent and irregularly shaped windows without the decoration of the native OS title bar. After initializing the stage, I also needed to create a Scene with a root node of type Group that will allow me to add child nodes. Even though the root node is transparent, it will need to be the same size as our logon screen. The logon screen is actually just a rounded rectangle having an arc height and width of 15 with a fill color of black with an opacity level of 55%. To give things a consistent look I’ve created a foregroundColor variable, containing a color of white with an opacity of 90 percent. The rectangle’s stroke color will use the variable foregroundColor to give it a white outline.

After setting up the initial rectangular background, the code creates a Text node that will represent the user’s user name as a read-only field (per the User class). To obtain the user’s real username, you can call the System.getProperty("user.name") method. The text node is set up with a 30 point SanSerif font having a fill color of the foregroundColor (white) variable. The text node’s text property is bound to the User object’s read-only userName property. Next, I created an HBox layout that will hold the Text node userName. The HBox (userNameCell) will be binding its preferred width property by using the fluent API to ensure that it doesn’t hog up all the horizontal space and allow the padlock icon to have room to be placed beside it. Shown in Figure 3-3 in the upper-right corner of the logon dialog window is the padlock icon. The following code snippet shows an HBox containing the TextNode (username) from Listing 3-32.

// wrap text node
HBox userNameCell = new HBox();
userNameCell.prefWidthProperty().bind(primaryStage.widthProperty().subtract(45));
userNameCell.getChildren().add(userName);

I have said very little about the HBox layout control here, because I will discuss it further in Chapter 4, on Layout and UI Controls. Another new node I have not mentioned is JavaFX’s SVGPath node to create the padlock icon. Recall that in Chapter 2 we discussed generating complex shapes using path elements to create an ice cream cone, as depicted in Figure 2-4. Having said this, the SVGPath class is very similar to the Path node, except that the child path elements are expressed in W3C SVG path notation as a string. To render a path the string representing the path notation is passed into the setContent() method on the SVGPath node. To see how to use SVG paths, visit http://www.w3.org/Graphics/SVG. To create the padlock icon, I headed over to the famous website of the JavaScript library Raphaël by Dmitry Baranovskiy. There I was able to obtain very nice SVG icons, at http://raphaeljs.com/icons. Because an SVGPath node is also a JavaFX shape, I simply filled it with the color white (foregroundColor) by using the setFill() method. Next, the code creates an HBox as a container for the first row (row1) to hold the userNameCell and padLock nodes.

After creating the first row, the code proceeds by creating the PasswordField UI control. Like the Text node, the PasswordField UI control’s font can be set. I added a 20 point SanSerif font to the password field. Next, the code will set the prompt text to let the user know what to type into the field. The prompt text is shown in Figure 3-3 of the initial logon dialog with the prompt text as “Password.”

For some final decorating of the password field I used JavaFX CSS styling, which you will get a chance to see in later chapters. The CSS style I applied to the password field was a white background with opacity of 80 percent. Still working with the password field, I used the fluent interface subtract() to bind the preferred width to subtract 55px to allow room for the stack pane that contains the granted and denied icons. Last, the user object’s passwordProperty is bound to the PasswordField’s textProperty. This is a unidirectional bind, which means that as the user enters text into the password field the user object’s password property changes and not the other way around.

Next, I create two more icons using the SVGPath node that represents an X and a check mark, denoting denied (deniedIcon) and granted (grantedIcon) access icons, respectively. These icons will be placed in a StackPane layout node. The StackPane layout allows child nodes to be stacked, hence the name. In this scenario the deniedIcon and grantedIcon nodes are stacked on top of each other. This trick will allows us to flip between icons using the setVisible() method. As a whole (stack pane) node, it will be positioned to the right of the password field. As the user types into the password field the Boolean property GRANTED_ACCESS is updated, which updates the grantedIcon’s visible property. The following code statement shows the visible property bound to the GRANTED_ACCESS property.

grantedIcon.visibleProperty().bind(GRANTED_ACCESS);

Completing the second row, I create another HBox similar to our first row, this time holding the password field and the stack pane containing the icons (denied and granted icons).

Next, the code continues by wiring up the UI controls and adding property change support (ChangeListeners) to various properties. The code proceeds with the passwordField by adding an action as a functional interface for the method setOnAction() when the user hits the Enter key. The following handler code is set on the password field when the user hits the Enter key.

passwordField.setOnAction(actionEvent -> {
    if (GRANTED_ACCESS.get()) {
      System.out.printf("User %s is granted access. ", user.getUserName());
      Platform.exit();
    } else {
      deniedIcon.setVisible(true);
    }
    ATTEMPTS.set(ATTEMPTS.add(1).get());
    System.out.println("Attempts: " + ATTEMPTS.get());
});

Essentially, if the password matches and the user hits Enter, the program exits and outputs the following:

User cdea is granted access.

You’ll also notice that the code increments the ATTEMPTS property by invoking the fluent interface method add().

Still working with the password field, I added a change listener to listen for keystrokes from the user. As the user types into the password field the password is compared against the stored (hardcoded) password. If the password is valid, the code sets the GRANTED_ACCESS property to true and hides the deniedIcon icon via the setVisible(false) method.

Finally, I added the last bit of wiring for when a user has too many failed attempts. I basically added a change listener onto the ATTEMPTS property. The change listener will compare MAX_ATTEMPTS against ATTEMPTS to see if they equal. If so, it will invoke the JavaFX’s Platform.exit() method to gracefully exit the application. The rest is assembling the rows into a VBox layout and adding it to the root node for the scene to be shown.

Summary

In this chapter we’ve covered a lot of ground with properties and bindings, especially relating to new Java 8 language features. You were introduced to the new features in Java 8 such as lambda expressions, stream APIs, and default methods. Starting with Java lambdas, you were able to learn the syntax and make use of functional interfaces. Next, you were able to use the stream API and common aggregate functions. After an example of using the stream APIs to manipulate elements in a collection, you then learned how to extend behavior on Java interfaces with default methods. Then we explored UI patterns, the Properties API, and the idea of change support. With a solid understanding of working with properties, you then learned about various binding strategies. To wrap up the chapter, you saw an example of how to create a logon dialog window, which employed all the concepts you’ve learned so far. Next, in Chapter 4 you’ll explore JavaFX UI layout and UI controls.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset