zul/src/main/java/org/zkoss/zul/Listbox.java

Summary

Maintainability
F
1 mo
Test Coverage
/* Listbox.java

    Purpose:

    Description:

    History:
        Wed Jun 15 17:25:00     2005, Created by tomyeh

Copyright (C) 2005 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
    This program is distributed under LGPL Version 2.1 in the hope that
    it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
 */
package org.zkoss.zul;

import static org.zkoss.lang.Generics.cast;

import java.lang.reflect.Method;
import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.AbstractSequentialList;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.zkoss.io.Serializables;
import org.zkoss.json.JSONAware;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Exceptions;
import org.zkoss.lang.Library;
import org.zkoss.lang.Objects;
import org.zkoss.lang.Strings;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.AuRequests;
import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.ui.AbstractComponent;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Components;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.Execution;
import org.zkoss.zk.ui.Executions;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.WrongValueException;
import org.zkoss.zk.ui.event.CheckEvent;
import org.zkoss.zk.ui.event.CloneableEventListener;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.event.SelectEvent;
import org.zkoss.zk.ui.event.SerializableEventListener;
import org.zkoss.zk.ui.ext.Blockable;
import org.zkoss.zk.ui.ext.render.Cropper;
import org.zkoss.zk.ui.sys.BooleanPropertyAccess;
import org.zkoss.zk.ui.sys.IntegerPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zk.ui.util.ComponentCloneListener;
import org.zkoss.zul.event.DataLoadingEvent;
import org.zkoss.zul.event.GroupsDataEvent;
import org.zkoss.zul.event.GroupsDataListener;
import org.zkoss.zul.event.ListDataEvent;
import org.zkoss.zul.event.ListDataListener;
import org.zkoss.zul.event.PageSizeEvent;
import org.zkoss.zul.event.PagingEvent;
import org.zkoss.zul.event.PagingListener;
import org.zkoss.zul.event.ZulEvents;
import org.zkoss.zul.ext.GroupsSelectableModel;
import org.zkoss.zul.ext.Pageable;
import org.zkoss.zul.ext.Paginal;
import org.zkoss.zul.ext.Selectable;
import org.zkoss.zul.ext.SelectionControl;
import org.zkoss.zul.ext.Sortable;
import org.zkoss.zul.impl.DataLoader;
import org.zkoss.zul.impl.GroupsListModel;
import org.zkoss.zul.impl.ListboxDataLoader;
import org.zkoss.zul.impl.MeshElement;
import org.zkoss.zul.impl.Padding;
import org.zkoss.zul.impl.Utils;
import org.zkoss.zul.impl.XulElement;

/**
 * A listbox.
 *
 * <p>
 * Event:
 * <ol>
 * <li>{@link org.zkoss.zk.ui.event.SelectEvent} is sent when user changes the
 * selection.</li>
 * <li>onAfterRender is sent when the model's data has been rendered.(since 5.0.4)</li>
 * <li>onCheckSelectAll is sent when user click on selectAll checkbox.(since 6.5.5)</li>
 * </ol>
 *
 * <p>
 * See <a href="package-summary.html">Specification</a>.
 * </p>
 *
 * <p>
 * Besides creating {@link Listitem} programmatically, you could assign a data
 * model (a {@link ListModel} or {@link GroupsModel} instance) to a listbox via
 * {@link #setModel(ListModel)} or {@link #setModel(GroupsModel)} and then the
 * listbox will retrieve data via {@link ListModel#getElementAt} when necessary.
 *
 * <p>
 * Besides assign a list model, you could assign a renderer (a
 * {@link ListitemRenderer} instance) to a listbox, such that the listbox will
 * use this renderer to render the data returned by
 * {@link ListModel#getElementAt}. If not assigned, the default renderer, which
 * assumes a label per list item, is used. In other words, the default renderer
 * adds a label to a Listitem by calling toString against the object returned by
 * {@link ListModel#getElementAt}</p>
 *
 * <p>To retrieve what are selected in Listbox with a {@link Selectable}
 * {@link ListModel}, you shall use {@link Selectable#getSelection} to get what
 * is currently selected object in {@link ListModel} rather than using
 * {@link Listbox#getSelectedItems}. That is, you shall operate on the item of
 * the {@link ListModel} rather than on the {@link Listitem} of the {@link Listbox}
 * if you use the {@link Selectable} {@link ListModel}.</p>
 *
 * <pre><code>
 * Set selection = ((Selectable)getModel()).getSelection();
 * </code></pre>
 * 
 * <p>[Since 6.0.0] If a model is set, whether the listbox allows
 * the multiple selection depends on {@link Selectable#setMultiple}.
 * In other words, the application shall not access listbox directly if
 * a model is assigned. Rather, the application shall access the model
 * directly.
 * 
 * <p>
 * There are two ways to handle long content: scrolling and paging. If
 * {@link #getMold} is "default", scrolling is used if {@link #setHeight} is
 * called and too much content to display. If {@link #getMold} is "paging",
 * paging is used if two or more pages are required. To control the number of
 * items to display in a page, use {@link #setPageSize}.
 *
 * <p>
 * If paging is used, the page controller is either created automatically or
 * assigned explicitly by {@link #setPaginal}. The paging controller specified
 * explicitly by {@link #setPaginal} is called the external page controller. It
 * is useful if you want to put the paging controller at different location
 * (other than as a child component), or you want to use the same controller to
 * control multiple listboxes.
 *
 * <p>
 * Default {@link #getZclass}: z-listbox.(since 3.5.0)
 *
 * <p>
 * To have a list box without stripping, you can specify a non-existent style
 * class to {@link #setOddRowSclass}.
 *
 * <h3>Clustering and Serialization</h3>
 *
 * <p>
 * When used in a clustering environment, you have to make
 * {@link ListitemRenderer} ({@link #setItemRenderer}) and {@link ListModel} (
 * {@link #setModel}) either serializable or re-assign them when
 * {@link #sessionDidActivate} is called.
 *
 * <h3>Render on Demand (rod)</h3>
 * [ZK EE]
 * [Since 5.0.0]
 *
 * <p>For huge data, you can turn on Listbox's ROD to request ZK engine to load from
 * {@link ListModel} only the required data chunk and create only the required
 * {@link Listitem}s in memory and render only the required DOM elements in
 * browser. So it saves both the memory and the processing time in both server
 * and browser for huge data. If you don't use the {@link ListModel} with the
 * Listbox, turn on the ROD will still have ZK engine to render only a chunk of
 * DOM elements in browser so it at least saves the memory and processing time
 * in browser. Note that ROD works only if the Listbox is configured to has a
 * limited "view port" height. That is, either the Listbox is in the "paging"
 * mold or you have to {@link #setHeight(String)},{@link #setVflex(String)},
 * or {@link #setRows(int)} of the Listbox to make ROD works.</p>
 *
 * <p>You can turn on/off ROD for all Listboxes in the application or only
 * for a specific Listbox. To turn on ROD for all Listboxes in the application,
 * you have to specify the Library Property "org.zkoss.zul.listbox.rod" to
 * "true" in WEB-INF/zk.xml. If you did not specify the Library Property,
 * default is false.</p>
 *
 * <pre>{@code
 *    <library-property>
 *        <name>org.zkoss.zul.listbox.rod</name>
 *        <value>true</value>
 *    </library-property>
 * }</pre>
 *
 * <p>To turn on ROD for a specific Listbox, you have to specify the Listbox's
 * attribute map with key "org.zkoss.zul.listbox.rod" to true. That is, for
 * example, if in a zul file, you shall specify &lt;custom-attributes&gt; of the
 * Listbox like this:</p>
 *
 * <pre>{@code
 *    <listbox ...>
 *    <custom-attributes org.zkoss.zul.listbox.rod="true"/>
 *  </listbox>
 * }</pre>
 *
 * <p>You can mix the Library Property and &lt;custom-attributes&gt; ways together.
 * The &lt;custom-attributes&gt; way always takes higher priority. So you
 * can turn OFF ROD in general and turn ON only some specific Listbox component.
 * Or you can turn ON ROD in general and turn OFF only some specific Listbox
 * component.</P>
 *
 * <p>Since only partial {@link Listitem}s are created and rendered in the
 * Listbox if you turn the ROD on, there will be some limitations on accessing
 * {@link Listitem}s. For example, if you call
 * <pre><code>
 * Listitem itemAt100 = (Listitem) getItemAtIndex(100);
 * </code></pre>
 * <p>The {@link Listitem} in index 100 is not necessary created yet if it is
 * not in the current "view port" and you will get "null" instead.</p>
 *
 * <p>And it is generally a bad idea to "cache" the created {@link Listitem}
 * in your application if you turn the ROD on because Listitems might be removed
 * later. Basically, you shall operate on the item of the {@link ListModel}
 * rather than on the {@link Listitem} of the {@link Listbox} if you use the
 * {@link ListModel} and ROD.</p>
 *
 * <h3>Custom Attributes</h3>
 * <dl>
 * <dt>org.zkoss.zul.listbox.rightSelect</dt>
 * <dd>Specifies whether the selection shall be toggled when user right clicks on
 * item, if the checkmark ({@link #isCheckmark}) is enabled.</br>
 * Notice that you could specify this attribute in any of its ancestor's attributes.
 * It will be inherited.</dd>
 * <dt>org.zkoss.zul.listbox.rod</dt>
 * <dd>Specifies whether to enable ROD (render-on-demand).</br>
 * Notice that you could specify this attribute in any of its ancestor's attributes.
 * It will be inherited.</dd>
 * <dt>org.zkoss.zul.listbox.autoSort</dt>.(since 5.0.7) 
 * <dd>Specifies whether to sort the model when the following cases:</br>
 * <ol>
 * <li>{@link #setModel} is called and {@link Listheader#setSortDirection} is set.</li>
 * <li>{@link Listheader#setSortDirection} is called.</li>
 * <li>Model receives {@link ListDataEvent} and {@link Listheader#setSortDirection} is set.</li>
 * </ol>
 * If you want to ignore sort when receiving {@link ListDataEvent}, 
 * you can specifies the value as "ignore.change".</br>
 * Notice that you could specify this attribute in any of its ancestor's attributes.
 * It will be inherited.</dd>
 * </dl>
 * <dt>org.zkoss.zul.listbox.groupSelect</dt>
 * <dd>Specifies whether Listgroups under this Listbox are selectable. Notice that 
 * you could specify this attribute in any of its ancestor's attributes. It will 
 * be inherited. Default value is false.</dd>
 * 
 * <dt>org.zkoss.zul.listbox.preloadSize</dt>.(since 5.0.8) 
 * <dd>Specifies the number of items to preload when receiving
 * the rendering request from the client.
 * <p>It is used only if live data ({@link #setModel(ListModel)} and
 * not paging ({@link #getPagingChild}).</dd>
 * 
 * <dt>org.zkoss.zul.listbox.initRodSize</dt>.(since 5.0.8) 
 * <dd>Specifies the number of items rendered when the Listbox first render.
 * <p>
 * It is used only if live data ({@link #setModel(ListModel)} and not paging
 * ({@link #getPagingChild}).</dd>
 * 
 * <dt>org.zkoss.zul.listbox.selectOnHighlight.disabled</dt>.(since 7.0.4)
 * <dd>Sets whether to disable select functionality when highlighting text 
 * content with mouse dragging or not.</dd>
 * 
 * @author tomyeh
 * @see ListModel
 * @see ListitemRenderer
 * @see ListitemRendererExt
 */
public class Listbox extends MeshElement {
    private static final long serialVersionUID = 2009111111L;
    public static final String LOADING_MODEL = "org.zkoss.zul.loadingModel";
    public static final String SYNCING_MODEL = "org.zkoss.zul.syncingModel";

    private static final Logger log = LoggerFactory.getLogger(Listbox.class);
    private static final String ATTR_ON_INIT_RENDER_POSTED = "org.zkoss.zul.onInitLaterPosted";
    private static final String ATTR_ON_PAGING_INIT_RENDERER_POSTED = "org.zkoss.zul.onPagingInitPosted";
    private static final int INIT_LIMIT = 50;

    private transient DataLoader _dataLoader;
    private transient List<Listitem> _items;
    private transient List<int[]> _groupsInfo;
    private transient List<Listgroup> _groups;
    /** A list of selected items. */
    private transient Set<Listitem> _selItems;
    /** A readonly copy of {@link #_selItems}. */
    private transient Set<Listitem> _roSelItems;
    private int _maxlength;
    private int _rows, _jsel = -1;
    private transient Listhead _listhead;
    private transient Listfoot _listfoot;
    private transient Frozen _frozen;
    private transient ListModel<?> _model;
    private transient ListitemRenderer<?> _renderer;
    private transient ListDataListener _dataListener;
    private transient GroupsDataListener _groupsDataListener;
    private transient Collection<Component> _heads;
    private int _hdcnt;
    private String _innerWidth = "100%";
    /** The name. */
    private String _name;
    /** The paging controller, used only if mold = "paging". */
    private transient Paginal _pgi;
    private transient boolean _isReplacingItem;
    private transient int _focusIndex = -1;

    /**
     * The paging controller, used only if mold = "paging" and user doesn't
     * assign a controller via {@link #setPaginal}. If exists, it is the last
     * child
     */
    private transient Paging _paging;
    private EventListener<Event> _pgListener, _pgImpListener, _modelInitListener;
    /** The style class of the odd row. */
    private String _scOddRow = null;
    /** the # of rows to preload. */
    private int _preloadsz = 50;
    /** maintain the number of the visible item in Paging mold. */
    private int _visibleItemCount;
    private int _currentTop = 0; // since 5.0.0 scroll position
    private int _currentLeft = 0;
    private int _topPad; // since 5.0.0 top padding
    private String _nonselTags; //since 5.0.5 for non-selectable tags

    private int _anchorTop = 0; //since ZK 5.0.11 , 6.0.0 anchor position
    private int _anchorLeft = 0;

    private boolean _multiple;
    private boolean _disabled, _checkmark;
    private boolean _renderAll; //since 5.0.0

    private transient boolean _rod;
    /** whether to ignore ListDataEvent.SELECTION_CHANGED */
    private transient boolean _ignoreDataSelectionEvent;
    /** the message to display when there are no items */
    private String _emptyMessage;
    //ZK-3103: if setSelectedIndex is called, should force scroll into view
    private boolean _shallSyncSelInView = false;
    //ZK-3982: opened group scrolled out of view
    private boolean _shallUpdateScrollPos = false;
    //ZK-4329: avoid scroll into view when selectAll is performed
    private transient boolean _ignoreSelectedItem;

