Wednesday, January 2, 2013

ZK Tree: ROD and Load on Demand TreeModel/TreeNode


Introduction

In ZK, tree does not support Render on Demand (ROD) and Load on Demand directly

There is a load on demand sample of tree at official demo site http://www.zkoss.org/zkdemo/tree/load_on_demand?search=tree which handle data directly to create a custom tree model to achieve load on demand.

In this article, we will implement ROD TreeModel and ROD TreeNode, so any data can work with ROD easily.

Pre-request

ZK Basic MVVM Pattern
http://ben-bai.blogspot.tw/2012/12/zk-basic-mvvm-pattern.html

The Program

tree_rod_load_on_demand.zul

Get model from VM and render node by template, also trigger updateSelectedDirectory while onSelect event to keep the selected data

<zk>
    <!-- tested with ZK 6.0.2 -->                                           
    <window apply="org.zkoss.bind.BindComposer"
        viewModel="@id('vm') @init('test.tree.rod.sample.TestVM')">
        <tree id="tree" model="@bind(vm.directoryModel)"
            width="600px" height="200px"
            onSelect="@command('updateSelectedDirectory')">
            <treecols>
                <treecol label="name" />
                <treecol label="path" />
            </treecols>
            <template name="model" var="node" status="s">
                <treeitem open="@load(node.open)">
                    <treerow>
                        <treecell label="@bind(node.data.name)" />
                        <treecell label="@bind(node.data.path)" />
                    </treerow>
                </treeitem>
            </template>
        </tree>
    </window>
</zk>


RODTreeNodeData.java

The interface that should be implemented by a data bean to make the data bean works with RODTreeNode/RODTreeModel

package test.tree.rod;

import java.util.List;

/**
 * tested with ZK 6.0.2
 * 
 * The interface for data bean that used in RODTreeModel and RODTreeNode
 * 
 * @author benbai123
 *
 */
public abstract class RODTreeNodeData {
    /**
     * get children of this data
     * @return
     */
    public abstract List<? extends RODTreeNodeData> getChildren();
    /**
     * get child count of this data, do not need to really get children
     * @return
     */
    public abstract int getChildCount ();
}


RODTreeNode.java

A TreeNode that supports ROD, let data bean handle the most of things with respect to the data (e.g., getChildren, getChildCount).

package test.tree.rod;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * tested with ZK 6.0.2
 * 
 * The TreeNode that supports ROD, with any data bean
 * that implements interface RODTreeNodeData<br><br>
 *
 * The basic rules:<br>
 * 
 * 1. Only call getChildren while you really want
 * to get children or the children has been loaded<br>
 * 2. Let data bean handle the 'data' side works
 * 
 * 
 * @author benbai123
 *
 * @param <T>
 */
public class RODTreeNode<T extends RODTreeNodeData> {

    private static final long serialVersionUID = -5106504924526127666L;

    // the data bean
    private T _data;
    // parent tree node
    private RODTreeNode<T> _parent;
    // child list
    private List<RODTreeNode<T>> _children;
    // whether this node is open
    private boolean _open;


    // constructor for receiving List children
    public RODTreeNode(T data, List<RODTreeNode<T>> children) {
        _data = data;
        _children = children;
        init();
    }
    // constructor for receiving Array children
    public RODTreeNode(T data, RODTreeNode<T>[] children) {
        _data = data;
        if (children != null
            && children.length > 0)
            _children = Arrays.asList(children);
        init();
    }

    /**
     * initial the parent-child relation
     */
    public void init () {
        if (_children != null) {
            for (RODTreeNode<T> child : _children) {
                child.setParent(this);
            }
        }
    }
    //setter/getter
    /**
     * @return T the data bean
     */
    public T getData () {
        return _data;
    }

    /**
     * @param open whether the node is open
     */
    public void setOpen (boolean open) {
        _open = open;
    }
    public boolean isOpen () {
        return _open;
    }
    public boolean getOpen () {
        return _open;
    }
    /**
     * @param parent parent tree node
     */
    public void setParent (RODTreeNode<T> parent) {
        _parent = parent;
    }
    public RODTreeNode<T> getParent () {
        return _parent;
    }
    /**
     * create TreeNode children based on the children of data bean
     * 
     * @see {@link RODTreeNodeData#getChildren()}
     * @return
     */
    @SuppressWarnings("unchecked")
    public List<RODTreeNode<T>> getChildren () {
        if (_children == null) {
            _children = new ArrayList<RODTreeNode<T>>();
            List<T> childrenData = (List<T>)_data.getChildren();
            for (T data : childrenData) {
                RODTreeNode<T> child = new RODTreeNode<T>(data, (List<RODTreeNode<T>>)null);
                child.setParent(this);
                _children.add(child);
            }
        }

        return _children;
    }
    /**
     * get child count from children size if children created,
     * or get child count from data
     * 
     * @see RODTreeNodeData#getChildCount()
     * @return
     */
    public int getChildCount () {
        int count = _children == null?
                    (_data == null? 0
                        : _data.getChildCount())
                : _children.size();
        return count;
    }
}


