Search code examples
javafxtreeviewjavafx-2custom-cell

Provide different TreeCell conditionally in CellFactory


I have a treeview in which the cells must display different information according to the real implementation of the TreeItem's value.

My domain model looks like:

Domain model

It seemed natural to me to split the behaviour of "how to display a Task in a cell" or "how to display a Group in a cell" in two different classes.

public abstract class ComponentTreeCell<T extends Component> extends TreeCell<T>
{
    @Override
    protected void updateItem(T item, boolean empty)
    {
        //Some common logic...
    }
}

public class GroupTreeCell extends ComponentTreeCell<Group>
{
    @Override
    protected void updateItem(Group item, boolean empty)
    {
        super.updateItem(item, empty);

        //Some group-specific-logic
    }    
}

public class TaskTreeCell extends ComponentTreeCell<Task>
{
    @Override
    protected void updateItem(Task item, boolean empty)
    {
        super.updateItem(item, empty);

        //Some task-specific-logic
    }    
}

The following controller class contains a TreeView where I set the CellFactory.

public class Controller implements Initializable
{
    @FXML
    private TreeView<Component> treeview;

    @Override
    public void initialize(URL url, ResourceBundle bundle)
    {
        treeview.setCellFactory(new Callback<TreeView<Component>, TreeCell<Component>>()
        {
            @Override
            public TreeCell<Component> call(TreeView<Component> arg0)
            {
                if(/* stuck */ instanceof Group)
                {
                    return new GroupTreeCell();
                }
                else if(/* stuck */ instanceof Task)
                {
                    return new TaskTreeCell();
                }
                else
                {
                    return new DefaultTreeCell();
                }
            }
        });
    }
}

But here I get stuck at the moment to decide which kind of cell I have to return. Indeed I only have in parameter the related TreeView and not the related TreeItem !

It seems to me like a kind of weakness of JavaFX. Why JavaFX gives the full TreeView to the user when you only need to retrieve one TreeCell ??

Is there a way to do it in this manner or do I have to implement the 2 different behaviour in the same custom TreeCell implementation ?

public class ComponentTreeCell extends TreeCell<Component>
{
        @Override
        protected void updateItem(Component item, boolean empty)
        {
            //Some common logic...

            if(item instanceof Group)
            {
                //Group-specific logic...
            }
            else if(item instanceof Task)
            {
                //Task-specific logic...
            }
            else
            {
                //Default logic...
            }
       }
}

Solution

  • Why JavaFX gives the full TreeView to the user when you only need to retrieve one TreeCell ??

    Because there isn't a 1-1 relationship between TreeItems and TreeCells: the TreeView will create only a small number of TreeCells (even if the tree has a very large number of items). The TreeCells are reused to display different TreeItems, for example if some nodes are expanded/collapsed, or if the user scrolls.

    It is done this way for performance. The actual cells that provide the rendering are quite large objects: they are UI components and carry CSS styling etc with them. The actual data displayed, i.e. the TreeItems are relatively lightweight; often they are just simple wrappers for Strings. So this mechanism allows to have TreeViews with huge amounts of data that do not impose a huge burden on performance. Creating a TreeCell for every TreeItem would not allow this.

    Because of this, the TreeCell you provide from the factory has to be able to handle any TreeItem that might be given to it by the TreeView. For example, as the user changes the items that are displayed (by expanding/collapsing or by scrolling), a TreeCell instance that was previously displaying a Task might be used to display a Group. This is the purpose of the updateItem(...) method; it is called when the TreeCell is reused.

    This means your setup simply won't work. You basically need the TreeCell<Component> implementation in your last code example. You could of course factor the configuration into separate classes if you like, something like:

    public class ComponentTreeCell extends TreeCell<Component>
    {
        @Override
        protected void updateItem(Component item, boolean empty)
        {
            super.updateItem(item, empty);
            CellConfiguratorFactory.getConfigurator(item).configure(this, item, empty);
        }
    
    }
    

    Factory:

    public class CellConfiguratorFactory {
    
        private static CellConfigurator<Task> taskCellConfigurator ;
        private static CellConfigurator<Group> groupCellConfigurator ;
        private static CellConfigurator<Component> defaultCellConfigurator ;
    
        private static CellConfigurator getConfigurator(Component item) {
            if (item instanceof Task) {
                if (taskCellConfigurator == null) {
                    taskCellConfigurator = new TaskCellConfigurator();
                }
                return taskCellConfigurator ;
            } else if (item instanceof Group) {
                if (groupCellConfigurator == null) {
                    groupCellConfigurator = new GroupCellConfigurator();
                }
                return groupCellConfigurator ;
            } else {
                if (defaultCellConfigurator == null) {
                    defaultCellConfigurator = new DefaultCellConfigurator();
                }
                return defaultCellConfigurator ;
            }
        }
    }
    

    (Note you can afford to assume the factory is only ever used from a single thread, since everything will happen on the FX Application Thread.)

    And then

    public interface CellConfigurator<T extends Component> {
        public void configureCell(ComponentTreeCell cell, T item, boolean empty);
    }
    

    with, for example,

    public class TaskCellConfigurator implements CellConfigurator<Task> {
        public void configureCell(TreeCell<Component> cell, Task item, boolean empty) {
            // task-specific implementation...
        }
    }
    

    However, I'm not sure the additional structure is worth the effort here.