Tree Model"

From Documentation
m
 
(47 intermediate revisions by 10 users not shown)
Line 3: Line 3:
 
__TOC__
 
__TOC__
  
Here we describe how to implement a tree model (<javadoc type="interface">org.zkoss.zul.TreeModel</javadoc>). For the concepts of component, model and render, please refer to [[ZK_Developer's_Reference/MVC/Model/List_Model#Model-driven_Display|the Model-driven Display section]].
+
Here we describe how to implement a tree model (<javadoc type="interface">org.zkoss.zul.TreeModel</javadoc>). You shall understand the interaction among a component, a model, and a renderer, please refer to [[ZK_Developer's_Reference/MVC/Model/List_Model#Model-driven_Display|the Model-driven Display section]].
  
A tree model is used to control how to display a tree-like component, such as <javadoc>org.zkoss.zul.Tree</javadoc>.
 
  
Instead of implementing <javadoc type="interface">org.zkoss.zul.TreeModel</javadoc> from scratch, it is suggested to extend from <javadoc>org.zkoss.zul.AbstractTreeModel</javadoc>, which will handle the data listeners transparently, while it allows the maximal flexibility, such as load-on-demand and caching.
+
= Choose a Proper Model Class=
 +
A <javadoc type="interface">org.zkoss.zul.TreeModel</javadoc> is the data model of a tree-like component, such as <javadoc>org.zkoss.zul.Tree</javadoc>.
  
In addition, if the tree is small enough to be loaded completely, you could use the default implementation, <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc>, which uses <javadoc>org.zkoss.zul.DefaultTreeNode</javadoc> to construct a tree<ref><javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> was available in 5.0.6. For 5.0.5 or prior, please use <javadoc>org.zkoss.zul.SimpleModel</javadoc>, which is similar except it assumes the tree structure is immutable</ref>.
+
If the tree data is small enough to be loaded completely into a TreeModel, you can use <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> which accepts <javadoc>org.zkoss.zul.DefaultTreeNode</javadoc> to construct a tree<ref><javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> is available since 5.0.6. For 5.0.5 or prior, please use <javadoc>org.zkoss.zul.SimpleModel</javadoc>, which is similar except it assumes the tree structure is immutable</ref>.
  
<blockquote>
 
----
 
<references/>
 
</blockquote>
 
 
= Example: Load-on-Demand Tree with AbstractTreeModel =
 
Implementing all <javadoc type="interface">org.zkoss.zul.TreeModel</javadoc> directly provides the maximal flexibility, such as load-on-demand and caching. For example, you don't have to load a node until <javadoc method="getChild(java.lang.Object, int)" type="interface">org.zkoss.zul.TreeModel</javadoc> is called. In addition, you could load and cache all children of a given node when <javadoc method="getChild(java.lang.Object, int)" type="interface">org.zkoss.zul.TreeModel</javadoc> 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):
 
 
<source lang="java">
 
public class MyModel extends AbstractTreeModel {
 
    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];
 
    }
 
...
 
</source>
 
 
By extending from <javadoc>org.zkoss.zul.AbstractTreeModel</javadoc>, you need only to implement three methods: <javadoc method="getChild(java.lang.Object, int)" type="interface">org.zkoss.zul.TreeModel</javadoc>, <javadoc method="getChildCount(java.lang.Object)" type="interface">org.zkoss.zul.TreeModel</javadoc>,  and <javadoc method="isLeaf(java.lang.Object)" type="interface">org.zkoss.zul.TreeModel</javadoc>. Optionally, you could implement <javadoc method="getIndexOfChild(java.lang.Object, java.langObject)" type="interface">org.zkoss.zul.TreeModel</javadoc><ref><javadoc method="getIndexOfChild(java.lang.Object, java.langObject)" type="interface">org.zkoss.zul.TreeModel</javadoc> is available in 5.0.6 and later.</ref>, 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:
 
 
<source lang="java">
 
package foo;
 
public class FooModel extends AbstractTreeModel {
 
    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;
 
    }
 
};
 
</source>
 
 
Then, we could have a ZUML document to display it as follows.
 
 
<source lang="xml">
 
<?taglib uri="http://www.zkoss.org/dsp/web/core" prefix="c" ?>
 
<tree model="${c:new('foo.FooModel')}">
 
    <treecols>
 
        <treecol label="Names"/>
 
    </treecols>
 
</tree>
 
</source>
 
  
And, the result
+
In a more complicated case, if you want to implement custom logic like load-on-demand and caching. Maybe the data is too large to load them all into a TreeModel at once. Then, we suggest you to extend <javadoc>org.zkoss.zul.AbstractTreeModel</javadoc>, which will handle the data listeners transparently.
  
[[Image:DrTreeModel1.png]]
 
  
 
<blockquote>
 
<blockquote>
Line 83: Line 19:
 
<references/>
 
<references/>
 
</blockquote>
 
</blockquote>
 +
  
 
= Example: In-Memory Tree with DefaultTreeModel =
 
= Example: In-Memory Tree with DefaultTreeModel =
  
Since 5.0.6
+
{{versionSince| 5.0.6}}
  
If you prefer to use <javadoc type="interface">org.zkoss.zul.TreeNode</javadoc> to construct the tree dynamically, you could use <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> and <javadoc>org.zkoss.zul.DefaultTreeNode</javadoc>. The use is straightfoward, but it means that the whole tree must be constructed before having it being displayed.
+
If you prefer to use <javadoc type="interface">org.zkoss.zul.TreeNode</javadoc> to construct the tree dynamically, you can use <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> and <javadoc>org.zkoss.zul.DefaultTreeNode</javadoc>. The usage is straightforward, but it means that the whole tree must be constructed before it is displayed.
  
For example, suppose we want to show up a tree of file information, and the file information is stored as <code>FileInfo</code>:
+
For example, suppose we want to show a tree of file information, and the file information is stored as <code>FileInfo</code>:
  
 
<source lang="java">
 
<source lang="java">
package foo;
 
 
public class FileInfo {
 
public class FileInfo {
 
     public final String path;
 
     public final String path;
Line 104: Line 40:
 
</source>
 
</source>
  
Then, we could create a tree of file information with <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> as follows.
+
Then, we can create a tree of file information with <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> as follows:
  
 
<source lang="java">
 
<source lang="java">
Line 125: Line 61:
 
</source>
 
</source>
  
To render <code>FileInfo</code>, you have to implement a custom renderer. For example,
+
Here, we render <code>FileInfo</code> in a custom renderer:
  
 
<source lang="java">
 
<source lang="java">
package foo;
 
 
import org.zkoss.zul.*;
 
import org.zkoss.zul.*;
public class FileInfoRenderer implements TreeitemRenderer {
+
public class FileInfoRenderer implements TreeitemRenderer<DefaultTreeNode<FileInfo>> {
     public void render(Treeitem item, Object data) throws Exception {
+
     public void render(Treeitem item, DefaultTreeNode<FileInfo> data, int index) throws Exception {
         FileInfo fi = (FileInfo)data.getData();
+
         FileInfo fi = data.getData();
 
         Treerow tr = new Treerow();
 
         Treerow tr = new Treerow();
 
         item.appendChild(tr);
 
         item.appendChild(tr);
Line 141: Line 76:
 
</source>
 
</source>
  
Then, we could put them together in a ZUML document:
+
 
 +
Next, bind them together in a composer:
 +
 
 +
<source lang="java">
 +
public class FileInfoTreeController extends SelectorComposer {
 +
    @Wire
 +
    private Tree tree;
 +
 
 +
    @Override
 +
    public void doAfterCompose(Div div) throws Exception{
 +
        super.doAfterCompose(div);
 +
        tree.setModel(new DefaultTreeModel(..../*as shown above*/));
 +
        tree.setItemRenderer(new FileInfoRenderer());
 +
    }
 +
}
 +
</source>
 +
 
 +
Then, we can put them together in a ZUML document:
  
 
<source lang="xml">
 
<source lang="xml">
<div apply="foo.FileInfoTreeController">
+
<div apply="org.zkoss.reference.developer.mvc.model.FileInfoTreeController">
 
     <tree id="tree">
 
     <tree id="tree">
 
         <treecols>
 
         <treecols>
Line 152: Line 104:
 
     </tree>
 
     </tree>
 
</div>
 
</div>
</source>
 
 
where we assume you have a controller, <code>foo.FileInfoTreeController</code>, to bind them together. For example,
 
 
<source lang="java">
 
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());
 
    }
 
}
 
 
</source>
 
</source>
  
Line 172: Line 110:
 
[[Image:DrTreeModel2.png]]
 
[[Image:DrTreeModel2.png]]
  
Notice that you could manipulate the tree dynamically (such as adding a node with <javadoc method="add(org.zkoss.zul.TreeNode)">org.zkoss.zul.DefaultTreeNode</javadoc>). The tree shown at the browser will be modified accordingly.
+
Notice that you can manipulate the tree dynamically (such as adding a node with <javadoc method="add(org.zkoss.zul.TreeNode)">org.zkoss.zul.DefaultTreeNode</javadoc>). The tree shown at the browser will be modified accordingly.
 
 
=Open Treeitem=
 
 
 
By default, the child treeitems (<javadoc>org.zkoss.zul.Treeitem</javadoc>) are closed. In other words, <javadoc method="isOpen()">org.zkoss.zul.Treeitem</javadoc> is default to false. If you prefer to open them, you could invoke <javadoc method="setOpen(boolean)">org.zkoss.zul.Treeitem</javadoc> with <code>true</code> in the renderer, such as:
 
 
 
<source lang="java" high="6">
 
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
 
        ...
 
    }
 
}
 
</source>
 
  
 
=Example: Create/Update/Delete operation with DefaultTreeNode=
 
=Example: Create/Update/Delete operation with DefaultTreeNode=
 +
{{versionSince| 5.0.6}}
  
Since 5.0.6
+
The benefit of using <code>DefaultTreeNode</code> is it will notify Tree (or the component associated with a TreeModel) about the node change including adding, deleting, and updating. (The underlying implementation is <code>DefaultTreeNode</code> will fire an event when you call its add(), insert(), setData(), removeFromParent()).
  
To demonstrate the example, first we add the following content in the ZUML document:
+
To demonstrate the example, first we add create, update and delete buttons in a .zul:
<source lang="xml">
+
<source lang="xml" highlight="20-22">
 
<tree id="tree">
 
<tree id="tree">
 
...
 
...
Line 219: Line 137:
 
         <row>
 
         <row>
 
             <cell colspan="2" align="center">
 
             <cell colspan="2" align="center">
                 index: <intbox id="index" /><button id="create" label="Add to selected parent node" />
+
                 index: <intbox id="index" />
 +
                <button id="create" label="Add to selected parent node" />
 
                 <button id="update" label="update" />
 
                 <button id="update" label="update" />
 
                 <button id="delete" label="delete" />
 
                 <button id="delete" label="delete" />
Line 227: Line 146:
 
</grid>
 
</grid>
 
</source>
 
</source>
 +
The intbox here is for specifying index to insert before the selected tree item.
  
 
==Add/Insert==
 
==Add/Insert==
<javadoc>org.zkoss.zul.DefaultTreeNode</javadoc> provide <javadoc method="add(org.zkoss.zul.TreeNode)">org.zkoss.zul.DefaultTreeNode</javadoc> and <javadoc method="insert(org.zkoss.zul.TreeNode, int)">org.zkoss.zul.DefaultTreeNode</javadoc> that can manipulate the tree dynamically.
+
<javadoc>org.zkoss.zul.DefaultTreeNode</javadoc> provides <javadoc method="add(org.zkoss.zul.TreeNode)">org.zkoss.zul.DefaultTreeNode</javadoc> and <javadoc method="insert(org.zkoss.zul.TreeNode, int)">org.zkoss.zul.DefaultTreeNode</javadoc> that can manipulate the tree dynamically.
  
 
Here we register onClick event to create Button in <code>foo.FileInfoTreeController</code>:
 
Here we register onClick event to create Button in <code>foo.FileInfoTreeController</code>:
 
<source lang="java">
 
<source lang="java">
//wire component as member fields
+
//wire components as member fields
 
private Textbox pathTbx;
 
private Textbox pathTbx;
 
private Textbox descriptionTbx;
 
private Textbox descriptionTbx;
Line 241: Line 161:
 
     String path = pathTbx.getValue();
 
     String path = pathTbx.getValue();
 
     String description = descriptionTbx.getValue();
 
     String description = descriptionTbx.getValue();
     if("".equals(path)) {
+
     if ("".equals(path)) {
 
         alert("no new content to add");
 
         alert("no new content to add");
 
     } else {
 
     } else {
Line 247: Line 167:
 
         DefaultTreeNode newNode = new DefaultTreeNode(new FileInfo(path, description));
 
         DefaultTreeNode newNode = new DefaultTreeNode(new FileInfo(path, description));
 
         DefaultTreeNode selectedTreeNode = null;
 
         DefaultTreeNode selectedTreeNode = null;
 +
        Integer i = index.getValue();
 
         // if no treeitem is selected, append child to root
 
         // if no treeitem is selected, append child to root
         if(selectedTreeItem == null) {
+
         if (selectedTreeItem == null) {
 
             selectedTreeNode = (DefaultTreeNode) ((DefaultTreeModel) tree.getModel()).getRoot();
 
             selectedTreeNode = (DefaultTreeNode) ((DefaultTreeModel) tree.getModel()).getRoot();
             if(index.getValue() == null)
+
             if (i == null) // if no index specified, append to last.
 
                 selectedTreeNode.add(newNode);
 
                 selectedTreeNode.add(newNode);
             else
+
             else // if index specified, insert before the index number.
                 selectedTreeNode.insert(newNode, index.getValue());
+
                 selectedTreeNode.insert(newNode, i);
 
         } else {
 
         } else {
 
             selectedTreeNode = (DefaultTreeNode) selectedTreeItem.getValue();
 
             selectedTreeNode = (DefaultTreeNode) selectedTreeItem.getValue();
             if(selectedTreeNode.isLeaf()) {
+
 
                 if(index.getValue() == null)
+
             if (selectedTreeNode.isLeaf())
                    selectedTreeNode.getParent().add(newNode);
+
                 selectedTreeNode = selectedTreeNode.getParent();
                else
+
 
                    selectedTreeNode.getParent().insert(newNode, index.getValue());
+
             if (i == null)
             } else {
+
                selectedTreeNode.add(newNode);
                if(index.getValue() == null)
+
            else
                    selectedTreeNode.add(newNode);
+
                selectedTreeNode.insert(newNode, i);
                else
 
                    selectedTreeNode.insert(newNode, index.getValue());
 
            }
 
 
         }
 
         }
 
     }
 
     }
 
}
 
}
 
</source>
 
</source>
If index is not specify, we add new node using <javadoc method="add(org.zkoss.zul.TreeNode)">org.zkoss.zul.DefaultTreeNode</javadoc> at bottom of the parent node by default, otherwise we use <javadoc method="insert(org.zkoss.zul.TreeNode, int)">org.zkoss.zul.DefaultTreeNode</javadoc> to insert new node before the specified index.
+
If index is not specified, we add a new node using <javadoc method="add(org.zkoss.zul.TreeNode)">org.zkoss.zul.DefaultTreeNode</javadoc> at the bottom of the parent node by default, or we can also use <javadoc method="insert(org.zkoss.zul.TreeNode, int)">org.zkoss.zul.DefaultTreeNode</javadoc> to insert a new node before the specified index.
  
 
==Update/Delete==
 
==Update/Delete==
<javadoc>org.zkoss.zul.DefaultTreeNode</javadoc> provide <javadoc method="setData(java.lang.Object)">org.zkoss.zul.DefaultTreeNode</javadoc>  that can update the selected tree item and <javadoc method="removeFromParent()">org.zkoss.zul.DefaultTreeNode</javadoc> that can delete the selected tree item from it's parent node.
+
<javadoc>org.zkoss.zul.DefaultTreeNode</javadoc> provides <javadoc method="setData(java.lang.Object)">org.zkoss.zul.DefaultTreeNode</javadoc>  which can update selected tree items and <javadoc method="removeFromParent()">org.zkoss.zul.DefaultTreeNode</javadoc> that can delete the selected tree item from its parent node.
  
 
Here we register onClick event to update and delete Button in <code>foo.FileInfoTreeController</code>:
 
Here we register onClick event to update and delete Button in <code>foo.FileInfoTreeController</code>:
Line 305: Line 223:
 
     }
 
     }
 
}
 
}
 +
