This article will cover a method that can be used to include more than one entity type in a tree (the same system will work for other widgets such as lists and tables) by using a proxy holder. The previous articles in this series covered setting up the demo application and getting started with JPA.
In the SampleFX4 demo there are two types of object that need to be included in the tree: Department
and Person
. With a simple tree such as the one shown in the official JavaFX tutorial it is possible to build the tree from TreeItem<String>
objects. This is all well and good if you only want the tree to display data but if you want to manipulate that data in some way or use the tree for item selection you need some way to link the TreeItem
back to the underlying item in your data model. Personally, I don’t think the official demo is terribly good, it purports to show editing of tree items (the names of employees) but those edits aren’t reflected in the actual Employee
objects that were used to build the tree*. If the change isn’t reflected in the underlying object it’s not really an edit of the data it’s just a fancy text box that temporarily replaced a tree node.
To actually support real editing of items either the entity itself needs to be held by the TreeItem
or some proxy for that item needs to be held. The job of the proxy is to provide the information required to find the real entity so that any changes can be made on it but at this point you’re probably wondering why you need a proxy at all. The problem is one of generics, the TreeView
takes a type which is the type of the TreeItem
nodes that build the tree. There can be only a single type so the whole tree must be built of that one node type. If you have a simple tree, lets say one that represents a management hierarchy, then it’s possible that all the nodes will be of the Employee
type from the root down. In this case you can make the generic type Employee
and access the data directly. In my experience though trees almost never have a single type of data in them, even the official demo shows two types although it does nothing to address this. The solution is to build the tree from proxy objects that in some way point to the real object.
There are two options regarding how proxy objects point to the real entity they represent. The first is to wrap the actual object in the proxy so it’s just a matter of calling getValue
on the proxy and casting the result to get the real object the other is to have the proxy hold just the information required for display and finding the real object. I strongly favour the second option for a number of reasons.
Having the proxy hold just the information required to for display and finding the real entity is beneficial because it means the proxy is very lightweight, it’s carrying around the bare minimum necessary to get the job done which means a low memory footprint. Conversely, holding the actual object may mean a large memory footprint which could impact the performance of the tree or even the whole application. Secondly, at some point you have to recover the type of the entity being represented by the proxy. If you hold the real object in the proxy your going to have to cast the object you get back at some point. You might think that you can use generics in the proxy or subclass the proxy to get the real type without a cast but it won’t work, you’ll always end up with a cast somewhere. The alternative is to hold a just a key and a type in the proxy and use that to look up the type, it’s essentially still a cast but you aren’t carrying around the baggage of the real object.
With that all said here is the proxy class that the demo application uses:
public class EntityProxy { private long id; private String label; private EntityProxyType type; public EntityProxy(long id, EntityProxyType type, String label) { this.id = id; this.type = type; this.label = label; } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getLabel() { return label; } public void setLabel(String label) { this.label = label; } public EntityProxyType getType() { return type; } public void setType(EntityProxyType type) { this.type = type; } @Override public String toString() { return getLabel(); } }
As you can see it really couldn’t be simpler, it holds the unique id of the entity, it’s type and the label that should be displayed in the TreeView
. The type is an enumeration which makes it very easy to code switch statements to recover the real type where it is needed such as when committing edits. In fact if you look at the edit committing code you’ll notice that there isn’t a type cast. The type is used to recover the appropriate service which then gets the entity based on it’s id.
departmentTree.setOnEditCommit( new EventHandler<TreeView.EditEvent<EntityProxy>>() { @Override public void handle(TreeView.EditEvent<EntityProxy> item) { EntityProxy node = item.getNewValue(); switch (node.getType()) { case DEPARTMENT: Department d = departmentService.find(node.getId()); d.setName(node.getLabel()); departmentService.merge(d); break; case PERSON: Person p = personService.find(node.getId()); p.setName(node.getLabel()); personService.merge(p); break; } } });
Is this method perfect? No, but then no method for adding multiple types to a tree will be perfect. This method has some distinct advantages over other methods and it scales nicely. I’ve used an enumeration to determine the type which restricts the types that can go into the tree to just those in the enumeration. For most applications I don’t think this would be much of an issue but for more complex applications where you can’t be sure what will be displayed in the tree you could replace the type enumeration with, for example, the class type of the entity – it doesn’t matter what it is as long as it allows you to recover the type.
With some thought it would probably also be possible to use the enumeration to carry out most of the operations without having to use a switch statement. For example, the enumeration might define a method setName
which is used to change the name of the underlying object. The method in the enumerated type would load the appropriate service and make the change. This pattern would be ideal where there’s a large number of different types in the tree which all share some similar attributes such as name.
* There are two ways to check of the changes ever make it back to the underlying Employee
object. The first is to place a System.out
in the Employee.setName
method to show any changes. This will catch all changes to the name because there is no way for the property to leak out of the Employee
class. If the name property was exposed by the class and bound to the TreeItem then a better method would be to place a ChangeListener on the name property like this (in the constructor):
this.name.addListener(new ChangeListener<String>() { @Override public void changed(ObservableValue<? extends String> ov, String t, String t1) { System.out.println("Name change from " + t + " to " + t1); } });