Tree Model
Here we describe how to implement a tree model (TreeModel). For the concepts of component, model and render, please refer to the Model-driven Display section.
A tree model is used to control how to display a tree-like component, such as Tree.
Instead of implementing TreeModel from scratch, it is suggested to extend from AbstractTreeModel, which will handle the data listeners transparently, while it allows the maximal flexibility, such as load-on-demand and caching.
In addition, if the tree is small enough to be loaded completely, you could use the default implementation, DefaultTreeModel, which uses DefaultTreeNode to construct a tree[1].
- ↑ DefaultTreeModel was available in 5.0.6. For 5.0.5 or prior, please use SimpleModel, which is similar except it assumes the tree structure is immutable
Example: Load-on-Demand Tree with AbstractTreeModel
Implementing all TreeModel directly provides the maximal flexibility, such as load-on-demand and caching. For example, you don't have to load a node until TreeModel.getChild(Object, int) is called. In addition, you could load and cache all children of a given node when TreeModel.getChild(Object, int) is called the first time against a particular node, and then return a child directly if it is in the cache.
For example (pseudo code):
public class MyModel extends AbstractTreeModel<Object> {
public Object getChild(Object parent, int index) {
Object[] children = _cache.get(parent); //assume you have a cache for children of a given node
if (children == null)
children = _cache.loadChildren(parent); //ask cache to load all children of a given node
return children[index];
}
...
By extending from AbstractTreeModel, you have to implement three methods: TreeModel.getChild(Object, int), TreeModel.getChildCount(Object), and TreeModel.isLeaf(Object). Optionally, you could implement TreeModel.getIndexOfChild(Object, langObject)[1], if you have a better algorithm than iterating through all children of a given parent.
Here is a simple example, which generates a four-level tree and each branch has five children:
package foo;
public class FooModel extends AbstractTreeModel<Object> {
public FooModel() {
super("Root");
}
public boolean isLeaf(Object node) {
return getLevel((String)node) >= 4; //at most 4 levels
}
public Object getChild(Object parent, int index) {
return parent + "." + index;
}
public int getChildCount(Object parent) {
return isLeaf(parent) ? 0: 5; //each node has 5 children
}
public int getIndexOfChild(Object parent, Object child) {
String data = (String)child;
int i = data.lastIndexOf('.');
return Integer.parseInt(data.substring(i + 1));
}
private int getLevel(String data) {
for (int i = -1, level = 0;; ++level)
if ((i = data.indexOf('.', i + 1)) < 0)
return level;
}
};
Then, we could have a ZUML document to display it as follows.
<?taglib uri="http://www.zkoss.org/dsp/web/core" prefix="c" ?>
<tree model="${c:new('foo.FooModel')}">
<treecols>
<treecol label="Names"/>
</treecols>
</tree>
And, the result
- ↑ TreeModel.getIndexOfChild(Object, langObject) is available in 5.0.6 and later.
Example: In-Memory Tree with DefaultTreeModel
Since 5.0.6
If you prefer to use TreeNode to construct the tree dynamically, you could use DefaultTreeModel and DefaultTreeNode. The use is straightfoward, but it means that the whole tree must be constructed before having it being displayed.
For example, suppose we want to show up a tree of file information, and the file information is stored as FileInfo
:
package foo;
public class FileInfo {
public final String path;
public final String description;
public FileInfo(String path, String description) {
this.path = path;
this.description = description;
}
}
Then, we could create a tree of file information with DefaultTreeModel as follows.
TreeModel model = new DefaultTreeModel(
new DefaultTreeNode(null,
new DefaultTreeNode[] {
new DefaultTreeNode(new FileInfo("/doc", "Release and License Notes")),
new DefaultTreeNode(new FileInfo("/dist", "Distribution"),
new DefaultTreeNode[] {
new DefaultTreeNode(new FileInfo("/lib", "ZK Libraries"),
new DefaultTreeNode[] {
new DefaultTreeNode(new FileInfo("zcommon.jar", "ZK Common Library")),
new DefaultTreeNode(new FileInfo("zk.jar", "ZK Core Library"))
}),
new DefaultTreeNode(new FileInfo("/src", "Source Code")),
new DefaultTreeNode(new FileInfo("/xsd", "XSD Files"))
})
}
));
To render FileInfo
, you have to implement a custom renderer. For example,
package foo;
import org.zkoss.zul.*;
public class FileInfoRenderer implements TreeitemRenderer {
public void render(Treeitem item, Object data) throws Exception {
FileInfo fi = (FileInfo)data.getData();
Treerow tr = new Treerow();
item.appendChild(tr);
tr.appendChild(new Treecell(fi.path));
tr.appendChild(new Treecell(fi.description));
}
}
Then, we could put them together in a ZUML document:
<div apply="foo.FileInfoTreeController">
<tree id="tree">
<treecols>
<treecol label="Path"/>
<treecol label="Description"/>
</treecols>
</tree>
</div>
where we assume you have a controller, foo.FileInfoTreeController
, to bind them together. For example,
package foo;
import org.zkoss.zul.Tree;
public class FileInfoTreeController implements org.zkoss.zk.ui.util.GenericForwardComposer {
private Tree tree;
public void doAfterCompose(org.zkoss.zk.ui.Component comp) {
tree.setModel(new DefaultTreeModel(..../*as shown above*/));
tree.setItemRenderer(new FooRenderer());
}
}
Then, the result:
Notice that you could manipulate the tree dynamically (such as adding a node with DefaultTreeNode.add(TreeNode)). The tree shown at the browser will be modified accordingly.
Open Treeitem
By default, the child treeitems (Treeitem) are closed. In other words, Treeitem.isOpen() is default to false. If you prefer to open them, you could invoke Treeitem.setOpen(boolean) with true
in the renderer, such as:
public class FileInfoRenderer implements TreeitemRenderer {
public void render(Treeitem item, Object data) throws Exception {
FileInfo fi = data.getData();
Treerow tr = new Treerow();
item.appendChild(tr);
item.setOpen(shallOpen(fi));
tr.appendChild(new Treecell(fi.path));
tr.appendChild(new Treecell(fi.description));
}
private boolean shallOpen(FileInfo fi) {
//check whether to open the given FileInfo
...
}
}
Example: Create/Update/Delete operation with DefaultTreeNode
Since 5.0.6
To demonstrate the example, first we add create, update and delete buttons in the ZUML document:
<tree id="tree">
...
</tree>
<grid>
<auxhead>
<auxheader colspan="2" label="Add/Edit FileInfo" />
</auxhead>
<columns visible="false">
<column />
<column />
</columns>
<rows>
<row>
<cell><textbox id="pathTbx" /></cell>
<cell><textbox id="descriptionTbx" width="300px"/></cell>
</row>
<row>
<cell colspan="2" align="center">
index: <intbox id="index" /><button id="create" label="Add to selected parent node" />
<button id="update" label="update" />
<button id="delete" label="delete" />
</cell>
</row>
</rows>
</grid>
The intbox here is for specifying index to insert before the selected tree item.
Add/Insert
DefaultTreeNode provides DefaultTreeNode.add(TreeNode) and DefaultTreeNode.insert(TreeNode, int) that can manipulate the tree dynamically.
Here we register onClick event to create Button in foo.FileInfoTreeController
:
//wire component as member fields
private Textbox pathTbx;
private Textbox descriptionTbx;
private Intbox index;
//register onClick event for creating new object into tree model
public void onClick$create() {
String path = pathTbx.getValue();
String description = descriptionTbx.getValue();
if ("".equals(path)) {
alert("no new content to add");
} else {
Treeitem selectedTreeItem = tree.getSelectedItem();
DefaultTreeNode newNode = new DefaultTreeNode(new FileInfo(path, description));
DefaultTreeNode selectedTreeNode = null;
Integer i = index.getValue();
// if no treeitem is selected, append child to root
if (selectedTreeItem == null) {
selectedTreeNode = (DefaultTreeNode) ((DefaultTreeModel) tree.getModel()).getRoot();
if (i == null) // if no index specified, append to last.
selectedTreeNode.add(newNode);
else // if index specified, insert before the index number.
selectedTreeNode.insert(newNode, i);
} else {
selectedTreeNode = (DefaultTreeNode) selectedTreeItem.getValue();
if (selectedTreeNode.isLeaf())
selectedTreeNode = selectedTreeNode.getParent();
if (i == null)
selectedTreeNode.add(newNode);
else
selectedTreeNode.insert(newNode, i);
}
}
}
If index is not specified, we add a new node using DefaultTreeNode.add(TreeNode) at the bottom of the parent node by default, or we can also use DefaultTreeNode.insert(TreeNode, int) to insert a new node before the specified index.
Update/Delete
DefaultTreeNode provides DefaultTreeNode.setData(Object) which can update selected tree items and DefaultTreeNode.removeFromParent() that can delete the selected tree item from its parent node.
Here we register onClick event to update and delete Button in foo.FileInfoTreeController
:
//register onClick event for updating edited data in tree model
public void onClick$update() {
Treeitem selectedTreeItem = treeGrid.getSelectedItem();
if(selectedTreeItem == null) {
alert("select one item to update");
} else {
DefaultTreeNode selectedTreeNode = (DefaultTreeNode) selectedTreeItem.getValue();
//get current FileInfo from selected tree node
FileInfo fileInfo = (FileInfo) selectedTreeNode.getData();
//set new value of current FileInfo
fileInfo.setPath(pathTbx.getValue());
fileInfo.setDescription(descriptionTbx.getValue());
//set current FileInfo in the selected tree node
selectedTreeNode.setData(fileInfo);
}
}
//register onClick event for removing data in tree model
public void onClick$delete() {
final Treeitem selectedTreeItem = treeGrid.getSelectedItem();
if(selectedTreeItem == null) {
alert("select one item to delete");
} else {
DefaultTreeNode selectedTreeNode = (DefaultTreeNode) selectedTreeItem.getValue();
selectedTreeNode.removeFromParent();
}
}
For updating tree node data, we have to modify foo.FileInfoRenderer
:
DefaultTreeNode treeNode = (DefaultTreeNode) data;
//for treeItem.getValue() in Controller class
item.setValue(treeNode);
//for update treeNode data
Treerow tr = item.getTreerow();
if(tr == null) {
tr = new Treerow();
} else {
tr.getChildren().clear();
}
//renderer...
Selection
Interface: TreeSelectableModel Implementation: Implemented by AbstractTreeModel
If your data model also provides the collection of selected elements, you shall also implement TreeSelectableModel. When using with a component supporting the selection (such as Tree), the component will invoke TreeSelectableModel.isPathSelected(int[]) to display the selected elements correctly. In additions, if the end user selects or deselects an item, TreeSelectableModel.addSelectionPath(int[]) and TreeSelectableModel.removeSelectionPath(int[]) will be called by the component to notify the model that the selection is changed. Then, you can update the selection into the persistent layer (such as database) if necessary.
On the other hand, when the model detects the selection is changed (such as TreeSelectableModel.addSelectionPath(int[]) is called), it has to fire the event, such as TreeDataEvent.SELECTION_CHANGED to notify the component. It will cause the component to correct the selection<reference>Don't worry. The component is smart enough to prevent the dead loop, even though components invokes addSelectionPath
to notify the model while the model fire the event to notify the component.</reference>.
All default implementations, including AbstractTreeModel and DefaultTreeModel implements TreeSelectableModel. Thus, your implementation generally doesn't have to implement it explicitly.
It is important to note that, once a tree is assigned with a tree model, the application shall not manipulate the tree items and/or change the selection of the tree directly. Rather, the application shall access only the list model to add, remove and select data elements. Let the model notify the component what have been changed.
Sorting
Interface: Sortable Implementation: You have to implement it explicitly
To support the sorting, the model must implement Sortable too. Thus, when the end user clicks the header to request the sorting, Sortable.sort(Comparator, boolean) will be called.
For example, (pseudo code)
public class FooModel extends AbstractTreeModel implements Sortable {
public void sort(Comparator cmpr, final boolean ascending) {
sortData(cmpr); //sort your data here
fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1); //ask component to reload all
}
...
Notice that the ascending parameter is used only for reference and you usually don't need it, since the cmpr is already a comparator capable to sort in the order specified in the ascending parameter.
Version History
Version | Date | Content |
---|---|---|
5.0.6 | January 2011 | TreeNode, DefaultTreeNode and DefaultTreeModel were intrdocued. |
6.0.0 | February 2012 | TreeSelectableModel and TreeOpenableModel were introduced to replace Selectable and Openable. |