</source>
 +
For updating tree node data, we have to modify <code>render()</code> of <code>foo.FileInfoRenderer</code>:
 +
<source lang="java" highlight="3, 4, 5,6,7">
 +
public void render(Treeitem item, DefaultTreeNode<FileInfo> data, int index) throws Exception {
 +
    FileInfo fi = data.getData();
 +
    if (tr == null) {
 +
        tr = new Treerow();
 +
    }else{
 +
        tr.getChildren().clear();
 +
    }   
 +
    item.appendChild(tr);
 +
    tr.appendChild(new Treecell(fi.path));
 +
    tr.appendChild(new Treecell(fi.description));
 +
}
 +
</source>
 +
 +
 +
 +
= Example: Load-on-Demand Tree with AbstractTreeModel =
 +
Implementing all <javadoc type="interface">org.zkoss.zul.TreeModel</javadoc> directly provides the maximal flexibility, such as load-on-demand and caching. For example, you don't have to load a node until <javadoc method="getChild(java.lang.Object, int)" type="interface">org.zkoss.zul.TreeModel</javadoc> is called. In addition, you could load and cache all children of a given node when <javadoc method="getChild(java.lang.Object, int)" type="interface">org.zkoss.zul.TreeModel</javadoc> 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):
 +
 +
<source lang="java">
 +
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];
 +
    }
 +