RODTreeModel

A TreeModel that supports ROD, implements getPath to prevent default dfs search. Also call getChildren as little as possible (e.g., replace node.getChildren().size() with node.getChildCount, let node to do the work, and then node can let data bean to do the work as needed).

package test.tree.rod;

import java.util.ArrayList;
import java.util.List;

import org.zkoss.zul.AbstractTreeModel;
import org.zkoss.zul.ext.TreeSelectableModel;

/**
 * tested with ZK 6.0.2
 * 
 * The TreeModel that supports ROD, with the RODTreeNode, and
 * any data bean that implements interface RODTreeNodeData<br><br>
 *
 * The basic rule is only call getChildren while you really want
 * to get children or the children has been loaded
 * 
 * @author benbai123
 *
 * @param <T> data bean implements RODTreeNodeData
 */
public class RODTreeModel<T extends RODTreeNodeData>
    extends AbstractTreeModel<RODTreeNode<T>> implements TreeSelectableModel {

    private static final long serialVersionUID = 7822729366554623684L;

    /**
     * Constructor, simply receive a root RODTreeNodeData
     */
    public RODTreeModel (RODTreeNode<T> root) {
        super(root);
    }
 
    /**
     * get child from parent node by index
     */
    public RODTreeNode<T> getChild(RODTreeNode<T> parent, int index) {
        List<RODTreeNode<T>> children = parent.getChildren();
        if (children == null) {
            return null;
        }
        if (index < 0 || index >= children.size()) {
            return null;
        }
        return children.get(index);
    }

    /**
     * call {@link RODTreeNode#getChildCount()} instead of
     * size of {@link RODTreeNode#getChildren()}
     */
    public int getChildCount(RODTreeNode<T> parent) {
        // get child count directly instead of get size of children
        return parent.getChildCount();
    }

    /**
     * simply determine whether node is a leaf node
     */
    public boolean isLeaf(RODTreeNode<T> node) {
        return getChildCount(node) == 0;
    }

    /**
     * here call getChildren since children
     * should already be loaded to get the child
     */
    public int getIndexOfChild(RODTreeNode<T> parent, RODTreeNode<T> child) {
        return parent.getChildren().indexOf(child);
    }

    /**
     * get path based on RODTreeNode, data independent
     */
    public int[] getPath (RODTreeNode<T> child) {
        if (child == null || child.getParent() == null) {
            int[] path = new int[1];
            path[0] = 0;
            return path;
        }
        int[] path = null;
        List<Integer> dPath = new ArrayList<Integer>();
        RODTreeNode<T> parent = child.getParent();
        while (parent != null) {
            dPath.add(0, parent.getChildren().indexOf(child));
            child = parent;
            parent = child.getParent();
        }
        path = new int[dPath.size()];
        for (int i = 0; i < dPath.size(); i++) {
            path[i] = dPath.get(i);
        }
        return path;
    }
}


FileBean.java

The data bean represent a file, implements the RODTreeNodeData so can work with ROD TreeModel/TreeNode

package test.tree.rod.sample;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

import test.tree.rod.RODTreeNodeData;

/**
 * tested with ZK 6.0.2
 * 
 * The data bean of File, implements RODTreeNodeData
 * to work with RODTreeModel/RODTreeNode
 * 
 * @author benbai123
 *
 */
public class FileBean extends RODTreeNodeData {
    private List<FileBean> _children;

    private File _file;
    private String _name;
    private String _path;

    // constructor
    public FileBean (File file) {
        _file = file;
        _name = file.getName();
        _path = file.getAbsolutePath();
    }
    // getter, setter
    public String getName () {
        return _name;
    }
    public String getPath () {
        return _path;
    }
    /**
     * implement {@link RODTreeNodeData#getChildren()}
     */
    public List<FileBean> getChildren() {
        if (_children == null) {
            _children = new ArrayList<FileBean>();
            File f = new File(_path);
            File[] filelist = f.listFiles();
            if (filelist != null) {
                for (File file : filelist) {
                    _children.add(new FileBean(file));
                }
            }
        }
        return _children;
    }
    /**
     * implement {@link RODTreeNodeData#getChildCount()}
     */
    public int getChildCount () {
        int childCount = 0;
        if (_children != null) {
            childCount =  _children.size();
        } else if (_file.isDirectory()) {
            File[] filelist = new File(_path).listFiles();
            childCount =  filelist == null? 0 : filelist.length;
        }
        return childCount;
    }
}


TestVM.java

The sample view model, provide directory model and record the selected file.

