© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2022
K. Sharan, P. SpäthLearn JavaFX 17https://doi.org/10.1007/978-1-4842-7848-2_3

3. Observable Collections

Kishori Sharan1   and Peter Späth2
(1)
Montgomery, AL, USA
(2)
Leipzig, Sachsen, Germany
 
In this chapter, you will learn:
  • What observable collections in JavaFX are

  • How to observe observable collections for invalidations and changes

  • How to use observable collections as properties

The examples of this chapter lie in the com.jdojo.collections package . In order for them to work, you must add a corresponding line to the module-info.java file :
...
opens com.jdojo.collections to javafx.graphics, javafx.base;
...

What Are Observable Collections?

Observable collections in JavaFX are extensions to collections in Java. The collections framework in Java has the List, Set, and Map interfaces. JavaFX adds the following three types of observable collections that may be observed for changes in their contents:
  • An observable list

  • An observable set

  • An observable map

JavaFX supports these types of collections through three new interfaces:
  • ObservableList

  • ObservableSet

  • ObservableMap

These interfaces inherit from List, Set, and Map from the java.util package. In addition to inheriting from the Java collection interfaces, JavaFX collection interfaces also inherit the Observable interface. All JavaFX observable collection interfaces and classes are in the javafx.collections package. Figure 3-1 shows a partial class diagram for the ObservableList, ObservableSet, and ObservableMap interfaces.
Figure 3-1

A partial class diagram for observable collection interfaces in JavaFX

The observable collections in JavaFX have two additional features:
  • They support invalidation notifications as they are inherited from the Observable interface.

  • They support change notifications. You can register change listeners to them, which are notified when their contents change.

The javafx.collections.FXCollections class is a utility class to work with JavaFX collections. It consists of all static methods.

JavaFX does not expose the implementation classes of observable lists, sets, and maps. You need to use one of the factory methods in the FXCollections class to create objects of the ObservableList, ObservableSet, and ObservableMap interfaces.

Tip

In simple terms, an observable collection in JavaFX is a list, set, or map that may be observed for invalidation and content changes.

Understanding ObservableList

An ObservableList is a java.util.List and an Observable with change notification features. Figure 3-2 shows the class diagram for the ObservableList interface .
Figure 3-2

A class diagram for the ObservableList interface

Tip

The methods filtered() and sorted() are missing in the diagram. You can use them for filtering and sorting the list elements. For details, see the API documentation.

The addListener() and removeListener() methods in the ObservableList interface allow you to add and remove ListChangeListeners, respectively. Other methods perform operations on the list, which affect multiple elements.

If you want to receive notifications when changes occur in an ObservableList, you need to add a ListChangeListener interface whose onChanged() method is called when a change occurs in the list. The Change class is a static inner class of the ListChangeListener interface. A Change object contains a report of the changes in an ObservableList. It is passed to the onChanged() method of the ListChangeListener. I will discuss list change listeners in detail later in this section.

You can add or remove invalidation listeners to or from an ObservableList using the following two methods that it inherits from the Observable interface:
  • void addListener(InvalidationListener listener)

  • void removeListener(InvalidationListener listener)

Note that an ObservableList contains all of the methods of the List interface as it inherits them from the List interface.

Tip

The JavaFX library provides two classes named FilteredList and SortedList that are in the javafx.collections.transformation package. A FilteredList is an ObservableList that filters its contents using a specified Predicate. A SortedList sorts its contents. I will not discuss these classes in this chapter. All discussions of observable lists apply to the objects of these classes as well.

Creating an ObservableList

You need to use one of the following factory methods of the FXCollections class to create an ObservableList:
  • <E> ObservableList<E> emptyObservableList()

  • <E> ObservableList<E> observableArrayList()

  • <E> ObservableList<E> observableArrayList(Collection<? extends E> col)

  • <E> ObservableList<E> observableArrayList(E... items)

  • <E> ObservableList<E> observableList(List<E> list)

  • <E> ObservableList<E> observableArrayList(Callback<E, Observable[]> extractor)

  • <E> ObservableList<E> observableList(List<E> list, Callback<E, Observable[]> extractor)

The emptyObservableList() method creates an empty, unmodifiable ObservableList. Often, this method is used when you need an ObservableList to pass to a method as an argument and you do not have any elements to pass to that list. You can create an empty ObservableList of String as follows:
ObservableList<String> emptyList = FXCollections.emptyObservableList();

The observableArrayList() method creates an ObservableList backed by an ArrayList. Other variants of this method create an ObservableList whose initial elements can be specified in a Collection as a list of items or as a List.

The last two methods in the preceding list create an ObservableList whose elements can be observed for updates. They take an extractor, which is an instance of the Callback<E, Observable[]> interface. An extractor is used to get the list of Observable values to observe for updates. I will cover the use of these two methods in the “Observing an ObservableList for Updates” section.

