Mofifying HasDataPresenter to be more resilient to state changes while it is resolving state changes. For example, if user code triggers a SelectionChangeEvent while selection is being resolved, we can handle that.
Review at http://gwt-code-reviews.appspot.com/1583804
Review by: dcavalcanti@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10728 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java b/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
index a7d87f5..f129db5 100644
--- a/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
+++ b/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
@@ -376,8 +376,9 @@
static final int PAGE_INCREMENT = 30;
/**
- * The maximum number of times we can try to {@link #resolvePendingState()}
- * before we assume there is an infinite loop.
+ * The maximum number of times we can try to
+ * {@link #resolvePendingState(JsArrayInteger)} before we assume there is an
+ * infinite loop.
*/
private static final int LOOP_MAXIMUM = 10;
@@ -429,9 +430,10 @@
private ScheduledCommand pendingStateCommand;
/**
- * A counter used to detect infinite loops in {@link #resolvePendingState()}.
- * An infinite loop can occur if user code, such as reading the
- * {@link SelectionModel}, causes the table to have a pending state.
+ * A counter used to detect infinite loops in
+ * {@link #resolvePendingState(JsArrayInteger)}. An infinite loop can occur if
+ * user code, such as reading the {@link SelectionModel}, causes the table to
+ * have a pending state.
*/
private int pendingStateLoop = 0;
@@ -513,15 +515,7 @@
* Flush pending changes to the view.
*/
public void flush() {
- /*
- * resolvePendingState can exit early user code applied more pending state,
- * so we need to loop until we are sure that the pending state is clear. If
- * the user calls this method while resolving pending state, then do not
- * attempt to resolve pending state again.
- */
- while (pendingStateCommand != null && !isResolvingState) {
- resolvePendingState();
- }
+ resolvePendingState(null);
}
/**
@@ -987,7 +981,7 @@
public void execute() {
// Verify that this command was the last one scheduled.
if (pendingStateCommand == this) {
- resolvePendingState();
+ resolvePendingState(null);
}
}
};
@@ -1060,14 +1054,28 @@
/**
* Resolve the pending state and push updates to the view.
+ *
+ * @param modifiedRows the modified rows that need to be updated, or null if
+ * none. The modified rows may be mutated.
+ * @return true if the state changed, false if not
*/
- private void resolvePendingState() {
+ private boolean resolvePendingState(JsArrayInteger modifiedRows) {
pendingStateCommand = null;
+ /*
+ * We are already resolving state. New changes will be flushed after the
+ * current flush is finished.
+ */
+ if (isResolvingState) {
+ return false;
+ }
+ isResolvingState = true;
+
// Early exit if there is no pending state.
if (pendingState == null) {
+ isResolvingState = false;
pendingStateLoop = 0;
- return;
+ return false;
}
/*
@@ -1076,6 +1084,7 @@
*/
pendingStateLoop++;
if (pendingStateLoop > LOOP_MAXIMUM) {
+ isResolvingState = false;
pendingStateLoop = 0; // Let user code handle exception and try again.
throw new IllegalStateException(
"A possible infinite loop has been detected in a Cell Widget. This "
@@ -1085,16 +1094,13 @@
}
/*
- * Check for conflicting state resolution code. This can happen if the
- * View's render methods modify the view and flush the pending state.
+ * Swap the states in case user code triggers more changes, which will
+ * create a new pendingState.
*/
- if (isResolvingState) {
- throw new IllegalStateException(
- "The Cell Widget is attempting to render itself within the render "
- + "loop. This usually happens when your render code modifies the "
- + "state of the Cell Widget then accesses data or elements " + "within the Widget.");
- }
- isResolvingState = true;
+ State<T> oldState = state;
+ PendingState<T> pending = pendingState;
+ state = pendingState;
+ pendingState = null;
/*
* Keep track of the absolute indexes of modified rows.
@@ -1102,11 +1108,11 @@
* Use a native array to avoid dynamic casts associated with emulated Java
* Collections.
*/
- JsArrayInteger modifiedRows = JavaScriptObject.createArray().cast();
+ if (modifiedRows == null) {
+ modifiedRows = JavaScriptObject.createArray().cast();
+ }
// Get the values used for calculations.
- State<T> oldState = state;
- PendingState<T> pending = pendingState;
int pageStart = pending.getPageStart();
int pageSize = pending.getPageSize();
int pageEnd = pageStart + pageSize;
@@ -1115,7 +1121,7 @@
/*
* Resolve keyboard selection. If the row value still exists, use its index.
* If the row value exists in multiple places, use the closest index. If the
- * row value not longer exists, use the current index.
+ * row value no longer exists, use the current index.
*/
pending.keyboardSelectedRow =
Math.max(0, Math.min(pending.keyboardSelectedRow, rowDataCount - 1));
@@ -1145,10 +1151,9 @@
}
/*
- * Update the SelectionModel based on the keyboard selected value. This must
- * happen before we read the selection state. We only bind to selection
- * after the user has interacted with the widget at least once. This
- * prevents values from being selected by default.
+ * Update the SelectionModel based on the keyboard selected value. We only
+ * bind to selection after the user has interacted with the widget at least
+ * once. This prevents values from being selected by default.
*/
try {
if (KeyboardSelectionPolicy.BOUND_TO_SELECTION == keyboardSelectionPolicy
@@ -1176,7 +1181,7 @@
if (oldValueWasSelected) {
selectionModel.setSelected(oldValue, false);
}
-
+
// Select the new value.
pending.selectedValue = newValue;
if (newValue != null && !newValueWasSelected) {
@@ -1187,10 +1192,17 @@
pending.selectedValue = null;
}
}
+
+ // User code may have created a new pending state, so we need to
+ // propagate this new selected value.
+ if (pendingState != null) {
+ pendingState.selectedValue = pending.selectedValue;
+ }
}
} catch (RuntimeException e) {
// Unlock the rendering loop if the user SelectionModel throw an error.
isResolvingState = false;
+ pendingStateLoop = 0;
throw e;
}
@@ -1206,37 +1218,52 @@
* longer have a SelectionModel but had selected rows, we still need to
* update the rows.
*/
- for (int i = pageStart; i < pageStart + rowDataCount; i++) {
- // Check the new selection state.
- T rowValue = pending.getRowDataValue(i - pageStart);
- boolean isSelected =
- (rowValue != null && selectionModel != null && selectionModel.isSelected(rowValue));
+ try {
+ for (int i = pageStart; i < pageStart + rowDataCount; i++) {
+ // Check the new selection state.
+ T rowValue = pending.getRowDataValue(i - pageStart);
+ boolean isSelected =
+ (rowValue != null && selectionModel != null && selectionModel.isSelected(rowValue));
- // Compare to the old selection state.
- boolean wasSelected = oldState.isRowSelected(i);
- if (isSelected) {
- pending.selectedRows.add(i);
- if (!wasSelected) {
+ // Compare to the old selection state.
+ boolean wasSelected = oldState.isRowSelected(i);
+ if (isSelected) {
+ pending.selectedRows.add(i);
+ if (pendingState != null) {
+ // Propagate changes if user code created a new pending state.
+ pendingState.selectedRows.add(i);
+ }
+ if (!wasSelected) {
+ modifiedRows.push(i);
+ }
+ } else if (wasSelected) {
modifiedRows.push(i);
}
- } else if (wasSelected) {
- modifiedRows.push(i);
}
+ } catch (RuntimeException e) {
+ // Unlock the rendering loop if the user SelectionModel throw an error.
+ isResolvingState = false;
+ pendingStateLoop = 0;
+ throw e;
}
/*
* We called methods in user code that could modify the view, so early exit
* if there is a new pending state waiting to be resolved.
*/
- if (pendingStateCommand != null) {
+ if (pendingState != null) {
isResolvingState = false;
- return;
- }
- pendingStateLoop = 0;
+ // Do not reset pendingStateLoop, or we will not detect the infinite loop.
- // Swap the states.
- state = pendingState;
- pendingState = null;
+ /*
+ * Try to resolve state again. If we are successful, then the modified
+ * rows will be redrawn. If we are not successful, then we still need to
+ * redraw the modified rows.
+ */
+ if (resolvePendingState(modifiedRows)) {
+ return true;
+ }
+ }
// Add the replaced ranges as modified rows.
boolean replacedEmptyRange = false;
@@ -1358,6 +1385,11 @@
*/
isResolvingState = false;
}
+
+ // If any new state exists, resolve it now. If there is no new state, reset
+ // the pendingStateLoop.
+ resolvePendingState(null);
+ return true;
}
/**