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

Summary

Maintainability
F
2 wks
Test Coverage
/* Grid.java

    Purpose:

    Description:

    History:
        Tue Oct 25 15:40:35     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.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

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

import org.zkoss.io.Serializables;
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.Component;
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.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.SerializableEventListener;
import org.zkoss.zk.ui.ext.render.Cropper;
import org.zkoss.zk.ui.sys.IntegerPropertyAccess;
import org.zkoss.zk.ui.sys.PropertyAccess;
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.RenderEvent;
import org.zkoss.zul.event.ZulEvents;
import org.zkoss.zul.ext.Pageable;
import org.zkoss.zul.ext.Paginal;
import org.zkoss.zul.ext.Sortable;
import org.zkoss.zul.impl.DataLoader;
import org.zkoss.zul.impl.GridDataLoader;
import org.zkoss.zul.impl.GroupsListModel;
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 grid is an element that contains both rows and columns elements.
 * It is used to create a grid of elements.
 * Both the rows and columns are displayed at once although only one will
 * typically contain content, while the other may provide size information.
 *
 * <p>Events: onAfterRender<br/>
 * onAfterRender is sent when the model's data has been rendered.(since 5.0.4)
 *
 * <p>Besides creating {@link Row} programmatically, you can assign
 * a data model (a {@link ListModel} or {@link GroupsModel} instance) to a grid via
 * {@link #setModel(ListModel)} or {@link #setModel(GroupsModel)}
 * and then the grid will retrieve data
 * by calling {@link ListModel#getElementAt} when necessary.
 *
 * <p>Besides assign a list model, you could assign a renderer
 * (a {@link RowRenderer} instance) to a grid, such that
 * the grid will use this
 * renderer to render the data returned by {@link ListModel#getElementAt}.
 * If not assigned, the default renderer, which assumes a label per row,
 * is used.
 * In other words, the default renderer adds a label to
 * a row by calling toString against the object returned
 * by {@link ListModel#getElementAt}
 *
 * <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 rows 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 grids.
 *
 * <p>Default {@link #getZclass}: z-grid.(since 3.5.0)
 *
 * <p>To have a grid 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 RowRenderer}
 * ({@link #setRowRenderer}) 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 Grid's ROD to request ZK engine to load from
 * {@link ListModel} only the required data chunk and create only the required
 * {@link Row}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 Grid, 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 Grid is configured to has a limited "view port"
 * height. That is, either the Grid is in the "paging" mold or you have to
 * {@link #setHeight(String)} or {@link #setVflex(String)} of the Grid to
 * make ROD works.</p>
 *
 * <p>You can turn on/off ROD for all Grids in the application or only
 * for a specific Grid. To turn on ROD for all Grids in the application, you
 * have to specify the Library Property "org.zkoss.zul.grid.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.grid.rod</name>
 *        <value>true</value>
 *    </library-property>
 * }</pre>
 *
 * <p>To turn on ROD for a specific Grid, you have to specify the Grid's attribute
 * map with key "org.zkoss.zul.grid.rod" to true. That is, for example, if in
 * a zul file, you shall specify &lt;custom-attributes&gt; of the Grid like this:</p>
 * <pre>{@code
 *    <grid ...>
 *    <custom-attributes org.zkoss.zul.grid.rod="true"/>
 *  </grid>
 * }</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 Grid component. Or
 * you can turn ON ROD in general and turn OFF only some specific Grid component.</P>
 *
 * <p>Since only partial {@link Row}s are created and rendered in the Grid if
 * you turn the ROD on, there will be some limitations on accessing {@link Row}s.
 * For example, if you call
 * <pre><code>
 * Row rowAt100 = (Row) getRows().getChildren().get(100);
 * </code></pre>
 * <p>The {@link Row} 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 Row} in your
 * application if you turn the ROD on because rows might be removed later.
 * Basically, you shall operate on the item of the ListModel rather than on the
 * {@link Row} if you use the ListModel and ROD.</p>
 *
 * <h3>Custom Attributes</h3>
 * <dl>
 * <dt>org.zkoss.zul.grid.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.grid.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 Column#setSortDirection} is set.</li>
 * <li>{@link Column#setSortDirection} is called.</li>
 * <li>Model receives {@link ListDataEvent} and {@link Column#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.grid.preloadSize</dt>.(since 5.0.8)
 * <dd>Specifies the number of rows 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.grid.initRodSize</dt>.(since 5.0.8)
 * <dd>Specifies the number of rows rendered when the Grid first render.
 * <p>
 * It is used only if live data ({@link #setModel(ListModel)} and not paging
 * ({@link #getPagingChild}).</dd>
 *
 * @author tomyeh
 * @see ListModel
 * @see RowRenderer
 * @see RowRendererExt
 */