Listing 3-1 shows how to create observable lists and how to use some of the methods of the ObservableList interface to manipulate the lists. At the end, it shows how to use the concat() method of the FXCollections class to concatenate elements of two observable lists.
// ObservableListTest.java
package com.jdojo.collections;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class ObservableListTest {
        public static void main(String[] args) {
            // Create a list with some elements
            ObservableList<String> list =
                    FXCollections.observableArrayList("one", "two");
            System.out.println("After creating list: " + list);
            // Add some more elements to the list
            list.addAll("three", "four");
            System.out.println("After adding elements: " + list);
            // You have four elements. Remove the middle two
            // from index 1 (inclusive) to index 3 (exclusive)
            list.remove(1, 3);
            System.out.println("After removing elements: " + list);
            // Retain only the element "one"
            list.retainAll("one");
            System.out.println("After retaining "one": " + list);
            // Create another ObservableList
            ObservableList<String> list2 =
                FXCollections.<String>observableArrayList(
                      "1", "2", "3");
            // Set list2 to list
            list.setAll(list2);
            System.out.println("After setting list2 to list: " +
                     list);
            // Create another list
            ObservableList<String> list3 =
                FXCollections.<String>observableArrayList(
                       "ten", "twenty", "thirty");
            // Concatenate elements of list2 and list3
            ObservableList<String> list4 =
                     FXCollections.concat(list2, list3);
            System.out.println("list2 is " + list2);
            System.out.println("list3 is " + list3);
            System.out.println(
                     "After concatenating list2 and list3:" + list4);
        }
}
After creating list: [one, two]
After adding elements: [one, two, three, four]
After removing elements: [one, four]
After retaining "one": [one]
After setting list2 to list: [1, 2, 3]
list2 is [1, 2, 3]
list3 is [ten, twenty, thirty]
After concatenating list2 and list3:[1, 2, 3, ten, twenty, thirty]
Listing 3-1

Creating and Manipulating Observable Lists

Observing an ObservableList for Invalidations

You can add invalidation listeners to an ObservableList as you do to any Observable. Listing 3-2 shows how to use an invalidation listener with an ObservableList.

Tip

In the case of the ObservableList, the invalidation listeners are notified for every change in the list, irrespective of the type of a change.

// ListInvalidationTest.java
package com.jdojo.collections;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class ListInvalidationTest {
        public static void main(String[] args) {
                // Create a list with some elements
                ObservableList<String> list =
                    FXCollections.observableArrayList("one", "two");
                // Add an InvalidationListener to the list
                list.addListener(ListInvalidationTest::invalidated);
                System.out.println("Before adding three.");
                list.add("three");
                System.out.println("After adding three.");
                System.out.println("Before adding four and five.");
                list.addAll("four", "five");
                System.out.println("Before adding four and five.");
                System.out.println("Before replacing one with one.");
                list.set(0, "one");
                System.out.println("After replacing one with one.");
        }
        public static void invalidated(Observable list) {
                System.out.println("List is invalid.");
        }
}
Before adding three.
List is invalid.
After adding three.
Before adding four and five.
List is invalid.
Before adding four and five .
Before replacing one with one.
List is invalid.
After replacing one with one.
Listing 3-2

Testing Invalidation Notifications for an ObservableList

Observing an ObservableList for Changes

Observing an ObservableList for changes is a bit tricky. There could be several kinds of changes to a list. Some of the changes could be exclusive, whereas some can occur along with other changes. Elements of a list can be permutated, updated, replaced, added, and removed. You need to be patient in learning this topic because I will cover it in bits and pieces.

You can add a change listener to an ObservableList using its addListener() method , which takes an instance of the ListChangeListener interface. The changed() method of the listeners is called every time a change occurs in the list. The following snippet of code shows how to add a change listener to an ObservableList of String. The onChanged() method is simple; it prints a message on the standard output when it is notified of a change:
// Create an observable list
ObservableList<String> list = FXCollections.observableArrayList();
// Add a change listener to the list
list.addListener(new ListChangeListener<String>() {
        @Override
        public void onChanged(ListChangeListener.Change<? extends String>
                  change) {
            System.out.println("List has changed.");
        }
});
Listing 3-3 contains the complete program showing how to detect changes in an ObservableList. It uses a lambda expression with a method reference, which are features of Java 8, to add a change listener. After adding a change listener, it manipulates the list four times, and the listener is notified each time, as is evident from the output that follows.
// SimpleListChangeTest.java
package com.jdojo.collections;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
public class SimpleListChangeTest {
        public static void main(String[] args) {
            // Create an observable list
            ObservableList<String> list =
                    FXCollections.observableArrayList();
            // Add a change listener to the list
            list.addListener(SimpleListChangeTest::onChanged);
            // Manipulate the elements of the list
            list.add("one");
            list.add("two");
            FXCollections.sort(list);
            list.clear();
        }
        public static void onChanged(
                   ListChangeListener.Change<? extends String> change) {
            System.out.println("List has changed");
        }
}
List has changed.
List has changed.
List has changed.
List has changed.
Listing 3-3

Detecting Changes in an ObservableList

Understanding the ListChangeListener.Change Class

Sometimes, you may want to analyze changes to a list in more detail rather than just knowing that the list has changed. The ListChangeListener.Change object that is passed to the onChanged() method contains a report to a change performed on the list. You need to use a combination of its methods to know the details of a change. Table 3-1 lists the methods in the ListChangeListener.Change class with their categories.
Table 3-1

Methods in the ListChangeListener.Change Class

Method

Category

ObservableList<E> getList()

General

boolean next()

void reset()

Cursor movement

boolean wasAdded()

boolean wasRemoved()

boolean wasReplaced()

boolean wasPermutated()

boolean wasUpdated()

Change type

int getFrom()

int getTo()

Affected range

int getAddedSize()

List<E> getAddedSubList()

Addition

List<E> getRemoved()

int getRemovedSize()

Removal

int getPermutation(int oldIndex)

Permutation

The getList() method returns the source list after changes have been made. A ListChangeListener.Change object may report a change in multiple chunks. This may not be obvious at first. Consider the following snippet of code:
ObservableList<String> list = FXCollections.observableArrayList();
// Add a change listener here...
list.addAll("one", "two", "three");
list.removeAll("one", "three");

In this code, the change listener will be notified twice: once for the addAll() method call and once for the removeAll() method call. The ListChangeListener.Change object reports the affected range of indexes. In the second change, you remove two elements that fall into two different ranges of indexes. Note that there is an element "two" between the two removed elements. In the second case, the Change object will contain a report of two changes. The first change will contain the information that, at index 0, the element "one" has been removed. Now, the list contains only two elements with the index 0 for the element "two" and index 1 for the element "three". The second change will contain the information that, at index 1, the element "three" has been removed.

