/heterogeneous-tree-example

Example of a JavaFX tree view that contains heterogeneous (but related) data.

Primary LanguageJava

heterogeneous-tree-example

Example of a JavaFX tree view that contains heterogeneous (but related) data.

A JavaFX TreeView contains TreeItems that are all of the same type (it is homogeneous). Typically, an object model that lends itself to a tree representation will comprise multiple different classes: in this example a Company has a collection of Departments, each of which has a collection of Employees, each of which has a collection of Roles.

The simple way to represent all of these in a TreeView is, of course, to make each class a subclass or implementation of a common superclass or interface. In this case we use EmploymentUnit as the common superclass. Typically this will result in lots of switching on type (using instanceof tests) when displaying the tree items and calculating the child nodes. By careful use of generics, we can allow each class to use the same method signature for representing it's child objects, and use listeners on the list of children to maintain the view state (i.e. the TreeItems) if the underlying model changes.

This example uses a ModelTree class to map the model into a TreeView. The ModelTree constructor takes a number of functions that describe how to map a model instance to its list of children, to an observable string for display in the cell, and (optionally) to CSS PseudoClass instances to allow for different types to be displayed in different styles. The model used here lends itself to easy provision of these functions by method references, but less amenable models should not cause too much trouble.

For example, if the model were implemented with naïve, unrelated classes

public class Company {
    private final ObservableList<Department> departments = FXCollections.observableArrayList();
    public ObservableList<Department> getDepartments() {
        return departments ;
    }
    
    // ...
}

public class Department {
    private final ObservableList<Employee> employees = FXCollections.observableArrayList();
    public ObservableList<Employee> getEmployees() {
        return employees ;
    }
    
    // ...
}

then (by nature of the homogeneity of TreeItem, where the type of the children list is the same as the type of value), you would necessarily have to test for types and downcast somewhere. This at least allows you to encapsulate that as a strategy:

Function<Object, ObservableList<? extends Object>> childNodeGenerator = obj -> {
    if (obj instanceof Company) {
        return ((Company)obj).getDepartments();
    }
    if (obj instanceof Department) {
        return ((Department)obj).getEmployees();
    }
    // ...
    return FXCollections.emptyObservableList();
};
ModelTree<Object> tree = new ModelTree(new Company(...), childNodeGenerator, 
    obj -> new ReadOnlyStringWrapper(obj.toString()));

In such cases you could also consider creating an interface with an appropriate getChildren() method and using an adapter for each of the individual classes.

Obvious enhancements could be made to, for example, allow editing in the tree.