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:
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...
}
}
}
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 TreeItem
s and TreeCell
s: the TreeView
will create only a small number of TreeCell
s (even if the tree has a very large number of items). The TreeCell
s are reused to display different TreeItem
s, 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 TreeItem
s are relatively lightweight; often they are just simple wrappers for String
s. So this mechanism allows to have TreeView
s 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.