A Change object contains a cursor that points to a specific change in the report. The next() and reset() methods are used to control the cursor. When the onChanged() method is called, the cursor points before the first change in the report. Calling the next() method the first time moves the cursor to the first change in the report. Before attempting to read the details for a change, you must point the cursor to the change by calling the next() method. The next() method returns true if it moves the cursor to a valid change. Otherwise, it returns false. The reset() method moves the cursor before the first change. Typically, the next() method is called in a while loop, as shown in the following snippet of code:
ObservableList<String> list = FXCollections.observableArrayList();
...
// Add a change listener to the list
list.addListener(new ListChangeListener<String>() {
    @Override
    public void onChanged(ListChangeListener.Change<? extends String>
             change) {
        while(change.next()) {
            // Process the current change here...
        }
    }
});

In the change type category, methods report whether a specific type of change has occurred. The wasAdded() method returns true if elements were added. The wasRemoved() method returns true if elements were removed. The wasReplaced() method returns true if elements were replaced. You can think of a replacement as a removal followed by an addition at the same index. If wasReplaced() returns true, both wasRemoved() and wasAdded() return true as well. The wasPermutated() method returns true if elements of a list were permutated (i.e., reordered) but not removed, added, or updated. The wasUpdated() method returns true if elements of a list were updated.

Not all five types of changes to a list are exclusive. Some changes may occur simultaneously in the same change notification. The two types of changes, permutations and updates, are exclusive. If you are interested in working with all types of changes, your code in the onChanged() method should look as follows:
public void onChanged(ListChangeListener.Change change) {
        while (change.next()) {
                if (change.wasPermutated()) {
                        // Handle permutations
                }
                else if (change.wasUpdated()) {
                        // Handle updates
                }
                else if (change.wasReplaced()) {
                        // Handle replacements
                }
                else {
                        if (change.wasRemoved()) {
                                // Handle removals
                        }
                        else if (change.wasAdded()) {
                                // Handle additions
                        }
                }
        }
}

In the affected range type category, the getFrom() and getTo() methods report the range of indexes affected by a change. The getFrom() method returns the beginning index, and the getTo() method returns the ending index plus one. If the wasPermutated() method returns true, the range includes the elements that were permutated. If the wasUpdated() method returns true, the range includes the elements that were updated. If the wasAdded() method returns true, the range includes the elements that were added. If the wasRemoved() method returns true and the wasAdded() method returns false, the getFrom() and getTo() methods return the same number—the index where the removed elements were placed in the list.

The getAddedSize() method returns the number of elements added. The getAddedSubList() method returns a list that contains the elements added. The getRemovedSize() method returns the number of elements removed. The getRemoved() method returns an immutable list of removed or replaced elements. The getPermutation(int oldIndex) method returns the new index of an element after permutation. For example, if an element at index 2 moves to index 5 during a permutation, the getPermutation(2) will return 5.

This completes the discussion about the methods of the ListChangeListener.Change class. However, you are not done with this class yet! I still need to discuss how to use these methods in actual situations, for example, when elements of a list are updated. I will cover handling updates to elements of a list in the next section. I will finish this topic with an example that covers everything that was discussed.

Observing an ObservableList for Updates

In the “Creating an ObservableList” section, I had listed the following two methods of the FXCollections class that create an ObservableList :
  • <E> ObservableList<E> observableArrayList(Callback<E, Observable[]> extractor)

  • <E> ObservableList<E> observableList(List<E> list, Callback<E, Observable[]> extractor)

If you want to be notified when elements of a list are updated, you need to create the list using one of these methods. Both methods have one thing in common: they take a Callback<E,Observable[]> object as an argument. The Callback<P,R> interface is in the javafx.util package. It is defined as follows:
public interface Callback<P,R> {
        R call(P param)
}

The Callback<P,R> interface is used in situations where further action is required by APIs at a later suitable time. The first generic type parameter specifies the type of the parameter passed to the call() method, and the second one specifies the return type of the call() method.

If you notice the declaration of the type parameters in Callback<E,Observable[]>, the first type parameter is E, which is the type of the elements of the list. The second parameter is an array of Observable. When you add an element to the list, the call() method of the Callback object is called. The added element is passed to the call() method as an argument. You are supposed to return an array of Observable from the call() method. If any of the elements in the returned Observable array changes, listeners will be notified of an “update” change for the element of the list for which the call() method had returned the Observable array.

Let’s examine why you need a Callback object and an Observable array to detect updates to elements of a list. A list stores references of its elements. Its elements can be updated using their references from anywhere in the program. A list does not know that its elements are being updated from somewhere else. It needs to know the list of Observable objects, where a change to any of them may be considered an update to its elements. The call() method of the Callback object fulfills this requirement. The list passes every element to the call() method. The call() method returns an array of Observable. The list watches for any changes to the elements of the Observable array. When it detects a change, it notifies its change listeners that its element associated with the Observable array has been updated. The reason this parameter is named extractor is that it extracts an array of Observable for an element of a list.

