Skip to content
Browse files

Merge pull request #132 from kriszyp/scrolling-large

Improvements to OnDemandList particularly for large grids
  • Loading branch information...
2 parents f4e72e7 + 2bd20d4 commit 2ed49e3168855d0938b2ee0c34e7a6e8775e253a SitePenKenFranqueiro committed Apr 12, 2012
Showing with 166 additions and 40 deletions.
  1. +10 −6 ColumnSet.js
  2. +42 −28 OnDemandList.js
  3. +2 −0 _StoreMixin.js
  4. +6 −6 test/complex_column.html
  5. +35 −0 test/data/DeferredWrapper.js
  6. +71 −0 test/performance_slow_network.html
View
16 ColumnSet.js
@@ -1,5 +1,5 @@
-define(["dojo/_base/kernel", "dojo/_base/declare", "dojo/on", "dojo/aspect", "dojo/query", "dojo/has", "put-selector/put", "xstyle/has-class", "./Grid", "dojo/_base/sniff", "xstyle/css!./css/columnset.css"],
-function(kernel, declare, listen, aspect, query, has, put, hasClass, Grid){
+define(["dojo/_base/kernel", "dojo/_base/declare", "dojo/_base/Deferred", "dojo/on", "dojo/aspect", "dojo/query", "dojo/has", "put-selector/put", "xstyle/has-class", "./Grid", "dojo/_base/sniff", "xstyle/css!./css/columnset.css"],
+function(kernel, declare, Deferred, listen, aspect, query, has, put, hasClass, Grid){
// summary:
// This module provides column sets to isolate horizontal scroll of sets of
// columns from each other. This mainly serves the purpose of allowing for
@@ -18,10 +18,14 @@ function(kernel, declare, listen, aspect, query, has, put, hasClass, Grid){
return row;
},
renderArray: function(){
- var rows = this.inherited(arguments);
- for(var i = 0; i < rows.length; i++){
- adjustScrollLeft(this, rows[i]);
- }
+ var grid = this,
+ rows = this.inherited(arguments);
+
+ Deferred.when(rows, function(rows){
+ for(var i = 0; i < rows.length; i++){
+ adjustScrollLeft(grid, rows[i]);
+ }
+ });
return rows;
},
renderHeader: function(){
View
70 OnDemandList.js
@@ -8,12 +8,18 @@ return declare([List, _StoreMixin], {
// maxRowsPerPage: Integer
// The maximum number of rows to request at one time.
maxRowsPerPage: 100,
-
// maxEmptySpace: Integer
// Defines the maximum size (in pixels) of unrendered space below the
- // currently-rendered rows.
- maxEmptySpace: 10000,
-
+ // currently-rendered rows. Setting this to less than Infinity can be useful if you
+ // wish to limit the initial vertical scrolling of the grid so that the scrolling is
+ // not excessively sensitive. With very large grids of data this may make scrolling
+ // easier to use, albiet it can limit the ability to instantly scroll to the end.
+ maxEmptySpace: Infinity,
+ // bufferRows: Integer
+ // The number of rows to keep ready on each side of the viewport area so that the user can
+ // perform local scrolling without seeing the grid being built. Increasing this number can
+ // improve perceived performance when the data is being retrieved over a slow network.
+ bufferRows: 10,
// farOffRemoval: Integer
// Defines the minimum distance (in pixels) from the visible viewport area
// rows must be in order to be removed. Setting to Infinity causes rows
@@ -33,7 +39,7 @@ return declare([List, _StoreMixin], {
// on scroll. This can be increased for low-bandwidth clients, or to
// reduce the number of requests against a server
pagingDelay: miscUtil.defaultDelay,
-
+
postCreate: function(){
this.inherited(arguments);
var self = this;
@@ -59,9 +65,9 @@ return declare([List, _StoreMixin], {
node: put(this.contentNode, "div.dgrid-preload", {
rowIndex: 0
}),
+ count: 0,
//topPreloadNode.preload = true;
query: query,
- count: 0,
next: preload
};
preload.node = preloadNode = put(this.contentNode, "div.dgrid-preload");
@@ -167,7 +173,9 @@ return declare([List, _StoreMixin], {
visibleTop = scrollNode.scrollTop + (transform ? -transform.match(/translate[\w]*\(.*?,(.*?)px/)[1] : 0),
visibleBottom = scrollNode.offsetHeight + visibleTop,
priorPreload, preloadNode, preload = grid.preload,
- lastScrollTop = grid.lastScrollTop;
+ lastScrollTop = grid.lastScrollTop,
+ requestBuffer = grid.bufferRows * grid.rowHeight,
+ searchBuffer = requestBuffer - grid.rowHeight; // Avoid rounding causing multiple queries
// XXX: I do not know why this happens.
// munging the actual location of the viewport relative to the preload node by a few pixels in either
@@ -182,7 +190,7 @@ return declare([List, _StoreMixin], {
var mungeAmount = 1;
grid.lastScrollTop = visibleTop;
-
+
function removeDistantNodes(preload, distanceOff, traversal, below){
// we check to see the the nodes are "far off"
var farOffRemoval = grid.farOffRemoval,
@@ -196,7 +204,7 @@ return declare([List, _StoreMixin], {
var toDelete = [];
while((row = nextRow)){
var rowHeight = grid._calcRowHeight(row);
- if(reclaimedHeight + rowHeight + farOffRemoval > distanceOff || nextRow.className.indexOf("dgrid-row") < 0){
+ if(reclaimedHeight + rowHeight + farOffRemoval > distanceOff || (nextRow.className.indexOf("dgrid-row") < 0 && nextRow.className.indexOf("dgrid-loading") < 0)){
// we have reclaimed enough rows or we have gone beyond grid rows, let's call it good
break;
}
@@ -210,7 +218,7 @@ return declare([List, _StoreMixin], {
observers[lastObserverIndex] = 0; // remove it so we don't call cancel twice
}
reclaimedHeight += rowHeight;
- count++;
+ count += row.count || 1;
lastObserverIndex = currentObserverIndex;
// we just do cleanup here, as we will do a more efficient node destruction in the setTimeout below
grid.removeRow(row, true);
@@ -238,9 +246,8 @@ return declare([List, _StoreMixin], {
}
}
- function adjustHeight(preload){
- var newHeight = preload.count * grid.rowHeight;
- preload.node.style.height = (preload.node.rowIndex > 0 ? Math.min(newHeight, grid.maxEmptySpace) : newHeight) + "px";
+ function adjustHeight(preload, noMax){
+ preload.node.style.height = Math.min(preload.count * grid.rowHeight, noMax ? Infinity : grid.maxEmptySpace) + "px";
}
// there can be multiple preloadNodes (if they split, or multiple queries are created),
// so we can traverse them until we find whatever is in the current viewport, making
@@ -252,20 +259,20 @@ return declare([List, _StoreMixin], {
var preloadTop = preloadNode.offsetTop;
var preloadHeight;
- if(visibleBottom + mungeAmount < preloadTop){
+ if(visibleBottom + mungeAmount + searchBuffer < preloadTop){
// the preload is below the line of sight
do{
preload = preload.previous;
}while(preload && !preload.node.offsetWidth); // skip past preloads that are not currently connected
- }else if(visibleTop - mungeAmount > (preloadTop + (preloadHeight = preloadNode.offsetHeight))){
+ }else if(visibleTop - mungeAmount - searchBuffer > (preloadTop + (preloadHeight = preloadNode.offsetHeight))){
// the preload is above the line of sight
do{
preload = preload.next;
}while(preload && !preload.node.offsetWidth);// skip past preloads that are not currently connected
}else{
// the preload node is visible, or close to visible, better show it
- var offset = ((preloadNode.rowIndex ? visibleTop : visibleBottom) - preloadTop) / grid.rowHeight;
- var count = (visibleBottom - visibleTop) / grid.rowHeight;
+ var offset = ((preloadNode.rowIndex ? visibleTop - requestBuffer : visibleBottom) - preloadTop) / grid.rowHeight;
+ var count = (visibleBottom - visibleTop + 2 * requestBuffer) / grid.rowHeight;
// utilize momentum for predictions
var momentum = Math.max(Math.min((visibleTop - lastScrollTop) * grid.rowHeight, grid.maxRowsPerPage/2), grid.maxRowsPerPage/-2);
count += Math.min(Math.abs(momentum), 10);
@@ -276,16 +283,16 @@ return declare([List, _StoreMixin], {
offset = Math.max(offset, 0);
if(offset < 10 && offset > 0 && count + offset < grid.maxRowsPerPage){
// connect to the top of the preloadNode if possible to avoid excessive adjustments
- count += offset;
+ count += Math.max(0, offset);
offset = 0;
}
count = Math.min(Math.max(count, grid.minRowsPerPage),
grid.maxRowsPerPage, preload.count);
if(count == 0){
return;
}
- offset = Math.round(offset);
- count = Math.round(count);
+ count = Math.ceil(count);
+ offset = Math.min(Math.floor(offset), preload.count - count);
var options = grid.get("queryOptions");
preload.count -= count;
var beforeNode = preloadNode,
@@ -300,10 +307,12 @@ return declare([List, _StoreMixin], {
// all of the nodes above were removed
offset = Math.min(preload.count, offset);
preload.previous.count += offset;
- adjustHeight(preload.previous);
+ adjustHeight(preload.previous, true);
preload.count -= offset;
preloadNode.rowIndex += offset;
queryRowsOverlap = 0;
+ }else{
+ count += offset;
}
}
options.start = preloadNode.rowIndex - queryRowsOverlap;
@@ -331,11 +340,12 @@ return declare([List, _StoreMixin], {
if(keepScrollTo){
keepScrollTo = beforeNode.offsetTop;
}
-
+
adjustHeight(preload);
// create a loading node as a placeholder while the data is loaded
var loadingNode = put(beforeNode, "-div.dgrid-loading[style=height:" + count * grid.rowHeight + "px]");
put(loadingNode, "div.dgrid-" + (below ? "below" : "above"), grid.loadingMessage);
+ loadingNode.count = count;
// use the query associated with the preload node to get the next "page"
options.query = preload.query;
// Query now to fill in these rows.
@@ -345,20 +355,23 @@ return declare([List, _StoreMixin], {
trackedResults = grid._trackError(function(){ return results; });
if(trackedResults === undefined){ return; } // sync query failed
-
- Deferred.when(grid.renderArray(results, loadingNode, options), function(){
+
+ // Isolate the variables in case we make multiple requests
+ // (which can happen if we need to render on both sides of an island of already-rendered rows)
+ (function(loadingNode, scrollNode, below, keepScrollTo, results){
+ Deferred.when(grid.renderArray(results, loadingNode, options), function(){
// can remove the loading node now
beforeNode = loadingNode.nextSibling;
put(loadingNode, "!");
- if(keepScrollTo){
+ if(keepScrollTo && beforeNode){ // beforeNode may have been removed if the query results loading node was a removed as a distant node before rendering
// if the preload area above the nodes is approximated based on average
// row height, we may need to adjust the scroll once they are filled in
// so we don't "jump" in the scrolling position
scrollNode.scrollTop += beforeNode.offsetTop - keepScrollTo;
}
if(below){
- // if it is below, we will use the total from the results to update
- // the count in case the total changes as later pages are retrieved
+ // if it is below, we will use the total from the results to update
+ // the count of the last preload in case the total changes as later pages are retrieved
// (not uncommon when total counts are estimated for db perf reasons)
Deferred.when(results.total || results.length, function(total){
// recalculate the count
@@ -367,7 +380,8 @@ return declare([List, _StoreMixin], {
adjustHeight(below);
});
}
- });
+ });
+ }).call(this, loadingNode, scrollNode, below, keepScrollTo, results);
preload = preload.previous;
}
}
View
2 _StoreMixin.js
@@ -102,6 +102,8 @@ function(kernel, declare, lang, Deferred, listen){
// Get a fresh queryOptions object, also including the current sort
var options = lang.delegate(this.queryOptions, {});
if(this._sort.length){
+ // Prevents SimpleQueryEngine from doing unnecessary "null" sorts (which can
+ // change the ordering in browsers that don't use a stable sort algorithm, eg Chrome)
options.sort = this._sort;
}
return options;
View
12 test/complex_column.html
@@ -38,8 +38,8 @@
<script type="text/javascript" src="../../dojo/dojo.js"
data-dojo-config="async: true"></script>
<script type="text/javascript">
- require(["dgrid/OnDemandGrid", "dgrid/GridWithColumnSetsFromHtml", "dgrid/ColumnSet", "dgrid/Selection", "dgrid/Keyboard", "dojo/_base/declare", "dojo/on", "dojo/parser", "dgrid/test/data/base", "dojo/domReady!"],
- function(Grid, GridFromHtml, ColumnSet, Selection, Keyboard, declare, on, parser, testStore){
+ require(["dgrid/OnDemandGrid", "dgrid/GridWithColumnSetsFromHtml", "dgrid/ColumnSet", "dgrid/Tree", "dgrid/Selection", "dgrid/Keyboard", "dojo/_base/declare", "dojo/on", "dojo/parser", "dgrid/test/data/base", "dgrid/test/data/DeferredWrapper", "dojo/domReady!"],
+ function(Grid, GridFromHtml, ColumnSet, Tree, Selection, Keyboard, declare, on, parser, testStore, DeferredWrapper){
var columnSets1 = [
[
[{label: 'Column 1', field: 'col1'},
@@ -109,17 +109,17 @@
var ComplexGrid = declare([Grid, ColumnSet, Selection, Keyboard]);
window.gridNoColumnSets = new (declare([Grid, Selection, Keyboard]))({
- store: testStore,
+ store: DeferredWrapper(testStore),
subRows: subRows1
}, "gridNoColumnSets");
window.gridSingleRow = new ComplexGrid({
- store: testStore,
+ store: DeferredWrapper(testStore),
columnSets: columnSetsSingleRow
}, "gridSingleRow");
window.grid = new ComplexGrid({
- store: testStore,
+ store: DeferredWrapper(testStore),
columnSets: columnSets1
}, "grid");
@@ -145,7 +145,7 @@
//NOTE: Grid == OnDemandGrid, GridFromHtml == ver w/ ColumnSet support
window.gridFromHtml = new HtmlGrid({
- store: testStore
+ store: DeferredWrapper(testStore)
}, "gridFromHtml");
window.gridFromHtml2 = new HtmlGrid({
store: testStore
View
35 test/data/DeferredWrapper.js
@@ -0,0 +1,35 @@
+define(["dojo/_base/lang", "dojo/_base/Deferred", "dojo/store/util/QueryResults"],function(lang, Deferred, QueryResults){
+ // summary:
+ // Creates a store that wraps the delegate store's query results and total in Deferred
+ // instances. If delay is set, the Deferreds will be resolved asynchronously after delay +/-50%
+ // milliseconds to simulate network requests that may come back out of order.
+ return function(store, delay){
+ return lang.delegate(store, {
+ query: function(query, options){
+ var queryResult = store.query(query, options);
+
+ var totalDeferred = new Deferred();
+ var resultsDeferred = new Deferred();
+ resultsDeferred.total = totalDeferred;
+
+ var resolveTotal = function(){
+ totalDeferred.resolve(queryResult.total);
+ };
+ var resolveResults = function(){
+ resultsDeferred.resolve(queryResult);
+ };
+
+ if(delay){
+ setTimeout(resolveTotal, delay * (Math.random() + 0.5));
+ setTimeout(resolveResults, delay * (Math.random() + 0.5));
+ }
+ else{
+ resolveTotal();
+ resolveResults();
+ }
+
+ return QueryResults(resultsDeferred);
+ }
+ });
+ }
+});
View
71 test/performance_slow_network.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+ "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+ <head>
+ <title>Test performance on a (simulated) slow network</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <meta name="viewport" content="width=570" />
+ <style type="text/css">
+ @import "../../dojo/resources/dojo.css";
+ @import "../css/dgrid.css";
+ @import "../css/skins/tundra.css";
+ .heading {
+ font-weight: bold;
+ margin-left: 12px;
+ padding-bottom: 0.25em;
+ }
+ .ui-widget{
+ margin: 10px;
+ }
+ /* this is not part of theme, but you can add odd-even coloring this way*/
+ .dgrid-row-odd {
+ background: #F2F5F9;
+ }
+
+ #grid {
+ width: 68em;
+ height: 50em;
+ padding: 1px;
+ }
+ </style>
+ <script type="text/javascript" src="../../dojo/dojo.js"
+ data-dojo-config="async: true"></script>
+ <script type="text/javascript">
+ require(["dgrid/List", "dgrid/OnDemandGrid","dgrid/Selection", "dgrid/Keyboard", "dojo/_base/declare", "dgrid/test/data/perf", "dgrid/test/data/DeferredWrapper", "dojo/domReady!"],
+ function(List, Grid, Selection, Keyboard, declare, testPerfStore, DeferredWrapper){
+ var columns = [
+ { name: 'Column 0', field: 'id', width: '10%' },
+ { name: 'Column 1', field: 'integer', width: '10%' },
+ { name: 'Column 2', field: 'floatNum', width: '10%' },
+ { name: 'Column 3', field: 'date', width: '10%' },
+ { name: 'Column 4', field: 'date2', width: '10%' },
+ { name: 'Column 5', field: 'text', width: '10%' },
+ { name: 'Column 6', field: 'bool', width: '10%' },
+ { name: 'Column 7', field: 'bool2', width: '10%' },
+ { name: 'Column 8', field: 'price', width: '10%' },
+ { name: 'Column 9', field: 'today', width: '10%' }
+ ];
+
+ var selection = {};
+ for(var i = 0; i < 20000; i += 10)
+ selection[i] = true;
+
+ var start = new Date().getTime();
+ window.grid = new (declare([Grid, Selection, Keyboard]))({
+ store: DeferredWrapper(testPerfStore, 500),
+ columns: columns,
+ selection: selection,
+ deselectOnRefresh: false
+ }, "grid");
+ console.log(new Date().getTime() - start);
+ });
+
+ </script>
+ </head>
+ <body class="tundra">
+ <h2 class="heading">Tests handling of large data sets on a (simulated) slow network, with results being returned
+ after a delay and out of order, and the total number of rows being returned at a different
+ time to the result set.</h2>
+ <div id="grid"></div>
+ </body>
+</html>

0 comments on commit 2ed49e3

Please sign in to comment.
Something went wrong with that request. Please try again.