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