...
 +
</source>
 +
 +
By extending from <javadoc>org.zkoss.zul.AbstractTreeModel</javadoc>, you have to implement three methods: <javadoc method="getChild(java.lang.Object, int)" type="interface">org.zkoss.zul.TreeModel</javadoc>, <javadoc method="getChildCount(java.lang.Object)" type="interface">org.zkoss.zul.TreeModel</javadoc>, and <javadoc method="isLeaf(java.lang.Object)" type="interface">org.zkoss.zul.TreeModel</javadoc>. Optionally, you could implement <javadoc method="getIndexOfChild(java.lang.Object, java.langObject)" type="interface">org.zkoss.zul.TreeModel</javadoc><ref><javadoc method="getIndexOfChild(java.lang.Object, java.langObject)" type="interface">org.zkoss.zul.TreeModel</javadoc> is available in 5.0.6 and later.</ref>, if you have a better algorithm than iterating through all children of a given parent.
 +
 +
== Improving Performance ==
 +
You '''should''' override <javadoc method="getPath(E)" type="interface">org.zkoss.zul.TreeModel</javadoc> to implement an efficient way to deduce the sibling index of each ancestor of a node. Because the default implementation of <code>getPath()</code> in <code>AbstractTreeModel</code> will traverse from the root node to compute the index which is inefficient with lots of nodes. Such node traversing will call <code>getChild()</code> and load unnecessary nodes from the data source which consume more memory.
 +
 +