    static {
        addClientEvent(Listbox.class, Events.ON_RENDER, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE);
        addClientEvent(Listbox.class, Events.ON_INNER_WIDTH, CE_DUPLICATE_IGNORE | CE_IMPORTANT);

        //ZK-925 We can't use CE_DUPLICATE_IGNORE in "onSelect" event since we need to sync the status when multiple select in ROD.
        addClientEvent(Listbox.class, Events.ON_SELECT, CE_IMPORTANT);
        addClientEvent(Listbox.class, Events.ON_FOCUS, CE_DUPLICATE_IGNORE);
        addClientEvent(Listbox.class, Events.ON_BLUR, CE_DUPLICATE_IGNORE);
        addClientEvent(Listbox.class, Events.ON_SCROLL_POS, CE_DUPLICATE_IGNORE | CE_IMPORTANT); // since 5.0.0
        addClientEvent(Listbox.class, Events.ON_TOP_PAD, CE_DUPLICATE_IGNORE); // since
        // 5.0.0
        addClientEvent(Listbox.class, Events.ON_DATA_LOADING, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE); // since 5.0.0
        addClientEvent(Listbox.class, ZulEvents.ON_PAGE_SIZE, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE); //since 5.0.2

        // since 6.0.0, F60-ZK-715
        addClientEvent(Listbox.class, Events.ON_ACROSS_PAGE, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE);

        // since 6.0.0/5.0.11, B50-ZK-798
        addClientEvent(Listbox.class, Events.ON_ANCHOR_POS, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
        // since 6.5.5 F65-ZK-2014
        addClientEvent(Listbox.class, Events.ON_CHECK_SELECT_ALL, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
    }

    public Listbox() {
        init();
    }

    private void init() {
        _items = new AbstractSequentialList<Listitem>() {
            public ListIterator<Listitem> listIterator(int index) {
                return new ItemIter(index);
            }

            public Listitem get(int j) {
                final Component o = Listbox.this.getChildren().get(j + _hdcnt);
                if (o instanceof Listitem)
                    return (Listitem) o;
                throw new IndexOutOfBoundsException("Wrong index: " + j);
            }

            public int size() {
                int sz = getChildren().size() - _hdcnt;
                if (_listfoot != null)
                    --sz;
                if (_paging != null)
                    --sz;
                if (_frozen != null)
                    --sz;
                return sz;
            }

            /**
             * override for Listgroup
             *
             * @since 3.5.1
             */
            protected void removeRange(int fromIndex, int toIndex) {
                ListIterator<Listitem> it = listIterator(toIndex);
                for (int n = toIndex - fromIndex; --n >= 0 && it.hasPrevious();) {
                    it.previous();
                    it.remove();
                }
            }

            /**
             * Override to remove unnecessary Listitem re-indexing (when ROD is on, clear() is called frequently). 
             */
            public void clear() {
                final boolean oldFlag = setReplacingItem(true);
                try {
                    //Bug ZK-1834: if there are selected items, clear first.
                    if (getSelectedCount() > 0) {
                        clearSelection();

                        // Bug ZK-1842 Listbox scroll bug listheader sort 
                        _anchorLeft = _anchorTop = 0;
                    }
                    super.clear();
                } finally {
                    setReplacingItem(oldFlag);
                }
            }
        };
        _selItems = new LinkedHashSet<Listitem>(4);
        _roSelItems = Collections.unmodifiableSet(_selItems);

        _heads = new AbstractCollection<Component>() {
            public int size() {
                return _hdcnt;
            }

            public Iterator<Component> iterator() {
                return new Iter();
            }
        };
        _groupsInfo = new LinkedList<int[]>();
        _groups = new AbstractList<Listgroup>() {
            public int size() {
                return getGroupCount();
            }

            public Iterator<Listgroup> iterator() {
                return new IterGroups();
            }

            public Listgroup get(int index) {
                return (Listgroup) getItemAtIndex(_groupsInfo.get(index)[0]);
            }
        };
    }

    private int getRealIndex(int index) {
        final int offset = _model != null && !_renderAll ? getDataLoader().getOffset() : 0;
        return index - (offset < 0 ? 0 : offset);
    }

    @SuppressWarnings("unchecked")
    public <T extends Component> List<T> getChildren() {
        return (List<T>) new Children();
    }

    protected class Children extends AbstractComponent.Children {
        protected void removeRange(int fromIndex, int toIndex) {
            ListIterator<Component> it = listIterator(toIndex);
            for (int n = toIndex - fromIndex; --n >= 0 && it.hasPrevious();) {
                it.previous();
                it.remove();
            }
        }
    }

    /**
     * Initializes _dataListener and register the listener to the model
     */
    private void initDataListener() {
        if (_dataListener == null)
            _dataListener = new ListDataListener() {
                public void onChange(ListDataEvent event) {
                    onListDataChange(event);
                }
            };
        _model.addListDataListener(_dataListener);

        // ZK-3088: for updating group status
        if (_model instanceof GroupsListModel) {
            if (_groupsDataListener == null) {
                _groupsDataListener = new GroupsDataListener() {
                    public void onChange(GroupsDataEvent event) {
                        onGroupsDataChange(event);
                    }
                };
            }
            ((GroupsListModel) _model).getGroupsModel().addGroupsDataListener(_groupsDataListener);
        }
    }

    /**
     * Returns {@link Listhead} belonging to this listbox, or null if no list
     * headers at all.
     */
    public Listhead getListhead() {
        return _listhead;
    }

    /**
     * Returns {@link Listfoot} belonging to this listbox, or null if no list
     * footers at all.
     */
    public Listfoot getListfoot() {
        return _listfoot;
    }

    /**
     * Returns the frozen child.
     *
     * @since 5.0.0
     */
    public Frozen getFrozen() {
        return _frozen;
    }

    /**
     * Returns a collection of heads, including {@link #getListhead} and
     * auxiliary heads ({@link Auxhead}) (never null).
     *
     * @since 3.0.0
     */
    public Collection<Component> getHeads() {
        return _heads;
    }

    /**
     * Returns whether the HTML's select tag is used.
     */
    /* package */ boolean inSelectMold() {
        return "select".equals(getMold());
    }

    /**
     * Returns whether the check mark shall be displayed in front of each item.
     * <p>
     * Default: false.
     */
    public boolean isCheckmark() {
        return _checkmark;
    }

    /**
     * Sets whether the check mark shall be displayed in front of each item.
     * <p>
     * The check mark is a checkbox if {@link #isMultiple} returns true. It is a
     * radio button if {@link #isMultiple} returns false.
     */
    public void setCheckmark(boolean checkmark) {
        if (_checkmark != checkmark) {
            _checkmark = checkmark;
            smartUpdate("checkmark", checkmark);
        }
    }

    /**
     * Sets the inner width of this component. The inner width is the width of
     * the inner table. By default, it is 100%. That is, it is the same as the
     * width of this component. However, it is changed when the user is sizing
     * the column's width.
     *
     * <p>
     * Application developers rarely call this method, unless they want to
     * preserve the widths of sizable columns changed by the user. To preserve
     * the widths, the developer have to store the widths of all columns and the
     * inner width ({@link #getInnerWidth}), and then restore them when
     * re-creating this component.
     *
     * @param innerWidth
     *            the inner width. If null, "100%" is assumed.
     * @since 3.0.0
     */
    public void setInnerWidth(String innerWidth) {
        if (innerWidth == null)
            innerWidth = "100%";
        if (!_innerWidth.equals(innerWidth)) {
            _innerWidth = innerWidth;
            smartUpdate("innerWidth", innerWidth);
        }
    }

    /**
     * Returns the inner width of this component. The inner width is the width
     * of the inner table.
     * <p>
     * Default: "100%"
     *
     * @see #setInnerWidth
     * @since 3.0.0
     */
    public String getInnerWidth() {
        return _innerWidth;
    }

    /**
     * Returns whether to grow and shrink vertical to fit their given space, so
     * called vertical flexibility.
     *
     * <p>
     * Note: this attribute is ignored if {@link #setRows} is specified
     *
     * <p>
     * Default: false.
     */
    public boolean isVflex() {
        final String vflex = getVflex();
        if ("true".equals(vflex) || "min".equals(vflex)) {
            return true;
        }
        if (Strings.isBlank(vflex) || "false".equals(vflex)) {
            return false;
        }
        return Integer.parseInt(vflex) > 0;
    }

    /**
     * Sets whether to grow and shrink vertical to fit their given space, so
     * called vertical flexibility.
     *
     * <p>
     * Note: this attribute is ignored if {@link #setRows} is specified
     */
    public void setVflex(boolean vflex) {
        if (isVflex() != vflex) {
            setVflex(String.valueOf(vflex));
        }
    }

    @Override
    public void setVflex(String flex) { //ZK-4296: Error indicating incorrect usage when using both vflex and rows
        if (_rows != 0)
            throw new UiException("Not allowed to set vflex and rows at the same time");

        super.setVflex(flex);
    }

    /**
     * Returns whether it is disabled.
     * <p>
     * Default: false.
     */
    public boolean isDisabled() {
        return _disabled;
    }

    /**
     * Sets whether it is disabled.
     * <p>Note that it is only applied when mold is "select".
     */
    public void setDisabled(boolean disabled) {
        if (_disabled != disabled) {
            _disabled = disabled;
            smartUpdate("disabled", _disabled);
        }
    }

    /**
     * Returns the rows. Zero means no limitation.
     * <p>
     * Default: 0.
     */
    public int getRows() {
        return _rows;
    }

    /**
     * Sets the rows.
     * <p>
     * Note: Not allowed to set rows and height/vflex at the same time
     */
    public void setRows(int rows) throws WrongValueException {
        checkBeforeSetRows();

        if (rows < 0) {
            throw new WrongValueException("Illegal rows: " + rows);
        }
        if (_rows != rows) {
            _rows = rows;
            smartUpdate("rows", _rows);
        }
    }

    @Override
    public void setHeight(String height) {
        if (_rows != 0)
            throw new UiException("Not allowed to set height and rows at the same time");

        super.setHeight(height);
    }

    /**
     * Returns the seltype.
     * <p>
     * Default: "single".
     */
    public String getSeltype() {
        return _multiple ? "multiple" : "single";
    }

    /**
     * Sets the seltype.
     * Allowed values:single,multiple
     * 
     */
    public void setSeltype(String seltype) throws WrongValueException {
        if ("single".equals(seltype))
            setMultiple(false);
        else if ("multiple".equals(seltype))
            setMultiple(true);
        else
            throw new WrongValueException("Unknown seltype: " + seltype);
    }

    /**
     * Returns whether multiple selections are allowed.
     * <p>
     * Default: false.
     */
    public boolean isMultiple() {
        return _multiple;
    }

    /**
     * Sets whether multiple selections are allowed.
     * <p>Notice that, if a model is assigned, it will change the model's
     * state (by {@link Selectable#setMultiple}).
     */
    @SuppressWarnings("rawtypes")
    public void setMultiple(boolean multiple) {
        if (_multiple != multiple) {
            _multiple = multiple;
            if (!_multiple && _selItems.size() > 1) {
                final Listitem item = getSelectedItem();
                for (Iterator<Listitem> it = _selItems.iterator(); it.hasNext();) {
                    final Listitem li = it.next();
                    if (li != item) {
                        li.setSelectedDirectly(false);
                        it.remove();
                    }
                }
                // No need to update selId because multiple will do the job at
                // client
            }
            if (_model != null)
                ((Selectable) _model).setMultiple(multiple);
            smartUpdate("multiple", _multiple);
        }
    }

    /**
     * Returns the maximal length of each item's label.
     * <p>
     * It is meaningful only for the select mold.
     */
    public int getMaxlength() {
        return _maxlength;
    }

    /**
     * Sets the maximal length of each item's label.
     * <p>
     * It is meaningful only for the select mold.
     */
    public void setMaxlength(int maxlength) {
        if (maxlength < 0)
            maxlength = 0;
        if (_maxlength != maxlength) {
            _maxlength = maxlength;
            smartUpdate("maxlength", maxlength);
        }
    }

    /**
     * Returns the name of this component.
     * <p>
     * Default: null.
     * <p>
     * The name is used only to work with "legacy" Web application that handles
     * user's request by servlets. It works only with HTTP/HTML-based browsers.
     * It doesn't work with other kind of clients.
     * <p>
     * Don't use this method if your application is purely based on ZK's
     * event-driven model.
     */
    public String getName() {
        return _name;
    }

    /**
     * Sets the name of this component.
     * <p>
     * The name is used only to work with "legacy" Web application that handles
     * user's request by servlets. It works only with HTTP/HTML-based browsers.
     * It doesn't work with other kind of clients.
     * <p>
     * Don't use this method if your application is purely based on ZK's
     * event-driven model.
     *
     * @param name
     *            the name of this component.
     */
    public void setName(String name) {
        if (name != null && name.length() == 0)
            name = null;
        if (!Objects.equals(_name, name)) {
            _name = name;
            smartUpdate("name", name);
        }
    }

    /** Sets a list of HTML tag names that shall <i>not</i> cause the list item
     * being selected if they are clicked.
     * <p>Default: null (it means button, input, textarea and a). If you want
     * to select no matter which tag is clicked, please specify an empty string.
     * @param tags a list of HTML tag names that will <i>not</i> cause the list item
     * being selected if clicked. Specify null to use the default and "" to
     * indicate none.
     * @since 5.0.5
     */
    public void setNonselectableTags(String tags) {
        if (!Objects.equals(_nonselTags, tags)) {
            _nonselTags = tags;
            smartUpdate("nonselectableTags", tags);
        }
    }

    /** Returns a list of HTML tag names that shall <i>not</i> cause the list item
     * being selected if they are clicked.
     * <p>Refer to {@link #setNonselectableTags} for details.
     * @since 5.0.5
     */
    public String getNonselectableTags() {
        return _nonselTags;
    }

    /**
     * Returns a live list of all {@link Listitem}. By live we mean you can add
     * or remove them directly with the List interface. In other words, you
     * could add or remove an item by manipulating the returned list directly.
     */
    public List<Listitem> getItems() {
        return _items;
    }

    /**
     * Returns the number of items.
     */
    public int getItemCount() {
        return _items.size();
    }

    /**
     * Returns the item at the specified index.
     *
     * <p>
     * Note: if live data is used ({@link #getModel} is not null), the returned
     * item might NOT be loaded yet. To ensure it is loaded, you have to invoke
     * {@link #renderItem}.
     */
    public Listitem getItemAtIndex(int index) {
        final int realindex = getRealIndex(index);
        return realindex < 0 || realindex >= _items.size() ? null : _items.get(realindex);
    }

    /**
     * Returns the index of the specified item, or -1 if not found.
     */
    public int getIndexOfItem(Listitem item) {
        return item == null ? -1 : item.getIndex();
    }

    /**
     * Returns the index of the selected item (-1 if no one is selected).
     */
    public int getSelectedIndex() {
        return _jsel;
    }

    /* package */boolean isLoadingModel() {
        return getAttribute(LOADING_MODEL) != null;
    }

    /**
     * Deselects all of the currently selected items and selects the item with
     * the given index.
     */
    @SuppressWarnings("rawtypes")
    public void setSelectedIndex(int jsel) {
        final int isz = _items.size();
        final int tsz = _model != null ? _model.getSize() : isz;
        if (jsel >= tsz)
            throw new UiException("Out of bound: " + jsel + " while size=" + tsz);

        if (jsel < -1)
            jsel = -1;
        if (jsel < 0) { // unselect all
            clearSelection();
        } else if (jsel != _jsel || (_multiple && _selItems.size() > 1) || !_selItems.contains(getItemAtIndex(_jsel))) {
            for (Listitem item : _selItems) {
                item.setSelectedDirectly(false);
            }
            _selItems.clear();
            _jsel = jsel;
            Listitem item = getItemAtIndex(_jsel);

            if (item == null) { // to be selected item is not there
                if (inPagingMold()) {
                    final int offset = _jsel - _jsel % getPageSize();
                    final int limit = getPageSize();
                    getDataLoader().syncModel(offset, limit); // force reloading
                    if (_jsel != jsel) //Bug ZK-1537: _jsel changed after syncModel if model is never synchronized
                        _jsel = jsel;
                    //ZK-3103: this will be triggered once with model, scrolling selection into view
                    _shallSyncSelInView = true;
                } else {
                    smartUpdate("selInView_", _jsel);
                }
            } else {
                if (!item.isDisabled()) { // Bug ZK-1715: not select item if disabled.
                    item.setSelectedDirectly(true);
                    _selItems.add(item);
                    //ZK-3103: should scroll selection into view
                    //if model is present, this will be erroneously triggered twice
                    if (_model == null || !_rod)
                        _shallSyncSelInView = true;
                }
            }

            if (inSelectMold()) {
                smartUpdate("selectedIndex", _jsel);
            } else if (item != null)
                smartUpdate("selectedItem", item);
            // Bug 1734950: don't count on index (since it may change)
            // On the other hand, it is OK with select-mold since
            // it invalidates if items are added or removed
        }

        if (_jsel >= 0 && inPagingMold()) {
            final Listitem item = getItemAtIndex(_jsel);
            int size = getDataLoader().getOffset();
            for (Iterator it = new VisibleChildrenIterator(true); it.hasNext(); size++)
                if (item.equals(it.next()))
                    break;

            final int pg = size / getPageSize();
            if (pg != getActivePage())
                setActivePage(pg);
        }
    }

    /**
     * Deselects all of the currently selected items and selects the given item.
     * <p>
     * It is the same as {@link #setSelectedItem}.
     *
     * @param item
     *            the item to select. If null, all items are deselected.
     */
    public void selectItem(Listitem item) {
        if (item == null) {
            setSelectedIndex(-1);
        } else {
            if (item.getParent() != this)
                throw new UiException("Not a child: " + item);
            if (_multiple || !item.isSelected())
                setSelectedIndex(item.getIndex());
        }
    }

    /**
     * Selects the given item, without deselecting any other items that are
     * already selected..
     *
     * <p>Notice that if you assign a model to a listbox ({@link #setModel}),
     * you shall not invoke this method directly. Rather, use {@link Selectable}
     * instead.
     */
    public void addItemToSelection(Listitem item) {
        if (item.getParent() != this)
            throw new UiException("Not a child: " + item);

        if (!item.isSelected()) {
            if (!_multiple) {
                selectItem(item);
            } else {
                if (item.getIndex() < _jsel || _jsel < 0) {
                    _jsel = item.getIndex();
                    if (!_ignoreSelectedItem) {
                        // ZK-866
                        // update the change of selected index
                        if (inSelectMold()) {
                            smartUpdate("selectedIndex", _jsel);
                        } else if (item != null)
                            smartUpdate("selectedItem", item);
                    }
                }
                if (!item.isDisabled()) { // Bug ZK-1715: not select item if disabled.
                    item.setSelectedDirectly(true);
                    _selItems.add(item);
                }

                // ZK-2113: should be same as normal mold
                //                if (inSelectMold()) {
                //                    item.smartUpdate("selected", true);
                //                } else {
                //                }

                smartUpdateSelection();
            }
        }
    }

    /**
     * Deselects the given item without deselecting other items.
     *
     * <p>Notice that if you assign a model to a listbox ({@link #setModel}),
     * you shall not invoke this method directly. Rather, use {@link Selectable}
     * instead.
     */
    public void removeItemFromSelection(Listitem item) {
        if (item.getParent() != this)
            throw new UiException("Not a child: " + item);

        if (item.isSelected()) {
            if (!_multiple) {
                clearSelection();
            } else {
                item.setSelectedDirectly(false);
                _selItems.remove(item);
                fixSelectedIndex(0);

                // ZK-2113: should be same as normal mold
                //                if (inSelectMold()) {
                //                    item.smartUpdate("selected", false);
                //                } else {
                //                }

                smartUpdateSelection();
            }
        }
    }

    /**
     * Note: we have to update all selection at once, since addItemToSelection
     * and removeItemFromSelection might be called interchangeably.
     */
    private void smartUpdateSelection() {
        final StringBuffer sb = new StringBuffer(80);
        for (Listitem item : _selItems) {
            if (sb.length() > 0)
                sb.append(',');
            sb.append(item.getUuid());
        }
        smartUpdate("chgSel", sb.toString());
    }

    /**
     * If the specified item is selected, it is deselected. If it is not
     * selected, it is selected. Other items in the list box that are selected
     * are not affected, and retain their selected state.
     */
    public void toggleItemSelection(Listitem item) {
        if (item.isSelected())
            removeItemFromSelection(item);
        else
            addItemToSelection(item);
    }

    /**
     * Clears the selection.
     */
    public void clearSelection() {
        if (!_selItems.isEmpty()) {
            for (Listitem item : _selItems) {
                item.setSelectedDirectly(false);
            }
            _selItems.clear();
            _jsel = -1;
            if (inSelectMold())
                smartUpdate("selectedIndex", -1);
            else
                smartUpdate("selectedItem", null);
            // Bug 1734950: don't count on index (since it may change)
        }
    }

    /**
     * Selects all items.
     */
    public void selectAll() {
        if (!_multiple)
            throw new UiException("Appliable only to the multiple seltype: " + this);

        if (_items.size() != _selItems.size()) {
            for (Listitem item : _items) {
                _selItems.add(item);
                item.setSelectedDirectly(true);
            }
            _jsel = _items.isEmpty() ? -1 : 0;
            smartUpdate("selectAll", true);
        }
    }

    /**
     * Returns the selected item.
     *
     * <p>
     * Note: if live data is used ({@link #getModel} is not null), the returned
     * item might NOT be loaded yet. To ensure it is loaded, you have to invoke
     * {@link #renderItem}.
     */
    public Listitem getSelectedItem() {
        return _jsel >= 0 ? _jsel > 0 && _selItems.size() == 1 // optimize for performance
                ? _selItems.iterator().next() : getItemAtIndex(_jsel) : null;
    }

    /**
     * Deselects all of the currently selected items and selects the given item.
     * <p>
     * It is the same as {@link #selectItem}.
     */
    public void setSelectedItem(Listitem item) {
        selectItem(item);
    }

    /**
     * Selects the given listitems.
     *
     * @since 3.6.0
     */
    public void setSelectedItems(Set<Listitem> listItems) {
        if (!isMultiple())
            throw new WrongValueException("Listbox must allow multiple selections.");
        for (Iterator<Listitem> it = listItems.iterator(); it.hasNext();) {
            addItemToSelection(it.next());
        }
    }

    /**
     * Returns all selected items.
     *
     * <p>
     * Note: if live data is used ({@link #getModel} is not null), the returned
     * item might NOT be loaded yet. To ensure it is loaded, you have to invoke
     * {@link #renderItem}.
     */
    public Set<Listitem> getSelectedItems() {
        return _roSelItems;
    }

    /**
     * Returns the number of items being selected.
     */
    public int getSelectedCount() {
        return _selItems.size();
    }

    /**
     * Appends an item.
     *
     * <p>
     * Note: if live data is used ({@link #getModel} is not null), the returned
     * item might NOT be loaded yet. To ensure it is loaded, you have to invoke
     * {@link #renderItem}.
     */
    public Listitem appendItem(String label, String value) {
        final Listitem item = new Listitem(label, value);
        item.applyProperties();
        item.setParent(this);
        return item;
    }

    /**
     * Removes the child item in the list box at the given index.
     *
     * <p>
     * Note: if live data is used ({@link #getModel} is not null), the returned
     * item might NOT be loaded yet. To ensure it is loaded, you have to invoke
     * {@link #renderItem}.
     *
     * @return the removed item.
     */
    public Listitem removeItemAt(int index) {
        final Listitem item = getItemAtIndex(index);
        removeChild(item);
        return item;
    }

    // --Paging--//
    /**
     * Returns the paging controller, or null if not available. Note: the paging
     * controller is used only if {@link #getMold} is "paging".
     *
     * <p>
     * If mold is "paging", this method never returns null, because a child
     * paging controller is created automatically (if not specified by
     * developers with {@link #setPaginal}).
     *
     * <p>
     * If a paging controller is specified (either by {@link #setPaginal}, or by
     * {@link #setMold} with "paging"), the listbox will rely on the paging
     * controller to handle long-content instead of scrolling.
     */
    public Paginal getPaginal() {
        return _pgi;
    }

    /**
     * Specifies the paging controller. Note: the paging controller is used only
     * if {@link #getMold} is "paging".
     *
     * <p>It is OK, though without any effect, to specify a paging controller
     * even if mold is not "paging".
     *
     * @param pgi the paging controller. If null and {@link #getMold} is
     * "paging", a paging controller is created automatically as a child
     * component (see {@link #getPagingChild}).
     */
    public void setPaginal(Paginal pgi) {
        if (!Objects.equals(pgi, _pgi)) {
            final Paginal old = _pgi;
            _pgi = pgi; // assign before detach paging, since removeChild
            // assumes it

            if (inPagingMold()) {
                if (old != null)
                    removePagingListener(old);
                if (_pgi == null) {
                    if (_paging != null) {
                        _pgi = _paging;
                    } else
                        newInternalPaging();
                } else { // _pgi != null
                    if (_pgi != _paging) {
                        if (_paging != null)
                            _paging.detach();
                        _pgi.setTotalSize(getDataLoader().getTotalSize());
                        addPagingListener(_pgi);
                        if (_pgi instanceof Component)
                            smartUpdate("paginal", _pgi);
                    }
                }
                // Bug ZK-1696: model also preserves paging information
                if (_model instanceof Pageable) {
                    Pageable m = (Pageable) _model;
                    m.setPageSize(_pgi.getPageSize());
                    m.setActivePage(_pgi.getActivePage());
                }
            }
        }
    }

    /**
     * Creates the internal paging component.
     */
    private void newInternalPaging() {
        final Paging paging = new InternalPaging();
        paging.setDetailed(true);
        paging.applyProperties();
        //min page size is 1
        if (_model instanceof Pageable && ((Pageable) _model).getPageSize() > 0) {
            paging.setPageSize(((Pageable) _model).getPageSize());
        }
        paging.setTotalSize(getDataLoader().getTotalSize());
        //min page index is 0
        if (_model instanceof Pageable && ((Pageable) _model).getActivePage() >= 0) {
            paging.setActivePage(((Pageable) _model).getActivePage());
        }
        paging.setParent(this);

        if (_pgi != null)
            addPagingListener(_pgi);
    }

    @SuppressWarnings("serial")
    private class PGListener implements PagingListener {
        public void onEvent(Event event) {
            //Bug ZK-1622: reset anchor position after changing page
            _anchorTop = 0;
            _anchorLeft = 0;
            if (event instanceof PagingEvent) {
                PagingEvent pe = (PagingEvent) event;
                int pgsz = pe.getPageable().getPageSize();
                int actpg = pe.getActivePage();
                if (PageableModel.INTERNAL_EVENT.equals(event.getName())) {
                    if (pgsz > 0) //min page size is 1
                        _pgi.setPageSize(pgsz);
                    if (actpg >= 0) //min page index is 0
                        _pgi.setActivePage(actpg);
                } else if (_model instanceof Pageable) {
                    // Bug ZK-1696: model also preserves paging information
                    // additional check to avoid infinite loop
                    ((Pageable) _model).setActivePage(actpg);
                }
                Events.postEvent(new PagingEvent(event.getName(), Listbox.this, pe.getPageable(), actpg));
            }
        }

        public Object willClone(Component comp) {
            return null; // skip to clone
        }
    }

    @SuppressWarnings("serial")
    private class PGImpListener implements PagingListener {
        public void onEvent(Event event) {
            if (_model != null && inPagingMold()) {
                final Paginal pgi = getPaginal();
                int pgsz = pgi.getPageSize();
                int ofs = pgi.getActivePage() * pgsz;
                if (event instanceof PagingEvent) {
                    // Bug ZK-1696: PagingEvent have the newest paging information
                    pgsz = ((PagingEvent) event).getPageable().getPageSize();
                    ofs = ((PagingEvent) event).getActivePage() * pgsz;
                }
                if (_model instanceof Pageable) {
                    ((Pageable) _model).setPageSize(pgsz);
                    ((Pageable) _model).setActivePage(pgi.getActivePage());
                }
                if (_rod) {
                    getDataLoader().syncModel(ofs, pgsz);
                }
                postOnPagingInitRender();
            }
            invalidate();
        }

        public Object willClone(Component comp) {
            return null; // skip to clone
        }
    }

    /** Adds the event listener for the onPaging event. */
    private void addPagingListener(Paginal pgi) {
        if (_pgListener == null)
            _pgListener = new PGListener();
        pgi.addEventListener(ZulEvents.ON_PAGING, _pgListener);
        if (_model instanceof PageableModel) {
            ((PageableModel) _model).addPagingEventListener((PagingListener) _pgListener);
        }
        if (_pgImpListener == null)
            _pgImpListener = new PGImpListener();

        pgi.addEventListener("onPagingImpl", _pgImpListener);
    }

    /** Removes the event listener for the onPaging event. */
    private void removePagingListener(Paginal pgi) {
        if (_model instanceof PageableModel) {
            ((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
        }
        pgi.removeEventListener(ZulEvents.ON_PAGING, _pgListener);
        pgi.removeEventListener("onPagingImpl", _pgImpListener);
    }

    /**
     * Returns the child paging controller that is created automatically, or
     * null if mold is not "paging", or the controller is specified externally
     * by {@link #setPaginal}.
     *
     * @since 3.0.7
     */
    public Paging getPagingChild() {
        return _paging;
    }

    protected Paginal pgi() {
        if (_pgi == null)
            throw new IllegalStateException("Available only the paging mold");
        return _pgi;
    }

    /**
     * Sets the active page in which the specified item is. The active page will
     * become the page that contains the specified item.
     *
     * @param item
     *            the item to show. If the item is null or doesn't belong to
     *            this listbox, nothing happens.
     * @since 3.0.4
     * @see #setActivePage(int)
     */
    public void setActivePage(Listitem item) {
        if (item != null && item.getParent() == this) {
            final int pg = item.getIndex() / getPageSize();
            if (pg != getActivePage())
                setActivePage(pg);
        }
    }

    @Override
    public void setActivePage(int pg) throws WrongValueException {
        // Bug ZK-1696: model also preserves paging information
        if (_model instanceof Pageable) {
            ((Pageable) _model).setActivePage(pg);
        }
        super.setActivePage(pg);
    }

    /**
     * Returns whether this listbox is in the paging mold.
     */
    /* package */boolean inPagingMold() {
        return "paging".equals(getMold());
    }

    /**
     * Returns the number of visible descendant {@link Listitem}.
     *
     * @since 3.5.1
     */
    public int getVisibleItemCount() {
        return _visibleItemCount;
    }

    /* package */void addVisibleItemCount(int count) {
        if (count != 0) {
            _visibleItemCount += count;
            if (inPagingMold()) {
                final Paginal pgi = getPaginal();
                int totalSize = getDataLoader().getTotalSize();
                if (count < 0 && _model instanceof Pageable) {
                    Pageable p = (Pageable) _model;
                    int actpg = p.getActivePage();
                    int maxPageIndex = p.getPageCount() - 1;
                    if (actpg > maxPageIndex) {
                        p.setActivePage(maxPageIndex);
                    }
                }
                pgi.setTotalSize(totalSize);
                invalidate(); // the set of visible items might change
            } else if (((Cropper) getDataLoader()).isCropper()) {
                getDataLoader().updateModelInfo();
            } else {
                smartUpdate("visibleItemCount", _visibleItemCount);
            }
        }
    }

    /**
     * Returns the style class for the odd rows.
     * <p>
     * Default: {@link #getZclass()}-odd. (since 3.5.0)
     *
     * @since 3.0.0
     */
    public String getOddRowSclass() {
        return _scOddRow == null ? getZclass() + "-odd" : _scOddRow;
    }

    /**
     * Sets the style class for the odd rows. If the style class doesn't exist,
     * the striping effect disappears. You can provide different effects by
     * providing the proper style classes.
     *
     * @since 3.0.0
     */
    public void setOddRowSclass(String scls) {
        if (scls != null && scls.length() == 0)
            scls = null;
        if (!Objects.equals(_scOddRow, scls)) {
            _scOddRow = scls;
            smartUpdate("oddRowSclass", scls);
        }
    }

    /**
     * Returns the number of listgroup
     *
     * @since 3.5.0
     */
    public int getGroupCount() {
        return _groupsInfo.size();
    }

    /**
     * Returns a list of all {@link Listgroup}.
     *
     * @since 3.5.0
     */
    public List<Listgroup> getGroups() {
        return _groups;
    }

    /**
     * Returns whether listgroup exists.
     *
     * @since 3.5.0
     */
    public boolean hasGroup() {
        return !_groupsInfo.isEmpty();
    }

    /** Sets true to avoid unnecessary Listitem re-indexing when render template.
     * @param b true to skip
     * @return original true/false status
     * @see Renderer#render
     */
    /* package */boolean setReplacingItem(boolean b) {
        final boolean old = _isReplacingItem;
        if (_model != null) // B60-ZK-898: only apply when model is used.
            _isReplacingItem = b;
        return old;
    }

    /* package */void fixItemIndices(int j, int to, boolean infront) {
        int realj = getRealIndex(j);
        if (realj < 0) {
            realj = 0;
        }
        if (realj < _items.size()) {
            final int beginning = j;
            for (Iterator<Listitem> it = _items.listIterator(realj); it.hasNext() && (to < 0 || j <= to); ++j) {
                Listitem o = it.next();
                o.setIndexDirectly(j);

                if (_isReplacingItem) //@see Renderer#render
                    break; //set only the first Listitem, skip handling GroupInfo

                // if beginning is a group, we don't need to change its groupInfo,
                // because
                // it is not reliable when infront is true.
                if ((!infront || beginning != j) && o instanceof Listgroup) {
                    int[] g = getLastGroupsInfoAt(j + (infront ? -1 : 1));
                    if (g != null) {
                        g[0] = j;
                        if (g[2] != -1)
                            g[2] += (infront ? 1 : -1);
                    }
                }
            }
        }
    }

    /**
     * Fix Childitem._index since j-th item.
     *
     * @param j
     *            the start index (inclusion)
     * @param to
     *            the end index (inclusion). If -1, up to the end.
     */
    private void fixItemIndices(int j, int to) {
        int realj = getRealIndex(j);
        if (realj < 0)
            realj = 0;
        if (realj < _items.size()) {
            for (Iterator<Listitem> it = _items.listIterator(realj); it.hasNext() && (to < 0 || j <= to); ++j)
                it.next().setIndexDirectly(j);
        }
    }
    
    /* package */Listgroup getListgroupAt(int index) {
        if (_groupsInfo.isEmpty())
            return null;
        final int[] g = getGroupsInfoAt(index);
        if (g != null) {
            return (Listgroup) getItemAtIndex(g[0]);
        }
        return null;
    }

    /**
     * Returns the group index which matches with the ListModel index.
     *
     * @param index
     *            the list item index
     * @return the associated group index of the list item index.
     */
    /* package */int getGroupIndex(int index) {
        int j = 0, gindex = -1;
        int[] g = null;
        for (Iterator<int[]> it = _groupsInfo.iterator(); it.hasNext(); ++j) {
            g = it.next();
            if (index == g[0])
                gindex = j;
            else if (index < g[0])
                break;
        }
        return gindex != -1 ? gindex
                : g != null && index < (g[0] + g[1]) ? (j - 1)
                        : g != null && index == (g[0] + g[1]) && g[2] == -1 ? (j - 1) : gindex;
    }

    /* package */int[] getGroupsInfoAt(int index) {
        return getGroupsInfoAt(index, false);
    }
    
    /**
     * Returns an int array that it has two length, one is an index of
     * listgroup, and the other is the number of items of listgroup(inclusive).
     */
    /* package */int[] getGroupsInfoAt(int index, boolean isListgroup) {
        for (int[] g : _groupsInfo) {
            if (isListgroup) {
                if (index == g[0])
                    return g;
            } else if ((index > g[0] && index <= g[0] + g[1]))
                return g;
        }
        return null;
    }

    /**
     * Returns the last groups info which matches with the same index. Because
     * dynamically maintain the index of the groups will occur the same index at
     * the same time in the loop.
     */
    /* package */int[] getLastGroupsInfoAt(int index) {
        int[] rg = null;
        for (int[] g : _groupsInfo) {
            if (index == g[0])
                rg = g;
            else if (index < g[0])
                break;
        }
        return rg;
    }

    public void beforeChildAdded(Component newChild, Component refChild) {
        if (newChild instanceof Listitem) {
            if (newChild instanceof Listgroupfoot) {
                if (!hasGroup())
                    throw new UiException("Listgroupfoot cannot exist alone, you have to add a Listgroup first");
                if (refChild == null) {
                    if (getLastChild() instanceof Listgroupfoot)
                        throw new UiException("Only one Listgroupfoot is allowed per Listgroup");
                }
            }
        } else if (newChild instanceof Listhead) {
            if (_listhead != null && _listhead != newChild)
                throw new UiException("Only one listhead is allowed: " + this);
        } else if (newChild instanceof Frozen) {
            if (_frozen != null && _frozen != newChild)
                throw new UiException("Only one frozen child is allowed: " + this);
            if (inSelectMold())
                log.warn("Mold select ignores frozen");
        } else if (newChild instanceof Listfoot) {
            if (_listfoot != null && _listfoot != newChild)
                throw new UiException("Only one listfoot is allowed: " + this);
            if (inSelectMold())
                log.warn("Mold select ignores listfoot");
        } else if (newChild instanceof Paging) {
            if (_paging != null && _paging != newChild)
                throw new UiException("Only one paging is allowed: " + this);
            if (_pgi != null)
                throw new UiException("External paging cannot coexist with child paging");
            if (!inPagingMold())
                throw new UiException("The child paging is allowed only in the paging mold");
        } else if (!(newChild instanceof Auxhead)) {
            throw new UiException("Unsupported child for Listbox: " + newChild);
        }
        super.beforeChildAdded(newChild, refChild);
    }

    private boolean hasGroupsModel() {
        return _model instanceof GroupsListModel;
    }

    public boolean insertBefore(Component newChild, Component refChild) {
        if (newChild instanceof Listitem) {
            final boolean isReorder = newChild.getParent() == this;
            //bug #3051305: Active Page not update when drag & drop item to the end
            if (isReorder) {
                checkInvalidateForMoved((Listitem) newChild, true);
            }
            fixGroupsInfoBeforeInsert(newChild, refChild, isReorder);
            // first: listhead or auxhead
            // last two: listfoot and paging
            if (refChild != null && refChild.getParent() != this)
                refChild = null; // Bug 1649625: it becomes the last child
            if (refChild != null && (refChild == _listhead || refChild instanceof Auxhead))
                refChild = getChildren().size() > _hdcnt ? getChildren().get(_hdcnt) : null;

            refChild = fixRefChildBeforeFoot(refChild);
            final Listitem newItem = (Listitem) newChild;
            final int jfrom = newItem.getParent() == this ? newItem.getIndex() : -1;

            if (super.insertBefore(newChild, refChild)) {
                // Maintain _items
                final int jto = refChild instanceof Listitem ? ((Listitem) refChild).getIndex() : -1,
                        fixFrom = jfrom < 0 || (jto >= 0 && jfrom > jto) ? jto : jfrom;
                // jfrom < 0: use jto
                // jto < 0: use jfrom
                // otherwise: use min(jfrom, jto)
                if (fixFrom < 0)
                    newItem.setIndexDirectly(_items.size() - 1 + getDataLoader().getOffset());
                else
                    fixItemIndices(fixFrom, jfrom >= 0 && jto >= 0 ? jfrom > jto ? jfrom : jto : -1, !isReorder);

                // Maintain selected
                final int newIndex = newItem.getIndex();
                if (newItem.isSelected()) {
                    if (_jsel < 0) {
                        _jsel = newIndex;
                        _selItems.add(newItem);
                        smartUpdateSelection();
                    } else if (_multiple) {
                        if (_jsel > newIndex) {
                            _jsel = newIndex;
                        }
                        _selItems.add(newItem);
                        smartUpdateSelection();
                    } else { // deselect
                        newItem.setSelectedDirectly(false);
                    }
                } else {
                    if (jfrom < 0) { // no existent child
                        if (!isLoadingModel() && _jsel >= newIndex)
                            ++_jsel;
                    } else if (_jsel >= 0) { // any selected
                        if (jfrom > _jsel) { // from below
                            if (jto >= 0 && jto <= _jsel)
                                ++_jsel;
                        } else { // from above
                            if (jto < 0 || jto > _jsel)
                                --_jsel;
                        }
                    }
                }

                fixGroupsInfoAfterInsert(newItem);

                //bug #3049167: Totalsize increase when drag & drop in paging Listbox/Grid
                if (!isReorder) { //if reorder, not an insert
                    afterInsert(newChild);
                }

                return true;
            } // insert
        } else if (newChild instanceof Listhead) {
            final boolean added = _listhead == null;
            refChild = fixRefChildForHeader(refChild);
            if (super.insertBefore(newChild, refChild)) {
                _listhead = (Listhead) newChild;
                if (added)
                    ++_hdcnt; // it may be moved, not inserted
                return true;
            }
        } else if (newChild instanceof Auxhead) {
            final boolean added = newChild.getParent() != this;
            refChild = fixRefChildForHeader(refChild);
            if (super.insertBefore(newChild, refChild)) {
                if (added)
                    ++_hdcnt; // it may be moved, not inserted
                return true;
            }
        } else if (newChild instanceof Frozen) {
            refChild = _paging; // the last two: listfoot and paging
            if (super.insertBefore(newChild, refChild)) {
                _frozen = (Frozen) newChild;
                return true;
            }
        } else if (newChild instanceof Listfoot) {
            // the last two: listfoot and paging
            if (_frozen != null)
                refChild = _frozen;
            else
                refChild = _paging;
            if (super.insertBefore(newChild, refChild)) {
                _listfoot = (Listfoot) newChild;
                return true;
            }
        } else if (newChild instanceof Paging) {
            refChild = null; // the last: paging
            if (super.insertBefore(newChild, refChild)) {
                _pgi = _paging = (Paging) newChild;
                return true;
            }
        } else {
            return super.insertBefore(newChild, refChild);
            // impossible but to make it more extensible
        }
        return false;
    }

    private Component fixRefChildForHeader(Component refChild) {
        if (refChild != null && refChild.getParent() != this)
            refChild = null;

        // try the first listitem
        if (refChild == null || (refChild != _listhead && !(refChild instanceof Auxhead)))
            refChild = getChildren().size() > _hdcnt ? getChildren().get(_hdcnt) : null;

        // try listfoot or paging if no listem
        refChild = fixRefChildBeforeFoot(refChild);
        return refChild;
    }

    private Component fixRefChildBeforeFoot(Component refChild) {
        if (refChild == null) {
            if (_listfoot != null)
                refChild = _listfoot;
            else if (_frozen != null)
                refChild = _frozen;
            else
                refChild = _paging;
        } else if (refChild == _paging) {
            if (_listfoot != null)
                refChild = _listfoot;
            else if (_frozen != null)
                refChild = _frozen;
        }
        return refChild;
    }

    /**
     * If the child is a listgroup, its listgroupfoot will be removed at the
     * same time.
     */
    public boolean removeChild(Component child) {
        if (_paging == child && _pgi == child && inPagingMold())
            throw new IllegalStateException(
                    "The paging component cannot be removed manually. It is removed automatically when changing the mold");
        // Feature 1906110: prevent developers from removing it accidently

        if (child instanceof Listitem && child.getParent() == this)
            beforeRemove(child);

        if (!super.removeChild(child))
            return false;

        if (_listhead == child) {
            _listhead = null;
            --_hdcnt;
        } else if (_listfoot == child) {
            _listfoot = null;
        } else if (_frozen == child) {
            _frozen = null;
        } else if (child instanceof Listitem) {
            // maintain items
            final Listitem item = (Listitem) child;
            final int index = item.getIndex();
            item.setIndexDirectly(-1); // mark

            // Maintain selected
            if (item.isSelected()) {
                _selItems.remove(item);
                if (_jsel == index) {
                    fixSelectedIndex(index);
                }

                smartUpdateSelection();
            } else {
                if (!isLoadingModel() && _jsel >= index) {
                    --_jsel;
                }
            }
            fixGroupsInfoAfterRemove(child, index);
        } else if (_paging == child) {
            _paging = null;
            if (_pgi == child)
                _pgi = null;
        } else if (child instanceof Auxhead) {
            --_hdcnt;
        }

        if (((Cropper) getDataLoader()).isCropper()) {
            getDataLoader().updateModelInfo();
        }

        return true;
    }

    /**
     * Callback if a list item has been inserted.
     * <p>
     * Note: it won't be called if other kind of child is inserted.
     * <p>
     * When this method is called, the index is correct.
     * <p>
     * Default: invalidate if it is the paging mold and it affects the view of
     * the active page.
     *
     * @since 3.0.5
     */
    protected void afterInsert(Component comp) {
        if (_isReplacingItem) //@see Renderer#render
            return; //called by #insertBefore(), skip handling GroupInfo

        updateVisibleCount((Listitem) comp, false);
        checkInvalidateForMoved((Listitem) comp, false);
    }

    /**
     * Callback if a list item will be removed (not removed yet). Note: it won't
     * be called if other kind of child is removed.
     * <p>
     * Default: invalidate if it is the paging mold and it affects the view of
     * the active page.
     *
     * @since 3.0.5
     */
    protected void beforeRemove(Component comp) {
        if (_isReplacingItem) //@see Renderer#render
            return; //called by #removeChild(), skip handling GroupInfo

        updateVisibleCount((Listitem) comp, true);
        checkInvalidateForMoved((Listitem) comp, true);
    }

    /**
     * Update the number of the visible item before it is removed or after it is
     * added.
     */
    private void updateVisibleCount(Listitem item, boolean isRemove) {
        if (item instanceof Listgroup || item.isVisible()) {
            final Listgroup g = getListgroupAt(item.getIndex());

            // We shall update the number of the visible item in the following
            // cases.
            // 1) If the item is a type of Listgroupfoot, it is always shown.
            // 2) If the item is a type of Listgroup, it is always shown.
            // 3) If the item doesn't belong to any group.
            // 4) If the group of the item is open.
            if (item instanceof Listgroupfoot || item instanceof Listgroup || g == null || g.isOpen())
                addVisibleItemCount(isRemove ? -1 : 1);

            if (item instanceof Listgroup) {
                final Listgroup group = (Listgroup) item;

                // If the previous group exists, we shall update the number of
                // the visible item from the number of the visible item of the
                // current group.
                if (item.getPreviousSibling() instanceof Listitem) {
                    final Listitem preRow = (Listitem) item.getPreviousSibling();
                    if (preRow == null) {
                        if (!group.isOpen()) {
                            addVisibleItemCount(isRemove ? group.getVisibleItemCount() : -group.getVisibleItemCount());
                        }
                    } else {
                        final Listgroup preGroup = preRow instanceof Listgroup ? (Listgroup) preRow
                                : getListgroupAt(preRow.getIndex());
                        if (preGroup != null) {
                            if (!preGroup.isOpen() && group.isOpen())
                                addVisibleItemCount(
                                        isRemove ? -group.getVisibleItemCount() : group.getVisibleItemCount());
                            else if (preGroup.isOpen() && !group.isOpen())
                                addVisibleItemCount(
                                        isRemove ? group.getVisibleItemCount() : -group.getVisibleItemCount());
                        } else {
                            if (!group.isOpen())
                                addVisibleItemCount(
                                        isRemove ? group.getVisibleItemCount() : -group.getVisibleItemCount());
                        }
                    }
                } else if (!group.isOpen()) {
                    addVisibleItemCount(isRemove ? group.getVisibleItemCount() : -group.getVisibleItemCount());
                }
            }
        }
        if (inPagingMold()) {
            int totalSize = getDataLoader().getTotalSize();
            if (isRemove && _model instanceof Pageable) {
                Pageable p = (Pageable) _model;
                int actpg = p.getActivePage();
                int maxPageIndex = p.getPageCount() - 1;
                if (actpg > maxPageIndex) {
                    p.setActivePage(maxPageIndex);
                }
            }
            getPaginal().setTotalSize(totalSize);
        }
    }

    /**
     * Checks whether to invalidate, when a child has been added or or will be
     * removed.
     *
     * @param bRemove
     *            if child will be removed
     */
    private void checkInvalidateForMoved(Listitem child, boolean bRemove) {
        // No need to invalidate if
        // 1) act == last and child in act
        // 2) act != last and child after act
        // Except removing last elem which in act and act has only one elem
        if (inPagingMold() && !isInvalidated()) {
            final int j = child.getIndex(), pgsz = getPageSize(), n = (getActivePage() + 1) * pgsz;
            if (j >= n)
                return; // case 2

            final int cnt = getItems().size(), n2 = n - pgsz;
            if (j >= n2 && cnt <= n && (!bRemove || cnt > n2 + 1))
                return; // case 1

            invalidate();
        }
    }

    /**
     * An iterator used by visible children.
     */
    @SuppressWarnings("rawtypes")
    private class VisibleChildrenIterator implements Iterator {
        private final ListIterator<Listitem> _it = getItems().listIterator();
        private int _count = 0;
        private boolean _isBeginning = false;

        private VisibleChildrenIterator() {
        }

        private VisibleChildrenIterator(boolean isBeginning) {
            _isBeginning = isBeginning;
        }

        public boolean hasNext() {
            if (!inPagingMold())
                return _it.hasNext();

            if (!_isBeginning && _count >= getPaginal().getPageSize()) {
                return false;
            }

            if (_count == 0 && !_isBeginning) {
                final Paginal pgi = getPaginal();
                int begin = pgi.getActivePage() * pgi.getPageSize();
                for (int i = 0; i < begin && _it.hasNext();) {
                    getVisibleRow((Listitem) _it.next());
                    i++;
                }
            }
            return _it.hasNext();
        }

        private Listitem getVisibleRow(Listitem item) {
            if (item instanceof Listgroup) {
                final Listgroup g = (Listgroup) item;
                if (!g.isOpen()) {
                    for (int j = 0, len = g.getItemCount(); j < len && _it.hasNext(); j++)
                        _it.next();
                }
            }
            while (!item.isVisible() && _it.hasNext())
                item = (Listitem) _it.next();
            return item;
        }

        public Object next() {
            if (!inPagingMold())
                return _it.next();
            _count++;
            final Listitem item = (Listitem) _it.next();
            return _it.hasNext() ? getVisibleRow(item) : item;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * Fix the selected index, _jsel, assuming there are no selected one before
     * (and excludes) j-the item.
     */
    private void fixSelectedIndex(int j) {
        if (!_selItems.isEmpty()) {
            int realj = getRealIndex(j);
            if (realj < 0)
                realj = 0;
            if (realj < _items.size()) {
                for (Iterator<Listitem> it = _items.listIterator(realj); it.hasNext(); ++j) {
                    final Listitem item = it.next();
                    if (item.isSelected()) {
                        _jsel = j;
                        return;
                    }
                }
            }
        }
        _jsel = -1;
    }

    private void fixGroupsInfoBeforeInsert(Component newChild, Component refChild, boolean isReorder) {
        if (_isReplacingItem) //@see Renderer#render
            return; //called by #insertBefore(), skip handling GroupInfo

        if (newChild instanceof Listgroupfoot) {
            if (refChild == null) {
                if (isReorder) {
                    final int idx = ((Listgroupfoot) newChild).getIndex();
                    final int[] ginfo = getGroupsInfoAt(idx);
                    if (ginfo != null) {
                        ginfo[1]--;
                        ginfo[2] = -1;
                    }
                }
                final int[] g = _groupsInfo.get(getGroupCount() - 1);

                g[2] = getItems().get(getItems().size() - 1).getIndex();
            } else if (refChild instanceof Listitem) {
                final int idx = ((Listitem) refChild).getIndex();
                final int[] g = getGroupsInfoAt(idx);
                if (g == null)
                    throw new UiException("Listgroupfoot cannot exist alone, you have to add a Listgroup first");
                if (g[2] != -1)
                    throw new UiException("Only one Listgroupfoot is allowed per Listgroup");
                if (idx != (g[0] + g[1]))
                    throw new UiException("Listgroupfoot must be placed after the last Row of the Listgroup");
                g[2] = idx - 1;
                if (isReorder) {
                    final int nindex = ((Listgroupfoot) newChild).getIndex();
                    final int[] ginfo = getGroupsInfoAt(nindex);
                    if (ginfo != null) {
                        ginfo[1]--;
                        ginfo[2] = -1;
                    }
                }
            } else {
                final Component preRefChild = refChild.getPreviousSibling();
                if (preRefChild instanceof Listitem) {
                    final int idx = ((Listitem) preRefChild).getIndex();
                    //bug 2936019: Execption when Listbox insertBefore() group + groupfoot
                    final int[] g = getGroupsInfoAt(idx, preRefChild instanceof Listgroup);
                    if (g == null)
                        throw new UiException("Listgroupfoot cannot exist alone, you have to add a Listgroup first");
                    if (g[2] != -1)
                        throw new UiException("Only one Listgroupfoot is allowed per Listgroup");
                    if (idx + 1 != (g[0] + g[1]))
                        throw new UiException("Listgroupfoot must be placed after the last Row of the Listgroup");
                    g[2] = idx;
                    if (isReorder) {
                        final int nindex = ((Listgroupfoot) newChild).getIndex();
                        final int[] ginfo = getGroupsInfoAt(nindex);
                        if (ginfo != null) {
                            ginfo[1]--;
                            ginfo[2] = -1;
                        }
                    }
                }
            }
        }
    }

    private void fixGroupsInfoAfterInsert(Listitem newItem) {
        if (_isReplacingItem) //@see Renderer#render
            return; //called by #insertBefore(), skip handling GroupInfo

        if (newItem instanceof Listgroup) {
            Listgroup lg = (Listgroup) newItem;
            if (_groupsInfo.isEmpty())
                _groupsInfo.add(new int[] { lg.getIndex(), getItemCount() - lg.getIndex(), -1 });
            else {
                int idx = 0;
                int[] prev = null, next = null;
                for (int[] g : _groupsInfo) {
                    if (g[0] <= lg.getIndex()) {
                        prev = g;
                        idx++;
                    } else {
                        next = g;
                        break;
                    }
                }
                if (prev != null) {
                    int index = lg.getIndex(), leng = index - prev[0], size = prev[1] - leng + 1;
                    prev[1] = leng;
                    _groupsInfo.add(idx, new int[] { index, size, size > 1 && prev[2] >= index ? prev[2] + 1 : -1 });
                    if (size > 1 && prev[2] > index)
                        prev[2] = -1; // reset listgroupfoot
                } else if (next != null) {
                    _groupsInfo.add(idx, new int[] { lg.getIndex(), next[0] - lg.getIndex(), -1 });
                }
            }
        } else if (!_groupsInfo.isEmpty()) {
            int index = newItem.getIndex();
            final int[] g = getGroupsInfoAt(index);
            if (g != null) {
                g[1]++;
                if (g[2] != -1 && (g[2] >= index || newItem instanceof Listgroupfoot))
                    g[2] = g[0] + g[1] - 1;
            }
        }
    }

    private void fixGroupsInfoAfterRemove(Component child, int index) {
        if (!_isReplacingItem) { //@see Renderer#render
            //called by #removeChild(), handling GroupInfo if !isReplcingItem
            if (child instanceof Listgroup) {
                int[] prev = null, remove = null;
                for (int[] g : _groupsInfo) {
                    if (g[0] == index) {
                        remove = g;
                        break;
                    }
                    prev = g;
                }
                if (prev != null && remove != null) {
                    prev[1] += remove[1] - 1;
                }
                fixItemIndices(index, -1, false);
                if (remove != null) {
                    _groupsInfo.remove(remove);
                    final int idx = remove[2];
                    if (idx != -1) {
                        final int realIndex = getRealIndex(idx) - 1;
                        if (realIndex >= 0 && realIndex < getItemCount())
                            removeChild(getChildren().get(realIndex));
                    }
                }
            } else if (!_groupsInfo.isEmpty()) {
                final int[] g = getGroupsInfoAt(index);
                if (g != null) {
                    g[1]--;
                    if (g[2] != -1)
                        g[2]--;
                    fixItemIndices(index, -1, false);
                } else
                    fixItemIndices(index, -1, false);

                if (child instanceof Listgroupfoot) {
                    final int[] g1 = getGroupsInfoAt(index);
                    if (g1 != null)
                        g1[2] = -1;
                }
            } else
                fixItemIndices(index, -1);
        }

        if (hasGroupsModel() && getItemCount() <= 0) { // remove to empty,
            // reset _groupsInfo
            _groupsInfo = new LinkedList<int[]>();
        }
        //bug 3057288
        //getDataLoader().updateModelInfo(); //itemsInvalidate after really removed
        //return true;
    }


    // -- ListModel dependent codes --//
    /**
     * Returns the model associated with this list box, or null if this list box
     * is not associated with any list data model.
     *
     * <p>
     * Note: if {@link #setModel(GroupsModel)} was called with a groups model,
     * this method returns an instance of {@link ListModel} encapsulating it.
     *
     * @see #setModel(ListModel)
     * @see #setModel(GroupsModel)
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public <T> ListModel<T> getModel() {
        return (ListModel) _model;
    }

    /**
     * Returns the list model associated with this list box, or null if this
     * list box is associated with a {@link GroupsModel} or not associated with
     * any list data model.
     *
     * @since 3.5.0
     * @see #setModel(ListModel)
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public <T> ListModel<T> getListModel() {
        return _model instanceof GroupsListModel ? null : (ListModel) _model;
    }

    /**
     * Returns the groups model associated with this list box, or null if this
     * list box is associated with a {@link ListModel} or not associated with
     * any list data model.
     *
     * @since 3.5.0
     * @see #setModel(GroupsModel)
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public <D, G, F> GroupsModel<D, G, F> getGroupsModel() {
        return _model instanceof GroupsListModel ? ((GroupsListModel) _model).getGroupsModel() : null;
    }

    /**
     * Sets the list model associated with this listbox. If a non-null model is
     * assigned, no matter whether it is the same as the previous, it will
     * always cause re-render.
     *
     * @param model the list model to associate, or null to dis-associate any
     * previous model. If not null, it must implement {@link Selectable}.
     * @exception UiException if failed to initialize with the model
     * @see #getListModel
     * @see #setModel(GroupsModel)
     */
    public void setModel(ListModel<?> model) {
        //ZK-3514: speed up
        if (_model != null && _model != model) {
            int threshold = Utils.getIntAttribute(this, "org.zkoss.zul.invalidateThreshold", 10, true);
            int diff = Math.abs((model != null ? model.getSize() : 0) - _model.getSize());
            if (diff > threshold)
                invalidate();
        }

        if (model != null) {
            if (!(model instanceof Selectable))
                throw new UiException(model.getClass() + " must implement " + Selectable.class);

            if (model instanceof GroupsSelectableModel) {
                ((GroupsSelectableModel) model).setGroupSelectable(isListgroupSelectable());
            }
            if (_model != model) {
                if (_model != null) {
                    _model.removeListDataListener(_dataListener);
                    if (_model instanceof PageableModel && _pgListener != null)
                        ((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
                    GroupsModel groupsModel = getGroupsModel();
                    if (groupsModel != null) {
                        ((GroupsListModel) _model).cleanInternalListener();
                        groupsModel.removeGroupsDataListener(_groupsDataListener);
                    }
                    /* Bug ZK-1512: should clear listitem anyway
                    if (_model instanceof GroupsListModel)
                        getItems().clear();*/

                    resetDataLoader(); // Bug 3357641

                    if (!isAutosort()) {
                        Listhead hds = getListhead();
                        if (hds != null) {
                            for (Component listheader : hds.getChildren()) {
                                ((Listheader) listheader).setSortDirection("natural");
                            }
                        }
                    }
                }
                getItems().clear(); // Bug 1807414, ZK-1512
                _visibleItemCount = 0; //Bug ZK-3735: should clear _visibleItemCount before syncModel.

                if (!inSelectMold()) {
                    smartUpdate("model",
                            model instanceof GroupsListModel || model instanceof GroupsModel ? "group" : true);
                }
                _model = model;
                initDataListener();
                setAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED, Boolean.TRUE);
                //ZK-3173: move the block here to avoid modifying pgi "again" before PagingEvent is handled
                if (inPagingMold()) {
                    if (_model instanceof PageableModel)
                        ((PageableModel) _model).addPagingEventListener((PagingListener) _pgListener);
                    //B30-2129667, B36-2782751, (ROD) exception when zul applyProperties
                    //must update paginal totalSize or exception in setActivePage
                    final Paginal pgi = getPaginal();
                    Pageable m = _model instanceof Pageable ? (Pageable) _model : null;
                    //if pageable model contain non-default values, sync from model to pgi
                    //otherwise, sync from pgi to model
                    if (m != null) {
                        if (m.getPageSize() > 0) { //min page size is 1
                            pgi.setPageSize(m.getPageSize());
                        } else {
                            m.setPageSize(pgi.getPageSize());
                        }
                    }
                    pgi.setTotalSize(getDataLoader().getTotalSize());
                    if (m != null) {
                        if (m.getActivePage() >= 0) { //min page index is 0
                            pgi.setActivePage(m.getActivePage());
                        } else {
                            m.setActivePage(pgi.getActivePage());
                        }
                    }
                }
            }

            final Execution exec = Executions.getCurrent();
            final boolean defer = exec == null ? false
                    : exec.getAttribute("zkoss.Listbox.deferInitModel_" + getUuid()) != null;
            final boolean rod = evalRod();
            //Always syncModel because it is easier for user to enfore reload
            if (!defer || !rod) { //if attached and rod, defer the model sync
                getDataLoader().syncModel(-1, -1);
                removeAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED);
            }
            if (!doSort(this))
                postOnInitRender();
            // Since user might setModel and setItemRender separately or
            // repeatedly,
            // we don't handle it right now until the event processing phase
            // such that we won't render the same set of data twice
            // --
            // For better performance, we shall load the first few row now
            // (to save a roundtrip)
        } else if (_model != null) {
            _model.removeListDataListener(_dataListener);
            if (_model instanceof PageableModel && _pgListener != null)
                ((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
            _model = null;
            getItems().clear();
            if (!inSelectMold())
                smartUpdate("model", false);
            getDataLoader().updateModelInfo();
        }
    }
    
    /**
     * Sets the groups model associated with this list box. If a non-null model
     * is assigned, no matter whether it is the same as the previous, it will
     * always cause re-render.
     *
     * <p>
     * The groups model is used to represent a list of data with grouping.
     *
     * @param model
     *            the groups model to associate, or null to dis-associate any
     *            previous model.
     * @exception UiException
     *                if failed to initialize with the model
     * @since 3.5.0
     * @see #setModel(ListModel)
     * @see #getGroupsModel()
     */
    @SuppressWarnings("rawtypes")
    public void setModel(GroupsModel<?, ?, ?> model) {
        if (model instanceof AbstractGroupsModel) {
            ((AbstractGroupsModel) model).setGroupSelectable(isListgroupSelectable());
        }
        setModel((ListModel) (model != null ? GroupsListModel.toListModel(model) : null));
    }

    private static boolean doSort(Listbox listbox) {
        Listhead hds = listbox.getListhead();
        if (!listbox.isAutosort() || hds == null)
            return false;
        for (Iterator<Component> it = hds.getChildren().iterator(); it.hasNext();) {
            final Listheader hd = (Listheader) it.next();
            String dir = hd.getSortDirection();
            if (!"natural".equals(dir)) {
                return hd.doSort("ascending".equals(dir));
            }
        }
        return false;
    }

    /**
     * Returns the renderer to render each item, or null if the default renderer
     * is used.
     */
    @SuppressWarnings("unchecked")
    public <T> ListitemRenderer<T> getItemRenderer() {
        return (ListitemRenderer<T>) _renderer;
    }

    /**
     * Sets the renderer which is used to render each item if {@link #getModel}
     * is not null.
     *
     * <p>
     * Note: changing a render will not cause the listbox to re-render. If you
     * want it to re-render, you could assign the same model again (i.e.,
     * setModel(getModel())), or fire an {@link ListDataEvent} event.
     *
     * @param renderer
     *            the renderer, or null to use the default.
     * @exception UiException
     *                if failed to initialize with the model
     */
    public void setItemRenderer(ListitemRenderer<?> renderer) {
        if (_renderer != renderer) {
            _renderer = renderer;

            if (_model != null) {
                if ((renderer instanceof ListitemRendererExt) || (_renderer instanceof ListitemRendererExt)) {
                    // bug# 2388345, a new renderer that might new own Listitem,
                    // shall clean all Listitems first
                    getItems().clear();
                    getDataLoader().syncModel(-1, -1); // we have to recreate
                    // all
                } else if (getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) {
                    getDataLoader().syncModel(-1, -1); // have to recreate all
                } else {
                    //bug# 3039282, we need to resyncModel if not in a defer mode
                    final Execution exec = Executions.getCurrent();
                    final boolean defer = exec == null ? false
                            : exec.getAttribute("zkoss.Listbox.deferInitModel_" + getUuid()) != null;
                    final boolean rod = evalRod();
                    if (!defer || !rod)
                        getDataLoader().syncModel(-1, -1);
                }
            }
        }
    }

    /**
     * Sets the renderer by use of a class name. It creates an instance
     * automatically.
     */
    @SuppressWarnings("rawtypes")
    public void setItemRenderer(String clsnm) throws ClassNotFoundException, NoSuchMethodException,
            IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
        if (clsnm != null)
            setItemRenderer((ListitemRenderer) Classes.newInstanceByThread(clsnm));
    }

    /**
     * Handles a private event, onInitRender. It is used only for
     * implementation, and you rarely need to invoke it explicitly.
     */
    public void onInitRender() {
        removeAttribute(ATTR_ON_INIT_RENDER_POSTED);
        doInitRenderer();
    }

    /**
     * Handles a private event, onPagingInitRender. It is used only for
     * implementation, and you rarely need to invoke it explicitly.
     */
    public void onPagingInitRender() {
        removeAttribute(ATTR_ON_PAGING_INIT_RENDERER_POSTED);
        doInitRenderer();
    }

    @SuppressWarnings("rawtypes")
    private void doInitRenderer() {
        if (getPage() == null)
            return;
        // sync the multiple status from model
        if (_model != null)
            setMultiple(((Selectable) _model).isMultiple());

        final Renderer renderer = new Renderer();
        try {
            int pgsz, ofs;
            if (inPagingMold()) {
                pgsz = _pgi.getPageSize();
                ofs = _pgi.getActivePage() * pgsz;
            } else {
                pgsz = inSelectMold() ? getItemCount() : getDataLoader().getLimit();
                ofs = inSelectMold() ? 0 : getDataLoader().getOffset();
                // we don't know # of visible rows, so a 'smart' guess
                // It is OK since client will send back request if not enough
            }
            final int cnt = getItemCount() + getDataLoader().getOffset();
            if (ofs >= cnt) { // not possible; just in case
                ofs = cnt - pgsz;
                if (ofs < 0)
                    ofs = 0;
            }

            int j = 0;
            int realOfs = ofs - getDataLoader().getOffset();
            if (realOfs < 0)
                realOfs = 0;
            boolean open = true;
            for (Listitem item = getItems().size() <= realOfs ? null : getItems().get(realOfs), nxt; j < pgsz
                    && item != null; item = nxt, j++) {
                nxt = nextListitem(item); //retrieve first since it might be changed

                if (item.isVisible() && (open || item instanceof Listgroupfoot || item instanceof Listgroup)) {
                    renderer.render(item, j + ofs);
                }
                if (item instanceof Listgroup)
                    open = ((Listgroup) item).isOpen();
            }

            getDataLoader().updateModelInfo();
        } catch (Throwable ex) {
            renderer.doCatch(ex);
        } finally {
            renderer.doFinally();
        }
        Events.postEvent(ZulEvents.ON_AFTER_RENDER, this, null); // notify the listbox when items have been rendered.
        removeAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED);
    }

    private static Listitem nextListitem(Listitem item) {
        final Component c = item.getNextSibling();
        return c instanceof Listitem ? (Listitem) c : null;
        //listitem must be placed contineously
    }

    private void postOnInitRender() {
        // 20080724, Henri Chen: optimize to avoid postOnInitRender twice
        if (getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) {
            setAttribute(ATTR_ON_INIT_RENDER_POSTED, Boolean.TRUE);
            // Bug ZL-1696: manipulate list from api might happen before list 
            // render, use sendEvent instead of postEvent to render list first
            Events.postEvent("onInitRender", this, null);
        }
    }

    private void postOnPagingInitRender() {
        if (getAttribute(ATTR_ON_PAGING_INIT_RENDERER_POSTED) == null
                && getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) { // B50-ZK-345
            setAttribute(ATTR_ON_PAGING_INIT_RENDERER_POSTED, Boolean.TRUE);
            Events.postEvent("onPagingInitRender", this, null);
        }
    }

    private void onGroupsDataChange(GroupsDataEvent event) {
        getDataLoader().doGroupsDataChange(event);
    }

    /**
     * Handles when the list model's content changed.
     */
    @SuppressWarnings("rawtypes")
    private void onListDataChange(ListDataEvent event) {
        int type = event.getType();
        // ZK-4549: should ignore before handling sorting
        if (getAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED) != null
                && (type == ListDataEvent.INTERVAL_ADDED || type == ListDataEvent.INTERVAL_REMOVED))
            return;
        //sort when add
        if ((type == ListDataEvent.INTERVAL_ADDED || type == ListDataEvent.CONTENTS_CHANGED)
                && !isIgnoreSortWhenChanged()) {
            if (doSort(this)) {
                getDataLoader().updateModelInfo();
            } else {
                getDataLoader().doListDataChange(event);
                postOnInitRender(); // to improve performance
            }
        } else if (type == ListDataEvent.SELECTION_CHANGED) {
            if (!_ignoreDataSelectionEvent) {
                if (event.getIndex0() > -1) {
                    setSelectedIndex(event.getIndex0());
                    // auto scrollIntoView, if item is not rendered
                }
                doSelectionChanged();
            }
        } else if (type == ListDataEvent.MULTIPLE_CHANGED) {
            setMultiple(((Selectable) _model).isMultiple());
        } else if (type == ListDataEvent.DISABLE_CLIENT_UPDATE) {
            _ignoreSelectedItem = true;
        } else if (type == ListDataEvent.ENABLE_CLIENT_UPDATE) {
            _ignoreSelectedItem = false;
        } else {
            getDataLoader().doListDataChange(event);
            postOnInitRender(); // to improve performance

            // TODO: We have to skip the synchronization of the target component
            // when the event is fired from it, i.e. No need to sync the sorting
            // status here.
            if (event.getType() == ListDataEvent.STRUCTURE_CHANGED && _model instanceof Sortable && _listhead != null) { //ZK-1705 added null check for _listhead
                Sortable<Object> smodel = cast(_model);
                List<Listheader> headers = cast(_listhead.getChildren());
                boolean found = false;
                for (Listheader col : headers) {
                    if (found) {
                        col.setSortDirection("natural");
                    } else {
                        Comparator<Object> cmpr = cast(col.getSortAscending());
                        String dir = smodel.getSortDirection(cmpr);
                        found = !"natural".equals(dir);
                        if (!found) {
                            cmpr = cast(col.getSortDescending());
                            dir = smodel.getSortDirection(cmpr);
                            found = !"natural".equals(dir);
                        }
                        col.setSortDirection(dir);
                    }
                }
            }
            if (_model.getSize() == 0) { // Bug ZK-1834: model is empty
                resetDataLoader(true);
            }
        }
    }

    /** Called when SELECTION_CHANGED is received. */
    private void doSelectionChanged() {
        final Selectable<Object> smodel = getSelectableModel();
        final Set<Object> selObjs = smodel.getSelection();
        final int expSelCnt = selObjs.size();
        if (expSelCnt == 0) {
            clearSelection();
            return;
        }

        //Optimize the single-selection case
        if (!_multiple && _jsel >= 0) {
            if (smodel.isSelected(_model.getElementAt(_jsel)))
                return; //nothing changed
            getSelectedItem().setSelected(false);
        }

        //Unfortunately, we have to scan all list items either ROD or not,
        //because the selection is meaningful only if a listitem is loaded
        //The performance is not good but it rarely happens (since it is called
        //if the selection is modified by app directly)
        int selCnt = 0;
        for (final Listitem item : _items) {
            if (item.isLoaded()) { //selected is meaningful only if loaded
                boolean sel = selObjs.contains(_model.getElementAt(item.getIndex()));
                if (sel)
                    ++selCnt;
                item.setSelected(sel);
            }
            if (selCnt == expSelCnt && selCnt >= _selItems.size())
                break; //done (all selected items are sync)
        }
    }

    @SuppressWarnings("unchecked")
    private Selectable<Object> getSelectableModel() {
        return (Selectable<Object>) _model;
    }

    /** Used to render listitem if _model is specified. */
    /* package */class Renderer { // use package for easy to call (if override)
        @SuppressWarnings("rawtypes")
        private final ListitemRenderer _renderer;
        private boolean _rendered, _ctrled;

        /* package */@SuppressWarnings("rawtypes")
        Renderer() {
            _renderer = (ListitemRenderer) getDataLoader().getRealRenderer();
        }

        /* package */@SuppressWarnings({ "unchecked", "rawtypes" })
        void render(Listitem item, int index) throws Throwable {
            if (item.isLoaded()) {
                return; // nothing to do
            }

            if (!_rendered && (_renderer instanceof RendererCtrl)) {
                ((RendererCtrl) _renderer).doTry();
                _ctrled = true;
            }

            final Listcell cell = (Listcell) item.getFirstChild();
            if (!(_renderer instanceof ListitemRendererExt)
                    || (((ListitemRendererExt) _renderer).getControls() & ListitemRendererExt.DETACH_ON_RENDER) != 0) { // detach
                // (default)
                cell.detach();
            }

            //bug #3039843: Paging Listbox without rod, ListModel shall not fully loaded
            //check if the item is a selected item and add into selected set
            final Object value = _model.getElementAt(index);

            final SelectionControl ctrl = getSelectableModel().getSelectionControl();
            final boolean selectable = ctrl == null ? true : ctrl.isSelectable(value);

            //bug #ZK-675: Selection was lost if a render replace the listitem
            final boolean selected = ((Selectable) _model).isSelected(value);
            final boolean oldFlag = setReplacingItem(true); //skipFixItemIndices when rendering
            try {
                try {
                    _renderer.render(item, value, index);
                } catch (AbstractMethodError ex) {
                    final Method m = _renderer.getClass().getMethod("render",
                            new Class<?>[] { Listitem.class, Object.class });
                    m.setAccessible(true);
                    m.invoke(_renderer, new Object[] { item, value });
                }
                Object v = item.getAttribute(Attributes.MODEL_RENDERAS);
                if (v != null) //a new listitem is created to replace the existent one
                    item = (Listitem) v;
            } catch (Throwable ex) {
                try {
                    item.setLabel(Exceptions.getMessage(ex));
                } catch (Throwable t) {
                    log.error("", t);
                }
                item.setLoaded(true);
                throw ex;
            } finally {
                setReplacingItem(oldFlag);
                if (item.getChildren().isEmpty())
                    cell.setParent(item);
            }

            //bug #ZK-675: Selection was lost if a render replace the listitem
            //Also we have to add it to selection when rendered
            if (selected)
                addItemToSelection(item);

            if (ctrl != null)
                item.setSelectable(selectable);

            item.setLoaded(true);
            _rendered = true;
        }

        /* package */void doCatch(Throwable ex) {
            if (_ctrled) {
                try {
                    ((RendererCtrl) _renderer).doCatch(ex);
                } catch (Throwable t) {
                    throw UiException.Aide.wrap(t);
                }
            } else {
                throw UiException.Aide.wrap(ex);
            }
        }

        /* package */void doFinally() {
            if (_ctrled)
                ((RendererCtrl) _renderer).doFinally();
        }
    }

    /**
     * Renders the specified {@link Listitem} if not loaded yet, with
     * {@link #getItemRenderer}.
     *
     * <p>
     * It does nothing if {@link #getModel} returns null. In other words, it is
     * meaningful only if live data model is used.
     *
     * @see #renderItems
     * @see #renderAll
     * @return the list item being passed to this method
     */
    public Listitem renderItem(Listitem li) {
        if (_model != null && li != null && !li.isLoaded()) {
            final Renderer renderer = new Renderer();
            try {
                renderer.render(li, li.getIndex());
            } catch (Throwable ex) {
                renderer.doCatch(ex);
            } finally {
                renderer.doFinally();
            }
        }
        return li;
    }

    /**
     * Renders all {@link Listitem} if not loaded yet, with
     * {@link #getItemRenderer}.
     *
     * @see #renderItem
     * @see #renderItems
     */
    public void renderAll() {
        if (_model == null)
            return;

        _renderAll = true;
        getDataLoader().setLoadAll(_renderAll);

        final Renderer renderer = new Renderer();
        if (!getItems().isEmpty())
            try {
                Listitem item = getItems().get(0);
                int index = item.getIndex();
                for (Listitem nxt; item != null; item = nxt) {
                    nxt = nextListitem(item); //retrieve first since it might be changed
                    renderer.render(item, index++);
                }
            } catch (Throwable ex) {
                renderer.doCatch(ex);
            } finally {
                renderer.doFinally();
            }
    }

    /** Renders the given set of list items.
     */
    public void renderItems(Set<? extends Listitem> items) {
        if (_model == null)
            return;

        if (items.isEmpty())
            return; // nothing to do

        final Renderer renderer = new Renderer();
        try {
            for (final Listitem item : items)
                renderer.render(item, item.getIndex());
        } catch (Throwable ex) {
            renderer.doCatch(ex);
        } finally {
            renderer.doFinally();
        }
    }

    /** Sets the mold to render this component.
     *
     * @param mold the mold. If null or empty, "default" is assumed.
     * Allowed values: default, select, paging 
     * @see org.zkoss.zk.ui.metainfo.ComponentDefinition
     */
    public void setMold(String mold) {
        final String old = getMold();
        if (!Objects.equals(old, mold)) {
            super.setMold(mold);
            // we have to change model before detaching paging,
            // since removeChild assumes it

            if ("paging".equals(old)) { // change from paging
                if (_paging != null) {
                    removePagingListener(_paging);
                    _paging.detach();
                } else if (_pgi != null) {
                    removePagingListener(_pgi);
                }
                if (getModel() != null) {
                    getDataLoader().syncModel(0, initRodSize()); // change offset back to 0
                    postOnInitRender();
                }
                invalidate(); // paging mold -> non-paging mold
            } else if (inPagingMold()) { // change to paging
                if (_pgi != null)
                    addPagingListener(_pgi);
                else
                    newInternalPaging();
                _topPad = 0;
                _currentTop = 0;
                _currentLeft = 0;
                // enforce a page loading
                // B50-ZK-345: speed up onPagingImpl to surpass onInitRender
                Events.postEvent(10001, new PagingEvent("onPagingImpl", (Component) _pgi, _pgi.getActivePage()));
                invalidate(); // non-paging mold -> paging mold
            }
        }
    }

    /**
     * Returns the message to display when there are no items
     * @return String
     * @since 5.0.7
     */
    public String getEmptyMessage() {
        return _emptyMessage;
    }

    /**
     * Sets the message to display when there are no items
     * @param emptyMessage
     * @since 5.0.7
     */
    public void setEmptyMessage(String emptyMessage) {
        if (!Objects.equals(emptyMessage, _emptyMessage)) {
            this._emptyMessage = emptyMessage;
            smartUpdate("emptyMessage", this._emptyMessage);
        }
    }

    public String getZclass() {
        return _zclass == null ? "z-listbox" : _zclass;
    }

    @SuppressWarnings("serial")
    private class ItemIter implements ListIterator<Listitem>, java.io.Serializable {
        private ListIterator<Listitem> _it;
        private int _j;
        private boolean _bNxt;

        private ItemIter(int index) {
            _j = index;
        }

        public void add(Listitem o) {
            prepare();
            _it.add(o);
            ++_j;
        }

        public boolean hasNext() {
            return _j < _items.size();
        }

        public boolean hasPrevious() {
            return _j > 0;
        }

        public Listitem next() {
            if (!hasNext()) //use _items.size() to control if reach listfoot
                throw new NoSuchElementException();

            prepare();
            final Listitem o = _it.next();
            ++_j;
            _bNxt = true;
            return o;
        }

        public Listitem previous() {
            if (!hasPrevious()) //use _j >= 0 to control if reach listhead
                throw new NoSuchElementException();

            prepare();
            final Listitem o = _it.previous();
            --_j;
            _bNxt = false;
            return o;
        }

        public int nextIndex() {
            return _j;
        }

        public int previousIndex() {
            return _j - 1;
        }

        public void remove() {
            if (_it == null)
                throw new IllegalStateException();
            _it.remove();
            if (_bNxt)
                --_j;
        }

        public void set(Listitem o) {
            if (_it == null)
                throw new IllegalStateException();
            _it.set(o);
        }

        private void prepare() {
            if (_it == null)
                _it = cast(getChildren().listIterator(_j + _hdcnt));
        }
    }

    private boolean evalRod() {
        return Utils.testAttribute(this, "org.zkoss.zul.listbox.rod", false, true)
                && !(_model instanceof GroupsListModel);
        //TODO: performance enhancement: support GroupsModel in ROD
    }

    private void setFocusIndex(int index) {
        // F60-ZK-715: notify Listbox widget to set _focusItem
        if (index != _focusIndex) {
            _focusIndex = index;
            smartUpdate("focusIndex", index);
        }
    }

    /* package */DataLoader getDataLoader() {
        if (_dataLoader == null) {
            _rod = evalRod();
            final String loadercls = Library.getProperty("org.zkoss.zul.listbox.DataLoader.class");
            try {
                _dataLoader = _rod && loadercls != null ? (DataLoader) Classes.forNameByThread(loadercls).newInstance()
                        : new ListboxDataLoader();
            } catch (Exception e) {
                throw UiException.Aide.wrap(e);
            }
            _dataLoader.init(this, 0, initRodSize());
        }
        return _dataLoader;
    }

    public void onPageAttached(Page newpage, Page oldpage) {
        super.onPageAttached(newpage, oldpage);
        if (oldpage == null) { // mark as a new attached Listbox
            final Execution exec = Executions.getCurrent();
            exec.setAttribute("zkoss.Listbox.deferInitModel_" + getUuid(), Boolean.TRUE);
            exec.setAttribute("zkoss.Listbox.attached_" + getUuid(), Boolean.TRUE);
            // prepare a right moment to init Listbox (must be as early as possible)
            this.addEventListener("onInitModel", _modelInitListener = new ModelInitListener());
            Events.postEvent(20000, new Event("onInitModel", this)); //first event to be called
        }
        GroupsModel groupsModel = getGroupsModel();
        if (_model != null || groupsModel != null) {
            getDataLoader().syncModel(-1, -1);
            postOnInitRender();
        }
        if (_model != null && _dataListener != null) {
            _model.removeListDataListener(_dataListener);
            _model.addListDataListener(_dataListener);
        }
        if (_model instanceof PageableModel && _pgListener != null) {
            ((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
            ((PageableModel) _model).addPagingEventListener((PagingListener) _pgListener);
        }
        if (groupsModel != null && _groupsDataListener != null) {
            groupsModel.removeGroupsDataListener(_groupsDataListener);
            groupsModel.addGroupsDataListener(_groupsDataListener);
        }
    }

    public void onPageDetached(Page page) {
        super.onPageDetached(page);
        if (_model != null && _dataListener != null)
            _model.removeListDataListener(_dataListener);
        if (_model instanceof PageableModel && _pgListener != null)
            ((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
        GroupsModel groupsModel = getGroupsModel();
        if (groupsModel != null && _groupsDataListener != null)
            groupsModel.removeGroupsDataListener(_groupsDataListener);
    }

    private void resetDataLoader() {
        resetDataLoader(true);
    }

    private void resetDataLoader(boolean shallReset) {
        if (_dataLoader != null) {
            if (shallReset) {
                _dataLoader.reset();
                smartUpdate("_lastoffset", 0); //reset for bug 3357641
            }
            _dataLoader = null;
        }

        if (shallReset) {
            // Bug ZK-373
            smartUpdate("resetDataLoader", true);
            _currentTop = 0;
            _currentLeft = 0;
            _anchorTop = 0;
            _anchorLeft = 0;
            _topPad = 0;
        }
    }

    @SuppressWarnings("serial")
    private class ModelInitListener implements SerializableEventListener<Event>, CloneableEventListener<Event> {
        public void onEvent(Event event) throws Exception {
            if (_modelInitListener != null) {
                Listbox.this.removeEventListener("onInitModel", _modelInitListener);
                _modelInitListener = null;
            }
            // initialize data loader
            // Tricky! might has been initialized when apply
            // properties
            if (_dataLoader != null) {
                final boolean rod = evalRod();
                if (_rod != rod || getItems().isEmpty()) {
                    if (_model != null) { // so has to recreate
                        // list items
                        getItems().clear();
                        resetDataLoader(); // enforce recreate the dataloader
                        initModel(); //init the model
                    } else {
                        resetDataLoader(); // enforce recreate the dataloader
                        // dataloader

                        // Bug ZK-1895
                        //The attribute shall be removed, otherwise DataLoader will not syncModel when setModel
                        Executions.getCurrent().removeAttribute("zkoss.Listbox.deferInitModel_" + getUuid());
                    }
                }
            } else if (_model != null) { //items in model not init yet
                initModel(); //init the model
            } else {
                //The attribute shall be removed, otherwise DataLoader will not syncModel when setModel
                Executions.getCurrent().removeAttribute("zkoss.Listbox.deferInitModel_" + getUuid());
            }
            final DataLoader loader = getDataLoader();

            // initialize paginal if any
            Paginal pgi = getPaginal();
            if (pgi != null)
                pgi.setTotalSize(loader.getTotalSize());
        }

        private void initModel() {
            Executions.getCurrent().removeAttribute("zkoss.Listbox.deferInitModel_" + getUuid());
            setModel(_model); //init the model
        }

        public Object willClone(Component comp) {
            return null; // skip to clone
        }
    }

    // Cloneable//
    @SuppressWarnings("rawtypes")
    public Object clone() {
        final Listbox clone = (Listbox) super.clone();
        clone.init();

        // remove cached listeners
        clone._pgListener = null;
        clone._pgImpListener = null;

        //recreate the DataLoader
        final int offset = clone.getDataLoader().getOffset();

        clone.afterUnmarshal(offset);

        // after _pgi ready, and then getLimit() will work
        final int limit = clone.getDataLoader().getLimit();
        clone.resetDataLoader(false); // no need to reset, it will reset the old reference.
        clone.getDataLoader().init(clone, offset, limit);

        if (clone._model != null) {
            if (clone._model instanceof ComponentCloneListener) {
                final ListModel model = (ListModel) ((ComponentCloneListener) clone._model).willClone(clone);
                if (model != null)
                    clone._model = model;
            }
            // we use the same data model but we have to create a new listener
            clone._dataListener = null;
            clone.initDataListener();

            // As the bug in tree - B30-1892446.zul, the component clone won't
            // clone the posted event, so we need to remove the attributes here.
            clone.removeAttribute(ATTR_ON_INIT_RENDER_POSTED);
            clone.removeAttribute(ATTR_ON_PAGING_INIT_RENDERER_POSTED);
            clone.getDataLoader().setLoadAll(_renderAll);
        }

        clone._groupsInfo.addAll(_groupsInfo);

        return clone;
    }

    private void afterUnmarshal(int index) {
        for (Iterator<Component> it = getChildren().iterator(); it.hasNext();) {
            final Object child = it.next();
            if (child instanceof Listitem) {
                final Listitem li = (Listitem) child;
                li.setIndexDirectly(index++); // since Listitem.clone() resets
                // index
                if (li.isSelected()) {
                    _selItems.add(li);
                }
            } else if (child instanceof Listhead) {
                _listhead = (Listhead) child;
            } else if (child instanceof Listfoot) {
                _listfoot = (Listfoot) child;
            } else if (child instanceof Frozen) {
                _frozen = (Frozen) child;
            } else if (child instanceof Paging) {
                _pgi = _paging = (Paging) child;
                addPagingListener(_pgi);
            }
        }
    }

    // -- Serializable --//
    // NOTE: they must be declared as private
    private synchronized void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
        s.defaultWriteObject();

        willSerialize(_model);
        Serializables.smartWrite(s, _model);
        willSerialize(_renderer);
        Serializables.smartWrite(s, _renderer);

        // keep the scrolling status after serialized
        if (_dataLoader != null) {
            s.writeInt(_dataLoader.getOffset());
            s.writeInt(_dataLoader.getLimit());
        } else {
            s.writeInt(0);
            s.writeInt(100);
        }

        int size = _groupsInfo.size();
        s.writeInt(size);
        if (size > 0)
            s.writeObject(_groupsInfo);
    }

    @SuppressWarnings("rawtypes")
    private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        _model = (ListModel) s.readObject();
        didDeserialize(_model);
        _renderer = (ListitemRenderer) s.readObject();
        didDeserialize(_renderer);

        init();

        int offset = s.readInt();
        afterUnmarshal(offset);

        int limit = s.readInt();
        resetDataLoader(false); // no need to reset, it will reset the old reference.
        getDataLoader().init(this, offset, limit);

        if (_model != null) {
            initDataListener();
            getDataLoader().setLoadAll(_renderAll);

            // Map#Entry cannot be serialized, we have to restore them
            if (_model instanceof ListModelMap) {
                for (Listitem item : getItems())
                    item.setValue(_model.getElementAt(item.getIndex()));
            }
        }

        int size = s.readInt();
        if (size > 0) {
            List groupsInfo = (List) s.readObject();
            for (int i = 0; i < size; i++)
                _groupsInfo.add((int[]) groupsInfo.get(i));
        }
    }

    public void sessionWillPassivate(Page page) {
        super.sessionWillPassivate(page);
        willPassivate(_model);
        willPassivate(_renderer);
    }

    public void sessionDidActivate(Page page) {
        super.sessionDidActivate(page);
        didActivate(_model);
        didActivate(_renderer);
    }

    // -- ComponentCtrl --//
    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer) throws java.io.IOException {
        super.renderProperties(renderer);

        // ZK-4970: reset invalidateListitems here.
        Executions.getCurrent().removeAttribute("zkoss.Listbox.invalidateListitems" + getUuid());

        if (_rows > 0)
            renderer.render("rows", getRows());

        render(renderer, "name", _name);

        render(renderer, "emptyMessage", _emptyMessage);

        if (inSelectMold()) {
            render(renderer, "multiple", isMultiple());
            render(renderer, "disabled", isDisabled());
            if (_maxlength > 0)
                renderer.render("maxlength", _maxlength);
        } else {
            render(renderer, "oddRowSclass", _scOddRow);

            render(renderer, "checkmark", isCheckmark());
            render(renderer, "multiple", isMultiple());

            if (_model != null) {
                render(renderer, "model",
                        _model instanceof GroupsListModel || _model instanceof GroupsModel ? "group" : true);

                if (isMultiple()) {
                    SelectionControl selectionControl = getSelectableModel().getSelectionControl();
                    if (selectionControl != null) {
                        // cannot use render(renderer) here, we have to send a false state
                        // to client.
                        renderer.render("$$selectAll", selectionControl.isSelectAll());
                    }
                }
            }
            if (!"100%".equals(_innerWidth))
                render(renderer, "innerWidth", _innerWidth);
            if (_currentTop != 0)
                renderer.render("_currentTop", _currentTop);
            if (_currentLeft != 0)
                renderer.render("_currentLeft", _currentLeft);

            //ZK-798
            if (_anchorTop != 0)
                renderer.render("_anchorTop", _anchorTop);
            if (_anchorLeft != 0)
                renderer.render("_anchorLeft", _anchorLeft);

            renderer.render("_topPad", _topPad);
            renderer.render("_totalSize", getDataLoader().getTotalSize());
            renderer.render("_offset", getDataLoader().getOffset());

            if (_rod && !_renderAll) {
                if (((Cropper) getDataLoader()).isCropper())//bug #2936064
                    renderer.render("_listbox$rod", true);
                int sz = initRodSize();
                if (sz != INIT_LIMIT)
                    renderer.render("initRodSize", initRodSize());
                if (!inPagingMold() && _jsel >= 0)
                    renderer.render("_selInView", _jsel); // B50-ZK-56
            }
            if (_nonselTags != null)
                renderer.render("nonselectableTags", _nonselTags);
            if (isCheckmarkDeselectOther())
                renderer.render("checkmarkDeselectOther", true);
            if (!isRightSelect())
                renderer.render("rightSelect", false);
            if (isListgroupSelectable())
                renderer.render("groupSelect", true);
            if (isSelectOnHighlightDisabled()) // F70-ZK-2433
                renderer.render("selectOnHighlightDisabled", true);
        }
        if (_pgi != null && _pgi instanceof Component) {
            renderer.render("paginal", _pgi);

            // ZK 8, if no model used in paging mold, we don't support select all in this case
            if (_model == null) {
                renderer.render("_listbox$noSelectAll", true); // B50-ZK-873, separate the select all condition and isCropper
            }
            
            //ZK-3103: only true when setSelectedIndex is called
            if (_shallSyncSelInView) {
                renderer.render("_listbox$shallSyncSelInView", true);
                _shallSyncSelInView = false;
            }
        }
        if (_focusIndex > -1)
            renderer.render("focusIndex", _focusIndex); // F60-ZK-715

        if (_shallUpdateScrollPos) {
            renderer.render("_listbox$shallUpdateScrollPos", true);
            _shallUpdateScrollPos = false;
        }
    }

    /** Returns whether to toggle a list item selection on right click
     */
    private boolean isRightSelect() {
        return Utils.testAttribute(this, "org.zkoss.zul.listbox.rightSelect", true, true);
    }

    protected boolean isAutohidePaging() {
        return Utils.testAttribute(this, "org.zkoss.zul.listbox.autohidePaging", true, true);
    }

    /** Returns whether to sort all of item when model or sort direction be changed.
     * @since 5.0.7
     */
    /*package*/ boolean isAutosort() {
        String attr = "org.zkoss.zul.listbox.autoSort";
        Object val = getAttribute(attr, true);
        if (val == null)
            val = Library.getProperty(attr);
        return val instanceof Boolean ? ((Boolean) val).booleanValue()
                : val != null ? "true".equals(val) || "ignore.change".equals(val) : false;
    }

    protected boolean isSelectOnHighlightDisabled() {
        return Utils.testAttribute(this, "org.zkoss.zul.listbox.selectOnHighlight.disabled", false, true);
    }

    /** 
     * Returns the number of items to preload when receiving the rendering
     * request from the client.
     * <p>
     * Default: 50. (Since 6.0.1)
     * <p>
     * It is used only if live data ({@link #setModel(ListModel)} and not paging
     * ({@link #getPagingChild}.
     */
    private int preloadSize() {
        final String size = (String) getAttribute("pre-load-size");
        int sz = size != null ? Integer.parseInt(size) : _preloadsz;

        if ((sz = Utils.getIntAttribute(this, "org.zkoss.zul.listbox.preloadSize", sz, true)) < 0)
            throw new UiException("nonnegative is required: " + sz);
        return sz;
    }

    /** 
     * Returns the number of items rendered when the Listbox first render.
     *  <p>
     * Default: 50. (Since 6.0.1)
     * <p>
     * It is used only if live data ({@link #setModel(ListModel)} and not paging
     * ({@link #getPagingChild}.
     */
    private int initRodSize() {
        int sz = Utils.getIntAttribute(this, "org.zkoss.zul.listbox.initRodSize", INIT_LIMIT, true);
        if ((sz) < 0)
            throw new UiException("nonnegative is required: " + sz);
        return sz;
    }

    /** Returns whether to sort all of item when model or sort direction be changed.
     * @since 5.0.7
     */
    private boolean isIgnoreSortWhenChanged() {
        String attr = "org.zkoss.zul.listbox.autoSort";
        Object val = getAttribute(attr, true);
        if (val == null)
            val = Library.getProperty(attr);
        return val == null ? true : "ignore.change".equals(val);
    }

    /** Returns whether to toggle the selection if clicking on a list item
     * with a checkmark.
     */
    private boolean isCheckmarkDeselectOther() {
        if (_ckDeselectOther == null) //ok to race
            _ckDeselectOther = Boolean
                    .valueOf("true".equals(Library.getProperty("org.zkoss.zul.listbox.checkmarkDeselectOthers")));
        return Utils.testAttribute(this, "org.zkoss.zul.listbox.checkmarkDeselectOthers", _ckDeselectOther.booleanValue(), true);
    }

    private static Boolean _ckDeselectOther;

    /**
     * Returns whether Listgroup is selectable.
     */
    private boolean isListgroupSelectable() {
        return Utils.testAttribute(this, "org.zkoss.zul.listbox.groupSelect", false, true);
    }

    private <T> Set<T> collectUnselectedObjects(Set<T> previousSelection, Set<T> currentSelection) {
        Set<T> prevSeldItems = previousSelection != null ? new LinkedHashSet<T>(previousSelection)
                : new LinkedHashSet<T>();
        if (currentSelection != null && prevSeldItems.size() > 0)
            prevSeldItems.removeAll(currentSelection);
        return prevSeldItems;
    }

    private static HashMap<String, PropertyAccess> _properties = new HashMap<String, PropertyAccess>(8);

    static {
        _properties.put("_currentTop", new IntegerPropertyAccess() {
            public void setValue(Component cmp, Integer currentTop) {
                ((Listbox) cmp)._currentTop = currentTop;
            }

            public Integer getValue(Component cmp) {
                return ((Listbox) cmp)._currentTop;
            }
        });
        _properties.put("_currentLeft", new IntegerPropertyAccess() {
            public void setValue(Component cmp, Integer currentLeft) {
                ((Listbox) cmp)._currentLeft = currentLeft;
            }

            public Integer getValue(Component cmp) {
                return ((Listbox) cmp)._currentLeft;
            }
        });
        _properties.put("_anchorTop", new IntegerPropertyAccess() {
            public void setValue(Component cmp, Integer anchorTop) {
                ((Listbox) cmp)._anchorTop = anchorTop;
            }

            public Integer getValue(Component cmp) {
                return ((Listbox) cmp)._anchorTop;
            }
        });
        _properties.put("_anchorLeft", new IntegerPropertyAccess() {
            public void setValue(Component cmp, Integer anchorLeft) {
                ((Listbox) cmp)._anchorLeft = anchorLeft;
            }

            public Integer getValue(Component cmp) {
                return ((Listbox) cmp)._anchorLeft;
            }
        });
        _properties.put("_topPad", new IntegerPropertyAccess() {
            public void setValue(Component cmp, Integer topPad) {
                ((Listbox) cmp)._topPad = topPad;
            }

            public Integer getValue(Component cmp) {
                return ((Listbox) cmp)._topPad;
            }
        });
        _properties.put("_selInView", new IntegerPropertyAccess() {
            public void setValue(Component cmp, Integer jsel) {
                ((Listbox) cmp)._jsel = jsel;
            }

            public Integer getValue(Component cmp) {
                return ((Listbox) cmp)._jsel;
            }
        });
        _properties.put("_listbox$shallSyncSelInView", new BooleanPropertyAccess() {
            public void setValue(Component cmp, Boolean shallSyncSelInView) {
                ((Listbox) cmp)._shallSyncSelInView = shallSyncSelInView;
            }

            public Boolean getValue(Component cmp) {
                return ((Listbox) cmp)._shallSyncSelInView;
            }
        });
        _properties.put("_listbox$shallUpdateScrollPos", new BooleanPropertyAccess() {
            public void setValue(Component cmp, Boolean shallUpdateScrollPos) {
                ((Listbox) cmp)._shallUpdateScrollPos = shallUpdateScrollPos;
            }

            public Boolean getValue(Component cmp) {
                return ((Listbox) cmp)._shallUpdateScrollPos;
            }
        });
    }

    public PropertyAccess getPropertyAccess(String prop) {
        PropertyAccess pa = _properties.get(prop);
        if (pa != null)
            return pa;
        return super.getPropertyAccess(prop);
    }

    /**
     * Processes an AU request.
     *
     * <p>
     * Default: in addition to what are handled by {@link XulElement#service},
     * it also handles onSelect.
     *
     * @since 5.0.0
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if (cmd.equals(Events.ON_DATA_LOADING)) {
            if (_rod) {
                Executions.getCurrent().setAttribute("zkoss.zul.listbox.onDataLoading." + this.getUuid(), Boolean.TRUE); //indicate doing dataloading
            }
            Events.postEvent(DataLoadingEvent.getDataLoadingEvent(request, preloadSize()));
        } else if (inPagingMold() && cmd.equals(ZulEvents.ON_PAGE_SIZE)) { //since 5.0.2
            final Map<String, Object> data = request.getData();
            final int oldsize = getPageSize();
            int size = AuRequests.getInt(data, "size", oldsize);
            if (size != oldsize) {
                int begin = getActivePage() * oldsize;
                int end = begin + oldsize;
                end = Math.min(getPaginal().getTotalSize(), end);
                int sel = getSelectedIndex();
                if (sel < 0 || sel < begin || sel >= end) { //not in selection range
                    sel = size > oldsize ? (end - 1) : begin;
                }
                int newpg = sel / size;
                setPageSize(size);
                setActivePage(newpg);
                // Bug: B50-3204965: onChangePageSize is not fired in autopaging scenario
                Events.postEvent(new PageSizeEvent(cmd, this, pgi(), size));
            }
        } else if (cmd.equals(Events.ON_SCROLL_POS)) {
            final Map<String, Object> data = request.getData();
            _currentTop = AuRequests.getInt(data, "top", 0);
            _currentLeft = AuRequests.getInt(data, "left", 0);
        } else if (cmd.equals(Events.ON_ANCHOR_POS)) {
            final Map<String, Object> data = request.getData();
            _anchorTop = AuRequests.getInt(data, "top", 0);
            _anchorLeft = AuRequests.getInt(data, "left", 0);
        } else if (cmd.equals(Events.ON_TOP_PAD)) {
            _topPad = AuRequests.getInt(request.getData(), "topPad", 0);
        } else if (cmd.equals(Events.ON_SELECT)) {
            if (_rod && Executions.getCurrent()
                    .getAttribute("zkoss.zul.listbox.onDataLoading." + this.getUuid()) != null) //indicate doing dataloading
                return; //skip all onSelect event after the onDataLoading

            Desktop desktop = request.getDesktop();
            Map data = request.getData();
            List<String> sitems = cast((List) data.get("items"));
            boolean selectAll = Boolean.parseBoolean(data.get("selectAll") + "");
            boolean paging = inPagingMold();
            Set<Listitem> prevSeldItems = new LinkedHashSet<Listitem>(_selItems);
            Set<Listitem> curSeldItems = AuRequests.convertToItems(desktop, sitems);
            Set<Listitem> realPrevSeldItems = new LinkedHashSet<Listitem>(prevSeldItems);
            Set<Object> prevSeldObjects = _model != null
                    ? new LinkedHashSet<Object>(getSelectableModel().getSelection()) : new LinkedHashSet<Object>();
            // fine tune with B50-ZK-547.
            Selectable<Object> smodel = _model != null ? getSelectableModel() : null;

            // ZK-2089: prevSeldItems should skip listgroup if listgroup is not selectable
            if (!isListgroupSelectable() && prevSeldItems.size() > 0) {
                // use toArray() to prevent java.util.ConcurrentModificationException
                for (Object item : prevSeldItems.toArray()) {
                    if (item instanceof Listgroup) {
                        prevSeldItems.remove(item);
                        realPrevSeldItems.remove(item);
                    }
                }
            }

            int from, to;
            Paginal pgi = getPaginal();
            if (pgi != null) {
                int pgsz = pgi.getPageSize();
                from = pgi.getActivePage() * pgsz;
                to = from + pgsz; // excluded
            } else {
                from = 0;
                to = 0;
            }

            // remove the selection in other page
            if (paging && (!isCheckmarkDeselectOther() || selectAll)) {
                // use toArray() to prevent java.util.ConcurrentModificationException
                for (Object item : realPrevSeldItems.toArray()) {
                    int index = ((Listitem) item).getIndex();
                    if (index >= to || index < from)
                        realPrevSeldItems.remove(item);
                }
            }

            if (curSeldItems == null)
                curSeldItems = new HashSet<Listitem>(); //just in case

            SelectionControl ctrl = smodel != null ? smodel.getSelectionControl() : null;
            int start = -1;
            int end = -1;
            if (_rod) { // Bug: ZK-592
                Map<String, Object> m = cast((Map) data.get("range"));
                if (m != null) {
                    curSeldItems.addAll(_selItems); // keep other selected items.
                    start = AuRequests.getInt(m, "start", -1);
                    end = AuRequests.getInt(m, "end", -1);
                    for (Iterator<Listitem> it = _items.iterator(); it.hasNext();) {
                        Listitem item = it.next();
                        int index = item.getIndex();
                        if (index >= start && index <= end) {
                            // the same logic come from JS file (SelectWidget)
                            // for Bug: 2030986
                            if (!item.isDisabled() && item.isSelectable())
                                curSeldItems.add(item);
                        }
                    }
                }
            }

            disableClientUpdate(true);
            final boolean oldIDSE = _ignoreDataSelectionEvent;
            _ignoreDataSelectionEvent = true;

            try {
                if (AuRequests.getBoolean(request.getData(), "clearFirst")) {
                    clearSelection();
                    if (_model != null)
                        ((Selectable) _model).clearSelection();
                }

                if (!_multiple || (_model == null && !_rod && !paging && curSeldItems.size() <= 1)) {
                    //If _model, selItems is only a subset (so we can't optimize it)
                    final Listitem item = curSeldItems.size() > 0 ? curSeldItems.iterator().next() : null;
                    selectItem(item);
                    if (_model != null) {
                        final List<Object> selObjs = new ArrayList<Object>();
                        if (item != null) {
                            Object ele = _model.getElementAt(item.getIndex());
                            if (ctrl == null || ctrl.isSelectable(ele))
                                selObjs.add(ele);
                        }
                        getSelectableModel().setSelection(selObjs);
                    }
                } else {
                    for (final Listitem item : curSeldItems) {
                        if (!_selItems.contains(item)) {
                            addItemToSelection(item);
                            if (smodel != null) { //still have to add selection if not multiple select
                                Object ele = _model.getElementAt(item.getIndex());
                                if (ctrl == null || ctrl.isSelectable(ele))
                                    smodel.addToSelection(ele);
                            }
                        }
                    }
                    if (smodel != null) {
                        while (start >= 0 && end >= 0 && start <= end) {
                            //ZK-2804: add those items not in _items as selected
                            Object ele = _model.getElementAt(start++);
                            if (ctrl == null || ctrl.isSelectable(ele))
                                smodel.addToSelection(ele);
                        }
                    }
                    if (prevSeldItems != null && prevSeldItems.size() > 0) {
                        int firstItemIndex = _items.get(0).getIndex();
                        int lastItemIndex = _items.get(_items.size() - 1).getIndex();
                        final Map<String, Object> rodItemIndexRange = cast((Map) data.get("rodItemIndexRange"));
                        if (rodItemIndexRange != null) {
                            final Integer clientFirstItemIndex = (Integer) rodItemIndexRange.get("start");
                            if (clientFirstItemIndex != null) {
                                firstItemIndex = clientFirstItemIndex;
                            }
                            final Integer clientLastItemIndex = (Integer) rodItemIndexRange.get("end");
                            if (clientLastItemIndex != null) {
                                lastItemIndex = clientLastItemIndex;
                            }
                        }
                        for (final Listitem item : prevSeldItems) {
                            final int index = item.getIndex();
                            if (firstItemIndex <= index && index <= lastItemIndex // ZK-2658
                                    && !curSeldItems.contains(item)
                                    && (!paging || (from <= index && index < to))
                            ) {
                                removeItemFromSelection(item);
                                if (smodel != null)
                                    smodel.removeFromSelection(_model.getElementAt(index));
                            }
                        }
                    }
                }
            } finally {
                _ignoreDataSelectionEvent = oldIDSE;
                disableClientUpdate(false);
            }

            Set<Listitem> unselectedItems;
            if (_model != null && paging) {
                prevSeldItems = null;
                unselectedItems = null;
            } else {
                unselectedItems = collectUnselectedObjects(realPrevSeldItems, curSeldItems);
            }

            Set<Object> unselectedObjects;
            Set<Object> selectedObjects = new LinkedHashSet<Object>();
            if (_model == null) {
                prevSeldObjects = null;
                unselectedObjects = null;
            } else {
                for (Listitem i : curSeldItems)
                    selectedObjects.add(_model.getElementAt(i.getIndex()));
                unselectedObjects = collectUnselectedObjects(prevSeldObjects, smodel.getSelection());
            }
            if (ctrl != null) {
                if (selectAll)
                    ctrl.setSelectAll(true);
            }
            if (sitems == null || sitems.isEmpty() || _model == null)
                selectedObjects = null;
            SelectEvent evt = new SelectEvent(Events.ON_SELECT, this, curSeldItems, prevSeldItems, unselectedItems,
                    selectedObjects, prevSeldObjects, unselectedObjects,
                    desktop.getComponentByUuidIfAny((String) data.get("reference")), null, AuRequests.parseKeys(data));
            Events.postEvent(evt);
        } else if (cmd.equals(Events.ON_INNER_WIDTH)) {
            final String width = AuRequests.getInnerWidth(request);
            _innerWidth = width == null ? "100%" : width;
        } else if (cmd.equals(Events.ON_RENDER)) {
            final Set<Listitem> items = AuRequests.convertToItems(request.getDesktop(), getRequestData(request));
            int cnt = items.size();
            if (cnt == 0)
                return; // nothing to do

            // B85-ZK-3572 get size using preloadSize()
            int size = preloadSize();
            cnt = size - cnt;
            if (cnt > 0 && size > 0) { // Feature 1740072: pre-load
                if (cnt > size)
                    cnt = size; // at most 8 more to load

                // 1. locate the first item found in items
                final List<Listitem> toload = new LinkedList<Listitem>();
                Iterator<Listitem> it = _items.iterator();
                while (it.hasNext()) {
                    final Listitem li = it.next();
                    if (items.contains(li)) // found
                        break;
                    if (!li.isLoaded())
                        toload.add(0, li); // reverse order
                }

                // 2. add unload items before the found one
                if (!toload.isEmpty()) {
                    int bfcnt = cnt / 3;
                    for (Iterator<Listitem> e = toload.iterator(); bfcnt > 0 && e.hasNext(); --bfcnt, --cnt) {
                        items.add(e.next());
                    }
                }

                // 3. add unloaded after the found one
                while (cnt > 0 && it.hasNext()) {
                    final Listitem li = it.next();
                    if (!li.isLoaded() && items.add(li))
                        --cnt;
                }
            }

            Listbox.this.renderItems(items);

        } else if (cmd.equals(Events.ON_ACROSS_PAGE)) { // F60-ZK-715
            final Map<String, Object> data = request.getData();
            int page = AuRequests.getInt(data, "page", 0);
            int offset = AuRequests.getInt(data, "offset", 0);
            int shift = AuRequests.getInt(data, "shift", 0);
            int pageSize = getPageSize();
            int index = page * pageSize + offset;
            int from = shift < 0 ? index + shift : index;
            int to = shift > 0 ? index + shift : index;

            //Update UI
            final int tsz = getItemCount();
            final int toUI = Math.min(to, tsz - 1); // capped by size
            if (!isMultiple() || shift == 0) {

                // B65-ZK-1969 and B65-1715
                if ((_model == null && index >= tsz) || (_model != null && index >= _model.getSize()))
                    index = tsz - 1;
                setSelectedIndex(index);
                setFocusIndex(offset < 0 ? pageSize - 1 : offset);
            } else {
                Set<Listitem> items = new HashSet<Listitem>();
                for (int i = from; i <= toUI; i++)
                    items.add(_items.get(i));
                setSelectedItems(items);
                setActivePage(index / pageSize);
                setFocusIndex(offset);
            }

            //Update Model
            if (_model != null) {
                final boolean oldIDSE = _ignoreDataSelectionEvent;
                _ignoreDataSelectionEvent = true;
                try {
                    to = Math.min(to, _model.getSize() - 1); // capped by size
                    final Selectable<Object> smodel = getSelectableModel();
                    if (!smodel.isMultiple() || shift == 0) {
                        if (!smodel.isMultiple())
                            from = to;
                        smodel.clearSelection();
                    }
                    while (from <= to)
                        smodel.addToSelection(_model.getElementAt(from++));
                } finally {
                    _ignoreDataSelectionEvent = oldIDSE;
                }
            }

            // B60-ZK-815: simulate onSelect event when going across page
            SelectEvent<Listitem, Object> evt = new SelectEvent<Listitem, Object>("onSelect", this, getSelectedItems(),
                    getItemAtIndex(index), shift != 0 ? SelectEvent.SHIFT_KEY : 0);
            Events.postEvent(evt);

            // a way to notify Client MVVM
            Events.postEvent(new Event(Events.ON_ACROSS_PAGE, this, data));
        } else if (cmd.equals(Events.ON_CHECK_SELECT_ALL)) { // F65-ZK-2014
            CheckEvent evt = CheckEvent.getCheckEvent(request);
            if (_model != null) {
                final Selectable<Object> selectableModel = getSelectableModel();
                SelectionControl selectionControl = selectableModel.getSelectionControl();
                if (selectionControl == null)
                    throw new IllegalStateException(
                            "SelectionControl cannot be null, please implement SelectionControl interface for SelectablModel");
                selectionControl.setSelectAll(evt.isChecked());
            }
            Events.postEvent(evt);
        } else if (cmd.equals("onUpdateSelectAll")) {
            if (_model != null) {
                final SelectionControl selectionControl = getSelectableModel().getSelectionControl();
                if (selectionControl != null) {
                    Clients.response(new AuInvoke(this, "$doService", new Object[] { cmd, new JSONAware() {
                        public String toJSONString() {
                            return String.valueOf(selectionControl.isSelectAll());
                        }
                    } }));
                }
            }
        } else
            super.service(request, everError);
    }

    @SuppressWarnings("unchecked")
    private List<String> getRequestData(org.zkoss.zk.au.AuRequest request) {
        return (List<String>) request.getData().get("items");
    }

    public Object getExtraCtrl() {
        return new ExtraCtrl();
    }

    /**
     * A utility class to implement {@link #getExtraCtrl}. It is used only by
     * component developers.
     */
    protected class ExtraCtrl extends XulElement.ExtraCtrl implements Cropper, Padding, Blockable {
        // --Padding--//
        public int getHeight() {
            return _topPad;
        }

        public void setHeight(int height) {
            _topPad = height;
        }

        // --Cropper--//
        public boolean isCropper() {
            return ((Cropper) getDataLoader()).isCropper();
        }

        public Component getCropOwner() {
            return Listbox.this;
        }

        public Set<? extends Component> getAvailableAtClient() {
            return ((Cropper) getDataLoader()).getAvailableAtClient();
        }

        public boolean shallBlock(AuRequest request) {
            return isDisabled() || !Components.isRealVisible(Listbox.this);
        }
    }

    /**
     * An iterator used by _heads.
     */
    private class Iter implements Iterator<Component> {
        private final Iterator<Component> _it = getChildren().iterator();
        private int _j;

        public boolean hasNext() {
            return _j < _hdcnt;
        }

        public Component next() {
            final Component o = _it.next();
            ++_j;
            return o;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * An iterator used by _groups.
     */
    private class IterGroups implements Iterator<Listgroup> {
        private final Iterator<int[]> _it = _groupsInfo.iterator();
        private int _j;

        public boolean hasNext() {
            return _j < getGroupCount();
        }

        public Listgroup next() {
            final Listgroup o = (Listgroup) getItemAtIndex(_it.next()[0]);
            ++_j;
            return o;
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }
    }

    @Override
    public void setPageSize(int pgsz) throws WrongValueException {
        // Bug ZK-1696: model also preserves paging information
        if (_model instanceof Pageable) {
            ((Pageable) _model).setPageSize(pgsz);
        }
        super.setPageSize(pgsz);
    }
    
    public void onAfterRender() {
        if (inPagingMold() && _model instanceof Pageable) {
            Pageable m = (Pageable) _model;
            if (m.getPageSize() > 0) { //min page size is 1
                _pgi.setPageSize(m.getPageSize());
            } else {
                m.setPageSize(_pgi.getPageSize());
            }
            _pgi.setTotalSize(getDataLoader().getTotalSize());
            if (m.getActivePage() >= 0) { //min page index is 0
                _pgi.setActivePage(m.getActivePage());
            } else {
                m.setActivePage(_pgi.getActivePage());
            }
        }
    }
    
    /**
     * Scroll to the specified item by the given index.
     * @param index the index of item
     * @since 8.5.2
     */
    public void scrollToIndex(int index) {
        ListModel<Object> model = getModel();
        int itemCount = model != null ? model.getSize() : getItemCount();
        if (index < 0 || index > itemCount - 1) {
            throw new IndexOutOfBoundsException("Illegal index: " + index);
        }
        response(new AuInvoke(this, "scrollToIndex", index, (double) index / itemCount));
    }

    /**
     * Sets whether to update the scroll position on init
     * <p>Default: false.
     * <p>Note: internal use only
     * @param shallUpdateScrollPos whether update the scroll position on init
     * @since 8.6.0
     */
    public void shallUpdateScrollPos(boolean shallUpdateScrollPos) {
        _shallUpdateScrollPos = shallUpdateScrollPos;
    }
}