package test.tree.rod.sample;

import java.io.File;
import java.util.List;
import java.util.Set;

import org.zkoss.bind.annotation.Command;
import org.zkoss.bind.annotation.ContextParam;
import org.zkoss.bind.annotation.ContextType;
import org.zkoss.zk.ui.event.SelectEvent;
import org.zkoss.zul.TreeModel;

import test.tree.rod.RODTreeModel;
import test.tree.rod.RODTreeNode;
/**
 * tested with ZK 6.0.2
 * 
 * @author benbai123
 *
 */
public class TestVM {
    RODTreeModel<FileBean> _directoryTreeModel;
    FileBean _selectedDirectory;
    File file = new File("C:" + File.separator + "Program Files");

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public TreeModel<RODTreeNode<FileBean>> getDirectoryModel () {
        RODTreeNode root = new RODTreeNode(null,
                new RODTreeNode[] {new RODTreeNode(new FileBean(file), (List)null)
        });
        if (_directoryTreeModel == null) {
            _directoryTreeModel = new RODTreeModel<FileBean>(root);
        }
        return _directoryTreeModel;
    }

    @SuppressWarnings({ "rawtypes", "unchecked" })
    @Command
    public void updateSelectedDirectory (@ContextParam(ContextType.TRIGGER_EVENT) SelectEvent event) {
        Set s = event.getSelectedObjects();
        if (s != null && s.size() > 0) {
            _selectedDirectory = ((RODTreeNode<FileBean>)s.iterator().next()).getData();
            System.out.println("selected: " + _selectedDirectory.getName() + ", path = " + _selectedDirectory.getPath());
        }
    }
}


The Result

View the demo flash on line
http://screencast.com/t/DaeBzKS8NX4R

References

ZK Template
http://books.zkoss.org/wiki/ZK%20Developer's%20Reference/UI%20Patterns/Templating/Templates

Tree Load on Demand Sample
http://www.zkoss.org/zkdemo/tree/load_on_demand

Download

Files at github

RODTreeNodeData.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/tree/rod/RODTreeNodeData.java

RODTreeNode.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/tree/rod/RODTreeNode.java

RODTreeModel.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/tree/rod/RODTreeModel.java

FileBean.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/tree/rod/sample/FileBean.java

TestVM.java
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/src/test/tree/rod/sample/TestVM.java

tree_rod_load_on_demand.zul
https://github.com/benbai123/ZK_Practice/blob/master/Components/projects/Components_Practice/WebContent/tree_rod_load_on_demand.zul

demo flash
https://github.com/benbai123/ZK_Practice/blob/master/Components/demos/tree_rod_load_on_demand.swf

11 comments:

  1. Great article. How do I change this code so that I can add new nodes? Thanks

    ReplyDelete
    Replies
    1. This implementation is completely rely on data, you can add an API "addChild" into RODTreeNodeData and implement it as needed.

      Delete
  2. Can't tell how helpful this post and the source code is. Awesome job. Saved me a ton of time.

    Are there any tricks/hooks that could help classes that extend RODTreeNodeData so that they wouldn't need to wrap calls to the underlying business object for every single property. For example instead of a FileBean I have an "Employee" implementation of RODTreeNodeData that takes an Employee in the constructor. All the calls to the properties of Employee have to be written over again (Adapter Pattern) in the RODTreeNodeData implementation (since I don't want to force the Employee domain object to extends specific ZK related objects.) If I wrote this in Groovy it would be easy I could just have it handle missingMethod and dynamically call the underlying employee object's method.

    Great stuff ! Thanks again.

    ReplyDelete
    Replies
    1. I'm glad it helped :)

      Regarding "they wouldn't need to wrap calls to the underlying business object", do you mean you do not want to write getChildren and getChildCount methods in Employee class?

      If so, you can try create another helper class to handle this,
      e.g. change code as the gist
      https://gist.github.com/benbai123/9b8d704506e4a99084eb

      This makes everything as add-on so Employee class can stay unchanged.

      Delete
  3. Cool Bai. Actually I was over-thinking it a bit (plus being an idiot.) It's all good..

    Rather than a helper class I just have a class EmployeeRODTreeNodeData that extends RODTreeNodeData (and has an accessor to the Employee passed in.. (Employee would be similar to your File object passed into your FileBean constructor.) I only need to make instances of these for the top level nodes where I then do the following in the VM's init:

    http://pastie.org/9441518

    Then in my tree cell



    I'll get the final code up at github at some point.

    ReplyDelete
  4. oops forgot no markup tags.. anyway tree cell

    treecell label="@load(item.data.employee.name)"

    ReplyDelete
    Replies
    1. In case anyone wants my version for reference it's here:
      https://github.com/rickcr/zk-tree-ondemand

      Thanks again Bai Ben!

      Delete