== An Example ==
 +
Here is a simple example, which generates a 4-level tree and each branch has 5 children:
 +
 +
<source lang="java">
 +
package foo;
 +
public class LoadOnDemandModel extends AbstractTreeModel<Object> {
 +
    public LoadOnDemandModel() {
 +
        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;
 +
    }
 +
};
 +
</source>
 +
 +
Then, we assign this model to a tree:
 +
 +
<source lang="xml">
 +
<?taglib uri="http://www.zkoss.org/dsp/web/core" prefix="c" ?>
 +
<tree model="${c:new('org.zkoss.reference.developer.mvc.model.LoadOnDemandModel')}">
 +
    <treecols>
 +
        <treecol label="Names"/>
 +
    </treecols>
 +
</tree>
 +
</source>
 +
 +
And, the result looks like this:
 +
 +
[[Image:DrTreeModel1.png]]
 +
 +
 +
 +
= Sorting =
 +
Interface: <javadoc type="interface">org.zkoss.zul.ext.Sortable</javadoc>
 +
Implementation: You have to implement it explicitly
 +
 +
To support the sorting, the model must implement <javadoc type="interface">org.zkoss.zul.ext.Sortable</javadoc> too. Thus, when the end user clicks the header to request the sorting, <javadoc method="sort(java.util.Comparator, boolean)" type="interface">org.zkoss.zul.ext.Sortable</javadoc> will be called.
 +
 +