public class Grid extends MeshElement {
    private static final Logger log = LoggerFactory.getLogger(Grid.class);
    private static final long serialVersionUID = 20091111L;

    private static final String ATTR_ON_INIT_RENDER_POSTED = "org.zkoss.zul.Grid.onInitLaterPosted";
    private static final String ATTR_ON_PAGING_INIT_RENDERER_POSTED = "org.zkoss.zul.Grid.onPagingInitLaterPosted";

    private static final int INIT_LIMIT = 50;

    private transient DataLoader _dataLoader;
    private transient Rows _rows;
    private transient Columns _cols;
    private transient Foot _foot;
    private transient Frozen _frozen;
    private transient Collection<Component> _heads;
    private transient ListModel<?> _model;
    private transient RowRenderer<?> _renderer;
    private transient ListDataListener _dataListener;
    private transient GroupsDataListener _groupsDataListener;
    /** The paging controller, used only if mold = "paging". */
    private transient Paginal _pgi;
    /** 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;
    private String _innerWidth = "100%";
    private int _currentTop = 0; //since 5.0.0 scroll position
    private int _currentLeft = 0;
    private int _topPad; //since 5.0.0 top padding
    private boolean _renderAll; //since 5.0.0

    private transient boolean _rod;
    /** the message to display when there are no items */
    private String _emptyMessage;
    private int _visibleRows; //since 8.5.0

    static {
        addClientEvent(Grid.class, Events.ON_RENDER, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE);
        addClientEvent(Grid.class, Events.ON_INNER_WIDTH, CE_DUPLICATE_IGNORE | CE_IMPORTANT);
        addClientEvent(Grid.class, Events.ON_SCROLL_POS, CE_DUPLICATE_IGNORE | CE_IMPORTANT); //since 5.0.0
        addClientEvent(Grid.class, Events.ON_TOP_PAD, CE_DUPLICATE_IGNORE); //since 5.0.0
        addClientEvent(Grid.class, Events.ON_DATA_LOADING, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE); //since 5.0.0
        addClientEvent(Grid.class, ZulEvents.ON_PAGE_SIZE, CE_DUPLICATE_IGNORE | CE_IMPORTANT | CE_NON_DEFERRABLE); //since 5.0.2
    }

    public Grid() {
        init();
    }

    private void init() {
        _heads = new AbstractCollection<Component>() {
            public int size() {
                int sz = getChildren().size();
                if (_rows != null)
                    --sz;
                if (_foot != null)
                    --sz;
                if (_paging != null)
                    --sz;
                if (_frozen != null)
                    --sz;
                return sz;
            }

            public Iterator<Component> iterator() {
                return new Iter();
            }
        };
    }

    public void onPageAttached(Page newpage, Page oldpage) {
        super.onPageAttached(newpage, oldpage);
        if (oldpage == null) {
            Executions.getCurrent().setAttribute("zkoss.Grid.deferInitModel_" + getUuid(), Boolean.TRUE);
            //prepare a right moment to init Grid(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;
            _topPad = 0;
        }
    }

