The default implementation of models, such as org.zkoss.zul.ListModelList and org.zkoss.zul.DefaultTreeModel assumes all data are available in the memory. It is not practical if a model has a lot of data. For huge amount of data, it is suggested to implement your own model by loading and caching only one portion of data at a time.

To implement your own model for a component it is recommended that you extend the correct abstract model type. For a ListModel extend from org.zkoss.zul.AbstractListModel, for a GroupsModel extend org.zkoss.zul.AbstractGroupsModel and for a TreeModel extend org.zkoss.zul.AbstractTreeModel as described in the Model section. To implement a model that supports sorting, you have to implement org.zkoss.zul.ext.Sortable too. Each time a user requires sorting, org.zkoss.zul.ext.Sortable#sort(java.util.Comparator, boolean) will be called and the implementation usually clears the cache and re-generates the SQL statement accordingly.

Here is some pseudocode for a custom ListModel:

public class FooListModel extends AbstractListModel implements Sortable {
    private int _size = -1;
    private Object[] _cache;
    private int _beginOffset;
    private String _orderBy;
    private boolean _ascending, _descending;
    private Comparator _sorting;
 
    public int getSize() {
        if (_size < 0)
            _size = /**SELECT COUNT(*) FROM ...*/
        return _size;
    }
    public Object getElementAt(int index) {
        if (_cache == null || index < _beginOffset || index >= _beginOffset + _cache.length) {
           loadToCache(index, 100); //SELECT ... FROM .... OFFSET index LIMIT 100
                //if _ascending, ORDER BY _orderBy ASC
                //if _descending, ORDER BY _orderBy DSC
        }
        return _cache[index - _beginOffset];
    }
    @Override
    public void sort(Comparator cmpr, boolean ascending) {
        _cache = null; //purge cache
        _size = -1; //so size will be reloaded
        _descending = !(_ascending = ascending);
        _orderBy = ((FieldComparator)cmpr).getRawOrderBy();
        _sorting = cmpr;
             //Here we assume sort="auto(fieldName)" is specified in ZUML, so cmpr is FieldComparator
             //On other hand, if you specifies your own comparator, such as sortAscending="${mycmpr}",
             //then, cmpr will be the comparator you assigned
        fireEvent(ListDataEvent.CONTENTS_CHANGED, -1, -1);
    }
    @Override
    public String getSortDirection(Comparator cmpr) {
        if (Objects.equals(_sorting, cmpr))
            return _ascending ?  "ascending" : "descending";
        return "natural";   
    }
}

The implementation of org.zkoss.zul.ext.Sortable#sort(java.util.Comparator, boolean) generally has to purge the cache, store the sorting direction and field, and then fire org.zkoss.zul.event.ListDataEvent#CONTENTS_CHANGED to reload the content.

The field to sort against has to be retrieved from the given comparator. If you specify "auto(fieldName)" to org.zkoss.zul.Listheader#setSort(java.lang.String), then the comparator is an instance of org.zkoss.zul.FieldComparator, and you could retrieve the field’s name from org.zkoss.zul.FieldComparator#getRawOrderBy().

If you’d like to use your own comparator, you have to carry the information in it and then retrieve it back when org.zkoss.zul.ext.Sortable#sort(java.util.Comparator, boolean) is called.

Also notice that we cache the size to improve the performance, since org.zkoss.zul.ListModel#getSize() might be called multiple times.

Here is some pseudo for a custom TreeModel which renders an Apache Commons VFS FileObject to be able to browse a filesystem:

public class VfsTreeModel extends AbstractTreeModel<FileObject> {
    public VfsTreeModel(FileObject root){
        super(root);
    }
    
    @Override
    public FileObject getChild(FileObject parent, int index) {
        FileObject child = null;
        try {
            FileObject[] children = parent.getChildren();
            child = children[index];
        } catch (FileSystemException e) {
            throw new IllegalArgumentException(e);
        }
        return child;
    }

    @Override
    public int getChildCount(FileObject node) {
        int childCount = 0;
        try {
            FileType type = node.getType();
            if( type == FileType.FOLDER ){
                childCount = node.getChildren().length;
            }
        } catch (FileSystemException e) {
            throw new IllegalArgumentException(e);
        }
        return childCount;
    }

    @Override
    public boolean isLeaf(FileObject node) {
        boolean isLeaf = false;
        try {
            FileType type = node.getType();
            isLeaf = (type == FileType.FILE );
        } catch (FileSystemException e) {
            throw new IllegalArgumentException(e);
        }
        return isLeaf;
    }
    /*
     * Return the sibling index of each node in walk down from the root. 
     */
    @Override
    public int[] getPath(FileObject node) {
        List<Integer> paths = new ArrayList<Integer>();
        try {
            // walk upwards to root getting sibling index of each child in each parent
            FileObject parent = node.getParent(); 
            while (parent != null && parent.getType().equals(FileType.FOLDER)) {
                FileObject[] children = parent.getChildren();
                for( int index = 0; index < children.length; index++){
                    FileObject c = children[index];
                    if( node.equals(c)){
                        paths.add(index);
                        break;
                    }
                }
                node = parent;
                parent = node.getParent();
            }
        } catch (FileSystemException e) {
            throw new IllegalArgumentException(e);
        }
        int[] p = new int[paths.size()];
        for( int index = 0; index < paths.size(); index++){
            p[index] = paths.get(p.length - 1 - index); // reverse
        }
        return p;
    }
}

When many treerows are open and further rows are expanded the re-rendering of the tree may visit many of the open rows. It is therefore recommended that you cache the results of any expensive calls where possible with a suitable eviction strategy.

For a real example, please refer to Small Talk: Handling sortable huge data using ZK and/or Small Talk: Handling huge data using ZK.

Version History

Version Date Content
6.0.0 02/03/2012 Sortable interface