For example, (pseudo code)
 +
 +
<source lang="java">
 +
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
 +
    }
 +
...
 +
</source>
 +
 +
Notice that the <code>ascending</code> parameter is used only for reference and you usually don't need it, since the <code>cmpr</code> is already a comparator capable to sort in the order specified in the <code>ascending</code> parameter.
 +
 +
=Selection=
 +
Interface: <javadoc type="interface">org.zkoss.zul.ext.TreeSelectableModel</javadoc>
 +
Implementation: Implemented by <javadoc>org.zkoss.zul.AbstractTreeModel</javadoc>
 +
 +
If your data model also provides the collection of selected elements, you shall also implement <javadoc type="interface">org.zkoss.zul.ext.TreeSelectableModel</javadoc>. When using with a component supporting the selection (such as <javadoc>org.zkoss.zul.Tree</javadoc>), the component will invoke <javadoc method="isPathSelected(int[])" type="interface">org.zkoss.zul.ext.TreeSelectableModel</javadoc> to display the selected elements correctly. In addition, if the end user selects or deselects an item, <javadoc method="addSelectionPath(int[])" type="interface">org.zkoss.zul.ext.TreeSelectableModel</javadoc> and <javadoc method="removeSelectionPath(int[])" type="interface">org.zkoss.zul.ext.TreeSelectableModel</javadoc> 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 <javadoc method="addSelectionPath(int[])" type="interface">org.zkoss.zul.ext.TreeSelectableModel</javadoc> is called), it has to fire the event, such as <javadoc method="SELECTION_CHANGED">org.zkoss.zul.event.TreeDataEvent
 +
