---
Subject: GridBody internal row cache poisoned by mouseMove during setData([]) transition — permanent blank grid (virtualScrolling + grouping + mid-scroll)
SmartClient Version: 14.1.7.26.25
Browser: Chrome Windows
Summary:
After recently enabling virtualScrolling on a large grouped, editable ListGrid (~2,200 rows), we began experiencing a permanent blank-grid condition during data refresh operations.
When listGrid.setData([]) is called to clear data before a server fetch, and the mouse cursor is hovering over the grid body while the viewport is scrolled to a mid-grid position,
GridRenderer.mouseMove → shouldShowRollOver → getEventRow() resolves to NaN on the empty body. This NaN propagates into 5 internal GridRenderer properties — 3 of which are
virtual-scrolling-specific — permanently breaking viewport calculation. The grid appears blank despite having valid data. We did not observe this behavior before enabling
virtualScrolling.
Conditions required (all must be true):
1. virtualScrolling: true
2. Grid is grouped (groupByField)
3. Grid is editable (canEdit: true)
4. Viewport is scrolled away from the top (mid-grid or further)
5. Mouse cursor is hovering over the grid body (user may have just clicked into/out of an edit cell)
6. setData([]) is called to clear data before reload
Affected internal properties (from modules-debug/ISC_Grids.js):
┌──────────┬───────────────────────┬────────────────────────────────┬───────────────────┬─────────────────────────────────────────────────────┐
│ Property │ Debug Name │ Virtual Scrolling Specific? │ Set By │ Purpose │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $252 │ _firstDrawnRow │ No (all incremental rendering) │ getTableHTML() │ First row index in rendered HTML table │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $253 │ _lastDrawnRow │ No (all incremental rendering) │ getTableHTML() │ Last row index in rendered HTML table │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $27y │ _targetRow │ Yes │ _storeTargetRow() │ Virtual scrolling anchor row │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $27z │ _rowOffset │ Yes │ _storeTargetRow() │ Pixel offset into target row for scroll restoration │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $514 │ _startRowSpacerHeight │ Yes │ getTableHTML() │ Spacer height above first drawn row │
└──────────┴───────────────────────┴────────────────────────────────┴───────────────────┴─────────────────────────────────────────────────────┘
The primary NaN entry point is _storeTargetRow() (ISC_Grids.js debug line 6768), which is a virtual scrolling method. It calls getEventRow() to find the scroll anchor row. On an
empty grid body, getEventRow() returns NaN instead of -2, which poisons _targetRow and _rowOffset. On the next getTableHTML() call, NaN propagates into _firstDrawnRow,
_lastDrawnRow, and _startRowSpacerHeight. Without virtualScrolling enabled, _storeTargetRow() does not run and 3 of the 5 properties would not exist on the body, which likely
explains why we never hit this before. Additionally, _storeTargetRow() only does meaningful work when the viewport is scrolled away from the top — at scrollTop: 0, the anchor row
and spacer height are trivially 0, which may explain why this only manifests when scrolled mid-grid.
Root cause chain:
1. listGrid.setData([]) clears grid data before fetch
2. Mouse is hovering over grid body — GridRenderer.mouseMove fires
3. shouldShowRollOver → getRecord → getEventRow() returns NaN on empty body
4. _storeTargetRow() caches NaN into _targetRow and _rowOffset
5. On next getTableHTML(), NaN propagates into _firstDrawnRow, _lastDrawnRow, _startRowSpacerHeight
6. getVisibleRows() returns [NaN, NaN], getDrawArea() returns [NaN, NaN, 0, N]
7. Body renders zero rows — grid appears permanently blank
8. body.redraw() does NOT fix it because the cached NaN values are reused
9. Subsequent setData(validResultSet) does NOT invalidate the poisoned cache
Note on existing guard: _storeTargetRow() has an isEmpty() check at debug line 6773 that should short-circuit on an empty grid. Either isEmpty() is not returning true immediately
after setData([]) on a grouped/editable grid (the Tree wrapper around the empty array, or editor teardown, may create a window where isEmpty() returns false), or the NaN is entering
through a different path before _storeTargetRow() is reached.
Reproduction:
// ATTEMPTED REPRODUCTION (not yet confirmed outside production)
// Closest approximation of production conditions.
// We were unable to trigger the bug in the SmartClient showcase
// despite matching: virtualScrolling, grouping, editing, mid-scroll,
// and mouse hover during setData([]) clear window.
// The bug likely requires additional application-specific conditions
// (DataSource/ResultSet binding, field formatters, frozen columns, etc.)
isc.ListGrid.create({
ID: "testGrid",
width: 500, height: 300,
canHover: true,
showRollOver: true,
virtualScrolling: true,
groupByField: "category",
groupStartOpen: "all",
groupByMaxRecords: 5000,
canEdit: true,
editEvent: "click",
editByCell: true,
listEndEditAction: "next",
fields: [
{name: "id", title: "ID", canEdit: false},
{name: "name", title: "Name", canEdit: true},
{name: "value", title: "Value", type: "float", canEdit: true},
{name: "category", title: "Category", canEdit: false}
]
});
var categories = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
var initialData = [];
for (var i = 0; i < 2200; i++) {
initialData.add({
id: i,
name: "Record " + i,
value: Math.round(Math.random() * 10000) / 100,
category: categories[i % 5]
});
}
testGrid.setData(initialData);
// Instrument mouseMove to verify events reach SmartClient
var origMouseMove = testGrid.body.mouseMove;
testGrid.body.mouseMove = function() {
if (this.getTotalRows() === 0) {
console.log(">>> mouseMove on EMPTY body, totalRows=0");
}
return origMouseMove ? origMouseMove.apply(this, arguments) : true;
};
// ATTEMPTED REPRODUCTION STEPS:
// 1. Scroll to middle of grid
testGrid.body.scrollTo(0, testGrid.body.getScrollHeight() / 2);
// 2. Click into a cell to start editing, then click elsewhere to end edit
// (leave cursor hovering over grid)
// 3. Run this to trigger the bug:
function triggerRefresh() {
console.log(">>> Clearing data — mouse should be over grid and scrolled mid-page");
testGrid.setData([]);
// Give mouseMove time to fire on empty body
isc.Timer.setTimeout(function() {
var newData = [];
var categories = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
for (var i = 0; i < 2200; i++) {
newData.add({
id: i,
name: "Refreshed " + i,
value: Math.round(Math.random() * 10000) / 100,
category: categories[i % 5]
});
}
testGrid.setData(newData);
isc.Timer.setTimeout(function() {
var vis = testGrid.body.getVisibleRows();
console.log("totalRows:", testGrid.body.getTotalRows());
console.log("visibleRows:", vis);
console.log("drawArea:", testGrid.body.getDrawArea());
if (isNaN(vis[0])) {
console.log(">>> BUG REPRODUCED — grid blank with valid data");
} else {
console.log(">>> No bug this attempt — retry with mouse wiggle during clear");
}
}, 100);
}, 50);
}
// After scrolling and doing an edit, call triggerRefresh() from console
// while hovering mouse over the grid body.
Manual reproduction steps:
1. Paste the above into the SmartClient console
2. Scroll the grid to the middle
3. Click a "Name" or "Value" cell to start editing, then click a non-editable cell or press Escape to end the edit (leave cursor on grid)
4. Type triggerRefresh() in the console and press Enter while your mouse hovers over the grid body
5. Wiggle the mouse slightly over the grid during the 50ms clear window
6. Check console output — if visibleRows shows [NaN, NaN], bug is reproduced
7. May require several attempts due to timing sensitivity
Diagnostic evidence from production (2,217-row grouped editable grid with virtualScrolling):
- getData().getLength() → 2217 (data present, Tree type from grouping)
- body.isDrawn() → true, body.isVisible() → true
- body.getTotalRows() → 2217
- body.getVisibleRows() → [NaN, NaN]
- body.getDrawArea() → [NaN, NaN, 0, 54]
- body.getViewportHeight() → 702, cellHeight → 22, scrollTop → 0 (all valid)
- body.getHandle().innerHTML.length → 1965 (structural HTML only, no row content)
- 5 hasOwnProperty NaN-valued numeric properties on body: $252, $253, $27y, $27z, $514
- Resetting all 5 to 0 + body.redraw() → grid renders correctly
Workaround: After setData() with non-empty data, we check getVisibleRows()[0] for NaN in a setTimeout. If poisoned, we scan the body for NaN-valued own properties, reset them to 0,
and force body.redraw(). The check must be in a setTimeout because the NaN poisoning happens asynchronously via mouseMove during the event loop gap. This is a no-op in the normal
case.
Note on reproduction difficulty: We have been unable to reproduce this outside of our production environment despite matching the configuration (virtualScrolling, grouping, editing,
mid-scroll position, mouse hover). The bug may require additional conditions present in our application — such as DataSource binding with ResultSet lifecycle, specific field types
or formatters, frozen columns, or other grid features. The diagnostic evidence from production is conclusive: the NaN values are present on the body, they map to documented internal
properties, and resetting them fixes the rendering. We are filing this report with full diagnostics to assist Isomorphic in identifying the code path even if the standalone
reproduction proves elusive.
Expected behavior:
Either (a) getEventRow() should return a safe sentinel (e.g., -2) rather than NaN when the grid body has no rows, or (b) setData() with a non-empty dataset should invalidate stale
body row caches from the prior empty state, or (c) _storeTargetRow() should validate the return value of getEventRow() before caching it.
Workaround (in production):
We added a clearNaNBodyCacheAT() method to isc.ListGrid that detects and repairs the poisoned cache. It is called both before and after setData() — the post-call invocation runs in
setTimeout(fn, 0) to catch NaN re-introduced during deferred redraws.
Method definition (added via isc.ATListGrid subclass):
clearNaNBodyCacheAT: function() {
if (!this.body) return false;
var visibleRows = this.body.getVisibleRows();
if (visibleRows != null && !isNaN(visibleRows[0])) return false;
var body = this.body;
var cleared = false;
for (var k in body) {
try {
if (body.hasOwnProperty(k) && typeof body[k] === 'number' && isNaN(body[k])) {
body[k] = 0;
cleared = true;
}
} catch(e) {}
}
if (cleared) {
var warnMessage = this.getID() + ": cleared NaN-poisoned body cache properties, forcing redraw";
if (console) {
console.warn(warnMessage);
}
isc.Log.logWarn(warnMessage);
body.redraw("cleared NaN body cache");
}
return cleared;
}
Call pattern (at every setData() call site):
fundAssetsGrid.clearNaNBodyCacheAT();
fundAssetsGrid.setData(resultSet);
setTimeout(function(){ fundAssetsGrid.clearNaNBodyCacheAT(); }, 0);
The method uses getVisibleRows()[0] as a fast canary — in the normal case the cost is one null check and one isNaN check. Only when corruption is detected does it scan body
own-properties for NaN values, reset them to 0, and force body.redraw(). The pre-setData() call clears any existing corruption before SmartClient's internal redraw; the setTimeout
post-call catches NaN re-introduced by mouseMove events during the deferred redraw cycle.
Subject: GridBody internal row cache poisoned by mouseMove during setData([]) transition — permanent blank grid (virtualScrolling + grouping + mid-scroll)
SmartClient Version: 14.1.7.26.25
Browser: Chrome Windows
Summary:
After recently enabling virtualScrolling on a large grouped, editable ListGrid (~2,200 rows), we began experiencing a permanent blank-grid condition during data refresh operations.
When listGrid.setData([]) is called to clear data before a server fetch, and the mouse cursor is hovering over the grid body while the viewport is scrolled to a mid-grid position,
GridRenderer.mouseMove → shouldShowRollOver → getEventRow() resolves to NaN on the empty body. This NaN propagates into 5 internal GridRenderer properties — 3 of which are
virtual-scrolling-specific — permanently breaking viewport calculation. The grid appears blank despite having valid data. We did not observe this behavior before enabling
virtualScrolling.
Conditions required (all must be true):
1. virtualScrolling: true
2. Grid is grouped (groupByField)
3. Grid is editable (canEdit: true)
4. Viewport is scrolled away from the top (mid-grid or further)
5. Mouse cursor is hovering over the grid body (user may have just clicked into/out of an edit cell)
6. setData([]) is called to clear data before reload
Affected internal properties (from modules-debug/ISC_Grids.js):
┌──────────┬───────────────────────┬────────────────────────────────┬───────────────────┬─────────────────────────────────────────────────────┐
│ Property │ Debug Name │ Virtual Scrolling Specific? │ Set By │ Purpose │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $252 │ _firstDrawnRow │ No (all incremental rendering) │ getTableHTML() │ First row index in rendered HTML table │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $253 │ _lastDrawnRow │ No (all incremental rendering) │ getTableHTML() │ Last row index in rendered HTML table │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $27y │ _targetRow │ Yes │ _storeTargetRow() │ Virtual scrolling anchor row │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $27z │ _rowOffset │ Yes │ _storeTargetRow() │ Pixel offset into target row for scroll restoration │
├──────────┼───────────────────────┼────────────────────────────────┼───────────────────┼─────────────────────────────────────────────────────┤
│ $514 │ _startRowSpacerHeight │ Yes │ getTableHTML() │ Spacer height above first drawn row │
└──────────┴───────────────────────┴────────────────────────────────┴───────────────────┴─────────────────────────────────────────────────────┘
The primary NaN entry point is _storeTargetRow() (ISC_Grids.js debug line 6768), which is a virtual scrolling method. It calls getEventRow() to find the scroll anchor row. On an
empty grid body, getEventRow() returns NaN instead of -2, which poisons _targetRow and _rowOffset. On the next getTableHTML() call, NaN propagates into _firstDrawnRow,
_lastDrawnRow, and _startRowSpacerHeight. Without virtualScrolling enabled, _storeTargetRow() does not run and 3 of the 5 properties would not exist on the body, which likely
explains why we never hit this before. Additionally, _storeTargetRow() only does meaningful work when the viewport is scrolled away from the top — at scrollTop: 0, the anchor row
and spacer height are trivially 0, which may explain why this only manifests when scrolled mid-grid.
Root cause chain:
1. listGrid.setData([]) clears grid data before fetch
2. Mouse is hovering over grid body — GridRenderer.mouseMove fires
3. shouldShowRollOver → getRecord → getEventRow() returns NaN on empty body
4. _storeTargetRow() caches NaN into _targetRow and _rowOffset
5. On next getTableHTML(), NaN propagates into _firstDrawnRow, _lastDrawnRow, _startRowSpacerHeight
6. getVisibleRows() returns [NaN, NaN], getDrawArea() returns [NaN, NaN, 0, N]
7. Body renders zero rows — grid appears permanently blank
8. body.redraw() does NOT fix it because the cached NaN values are reused
9. Subsequent setData(validResultSet) does NOT invalidate the poisoned cache
Note on existing guard: _storeTargetRow() has an isEmpty() check at debug line 6773 that should short-circuit on an empty grid. Either isEmpty() is not returning true immediately
after setData([]) on a grouped/editable grid (the Tree wrapper around the empty array, or editor teardown, may create a window where isEmpty() returns false), or the NaN is entering
through a different path before _storeTargetRow() is reached.
Reproduction:
// ATTEMPTED REPRODUCTION (not yet confirmed outside production)
// Closest approximation of production conditions.
// We were unable to trigger the bug in the SmartClient showcase
// despite matching: virtualScrolling, grouping, editing, mid-scroll,
// and mouse hover during setData([]) clear window.
// The bug likely requires additional application-specific conditions
// (DataSource/ResultSet binding, field formatters, frozen columns, etc.)
isc.ListGrid.create({
ID: "testGrid",
width: 500, height: 300,
canHover: true,
showRollOver: true,
virtualScrolling: true,
groupByField: "category",
groupStartOpen: "all",
groupByMaxRecords: 5000,
canEdit: true,
editEvent: "click",
editByCell: true,
listEndEditAction: "next",
fields: [
{name: "id", title: "ID", canEdit: false},
{name: "name", title: "Name", canEdit: true},
{name: "value", title: "Value", type: "float", canEdit: true},
{name: "category", title: "Category", canEdit: false}
]
});
var categories = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
var initialData = [];
for (var i = 0; i < 2200; i++) {
initialData.add({
id: i,
name: "Record " + i,
value: Math.round(Math.random() * 10000) / 100,
category: categories[i % 5]
});
}
testGrid.setData(initialData);
// Instrument mouseMove to verify events reach SmartClient
var origMouseMove = testGrid.body.mouseMove;
testGrid.body.mouseMove = function() {
if (this.getTotalRows() === 0) {
console.log(">>> mouseMove on EMPTY body, totalRows=0");
}
return origMouseMove ? origMouseMove.apply(this, arguments) : true;
};
// ATTEMPTED REPRODUCTION STEPS:
// 1. Scroll to middle of grid
testGrid.body.scrollTo(0, testGrid.body.getScrollHeight() / 2);
// 2. Click into a cell to start editing, then click elsewhere to end edit
// (leave cursor hovering over grid)
// 3. Run this to trigger the bug:
function triggerRefresh() {
console.log(">>> Clearing data — mouse should be over grid and scrolled mid-page");
testGrid.setData([]);
// Give mouseMove time to fire on empty body
isc.Timer.setTimeout(function() {
var newData = [];
var categories = ["Alpha", "Beta", "Gamma", "Delta", "Epsilon"];
for (var i = 0; i < 2200; i++) {
newData.add({
id: i,
name: "Refreshed " + i,
value: Math.round(Math.random() * 10000) / 100,
category: categories[i % 5]
});
}
testGrid.setData(newData);
isc.Timer.setTimeout(function() {
var vis = testGrid.body.getVisibleRows();
console.log("totalRows:", testGrid.body.getTotalRows());
console.log("visibleRows:", vis);
console.log("drawArea:", testGrid.body.getDrawArea());
if (isNaN(vis[0])) {
console.log(">>> BUG REPRODUCED — grid blank with valid data");
} else {
console.log(">>> No bug this attempt — retry with mouse wiggle during clear");
}
}, 100);
}, 50);
}
// After scrolling and doing an edit, call triggerRefresh() from console
// while hovering mouse over the grid body.
Manual reproduction steps:
1. Paste the above into the SmartClient console
2. Scroll the grid to the middle
3. Click a "Name" or "Value" cell to start editing, then click a non-editable cell or press Escape to end the edit (leave cursor on grid)
4. Type triggerRefresh() in the console and press Enter while your mouse hovers over the grid body
5. Wiggle the mouse slightly over the grid during the 50ms clear window
6. Check console output — if visibleRows shows [NaN, NaN], bug is reproduced
7. May require several attempts due to timing sensitivity
Diagnostic evidence from production (2,217-row grouped editable grid with virtualScrolling):
- getData().getLength() → 2217 (data present, Tree type from grouping)
- body.isDrawn() → true, body.isVisible() → true
- body.getTotalRows() → 2217
- body.getVisibleRows() → [NaN, NaN]
- body.getDrawArea() → [NaN, NaN, 0, 54]
- body.getViewportHeight() → 702, cellHeight → 22, scrollTop → 0 (all valid)
- body.getHandle().innerHTML.length → 1965 (structural HTML only, no row content)
- 5 hasOwnProperty NaN-valued numeric properties on body: $252, $253, $27y, $27z, $514
- Resetting all 5 to 0 + body.redraw() → grid renders correctly
Workaround: After setData() with non-empty data, we check getVisibleRows()[0] for NaN in a setTimeout. If poisoned, we scan the body for NaN-valued own properties, reset them to 0,
and force body.redraw(). The check must be in a setTimeout because the NaN poisoning happens asynchronously via mouseMove during the event loop gap. This is a no-op in the normal
case.
Note on reproduction difficulty: We have been unable to reproduce this outside of our production environment despite matching the configuration (virtualScrolling, grouping, editing,
mid-scroll position, mouse hover). The bug may require additional conditions present in our application — such as DataSource binding with ResultSet lifecycle, specific field types
or formatters, frozen columns, or other grid features. The diagnostic evidence from production is conclusive: the NaN values are present on the body, they map to documented internal
properties, and resetting them fixes the rendering. We are filing this report with full diagnostics to assist Isomorphic in identifying the code path even if the standalone
reproduction proves elusive.
Expected behavior:
Either (a) getEventRow() should return a safe sentinel (e.g., -2) rather than NaN when the grid body has no rows, or (b) setData() with a non-empty dataset should invalidate stale
body row caches from the prior empty state, or (c) _storeTargetRow() should validate the return value of getEventRow() before caching it.
Workaround (in production):
We added a clearNaNBodyCacheAT() method to isc.ListGrid that detects and repairs the poisoned cache. It is called both before and after setData() — the post-call invocation runs in
setTimeout(fn, 0) to catch NaN re-introduced during deferred redraws.
Method definition (added via isc.ATListGrid subclass):
clearNaNBodyCacheAT: function() {
if (!this.body) return false;
var visibleRows = this.body.getVisibleRows();
if (visibleRows != null && !isNaN(visibleRows[0])) return false;
var body = this.body;
var cleared = false;
for (var k in body) {
try {
if (body.hasOwnProperty(k) && typeof body[k] === 'number' && isNaN(body[k])) {
body[k] = 0;
cleared = true;
}
} catch(e) {}
}
if (cleared) {
var warnMessage = this.getID() + ": cleared NaN-poisoned body cache properties, forcing redraw";
if (console) {
console.warn(warnMessage);
}
isc.Log.logWarn(warnMessage);
body.redraw("cleared NaN body cache");
}
return cleared;
}
Call pattern (at every setData() call site):
fundAssetsGrid.clearNaNBodyCacheAT();
fundAssetsGrid.setData(resultSet);
setTimeout(function(){ fundAssetsGrid.clearNaNBodyCacheAT(); }, 0);
The method uses getVisibleRows()[0] as a fast canary — in the normal case the cost is one null check and one isNaN check. Only when corruption is detected does it scan body
own-properties for NaN values, reset them to 0, and force body.redraw(). The pre-setData() call clears any existing corruption before SmartClient's internal redraw; the setTimeout
post-call catches NaN re-introduced by mouseMove events during the deferred redraw cycle.
Comment