Listing 3-4 shows how to create an ObservableList that can notify its change listeners when its elements are updated.
// ListUpdateTest.java
package com.jdojo.collections;
import java.util.List;
import javafx.beans.Observable;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.util.Callback;
public class ListUpdateTest {
        public static void main(String[] args) {
            // Create an extractor for IntegerProperty.
            Callback<IntegerProperty, Observable[]> extractor =
                    (IntegerProperty p) -> {
                   // Print a message to know when it is called
                   System.out.println("The extractor is called for " + p);
                   // Wrap the parameter in an Observable[] and return it
                   return new Observable[]{p};
               };
            // Create an empty observable list with a callback to
            // extract the observable values for each element of the list
            ObservableList<IntegerProperty> list =
                FXCollections.observableArrayList(extractor);
            // Add two elements to the list
            System.out.println("Before adding two elements...");
            IntegerProperty p1 = new SimpleIntegerProperty(10);
            IntegerProperty p2 = new SimpleIntegerProperty(20);
            list.addAll(p1, p2); // Will call the call() method of the
                          // extractor - once for p1 and once for p2.
            System.out.println("After adding two elements...");
            // Add a change listener to the list
            list.addListener(ListUpdateTest::onChanged);
            // Update p1 from 10 to 100, which will trigger
            // an update change for the list
            p1.set(100);
        }
        public static void onChanged(
               ListChangeListener.Change<? extends IntegerProperty>
                    change) {
            System.out.println("List is " + change.getList());
            // Work on only updates to the list
            while (change.next()) {
                if (change.wasUpdated()) {
                    // Print the details of the update
                    System.out.println("An update is detected.");
                    int start = change.getFrom();
                    int end = change.getTo();
                    System.out.println("Updated range: [" + start +
                               ", " + end + "]");
                    List<? extends IntegerProperty> updatedElementsList;
                    updatedElementsList =
                               change.getList().subList(start, end);
                    System.out.println("Updated elements: " +
                                updatedElementsList);
                }
            }
        }
}
Before adding two elements...
The extractor is called for IntegerProperty [value: 10]
The extractor is called for IntegerProperty [value: 20]
After adding two elements...
List is [IntegerProperty [value: 100], IntegerProperty [value: 20]]
An update is detected.
Updated range: [0, 1]
Updated elements: [IntegerProperty [value: 100]]
Listing 3-4

Observing a List for Updates of Its Elements

The main() method of the ListUpdateTest class creates an extractor that is an object of the Callback<IntegerProperty, Observable[]> interface. The call() method takes an IntegerProperty argument and returns the same by wrapping it in an Observable array. It also prints the object that is passed to it.

The extractor is used to create an ObservableList. Two IntegerProperty objects are added to the list. When the objects are being added, the call() method of the extractor is called with the object being added as its argument. This is evident from the output. The call() method returns the object being added. This means that the list will watch for any changes to the object (the IntegerProperty) and notify its change listeners of the same.

A change listener is added to the list. It handles only updates to the list. At the end, you change the value for the first element of the list from 10 to 100 to trigger an update change notification.

A Complete Example of Observing an ObservableList for Changes

This section provides a complete example that shows how to handle the different kinds of changes to an ObservableList.

Our starting point is a Person class as shown in Listing 3-5. Here, you will work with an ObservableList of Person objects. The Person class has two properties: firstName and lastName. Both properties are of the StringProperty type. Its compareTo() method is implemented to sort Person objects in ascending order by the first name then by the last name. Its toString() method prints the first name, a space, and the last name.
// Person.java
package com.jdojo.collections;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Person implements Comparable<Person> {
        private StringProperty firstName = new SimpleStringProperty();
        private StringProperty lastName = new SimpleStringProperty();
        public Person() {
                this.setFirstName("Unknown");
                this.setLastName("Unknown");
        }
        public Person(String firstName, String lastName) {
                this.setFirstName(firstName);
                this.setLastName(lastName);
        }
        // Complete listing part of the example sources download for
        // the book
        ...
}
Listing 3-5

A Person Class with Two Properties Named firstName and lastName

The PersonListChangeListener class , as shown in Listing 3-6, is a change listener class. It implements the onChanged() method of the ListChangeListener interface to handle all types of change notifications for an ObservableList of Person objects.
// PersonListChangeListener.java
// Listing part of the example sources download for the book
Listing 3-6

A Change Listener for an ObservableList of Person Objects

The ListChangeTest class , as shown in Listing 3-7, is a test class. It creates an ObservableList with an extractor. The extractor returns an array of firstName and lastName properties of a Person object. That means when one of these properties is changed, a Person object as an element of the list is considered updated, and an update notification will be sent to all change listeners. It adds a change listener to the list. Finally, it makes several kinds of changes to the list to trigger change notifications. The details of a change notification are printed on the standard output.

This completes one of the most complex discussions about writing a change listener for an ObservableList. Aren’t you glad that JavaFX designers didn’t make it more complex?
// ListChangeTest.java
// Listing part of the example sources download for the book
Before adding Li Na: []
Change Type: Added
Added Size: 1
Added Range: [0, 1]
Added List: [Li Na]
After adding Li Na: [Li Na]
Before adding Vivi Gin and Li He: [Li Na]
Change Type: Added
Added Size: 2
Added Range: [1, 3]
Added List: [Vivi Gin, Li He]
After adding Vivi Gin and Li He: [Li Na, Vivi Gin, Li He]
Before sorting the list:[Li Na, Vivi Gin, Li He]
Change Type: Permutated
Permutated Range: [0, 3]
index[0] moved to index[1]
index[1] moved to index[2]
index[2] moved to index[0]
After sorting the list:[Li He, Li Na, Vivi Gin]
Before updating Li Na: [Li He, Li Na, Vivi Gin]
Change Type: Updated
Updated Range : [1, 2]
Updated elements are: [Li Smith]
After updating Li Smith: [Li He, Li Smith, Vivi Gin]
Before replacing Li He with Simon Ng: [Li He, Li Smith, Vivi Gin]
Change Type: Replaced
Change Type: Removed
Removed Size: 1
Removed Range: [0, 1]
Removed List: [Li He]
Change Type: Added
Added Size: 1
Added Range: [0, 1]
Added List: [Simon Ng]
After replacing Li He with Simon Ng: [Simon Ng, Li Smith, Vivi Gin]
Before setAll(): [Simon Ng, Li Smith, Vivi Gin]
Change Type: Replaced
Change Type: Removed
Removed Size: 3
Removed Range: [0, 3]
Removed List: [Simon Ng, Li Smith, Vivi Gin]
Change Type: Added
Added Size: 3
Added Range: [0, 3]
Added List: [Lia Li, Liz Na, Li Ho]
After setAll(): [Lia Li, Liz Na, Li Ho]
Before removeAll(): [Lia Li, Liz Na, Li Ho]
Change Type: Removed
Removed Size: 1
Removed Range: [0, 0]
Removed List: [Lia Li]
Change Type: Removed
Removed Size: 1
Removed Range: [1, 1]
Removed List: [Li Ho]
After removeAll(): [Liz Na]
Listing 3-7