</javadoc> to notify the component. It will cause the component to correct the selection<ref>Don't worry. The component is smart enough to prevent the dead loop, even though the component invokes <code>addSelectionPath()</code> to notify the model while the model fires the event to notify the component.</ref>.
 +
 +
All default implementations, including <javadoc>org.zkoss.zul.AbstractTreeModel</javadoc> and <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> implement <javadoc>org.zkoss.zul.ext.TreeSelectableModel</javadoc>.  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 has been changed.
 +
 +
<blockquote>
 +
----
 +
<references/>
 +
</blockquote>
 +
 +
== Selection Control ==
 +
{{versionSince| 8.0.0}}
 +
 +
With the multiple selection function in a data model, you have to implement a class for the <javadoc type="interface">org.zkoss.zul.ext.SelectionControl</javadoc> to tell the data model which items are selectable and what it will perform a "select all" function with. The following implementation which extends <javadoc type="class">org.zkoss.zul.AbstractTreeModel.DefaultSelectionControl</javadoc> is a simple example to change "selectable" items.
 +
 +
Please note that if your data model is much larger, you may implement on it your own to get rid of the performance impact.
 +
 +
<source lang="java" highlight="2">
 +
model.setSelectionControl(new AbstractTreeModel.DefaultSelectionControl(model) {
 +
public boolean isSelectable(Object e) {
 +
int i = model.indexOf(e);
 +
return i % 2 == 0;
 +
}
 +
});
 +
</source>
 +
 +
=Open Tree Nodes=
 +
Interface: <javadoc type="interface">org.zkoss.zul.ext.TreeOpenableModel</javadoc>
 +
Implementation: Implemented by <javadoc>org.zkoss.zul.AbstractTreeModel</javadoc>
 +
 +
By default, all tree nodes are closed. To control whether to open a tree node, you could implement <javadoc type="interface">org.zkoss.zul.ext.TreeOpenableModel</javadoc>. More importantly, to open a tree node, the application shall access the model's <javadoc type="interface">org.zkoss.zul.ext.TreeOpenableModel</javadoc> API, rather than accessing <javadoc>org.zkoss.zul.Treeitem</javadoc> directly.
 +
 +
All default implementations, including <javadoc>org.zkoss.zul.AbstractTreeModel</javadoc> and <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> implement <javadoc>org.zkoss.zul.ext.TreeOpenableModel</javadoc>.  Thus, your implementation generally doesn't have to implement it explicitly.
 +
 +
 +