    private class ModelInitListener implements SerializableEventListener<Event>, CloneableEventListener<Event> {
        public void onEvent(Event event) throws Exception {
            if (_modelInitListener != null) {
                Grid.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 || getRows() == null || getRows().getChildren().isEmpty()) {
                    if (_model != null) { //so has to recreate rows and items
                        if (getRows() != null)
                            getRows().getChildren().clear();
                        resetDataLoader(); //enforce recreate dataloader, must after getRows().getChildren().clear()
                        initModel();
                    } else {
                        resetDataLoader(); //enforce recreate dataloader

                        // Bug ZK-1895
                        //The attribute shall be removed, otherwise DataLoader will not syncModel when setModel
                        Executions.getCurrent().removeAttribute("zkoss.Grid.deferInitModel_" + getUuid());
                    }
                }
            } else if (_model != null) { //rows not created yet
                initModel();
            } else {
                //bug# 3039282: NullPointerException when assign a model to Grid at onCreate
                //The attribute shall be removed, otherwise DataLoader will not syncModel when setModel
                Executions.getCurrent().removeAttribute("zkoss.Grid.deferInitModel_" + getUuid());
            }
            final DataLoader loader = getDataLoader();

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

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

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

    /** Returns whether to grow and shrink vertical to fit their given space,
     * so called vertical flexibility.
     *
     * <p>Default: false.
     * @since 3.5.0
     */
    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.
     *
     * @since 3.5.0
     */
    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 (_visibleRows != 0)
            throw new UiException("Not allowed to set vflex and visibleRows at the same time");

        super.setVflex(flex);
    }

    /** Returns the rows.
     */
    public Rows getRows() {
        return _rows;
    }

    /** Returns the columns.
     */
    public Columns getColumns() {
        return _cols;
    }

    /** Returns the foot.
     */
    public Foot getFoot() {
        return _foot;
    }

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

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

    /** Returns the specified cell, or null if not available.
     * @param row which row to fetch (starting at 0).
     * @param col which column to fetch (starting at 0).
     */
    public Component getCell(int row, int col) {
        final Rows rows = getRows();
        if (rows == null)
            return null;

        List children = rows.getChildren();
        if (children.size() <= row)
            return null;

        children = ((Row) children.get(row)).getChildren();
        return children.size() <= col ? null : (Component) children.get(col);
    }

    /** Returns the visible rows. Zero means no limitation.
     * <p>Default: 0.
     * @since 8.5.0
     */
    public int getVisibleRows() {
        return _visibleRows;
    }

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

        if (visibleRows < 0)
            throw new WrongValueException("Illegal rows: " + visibleRows);