Testing an ObservableList of Person Objects for All Types of Changes

Understanding ObservableSet

If you survived learning the ObservableList and list change listeners, learning about the ObservableSet will be easy! Figure 3-3 shows the class diagram for the ObservableSet interface .
Figure 3-3

A class diagram for the ObservableSet interface

It inherits from the Set and Observable interfaces. It supports invalidation and change notifications, and it inherits the methods for the invalidation notification support from the Observable interface. It adds the following two methods to support change notifications:
  • void addListener(SetChangeListener<? super E> listener)

  • void removeListener(SetChangeListener<? super E> listener)

An instance of the SetChangeListener interface listens for changes in an ObservableSet. It declares a static inner class named Change, which represents a report of changes in an ObservableSet.

Note

A set is an unordered collection. This section shows the elements of several sets in outputs. You may get a different output showing the elements of sets in a different order than shown in those examples.

Creating an ObservableSet

You need to use one of the following factory methods of the FXCollections class to create an ObservableSet :
  • <E> ObservableSet<E> observableSet(E... elements)

  • <E> ObservableSet<E> observableSet(Set<E> set)

  • <E> ObservableSet<E> emptyObservableSet()

Since working with observable sets does not differ much from working with observable lists, we do not further investigate on this topic. You can consult the API documentation and the example classes in the com.jdojo.collections package to learn more about observable sets.

Understanding ObservableMap

Figure 3-4 shows the class diagram for the ObservableMap interface . It inherits from the Map and Observable interfaces. It supports invalidation and change notifications. It inherits the methods for the invalidation notification support from the Observable interface, and it adds the following two methods to support change notifications:
  • void addListener(MapChangeListener<? super K, ? super V> listener)

  • void removeListener(MapChangeListener<? super K, ? super V> listener)

Figure 3-4

A class diagram for the ObservableMap interface

An instance of the MapChangeListener interface listens for changes in an ObservableMap. It declares a static inner class named Change, which represents a report of changes in an ObservableMap.

Creating an ObservableMap

You need to use one of the following factory methods of the FXCollections class to create an ObservableMap:
  • <K,V> ObservableMap<K, V> observableHashMap()

  • <K,V> ObservableMap<K, V> observableMap(Map<K, V> map)

  • <K,V> ObservableMap<K,V> emptyObservableMap()

The first method creates an empty observable map that is backed by a HashMap. The second method creates an ObservableMap that is backed by the specified map. Mutations performed on the ObservableMap are reported to the listeners. Mutations performed directly on the backing map are not reported to the listeners. The third method creates an empty unmodifiable observable map. Listing 3-8 shows how to create ObservableMaps .
// ObservableMapTest.java
package com.jdojo.collections;
import java.util.HashMap;
import java.util.Map;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
public class ObservableMapTest {
        public static void main(String[] args) {
            ObservableMap<String, Integer> map1 =
                    FXCollections.observableHashMap();
            map1.put("one", 1);
            map1.put("two", 2);
            System.out.println("Map 1: " + map1);
            Map<String, Integer> backingMap = new HashMap<>();
            backingMap.put("ten", 10);
            backingMap.put("twenty", 20);
            ObservableMap<String, Integer> map2 =
                    FXCollections.observableMap(backingMap);
            System.out.println("Map 2: " + map2);
        }
}
Map 1: {two=2, one=1}
Map 2: {ten=10, twenty=20}
Listing 3-8

Creating ObservableMaps

Since working with observable maps does not differ much from working with observable lists and sets, we do not further investigate on this topic. You can consult the API documentation and the example classes in the com.jdojo.collections package to learn more about observable maps.

Properties and Bindings for JavaFX Collections

The ObservableList, ObservableSet, and ObservableMap collections can be exposed as Property objects. They also support bindings using high-level and low-level binding APIs. Property objects representing single values were discussed in Chapter 2. Make sure you have read that chapter before proceeding in this section.

Understanding ObservableList Property and Binding

Figure 3-5 shows a partial class diagram for the ListProperty class. The ListProperty class implements the ObservableValue and ObservableList interfaces. It is an observable value in the sense that it wraps the reference of an ObservableList. Implementing the ObservableList interface makes all of its methods available to a ListProperty object. Calling methods of the ObservableList on a ListProperty has the same effect as if they were called on the wrapped ObservableList.
Figure 3-5

A partial class diagram for the ListProperty class

You can use one of the following constructors of the SimpleListProperty class to create an instance of the ListProperty:
  • SimpleListProperty()

  • SimpleListProperty(ObservableList<E> initialValue)

  • SimpleListProperty(Object bean, String name)

  • SimpleListProperty(Object bean, String name, ObservableList<E> initialValue)