'''Note:''' If your tree model contains a lot of nodes, please also implement <javadoc method="getPath(E)" type="interface">org.zkoss.zul.TreeModel</javadoc> to get better performance, by default it is implemented by [http://en.wikipedia.org/wiki/Depth-first_search Depth-first search] to get the path from a tree node.
 +
 +
=Leaf Node=
 +
The <javadoc>org.zkoss.zul.DefaultTreeNode</javadoc> has 2 constructors:
 +
* <code>DefaultTreeNode(data)</code>: create a [https://en.wikipedia.org/wiki/Tree_(data_structure)#Terminology leaf node ]which cannot have children added to it.
 +
* <code>DefaultTreeNode(data, children)</code>: create a [https://en.wikipedia.org/wiki/Tree_(data_structure)#Terminology branch node] which can have children added to it.
 +
 +
ZK renders a rotating triangle to expand/collapse a node in front of a branch node, but a leaf node doesn't have that triangle.
 +
 +
[[File:leafBranch.jpg | center]]
 +
 +
If you want to display a leaf node, you should use <code>DefaultTreeNode(data)</code>, otherwise even if you provide a zero-size list for <code>DefaultTreeNode(data, children)</code> constructor, ZK tree will still render the node as a branch node that contains no children. So when you expand the node, it shows nothing. It might confuse users.
 +
 +
{{versionSince| 5.0.12 / 6.0.3 / 6.5.1}}
 +
 +
<javadoc>org.zkoss.zul.DefaultTreeModel</javadoc>'s constructor accepts the 2nd boolean argument to determine whether to render a branch node without children as a leaf node.
 +
<source lang="java">
 +
DefaultTreeModel model2 = new DefaultTreeModel(root, true);
 
</source>
 
</source>
  
 
=Version History=
 
=Version History=
{{LastUpdated}}
+
 
{| border='1px' | width="100%"
+
{| class='wikitable' | width="100%"
 
! Version !! Date !! Content
 
! Version !! Date !! Content
 
|-
 
|-
Line 316: Line 401:
 
| <javadoc type="interface">org.zkoss.zul.TreeNode</javadoc>, <javadoc>org.zkoss.zul.DefaultTreeNode</javadoc> and <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> were intrdocued.
 
| <javadoc type="interface">org.zkoss.zul.TreeNode</javadoc>, <javadoc>org.zkoss.zul.DefaultTreeNode</javadoc> and <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> were intrdocued.
 
|-
 
|-
| 5.0.6
+
| 6.0.0
| October 2011
+
| February 2012
| <javadoc method="add(org.zkoss.zul.TreeNode)">org.zkoss.zul.DefaultTreeNode</javadoc>, <javadoc method="insert(org.zkoss.zul.TreeNode, int)">org.zkoss.zul.DefaultTreeNode</javadoc>, <javadoc method="setData(java.lang.Object)">org.zkoss.zul.DefaultTreeNode</javadoc> and <javadoc method="removeFromParent()">org.zkoss.zul.DefaultTreeNode</javadoc> were intrdocued.
+
| <javadoc type="interface">org.zkoss.zul.ext.TreeSelectableModel</javadoc> and <javadoc type="interface">org.zkoss.zul.ext.TreeOpenableModel</javadoc> were introduced to replace <javadoc type="interface">org.zkoss.zul.ext.Selectable</javadoc> and <javadoc type="interface">org.zkoss.zul.ext.Openable</javadoc>.
 +
|-
 +
| 5.0.12 / 6.0.3 / 6.5.1
 +
| October 2012
 +
| <javadoc>org.zkoss.zul.DefaultTreeModel</javadoc> adds a new constructor for configuring whether to treat the zero size of children node as a leaf node.
 
|}
 
|}
  
 
{{ZKDevelopersReferencePageFooter}}
 
{{ZKDevelopersReferencePageFooter}}

Latest revision as of 10:20, 29 January 2024

Here we describe how to implement a tree model (TreeModel). You shall understand the interaction among a component, a model, and a renderer, please refer to the Model-driven Display section.


Choose a Proper Model Class

A TreeModel is the data model of a tree-like component, such as Tree.

If the tree data is small enough to be loaded completely into a TreeModel, you can use DefaultTreeModel which accepts DefaultTreeNode to construct a tree[1].


In a more complicated case, if you want to implement custom logic like load-on-demand and caching. Maybe the data is too large to load them all into a TreeModel at once. Then, we suggest you to extend AbstractTreeModel, which will handle the data listeners transparently.



  1. DefaultTreeModel is available since 5.0.6. For 5.0.5 or prior, please use SimpleModel, which is similar except it assumes the tree structure is immutable


Example: In-Memory Tree with DefaultTreeModel

Since 5.0.6

If you prefer to use TreeNode to construct the tree dynamically, you can use DefaultTreeModel and DefaultTreeNode. The usage is straightforward, but it means that the whole tree must be constructed before it is displayed.

For example, suppose we want to show a tree of file information, and the file information is stored as FileInfo:

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 can 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"))
        })
      }
  ));

Here, we render FileInfo in a custom renderer:

import org.zkoss.zul.*;
public class FileInfoRenderer implements TreeitemRenderer<DefaultTreeNode<FileInfo>> {
    public void render(Treeitem item, DefaultTreeNode<FileInfo> data, int index) throws Exception {
        FileInfo fi = data.getData();
        Treerow tr = new Treerow();
        item.appendChild(tr);
        tr.appendChild(new Treecell(fi.path));
        tr.appendChild(new Treecell(fi.description));
    }
}


Next, bind them together in a composer:

public class FileInfoTreeController extends SelectorComposer {
    @Wire
    private Tree tree;

    @Override
    public void doAfterCompose(Div div) throws Exception{
        super.doAfterCompose(div);
        tree.setModel(new DefaultTreeModel(..../*as shown above*/));
        tree.setItemRenderer(new FileInfoRenderer());
    }
}

Then, we can put them together in a ZUML document:

<div apply="org.zkoss.reference.developer.mvc.model.FileInfoTreeController">
    <tree id="tree">
        <treecols>
            <treecol label="Path"/>
            <treecol label="Description"/>
        </treecols>
    </tree>
</div>

Then, the result:

DrTreeModel2.png

Notice that you can manipulate the tree dynamically (such as adding a node with DefaultTreeNode.add(TreeNode)). The tree shown at the browser will be modified accordingly.