        if (_visibleRows != visibleRows) {
            _visibleRows = visibleRows;
            smartUpdate("visibleRows", _visibleRows);
        }
    }

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

        super.setHeight(height);
    }

    //--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 grid 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(_rows != null ? getDataLoader().getTotalSize() : 0);
                        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.setActivePage(_pgi.getActivePage());
                    m.setPageSize(_pgi.getPageSize());
                }
            }
        }
    }

    /** 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(_rows != null ? getDataLoader().getTotalSize() : 0);
        //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);
    }

    private class PGListener implements PagingListener {
        public void onEvent(Event event) {
            if (event instanceof PagingEvent) {
                PagingEvent pe = (PagingEvent) event;
                int pgsz = pe.getPageable().getPageSize();
                int actpg = pe.getActivePage();
                if (PageableModel.INTERNAL_EVENT.equals(pe.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
                    ((Pageable) _model).setActivePage(actpg);
                }
                Events.postEvent(new PagingEvent(event.getName(), Grid.this, pe.getPageable(), actpg));
            }
        }

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

    private class PGImpListener implements PagingListener {
        public void onEvent(Event event) {
            if (_rows != null && _model != null && inPagingMold()) {
                //theoretically, _rows shall not be null if _model is not null when
                //this method is called. But, just in case -- if sent manually
                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();
            }

            if (getModel() != null || getPagingPosition().equals("both"))
                invalidate(); // just in case.
            else if (_rows != null) {
                _rows.invalidate();

                // Bug 3218078
                if (_frozen != null)
                    _frozen.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;
    }

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

    //-- ListModel dependent codes --//
    /** Returns the model associated with this grid, or null
     * if this grid 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")
    public <T> ListModel<T> getModel() {
        return (ListModel) _model;
    }

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

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

    /** Sets the list model associated with this grid.
     * 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 dissociate
     * any previous model.
     * @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 != 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 row anyway
                    if (_model instanceof GroupsListModel)
                        _rows.getChildren().clear();*/

                    resetDataLoader(); // Bug 3357641

                    if (!isAutosort()) {
                        Columns cols = getColumns();
                        if (cols != null) {
                            for (Component column : cols.getChildren()) {
                                ((Column) column).setSortDirection("natural");
                            }
                        }
                    }
                }
                if (_rows != null)
                    _rows.getChildren().clear(); //Bug 1807414, ZK-1512

                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.Grid.deferInitModel_" + getUuid()) != null;
            final boolean rod = evalRod();
            //Always syncModel because it is easier for user to enforce reload
            if (!defer || !rod) { //if attached and rod, defer the model sync
                getDataLoader().syncModel(-1, -1); //create rows if necessary
                removeAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED);
            }
            if (!doSort(this))
                postOnInitRender();
            //Since user might setModel and setRender 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 round trip)
        } else if (_model != null) {
            _model.removeListDataListener(_dataListener);
            if (_model instanceof PageableModel && _pgListener != null)
                ((PageableModel) _model).removePagingEventListener((PagingListener) _pgListener);
            GroupsModel g = getGroupsModel();
            if (g != null)
                g.removeGroupsDataListener(_groupsDataListener);
            _model = null;
            if (_rows != null)
                _rows.getChildren().clear();
            smartUpdate("model", false);
            getDataLoader().updateModelInfo();
        }
    }

    /** Sets the groups model associated with this grid.
     * 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 dissociate
     * any previous model.
     * @exception UiException if failed to initialize with the model
     * @since 3.5.0
     * @see #setModel(ListModel)
     * @see #getGroupsModel()
     */
    public void setModel(GroupsModel<?, ?, ?> model) {
        setModel((model != null ? GroupsListModel.toListModel(model) : null));
    }

    private void initDataListener() {
        if (_dataListener == null)
            _dataListener = new ListDataListener() {
                public void onChange(ListDataEvent event) {
                    // ZK-1864: share listmodelist cause un-predictable reload
                    if (event.getType() != ListDataEvent.SELECTION_CHANGED)
                        onListDataChange(event);
                }
            };
        _model.addListDataListener(_dataListener);

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

    /**
     * Sort the rows based on {@link Column#getSortDirection}.
     * @return whether the method susseed or not
     */
    private static boolean doSort(Grid grid) {
        Columns cols = grid.getColumns();
        if (!grid.isAutosort() || cols == null)
            return false;
        for (Iterator it = cols.getChildren().iterator(); it.hasNext();) {
            final Column hd = (Column) it.next();
            String dir = hd.getSortDirection();
            if (!"natural".equals(dir)) {
                return hd.doSort("ascending".equals(dir));
            }
        }
        return false;
    }

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

    /** Sets the renderer which is used to render each row
     * if {@link #getModel} is not null.
     *
     * <p>Note: changing a render will not cause the grid 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 setRowRenderer(RowRenderer<?> renderer) {
        if (_renderer != renderer) {
            _renderer = renderer;

            if (_model != null) {
                if ((renderer instanceof RowRendererExt) || (_renderer instanceof RowRendererExt)) {
                    //bug# 2388345, a new renderer that might new own Row, shall clean all Row first
                    getRows().getChildren().clear();
                    getDataLoader().syncModel(-1, -1); //we have to recreate all
                } else if (getAttribute(ATTR_ON_INIT_RENDER_POSTED) == null) {
                    getDataLoader().syncModel(-1, -1); //we 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.Grid.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.
     */
    public void setRowRenderer(String clsnm) throws ClassNotFoundException, NoSuchMethodException,
            IllegalAccessException, InstantiationException, java.lang.reflect.InvocationTargetException {
        if (clsnm != null)
            setRowRenderer((RowRenderer) Classes.newInstanceByThread(clsnm));
    }

    /**
     * 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;
    }

    /** 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();
    }

    private void doInitRenderer() {
        if (getPage() == null)
            return;
        final Renderer renderer = new Renderer();
        try {
            int pgsz, ofs;
            if (inPagingMold()) {
                pgsz = _pgi.getPageSize();
                ofs = _pgi.getActivePage() * pgsz;
            } else {
                pgsz = getDataLoader().getLimit();
                ofs = 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 List<Component> rowChildren = _rows != null ? _rows.getChildren() : Collections.emptyList();
            final int cnt = rowChildren.size() + getDataLoader().getOffset();
            if (ofs >= cnt) { //not possible; just in case
                ofs = cnt - pgsz;
                if (ofs < 0)
                    ofs = 0;
            }

            int j = 0;
            int index = 0; // ZK-1867: Set visible of row doesn't work correctly
            int realOfs = ofs - getDataLoader().getOffset();
            if (realOfs < 0)
                realOfs = 0;
            boolean open = true;
            for (Row row = rowChildren.size() <= realOfs ? null
                    : (Row) rowChildren.get(realOfs), nxt; j < pgsz && row != null; row = nxt) {
                nxt = (Row) row.getNextSibling();

                if (row.isVisible() && (open || row instanceof Groupfoot || row instanceof Group)) {
                    renderer.render(row, index + ofs);
                    ++j;
                }
                if (row instanceof Group)
                    open = ((Group) row).isOpen();

                // B65-ZK-1867 and Z60-Grid-GroupsModelArray-Paging-noROD.zul
                index++;
            }

        } catch (Throwable ex) {
            renderer.doCatch(ex);
        } finally {
            renderer.doFinally();
        }
        Events.postEvent(ZulEvents.ON_AFTER_RENDER, this, null); // notify the grid when all of the row have been rendered.
        removeAttribute(Attributes.BEFORE_MODEL_ITEMS_RENDERED);
    }

    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);
            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.
     */
    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 {
            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 (type == ListDataEvent.STRUCTURE_CHANGED && _model instanceof Sortable && _cols != null) { //ZK-1704 added null check for _cols
                Sortable<Object> smodel = cast(_model);
                List<Column> cols = cast(_cols.getChildren());
                boolean found = false;
                for (Column col : cols) {
                    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);
                    }
                }
            }
        }
    }

    /** Returns the label for the cell generated by the default renderer.
     */
    private static Label newRenderLabel(String value) {
        final Label label = new Label(value != null && value.length() > 0 ? value : " ");
        label.setPre(true); //to make sure &nbsp; is generated, and then occupies some space
        return label;
    }

    /** Used to render row if _model is specified. */
    /*package*/ class Renderer {
        private final RowRenderer _renderer;
        private boolean _rendered, _ctrled;

        /*package*/ Renderer() {
            _renderer = (RowRenderer) getDataLoader().getRealRenderer();
        }

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

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

            final Component cell = row.getFirstChild();
            if (!(_renderer instanceof RowRendererExt)
                    || (((RowRendererExt) _renderer).getControls() & RowRendererExt.DETACH_ON_RENDER) != 0) { //detach (default)
                cell.detach();
            }

            final boolean oldFlag = _rows.setReplacingRow(true);
            try {
                final Object value = _model.getElementAt(index);
                try {
                    _renderer.render(row, value, index);
                } catch (AbstractMethodError ex) {
                    final Method m = _renderer.getClass().getMethod("render",
                            new Class<?>[] { Row.class, Object.class });
                    m.setAccessible(true);
                    m.invoke(_renderer, new Object[] { row, value });
                }
                Object v = row.getAttribute(Attributes.MODEL_RENDERAS);
                if (v != null) //a new row is created to replace the existent one
                    row = (Row) v;
            } catch (Throwable ex) {
                try {
                    final Label label = newRenderLabel(Exceptions.getMessage(ex));
                    label.applyProperties();
                    label.setParent(row);
                } catch (Throwable t) {
                    log.error("", t);
                }
                row.setLoaded(true);
                throw ex;
            } finally {
                _rows.setReplacingRow(oldFlag);
                if (row.getChildren().isEmpty())
                    cell.setParent(row);
            }

            row.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 Row} if not loaded yet,
     * with {@link #getRowRenderer}.
     *
     * <p>It does nothing if {@link #getModel} returns null.
     * In other words, it is meaningful only if live data model is used.
     */
    public void renderRow(Row row) {
        if (_model == null)
            return;

        final Renderer renderer = new Renderer();
        try {
            renderer.render(row, row.getIndex());
        } catch (Throwable ex) {
            renderer.doCatch(ex);
        } finally {
            renderer.doFinally();
        }
    }

    /** Renders all {@link Row} if not loaded yet,
     * with {@link #getRowRenderer}.
     */
    public void renderAll() {
        if (_model == null)
            return;

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

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

    /** Renders a set of specified rows.
     * It is the same as {@link #renderItems}.
     */
    public void renderRows(Set<? extends Row> rows) {
        renderItems(rows);
    }

    public void renderItems(Set<? extends Row> rows) {
        if (_model == null) { //just in case that application developers might change it
            if (log.isDebugEnabled())
                log.debug("No model no render");
            return;
        }

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

        final Renderer renderer = new Renderer();
        try {
            for (final Row row : rows)
                renderer.render(row, row.getIndex());
        } catch (Throwable ex) {
            renderer.doCatch(ex);
        } finally {
            renderer.doFinally();
        }
    }

    /** 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);
        }
    }

    /** Sets the mold to render this component.
     *
     * @param mold the mold. If null or empty, "default" is assumed.
     * Allowed values: default, paging
     * @see org.zkoss.zk.ui.metainfo.ComponentDefinition
     */
    //-- super --//
    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
            }
        }
    }

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

    //-- Component --//
    public void beforeChildAdded(Component newChild, Component refChild) {
        if (newChild instanceof Rows) {
            if (_rows != null && _rows != newChild)
                throw new UiException("Only one rows child is allowed: " + this
                        + "\nNote: rows is created automatically if live data");
        } else if (newChild instanceof Columns) {
            if (_cols != null && _cols != newChild)
                throw new UiException("Only one columns child is allowed: " + this);
        } else if (newChild instanceof Frozen) {
            if (_frozen != null && _frozen != newChild)
                throw new UiException("Only one frozen child is allowed: " + this);
        } else if (newChild instanceof Paging) {
            if (_pgi != null)
                throw new UiException("External paging cannot coexist with child paging");
            if (_paging != null && _paging != newChild)
                throw new UiException("Only one paging is allowed: " + this);
            if (!inPagingMold())
                throw new UiException("The child paging is allowed only in the paging mold");
        } else if (newChild instanceof Foot) {
            if (_foot != null && _foot != newChild)
                throw new UiException("Only one foot child is allowed: " + this);
        } else if (!(newChild instanceof Auxhead)) {
            throw new UiException("Unsupported child for grid: " + newChild);
        }

        super.beforeChildAdded(newChild, refChild);
    }

    public boolean insertBefore(Component newChild, Component refChild) {
        if (newChild instanceof Rows) {
            if (super.insertBefore(newChild, refChild)) {
                _rows = (Rows) newChild;
                return true;
            }
        } else if (newChild instanceof Columns) {
            if (super.insertBefore(newChild, refChild)) {
                _cols = (Columns) newChild;
                return true;
            }
        } else if (newChild instanceof Frozen) {
            if (super.insertBefore(newChild, refChild)) {
                _frozen = (Frozen) newChild;
                return true;
            }
        } else if (newChild instanceof Paging) {
            if (super.insertBefore(newChild, refChild)) {
                _pgi = _paging = (Paging) newChild;
                return true;
            }
        } else if (newChild instanceof Foot) {
            if (super.insertBefore(newChild, refChild)) {
                _foot = (Foot) newChild;
                return true;
            }
        } else {
            return super.insertBefore(newChild, refChild);
        }
        return false;
    }

    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 (!super.removeChild(child))
            return false;

        if (_rows == child)
            _rows = null;
        else if (_cols == child)
            _cols = null;
        else if (_frozen == child)
            _frozen = null;
        else if (_foot == child)
            _foot = null;
        else if (_paging == child) {
            _paging = null;
            if (_pgi == child)
                _pgi = null;
        }
        return true;
    }

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

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

    /** Returns whether to sort all items when model or sort direction be changed.
     * @since 5.0.7
     */
    /*package*/ boolean isAutosort() {
        String attr = "org.zkoss.zul.grid.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;
    }

    /**
     * Returns the number of rows 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.grid.preloadSize", sz, true)) < 0)
            throw new UiException("nonnegative is required: " + sz);
        return sz;
    }

    /**
     * Returns the number of rows rendered when the Grid 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.grid.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.grid.autoSort";
        Object val = getAttribute(attr, true);
        if (val == null)
            val = Library.getProperty(attr);
        return val == null ? true : "ignore.change".equals(val);
    }

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

    //Cloneable//
    public Object clone() {
        final Grid clone = (Grid) super.clone();
        clone.init();

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

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

        int cnt = 0;
        if (clone._rows != null)
            ++cnt;
        if (clone._cols != null)
            ++cnt;
        if (clone._foot != null)
            ++cnt;
        if (clone._frozen != null)
            ++cnt;
        if (clone._paging != null)
            ++cnt;
        if (cnt > 0)
            clone.afterUnmarshal(cnt);

        // after _pgi ready, and then getLimit() will work
        final int limit = clone.getDataLoader().getLimit();
        clone.resetDataLoader(false);
        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;
            }
            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);
        }

        return clone;
    }

    /** @param cnt # of children that need special handling (used for optimization).
     * -1 means process all of them
     */
    private void afterUnmarshal(int cnt) {
        for (Iterator it = getChildren().iterator(); it.hasNext();) {
            final Object child = it.next();
            if (child instanceof Rows) {
                _rows = (Rows) child;
                if (--cnt == 0)
                    break;
            } else if (child instanceof Columns) {
                _cols = (Columns) child;
                if (--cnt == 0)
                    break;
            } else if (child instanceof Paging) {
                _pgi = _paging = (Paging) child;
                addPagingListener(_pgi);
                if (--cnt == 0)
                    break;
            } else if (child instanceof Frozen) {
                _frozen = (Frozen) child;
                if (--cnt == 0)
                    break;
            } else if (child instanceof Foot) {
                _foot = (Foot) child;
                if (--cnt == 0)
                    break;
            }
        }
    }

    /**
     * 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)) {
            _emptyMessage = emptyMessage;
            smartUpdate("emptyMessage", _emptyMessage);
        }
    }

    //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);
        }
    }

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

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

        init();
        afterUnmarshal(-1);

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

        //TODO: how to marshal _pgi if _pgi != _paging
        //TODO: re-register event listener for onPaging

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

            // Map#Entry cannot be serialized, we have to restore them
            if (_model instanceof ListModelMap && _rows != null) {
                for (Component o : _rows.getChildren()) {
                    Row item = (Row) o;
                    item.setValue(_model.getElementAt(item.getIndex()));
                }
            }
        }
    }

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

        render(renderer, "oddRowSclass", _scOddRow);

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

        if (!"100%".equals(_innerWidth))
            render(renderer, "innerWidth", _innerWidth);
        if (_currentTop != 0)
            renderer.render("_currentTop", _currentTop);
        if (_currentLeft != 0)
            renderer.render("_currentLeft", _currentLeft);

        renderer.render("_topPad", _topPad);

        renderer.render("emptyMessage", _emptyMessage);

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

        if (_visibleRows > 0)
            renderer.render("visibleRows", _visibleRows);

        if (_rod && !_renderAll) {
            if (((Cropper) getDataLoader()).isCropper())//bug #2936064
                renderer.render("_grid$rod", true);
            int sz = initRodSize();
            if (sz != INIT_LIMIT)
                renderer.render("initRodSize", initRodSize());
        }

        if (_pgi != null && _pgi instanceof Component)
            renderer.render("paginal", _pgi);

    }

    /*package*/ boolean isRod() {
        return _rod;
    }

    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 --//
    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 Padding {
        //-- Padding --//
        public int getHeight() {
            return _topPad;
        }

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

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

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

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

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

            public Integer getValue(Component cmp) {
                return ((Grid) cmp)._topPad;
            }
        });
    }

    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(AuRequest, boolean)},
     * it also handles onSelect.
     * @since 5.0.0
     */
    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();
        if (cmd.equals(Events.ON_DATA_LOADING)) {
            Events.postEvent(DataLoadingEvent.getDataLoadingEvent(request, preloadSize()));
        } else if (inPagingMold() && cmd.equals(ZulEvents.ON_PAGE_SIZE)) {
            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 = size > oldsize ? (end - 1) : begin;
                int newpg = sel / size;
                setPageSize(size);
                setActivePage(newpg);
                // Bug: B50-3204965: onPageSize 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_TOP_PAD)) {
            _topPad = AuRequests.getInt(request.getData(), "topPad", 0);
        } 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 RenderEvent<Row> event = RenderEvent.getRenderEvent(request);
            final Set<Row> items = event.getItems();

            int cnt = items.size();
            if (cnt == 0)
                return; //nothing to do

            cnt = 20 - cnt;
            if (cnt > 0 && _preloadsz > 0) { //Feature 1740072: pre-load
                if (cnt > _preloadsz)
                    cnt = _preloadsz;

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

                //2. add unload items before the found one
                if (!toload.isEmpty()) {
                    int bfcnt = cnt / 3;
                    for (Iterator<Row> 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 Row row = (Row) it.next();
                    if (!row.isLoaded() && items.add(row))
                        --cnt;
                }
            }
            renderItems(items);
        } else
            super.service(request, everError);
    }

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

        public boolean hasNext() {
            while (_it.hasNext()) {
                Component o = _it.next();
                if (o instanceof Columns || o instanceof Auxhead) {
                    _it.previous();
                    return true;
                }
            }
            return false;
        }

        public Component next() {
            for (;;) {
                Component o = _it.next();
                if (o instanceof Columns || o instanceof Auxhead)
                    return o;
            }
        }

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

    @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);
    }

    @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 row by the given index.
     * @param index the index of row
     * @since 8.5.2
     */
    public void scrollToIndex(int index) {
        ListModel<Object> model = getModel();
        int rowCount = model != null ? model.getSize() : getRows().getChildren().size();
        if (index < 0 || index > rowCount - 1) {
            throw new IndexOutOfBoundsException("Illegal index: " + index);
        }
        response(new AuInvoke(this, "scrollToIndex", index, (double) index / rowCount));
    }
}