One of the common mistakes in using the ListProperty class is not passing an ObservableList to its constructor before using it. A ListProperty must have a reference to an ObservableList before you can perform a meaningful operation on it. If you do not use an ObservableList to create a ListProperty object, you can use its set() method to set the reference of an ObservableList. The following snippet of code generates an exception:
ListProperty<String> lp = new SimpleListProperty<String>();
// No ObservableList to work with. Generates an exception.
lp.add("Hello");
Exception in thread "main" java.lang.UnsupportedOperationException
        at java.util.AbstractList.add(AbstractList.java:148)
        at java.util.AbstractList.add(AbstractList.java:108)
        at javafx.beans.binding.ListExpression.add(ListExpression.java:262)
Tip

Operations performed on a ListProperty that wraps a null reference are treated as if the operations were performed on an immutable empty ObservableList.

The following snippet of code shows how to create and initialize a ListProperty before using it:
ObservableList<String> list1 = FXCollections.observableArrayList();
ListProperty<String> lp1 = new SimpleListProperty<String>(list1);
lp1.add("Hello");
ListProperty<String> lp2 = new SimpleListProperty<String>();
lp2.set(FXCollections.observableArrayList());
lp2.add("Hello");

Observing a ListProperty for Changes

You can attach three types of listeners to a ListProperty :
  • An InvalidationListener

  • A ChangeListener

  • A ListChangeListener

All three listeners are notified when the reference of the ObservableList, which is wrapped in the ListProperty, changes or the content of the ObservableList changes. When the content of the list changes, the changed() method of ChangeListeners receives the reference to the same list as the old and new value. If the wrapped reference of the ObservableList is replaced with a new one, this method receives references of the old list and the new list. To handle the list change events, please refer to the “Observing an ObservableList for Changes” section in this chapter.

The program in Listing 3-9 shows how to handle all three types of changes to a ListProperty. The list change listener handles the changes to the content of the list in a brief and generic way. Please refer to the “Observing an ObservableList for Changes” section in this chapter on how to handle the content change events for an ObservableList in detail.
// ListPropertyTest.java
// Listing part of the example sources download for the book
Before addAll()
List property is invalid.
List Property has changed. Old List: [one, two, three], New List: [one, two, three]
Action taken on the list: Added. Removed: [], Added: [one, two, three]
After addAll()
Before set()
List property is invalid.
List Property has changed. Old List: [one, two, three], New List: [two, three]
Action taken on the list: Replaced. Removed: [one, two, three], Added: [two, three]
After set()
Before remove()
List property is invalid .
List Property has changed. Old List: [three], New List: [three]
Action taken on the list: Removed. Removed: [two], Added: []
After remove()
Listing 3-9

Adding Invalidation, Change, and List Change Listeners to a ListProperty

Binding the size and empty Properties of a ListProperty

A ListProperty exposes two properties, size and empty, which are of type ReadOnlyIntegerProperty and ReadOnlyBooleanProperty, respectively. You can access them using the sizeProperty() and emptyProperty() methods. The size and empty properties are useful for binding in GUI applications. For example, the model in a GUI application may be backed by a ListProperty, and you can bind these properties to the text property of a label on the screen. When the data changes in the model, the label will be updated automatically through binding. The size and empty properties are declared in the ListExpression class.

The program in Listing 3-10 shows how to use the size and empty properties. It uses the asString() method of the ListExpression class to convert the content of the wrapped ObservableList to a String.
// ListBindingTest.java
package com.jdojo.collections;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
public class ListBindingTest {
        public static void main(String[] args) {
            ListProperty<String> lp =
                        new SimpleListProperty<>(FXCollections.observableArrayList());
            // Bind the size and empty properties of the ListProperty
            // to create a description of the list
            StringProperty initStr = new SimpleStringProperty("Size: " );
            StringProperty desc = new SimpleStringProperty();
            desc.bind(initStr.concat(lp.sizeProperty())
                             .concat(", Empty: ")
                             .concat(lp.emptyProperty())
                             .concat(", List: ")
                             .concat(lp.asString()));
            System.out.println("Before addAll(): " + desc.get());
            lp.addAll("John", "Jacobs");
            System.out.println("After addAll(): " + desc.get());
        }
}
Before addAll(): Size: 0, Empty: true, List: []
After addAll(): Size: 2, Empty: false, List: [John, Jacobs]
Listing 3-10

Using the size and empty Properties of a ListProperty Object

Binding to List Properties and Content

Methods to support high-level binding for a list property are in the ListExpression and Bindings classes. Low-level binding can be created by subclassing the ListBinding class. A ListProperty supports two types of bindings:
  • Binding the reference of the ObservableList that it wraps

  • Binding the content of the ObservableList that it wraps

The bind() and bindBidirectional() methods are used to create the first kind of binding. The program in Listing 3-11 shows how to use these methods. As shown in the following output, notice that both list properties have the reference of the same ObservableList after binding.
// BindingListReference.java
package com.jdojo.collections;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
public class BindingListReference {
        public static void main(String[] args) {
            ListProperty<String> lp1 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());
            ListProperty<String> lp2 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());
            lp1.bind(lp2);
            print("Before addAll():", lp1, lp2);
            lp1.addAll("One", "Two");
            print("After addAll():", lp1, lp2);
            // Change the reference of the ObservableList in lp2
            lp2.set(FXCollections.observableArrayList("1", "2"));
            print("After lp2.set():", lp1, lp2);
            // Cannot do the following as lp1 is a bound property
            // lp1.set(FXCollections.observableArrayList("1", "2"));
            // Unbind lp1
            lp1.unbind();
            print("After unbind():", lp1, lp2);
            // Bind lp1 and lp2 bidirectionally
            lp1.bindBidirectional(lp2);
            print("After bindBidirectional():", lp1, lp2);
            lp1.set(FXCollections.observableArrayList("X", "Y"));
            print("After lp1.set():", lp1, lp2);
        }
        public static void print(String msg, ListProperty<String> lp1,
                   ListProperty<String> lp2) {
            System.out.println(msg);
            System.out.println("lp1: " + lp1.get() + ", lp2: " +
                    lp2.get() + ", lp1.get() == lp2.get(): " +
                    (lp1.get() == lp2.get()));
            System.out.println("---------------------------");
        }
}
Before addAll():
lp1: [], lp2: [], lp1.get() == lp2.get(): true
---------------------------
After addAll():
lp1: [One, Two], lp2: [One, Two], lp1.get() == lp2.get(): true
---------------------------
After lp2.set():
lp1: [1, 2], lp2: [1, 2], lp1.get() == lp2.get(): true
---------------------------
After unbind():
lp1: [1, 2], lp2: [1, 2], lp1.get() == lp2.get(): true
---------------------------
After bindBidirectional():
lp1: [1, 2], lp2: [1, 2], lp1.get() == lp2.get(): true
---------------------------
After lp1.set():
lp1: [X, Y], lp2: [X, Y], lp1.get() == lp2.get(): true
---------------------------
Listing 3-11

