That said, what are you using to let the user control paging? How does it work if two different nodes at two different levels need to page? I'm very curious as to how this UI works.
I've implemented a TreePagingHandler, which triggers load requests upon onBodyScroll, onCollapse and onResize. This should cover all cases were content needs to be loaded. The paging size itself is determined dynamically based on the height of the grid. The current implementation only supports paging on the out most level (as this was sufficient for our needs). But you can load multiple levels at once with only one request.
I have created a small self contained sample as a showcase. In analogy to the treegrid sample in the GXT demo I reused the FileModel and FolderModel. See GxtTreePagingGrid-sample-src.zip for the source code of the showcase. For this to work you will need a modified gxt.jar because some changes in GXT itself were necessary as mentioned in my previous post. See gxt-mod-3.0.1-patch.txt for a patch of the changed and added source files in GXT.
For backwards compatibility I left the implementing class names unchanged and added interfaces with "Interface" suffixes (which isn't really good naming style)
- Download both attached files and GXT 3.0.1
- Extract gxt-mod-3.0.1-src.zip into the src folder where you extracted GXT 3.0.1 (overwrite)
- Build GXT 3.0.1 with the modified source and package as jar
- Create a new GWT 2.5.0 project and extract GxtTreePagingGrid-sample.src into the project folder
- Add the necessary libs to the war/WEB-INF/lib folder: gxt-mod-3.0.1.jar
- For the tests to work you will additionally need mockito-core-1.9.5.jar and objenesis-1.0.jar
Alternatively you can download the compiled war from another host (filesize too large):
http://www.xup.in/dl,27854454/GxtTre...id-sample.war/
Some selected source files:
The main work is done in the TreePagingHandler:
Code:
package com.sencha.gxt.examples.client.tree;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.sencha.gxt.data.shared.loader.BeforeLoadEvent;
import com.sencha.gxt.data.shared.loader.BeforeLoadEvent.BeforeLoadHandler;
import com.sencha.gxt.data.shared.loader.LoadEvent;
import com.sencha.gxt.data.shared.loader.LoadExceptionEvent;
import com.sencha.gxt.data.shared.loader.LoadExceptionEvent.LoadExceptionHandler;
import com.sencha.gxt.data.shared.loader.LoadHandler;
import com.sencha.gxt.examples.shared.loader.TreeFilterPagingLoadConfig;
import com.sencha.gxt.examples.shared.loader.TreeFilterPagingLoadResult;
import com.sencha.gxt.widget.core.client.event.BodyScrollEvent;
import com.sencha.gxt.widget.core.client.event.BodyScrollEvent.BodyScrollHandler;
import com.sencha.gxt.widget.core.client.event.CollapseItemEvent;
import com.sencha.gxt.widget.core.client.event.CollapseItemEvent.CollapseItemHandler;
import com.sencha.gxt.widget.core.client.event.ExpandItemEvent;
import com.sencha.gxt.widget.core.client.event.ExpandItemEvent.ExpandItemHandler;
/**
* This TreePagingHandler handles the paging for a TreePagingGrid
*
* @param <M> the model data
* @param <C> the request config
* @param <D> the result
* @author cwat-cfischer
*/
public class TreePagingHandler<M, C extends TreeFilterPagingLoadConfig<M>, D extends TreeFilterPagingLoadResult<M>>
implements BodyScrollHandler, BeforeLoadHandler<C>, LoadHandler<C, D>, LoadExceptionHandler<C>, ResizeHandler, CollapseItemHandler<M>, ExpandItemHandler<M> {
/** PRE_LOAD_FACTOR: we want to load a little bit more than exactly one page size */
private static final double PRE_LOAD_FACTOR = 1.5; // TODO: maybe we want to use 2? if clicking under the scrollbar dragger(?)
/** PRE_LOAD_OFFSET: we probably should already load a little bit before having reached the scroll limit (size in pixel) */
private static final int PRE_LOAD_OFFSET = 150; // TODO: this should be fine-tuned in the future
/** MIN_PAGING_SIZE: the minimum paging size (we do not want to bother the server with many small requests) */
private static final int MIN_PAGING_SIZE = 15;
/** DEFAULT_ROW_HEIGHT: for the initial request there are no rows so we have no way to calculate the height */
private static final int DEFAULT_ROW_HEIGHT = 23;
private int pendingOffset;
private int totalCount;
private int remainingCount;
private C loadConfig;
private TreePagingGrid<M, C, D> treeGrid;
private TreePagingLoaderInterface<M, C, D> loader;
private HandlerRegistration beforeLoadHandler;
/**
* Creates a new TreePagingHandler
*
* @param treeGrid the {@link TreePagingGridImpl}
*/
public TreePagingHandler(TreePagingGrid<M, C, D> treeGrid) {
if (null == treeGrid) {
throw new IllegalArgumentException("treeGrid must not be null!");
}
this.treeGrid = treeGrid;
this.loader = null;
}
/**
* Set the TreePagingLoader
*
* @param loader the TreePagingLoader
*/
public void setLoader(TreePagingLoaderInterface<M, C, D> loader) {
this.loader = loader;
if (null != beforeLoadHandler) {
beforeLoadHandler.removeHandler();
}
beforeLoadHandler = loader.addBeforeLoadHandler(this);
// we adapt this dynamically depending on the browsers client area
adaptPagingSize();
}
private int getRowHeight() {
int rowHeight = treeGrid.getRowHeight(0);
if (0 == rowHeight) {
rowHeight = DEFAULT_ROW_HEIGHT;
}
return rowHeight;
}
/**
* Returns the number of visible rows for one "page"
* Has a minimum of MIN_PAGING_SIZE
*
* @param factor a pre load factor to scale the row number
* @return the number of visible rows per page
*/
private int getRowsPerPage(double factor) {
int scrollerHeight = treeGrid.getScrollerHeight();
int rowHeight = getRowHeight();
return (int)Math.max(MIN_PAGING_SIZE, Math.ceil(scrollerHeight * factor / rowHeight));
}
private void adaptPagingSize() {
if (null != loader) {
loader.setLimit(getRowsPerPage(PRE_LOAD_FACTOR));
}
}
/**
* Returns true, if loading the next part of nodes is necessary
*
* @return true if load of next page is indicated, false otherwise
*/
private boolean isLoadIndicated() {
int scrollTop = treeGrid.getScrollTop();
int scrollerHeight = treeGrid.getScrollerHeight();
int loadedHeight = treeGrid.getContentHeight();
int offset = (pendingOffset - totalCount + remainingCount) * getRowHeight();
return null != loader
&& pendingOffset < totalCount
&& loadedHeight + offset - PRE_LOAD_OFFSET < scrollTop + scrollerHeight;
}
/**
* Loads the next part of nodes (i.e. the next "page")
*/
private void loadNextPage() {
// ensure that even with limit changes we load each node just once
int nextOffset = pendingOffset;
int nextLimit = loader.getLimit();
// avoid side effects from loading results by reusing load config from paging
loader.useLoadConfig(loadConfig);
loader.load(nextOffset, nextLimit);
pendingOffset = nextOffset + nextLimit;
loadConfig = loader.getLastLoadConfig();
}
/**
* Sets the body height of the tree grid according to the total count
* Should also be called on expanding and collapsing
*/
private void adaptBodyHeight() {
int newHeight;
if (remainingCount <= 0) {
newHeight = 0;
} else {
newHeight = remainingCount * getRowHeight() + treeGrid.getContentHeight();
}
treeGrid.setContentHeight(newHeight);
}
@Override
public void onBeforeLoad(BeforeLoadEvent<C> event) {
C config = event.getLoadConfig();
if (null == config.getParent()) {
treeGrid.displayLoadingIndicator();
}
}
/**
* Trigger new load request on scroll
*/
@Override
public void onBodyScroll(BodyScrollEvent event) {
if (isLoadIndicated()) {
loadNextPage();
}
}
/**
* Trigger new load request on resize
*/
@Override
public void onResize(ResizeEvent event) {
adaptPagingSize();
if (isLoadIndicated()) {
loadNextPage();
}
}
@Override
public void onExpand(ExpandItemEvent<M> event) {
adaptBodyHeight();
}
@Override
public void onCollapse(CollapseItemEvent<M> event) {
adaptBodyHeight();
if (isLoadIndicated()) {
loadNextPage();
}
}
@Override
public void onLoadException(LoadExceptionEvent<C> event) {
treeGrid.hideLoadingIndicator();
}
@Override
public void onLoad(LoadEvent<C, D> event) {
treeGrid.hideLoadingIndicator();
D result = event.getLoadResult();
if (null == result.getParent()) {
if (result.isInitialResult()) {
loadConfig = loader.getLastLoadConfig();
totalCount = result.getTotalLength();
remainingCount = totalCount;
pendingOffset = loadConfig.getLimit();
}
remainingCount -= result.getCount();
adaptBodyHeight();
}
}
}
The TreeStoreBinding does not replace the nodes any more, but inserts them:
Code:
package com.sencha.gxt.examples.client.tree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.sencha.gxt.data.shared.TreeStore;
import com.sencha.gxt.data.shared.event.StoreDataChangeEvent;
import com.sencha.gxt.data.shared.loader.LoadEvent;
import com.sencha.gxt.data.shared.loader.LoadHandler;
import com.sencha.gxt.examples.shared.loader.TreeFilterPagingLoadConfig;
import com.sencha.gxt.examples.shared.loader.TreeFilterPagingLoadResult;
/**
* Event handler for the load event fired when a loader has finished loading data.
* It expects multiple parents along with their respective children in the result config.
*
* @param <M> the type of objects that populate the store
* @param <C> the type of load config used in the loader request
* @param <D> the type of load result returned for the loader request
* @author cwat-cfischer
*/
public class TreeStoreBinding<M, C extends TreeFilterPagingLoadConfig<M>, D extends TreeFilterPagingLoadResult<M>> implements
LoadHandler<C, D> {
private final TreeStore<M> store;
/**
* Creates a new TreeStoreBinding which can insert multiple different tree fragments
*
* @param store
* @param tree
*/
public TreeStoreBinding(TreeStore<M> store) {
this.store = store;
}
@Override
public void onLoad(LoadEvent<C, D> event) {
C config = event.getLoadConfig();
M parent = config.getParent();
D result = event.getLoadResult();
if (result.isInitialResult()) {
store.clear();
}
insertNodes(parent, result);
}
private void insertNodes(M parent, D result) {
List<M> children = result.getData(parent);
if (null != children) {
if (null == parent) {
List<M> newRoots = new ArrayList<M>(children);
newRoots.removeAll(store.getRootItems());
store.add(newRoots);
} else {
assert Collections.disjoint(store.getChildren(parent), children) : "store must not already contain children of this parent";
store.add(parent, children);
}
if (null != parent || result.isInitialResult()) {
// we need to fire this event so the grid/store knows the node is loaded and can cache it
store.fireEvent(new StoreDataChangeEvent<M>(parent));
}
for (M child : children) {
insertNodes(child, result);
}
}
}
}
And the TreePagingLoader handles the loading of the nodes:
Code:
package com.sencha.gxt.examples.client.tree;
import java.util.LinkedList;
import java.util.Queue;
import com.sencha.gxt.data.shared.loader.DataProxy;
import com.sencha.gxt.data.shared.loader.DataReader;
import com.sencha.gxt.data.shared.loader.PagingLoader;
import com.sencha.gxt.examples.shared.loader.TreeFilterPagingLoadConfig;
import com.sencha.gxt.examples.shared.loader.TreeFilterPagingLoadConfigBean;
import com.sencha.gxt.examples.shared.loader.TreeFilterPagingLoadResult;
/**
* A Loader for TreeGrid to support filtering and "single page paging"
*
* @param <C> the type of the load config
* @param <D> the type of the load result
* @author cwat-cfischer
*/
public class TreePagingLoader<M, C extends TreeFilterPagingLoadConfig<M>, D extends TreeFilterPagingLoadResult<M>> extends
PagingLoader<C, D> implements TreePagingLoaderInterface<M, C, D> {
private Queue<C> queuedRequests = new LinkedList<C>();
private C pendingRequest;
private M parent;
/**
* Creates a new tree paging loader
*
* @param proxy
*/
public TreePagingLoader(DataProxy<C, D> proxy) {
super(proxy);
}
/**
* Creates a new tree paging loader
*
* @param proxy
* @param reader
*/
public <T> TreePagingLoader(DataProxy<C, T> proxy, DataReader<D, T> reader) {
super(proxy, reader);
}
@Override
public void setParent(M parent) {
this.parent = parent;
}
@Override
public M getParent() {
return parent;
}
@Override
public boolean hasChildren(M parent) {
return false;
}
@Override
public boolean loadChildren(M parent) {
this.parent = parent;
return load();
}
@Override
public void cancelPending() {
queuedRequests.clear();
}
@Override
public boolean load() {
C loadConfig = prepareLoadConfig(newLoadConfig());
if (!loadConfig.equals(pendingRequest) && !queuedRequests.contains(loadConfig)) {
queuedRequests.add(loadConfig);
}
return loadNextQueued();
}
protected boolean loadNextQueued() {
boolean requested = false;
if (null == pendingRequest && !queuedRequests.isEmpty()) {
pendingRequest = queuedRequests.poll();
requested = super.load(pendingRequest);
}
return requested;
}
@Override
protected void onLoadSuccess(C loadConfig, D result) {
super.onLoadSuccess(loadConfig, result);
pendingRequest = null;
loadNextQueued();
}
@Override
protected void onLoadFailure(C loadConfig, Throwable t) {
super.onLoadFailure(loadConfig, t);
pendingRequest = null;
loadNextQueued();
}
@SuppressWarnings("unchecked")
@Override
protected C newLoadConfig() {
return (C)new TreeFilterPagingLoadConfigBean<M>();
}
@Override
public void useLoadConfig(C loadConfig) {
super.useLoadConfig(loadConfig);
parent = loadConfig.getParent();
}
@Override
protected C prepareLoadConfig(C config) {
config = super.prepareLoadConfig(config);
config.setParent(parent);
return config;
}
}
The TreeGrid itself was extended as TreePagingGrid to provide some additional helpers:
Code:
package com.sencha.gxt.examples.client.tree;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.ui.Image;
import com.sencha.gxt.core.client.dom.XElement;
import com.sencha.gxt.data.shared.TreeStore;
import com.sencha.gxt.data.shared.loader.TreeLoaderInterface;
import com.sencha.gxt.examples.shared.loader.TreeFilterPagingLoadConfig;
import com.sencha.gxt.examples.shared.loader.TreeFilterPagingLoadResult;
import com.sencha.gxt.theme.blue.client.status.BlueStatusAppearance;
import com.sencha.gxt.widget.core.client.grid.ColumnConfig;
import com.sencha.gxt.widget.core.client.grid.ColumnModel;
import com.sencha.gxt.widget.core.client.grid.GridView.GridAppearance;
import com.sencha.gxt.widget.core.client.tree.Tree.TreeAppearance;
import com.sencha.gxt.widget.core.client.treegrid.TreeGrid;
/**
* {@link TreeGrid}, which supports paging on the outmost level.
* Paging is done in a similar way like the <a href="http://www.sencha.com/examples/#ExamplePlace:livegrid">LiveGrid example</a>:
* on scrolling down, the next part of nodes is loaded.
* The {@link TreeFilterPagingLoadResult} can hold multiple and independent subtrees (or only parts thereof),
* to reduce the number of necessary roundtrips to the server on loading data.
* Has to be used in conjunction with the {@link TreeStoreBinding}.
*
* @param <M> the model type
* @param <C> the load config type, has to be subtype of {@link TreeFilterPagingLoadConfig}
* @param <D> the load result type, has to be subtype of {@link TreeFilterPagingLoadResult}
* @author cwat-cfischer
*/
public class TreePagingGridImpl<M, C extends TreeFilterPagingLoadConfig<M>, D extends TreeFilterPagingLoadResult<M>> extends TreeGrid<M>
implements TreePagingGrid<M, C, D> {
private TreePagingHandler<M, C, D> pagingHandler = new TreePagingHandler<M, C, D>(this);
private Element loadingIndicator = null;
/**
* Creates a new paging tree grid.
*
* @param store the tree store
* @param cm the column model
* @param treeColumn the tree column
*/
public TreePagingGridImpl(TreeStore<M> store, ColumnModel<M> cm, ColumnConfig<M, ?> treeColumn) {
super(store, cm, treeColumn);
registerPagingHandler();
}
/**
* Creates a new paging tree grid.
*
* @param store the tree store
* @param cm the column model
* @param treeColumn the tree column
* @param appearance the grid appearance
*/
public TreePagingGridImpl(TreeStore<M> store, ColumnModel<M> cm, ColumnConfig<M, ?> treeColumn, GridAppearance appearance) {
super(store, cm, treeColumn, appearance);
registerPagingHandler();
}
/**
* Creates a new paging tree grid.
*
* @param store the tree store
* @param cm the column model
* @param treeColumn the tree column
* @param appearance the grid appearance
* @param treeAppearance the tree appearance
*/
public TreePagingGridImpl(TreeStore<M> store, ColumnModel<M> cm, ColumnConfig<M, ?> treeColumn, GridAppearance appearance,
TreeAppearance treeAppearance) {
super(store, cm, treeColumn, appearance, treeAppearance);
registerPagingHandler();
}
private void registerPagingHandler() {
addResizeHandler(pagingHandler);
addBodyScrollHandler(pagingHandler);
addExpandHandler(pagingHandler);
addCollapseHandler(pagingHandler);
}
@Override
public void setTreeLoader(TreeLoaderInterface<M, ?, ?> treeLoader) {
throw new UnsupportedOperationException("Do not use setTreeLoader! Use setTreePagingLoader instead!");
}
@Override
public void setTreePagingLoader(TreePagingLoaderInterface<M, C, D> treeLoader) {
super.setTreeLoader(treeLoader);
treeLoader.addLoadHandler(pagingHandler);
treeLoader.addLoadExceptionHandler(pagingHandler);
pagingHandler.setLoader(treeLoader);
}
// TODO: introduce new event for showing and hiding the loadIndicator?
@Override
public void displayLoadingIndicator() {
Element parent = getTreeView().getBody().getFirstChildElement().cast();
if (null == loadingIndicator) {
loadingIndicator = XElement.createElement("div");
ImageResource throbber = (new BlueStatusAppearance()).getBusyIcon();
loadingIndicator.appendChild(new Image(throbber).getElement());
loadingIndicator.appendChild(XElement.createElement("span"));
loadingIndicator.getFirstChildElement().getNextSiblingElement().setInnerText("loading nodes...");
}
if (!parent.isOrHasChild(loadingIndicator)) {
parent.appendChild(loadingIndicator);
}
}
@Override
public void hideLoadingIndicator() {
if (null != loadingIndicator) {
loadingIndicator.removeFromParent();
}
}
@Override
public int getContentHeight() {
return treeGridView.getBody().getFirstChildElement().getOffsetHeight();
}
@Override
public void setContentHeight(int height) {
treeGridView.getBody().setHeight(height);
}
@Override
public int getScrollerHeight() {
Element scroller = treeGridView.getScroller();
return null != scroller ? scroller.getOffsetHeight() : 0;
}
@Override
public int getRowHeight(int idx) {
return treeStore.getRootCount() > 0 ? treeGridView.getRow(idx).getOffsetHeight() : 0;
}
@Override
public int getScrollTop() {
return treeGridView.getScrollState().getY();
}
}
The usage of the TreePagingGrid is quite similar to the TreeGrid, with the distinction, that you have to use setTreePagingLoader instead of setTreeLoader. If you want to see the loading indicators in the grid, you have to set the view to an instance of TreeGridLoadingView.
I hope you find this extension useful and it will find its way into the codebase of GXT.
Regards