Example: Create/Update/Delete operation with DefaultTreeNode

Since 5.0.6

The benefit of using DefaultTreeNode is it will notify Tree (or the component associated with a TreeModel) about the node change including adding, deleting, and updating. (The underlying implementation is DefaultTreeNode will fire an event when you call its add(), insert(), setData(), removeFromParent()).

To demonstrate the example, first we add create, update and delete buttons in a .zul:

<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 components 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 render() of foo.FileInfoRenderer:

public void render(Treeitem item, DefaultTreeNode<FileInfo> data, int index) throws Exception {
    FileInfo fi = data.getData();
    if (tr == null) {
        tr = new Treerow();
    }else{
        tr.getChildren().clear();
    }    
    item.appendChild(tr);
    tr.appendChild(new Treecell(fi.path));
    tr.appendChild(new Treecell(fi.description));
}


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.

Improving Performance

You should override TreeModel.getPath(E) to implement an efficient way to deduce the sibling index of each ancestor of a node. Because the default implementation of getPath() in AbstractTreeModel will traverse from the root node to compute the index which is inefficient with lots of nodes. Such node traversing will call getChild() and load unnecessary nodes from the data source which consume more memory.

An Example

Here is a simple example, which generates a 4-level tree and each branch has 5 children:

package foo;
public class LoadOnDemandModel extends AbstractTreeModel<Object> {
    public LoadOnDemandModel() {
        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 assign this model to a tree:

<?taglib uri="http://www.zkoss.org/dsp/web/core" prefix="c" ?>
<tree model="${c:new('org.zkoss.reference.developer.mvc.model.LoadOnDemandModel')}">
    <treecols>
        <treecol label="Names"/>
    </treecols>
</tree>

And, the result looks like this:

DrTreeModel1.png


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.

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 addition, 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[2].

All default implementations, including AbstractTreeModel and DefaultTreeModel implement 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 has been changed.


  1. TreeModel.getIndexOfChild(Object, langObject) is available in 5.0.6 and later.
  2. Don't worry. The component is smart enough to prevent the dead loop, even though the component invokes addSelectionPath() to notify the model while the model fires the event to notify the component.

Selection Control

Since 8.0.0

With the multiple selection function in a data model, you have to implement a class for the SelectionControl to tell the data model which items are selectable and what it will perform a "select all" function with. The following implementation which extends AbstractTreeModel.DefaultSelectionControl is a simple example to change "selectable" items.

Please note that if your data model is much larger, you may implement on it your own to get rid of the performance impact.

model.setSelectionControl(new AbstractTreeModel.DefaultSelectionControl(model) {
	public boolean isSelectable(Object e) {
		int i = model.indexOf(e);
		return i % 2 == 0;
	}
});

Open Tree Nodes

Interface: TreeOpenableModel
Implementation: Implemented by AbstractTreeModel

By default, all tree nodes are closed. To control whether to open a tree node, you could implement TreeOpenableModel. More importantly, to open a tree node, the application shall access the model's TreeOpenableModel API, rather than accessing Treeitem directly.

All default implementations, including AbstractTreeModel and DefaultTreeModel implement TreeOpenableModel. Thus, your implementation generally doesn't have to implement it explicitly.


Note: If your tree model contains a lot of nodes, please also implement TreeModel.getPath(E) to get better performance, by default it is implemented by Depth-first search to get the path from a tree node.

Leaf Node

The DefaultTreeNode has 2 constructors:

  • DefaultTreeNode(data): create a leaf node which cannot have children added to it.
  • DefaultTreeNode(data, children): create a branch node which can have children added to it.

ZK renders a rotating triangle to expand/collapse a node in front of a branch node, but a leaf node doesn't have that triangle.

LeafBranch.jpg

If you want to display a leaf node, you should use DefaultTreeNode(data), otherwise even if you provide a zero-size list for DefaultTreeNode(data, children) constructor, ZK tree will still render the node as a branch node that contains no children. So when you expand the node, it shows nothing. It might confuse users.

Since 5.0.12 / 6.0.3 / 6.5.1

DefaultTreeModel's constructor accepts the 2nd boolean argument to determine whether to render a branch node without children as a leaf node.

DefaultTreeModel model2 = new DefaultTreeModel(root, true);

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.
5.0.12 / 6.0.3 / 6.5.1 October 2012 DefaultTreeModel adds a new constructor for configuring whether to treat the zero size of children node as a leaf node.



Last Update : 2024/01/29

Copyright © Potix Corporation. This article is licensed under GNU Free Documentation License.