Binding the References of List Properties

The bindContent() and bindContentBidirectional() methods let you bind the content of the ObservableList that is wrapped in a ListProperty to the content of another ObservableList in one direction and both directions, respectively. Make sure to use the corresponding methods, unbindContent() and unbindContentBidirectional(), to unbind contents of two observable lists.

Tip

You can also use methods of the Bindings class to create bindings for references and contents of observable lists.

It is allowed, but not advisable, to change the content of a ListProperty whose content has been bound to another ObservableList. In such cases, the bound ListProperty will not be synchronized with its target list. Listing 3-12 shows examples of both types of content binding.
// BindingListContent.java
package com.jdojo.collections;
import javafx.beans.property.ListProperty;
import javafx.beans.property.SimpleListProperty;
import javafx.collections.FXCollections;
public class BindingListContent {
        public static void main(String[] args) {
            ListProperty<String> lp1 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());
            ListProperty<String> lp2 =
                new SimpleListProperty<>(
                         FXCollections.observableArrayList());
            // Bind the content of lp1 to the content of lp2
            lp1.bindContent(lp2);
            /* At this point, you can change the content of lp1. However,
             * that will defeat the purpose of content binding, because
             * the content of lp1 is no longer in sync with the content of
             * lp2.
             * Do not do this:
             * lp1.addAll("X", "Y");
             */
            print("Before lp2.addAll():", lp1, lp2);
            lp2.addAll("1", "2");
            print("After lp2.addAll():", lp1, lp2);
            lp1.unbindContent(lp2);
            print("After lp1.unbindContent(lp2):", lp1, lp2);
            // Bind lp1 and lp2 contents bidirectionally
            lp1.bindContentBidirectional(lp2);
            print("Before lp1.addAll():", lp1, lp2);
            lp1.addAll("3", "4");
            print("After lp1.addAll():", lp1, lp2);
            print("Before lp2.addAll():", lp1, lp2);
            lp2.addAll("5", "6");
            print("After lp2.addAll():", lp1, lp2);
        }
        public static void print(String msg, ListProperty<String> lp1,
                   ListProperty<String> lp2) {
            System.out.println(msg + " lp1: " + lp1.get() +
                    ", lp2: " + lp2.get());
        }
}
Before lp2.addAll(): lp1: [], lp2: []
After lp2.addAll(): lp1: [1, 2], lp2: [1, 2]
After lp1.unbindContent(lp2): lp1: [1, 2], lp2: [1, 2]
Before lp1.addAll(): lp1: [1, 2], lp2: [1, 2]
After lp1.addAll(): lp1: [1, 2, 3, 4], lp2: [1, 2, 3, 4]
Before lp2.addAll(): lp1: [1, 2, 3, 4], lp2: [1, 2, 3, 4]
After lp2.addAll(): lp1: [1, 2, 3, 4, 5, 6], lp2: [1, 2, 3, 4, 5, 6]
Listing 3-12

Binding Contents of List Properties

Binding to Elements of a List

ListProperty provides so many useful features that I can keep discussing this topic for at least 50 more pages! I will wrap this topic up with one more example.

It is possible to bind to a specific element of the ObservableList wrapped in a ListProperty using one of the following methods of the ListExpression class:
  • ObjectBinding<E> valueAt(int index)

  • ObjectBinding<E> valueAt(ObservableIntegerValue index)

The first version of the method creates an ObjectBinding to an element in the list at a specific index. The second version of the method takes an index as an argument, which is an ObservableIntegerValue that can change over time. When the bound index in the valueAt() method is outside the list range, the ObjectBinding contains null.

Let’s use the second version of the method to create a binding that will bind to the last element of a list. Here, you can make use of the size property of the ListProperty in creating the binding expression. The program in Listing 3-13 shows how to use the valueAt() method .
// BindingToListElements.java
// Listing part of the example sources download for the book
List:[], Last Value: null
List:[John], Last Value: John
List:[John, Donna, Geshan], Last Value: Geshan
List:[John, Donna], Last Value: Donna
List:[], Last Value: null
Listing 3-13

Binding to the Elements of a List

Understanding ObservableSet Property and Binding

A SetProperty object wraps an ObservableSet. Working with a SetProperty is very similar to working with a ListProperty. I am not going to repeat what has been discussed in the previous sections about properties and bindings of an ObservableList. The same discussions apply to properties and bindings of ObservableSet. The following are the salient points to remember while working with a SetProperty:
  • The class diagram for the SetProperty class is similar to the one shown in Figure 3-5 for the ListProperty class. You need to replace the word “List” with the word “Set” in all names.

  • The SetExpression and Bindings classes contain methods to support high-level bindings for set properties. You need to subclass the SetBinding class to create low-level bindings.

  • Like the ListProperty, the SetProperty exposes the size and empty properties .

  • Like the ListProperty, the SetProperty supports bindings of the reference and the content of the ObservableSet that it wraps.

  • Like the ListProperty, the SetProperty supports three types of notifications: invalidation notifications, change notifications, and set change notifications.

  • Unlike a list, a set is an unordered collection of items. Its elements do not have indexes. It does not support binding to its specific elements. Therefore, the SetExpression class does not contain a method like valueAt() as the ListExpression class does.

You can use one of the following constructors of the SimpleSetProperty class to create an instance of the SetProperty:
  • SimpleSetProperty()

  • SimpleSetProperty(ObservableSet<E> initialValue)

  • SimpleSetProperty(Object bean, String name)

  • SimpleSetProperty(Object bean, String name, ObservableSet<E> initialValue)

The following snippet of code creates an instance of the SetProperty and adds two elements to the ObservableSet that the property wraps. In the end, it gets the reference of the ObservableSet from the property object using the get() method :
// Create a SetProperty object
SetProperty<String> sp = new SimpleSetProperty<String>(FXCollections.observableSet());
// Add two elements to the wrapped ObservableSet
sp.add("one");
sp.add("two");
// Get the wrapped set from the sp property
ObservableSet<String> set = sp.get();
The program in Listing 3-14 demonstrates how to use binding with SetProperty objects .
// SetBindingTest.java
// Listing part of the example sources download for the book
Before sp1.add(): Size: 0, Empty: true, Set: []
After sp1.add(): Size: 2, Empty: false, Set: [Jacobs, John]
Called sp1.bindContent(sp2)...
Before sp2.add(): sp1: [], sp2: []
After sp2.add(): sp1: [1], sp2: [1]
After sp1.unbindContent(sp2): sp1: [1], sp2: [1]
Before sp2.add(): sp1: [1], sp2: [1]
After sp2.add(): sp1: [1, 2], sp2: [2, 1]
Listing 3-14

Using Properties and Bindings for Observable Sets

Understanding ObservableMap Property and Binding

A MapProperty object wraps an ObservableMap. Working with a MapProperty is very similar to working with a ListProperty. I am not going to repeat what has been discussed in the previous sections about properties and bindings of an ObservableList. The same discussions apply to properties and bindings of ObservableMap. The following are the salient points to remember while working with a MapProperty:
  • The class diagram for the MapProperty class is similar to the one shown in Figure 3-5 for the ListProperty class. You need to replace the word “List” with the word “Map” in all names and the generic type parameter <E> with <K, V>, where K and V stand for the key type and value type, respectively, of entries in the map.

  • The MapExpression and Bindings classes contain methods to support high-level bindings for map properties. You need to subclass the MapBinding class to create low-level bindings.

  • Like the ListProperty, the MapProperty exposes size and empty properties .

  • Like the ListProperty, the MapProperty supports bindings of the reference and the content of the ObservableMap that it wraps.

  • Like the ListProperty, the MapProperty supports three types of notifications: invalidation notifications, change notifications, and map change notifications.

  • The MapProperty supports binding to the value of a specific key using its valueAt() method.

Use one of the following constructors of the SimpleMapProperty class to create an instance of the MapProperty:
  • SimpleMapProperty()

  • SimpleMapProperty(Object bean, String name)

  • SimpleMapProperty(Object bean, String name, ObservableMap<K,V> initialValue)

  • SimpleMapProperty(ObservableMap<K,V> initialValue)

The following snippet of code creates an instance of the MapProperty and adds two entries. In the end, it gets the reference of the wrapped ObservableMap using the get() method :
// Create a MapProperty object
MapProperty<String, Double> mp =
        new SimpleMapProperty<String, Double>(FXCollections.observableHashMap());
// Add two entries to the wrapped ObservableMap
mp.put("Ken", 8190.20);
mp.put("Jim", 8990.90);
// Get the wrapped map from the mp property
ObservableMap<String, Double> map = mp.get();
The program in Listing 3-15 shows how to use binding with MapProperty objects. It shows the content binding between two maps. You can also use unidirectional and bidirectional simple binding between two map properties to bind the references of the maps they wrap.
// MapBindingTest.java
// Listing part of the example sources download for the book
Ken Salary: null
Before mp1.put(): Size: 0, Empty: true, Map: {}, Ken Salary: null
After mp1.put(): Size: 3, Empty: false, Map: {Jim=9800.8, Lee=6000.2, Ken=7890.9}, Ken Salary: 7890.9
Called mp1.bindContent(mp2)...
Before mp2.put(): Size: 0, Empty: true, Map: {}, Ken Salary: null
After mp2.put(): Size: 2, Empty: false, Map: {Cindy=7800.2, Ken=7500.9}, Ken Salary: 7500.9
Listing 3-15

Using Properties and Bindings for Observable Maps

Summary

JavaFX extends the collections framework in Java by adding support for observable lists, sets, and maps that are called observable collections. An observable collection is a list, set, or map that may be observed for invalidation and content changes. Instances of the ObservableList, ObservableSet, and ObservableMap interfaces in the javafx.collections package represent observable interfaces in JavaFX. You can add invalidation and change listeners to instances of these observable collections.

The FXCollections class is a utility class to work with JavaFX collections. It consists of all static methods. JavaFX does not expose the implementation classes of observable lists, sets, and maps. You need to use one of the factory methods in the FXCollections class to create objects of the ObservableList, ObservableSet, and ObservableMap interfaces.

The JavaFX library provides two classes named FilteredList and SortedList that are in the javafx.collections.transformation package. A FilteredList is an ObservableList that filters its contents using a specified Predicate. A SortedList sorts its contents.

The next chapter will discuss how to create and customize stages in JavaFX applications.

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

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