Clarifies the order in which onLoad() and onUnload() will be called. onLoad() will always be
called *after* a Widget becomes attached, and onUnload() will be called just *before* a
Widget is detached.

Also adds template methods doAttachChildren() and doDetachChildren() making it easier for
Widgets to maintain this contract correctly.

Patch by: jgw
Review by: knorton, sandymac
Issue: 1121



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@1229 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/client/ui/CheckBox.java b/user/src/com/google/gwt/user/client/ui/CheckBox.java
index 357f1e6..e939231 100644
--- a/user/src/com/google/gwt/user/client/ui/CheckBox.java
+++ b/user/src/com/google/gwt/user/client/ui/CheckBox.java
@@ -163,11 +163,10 @@
    * onAttach needs special handling for the CheckBox case. Must still call
    * {@link Widget#onAttach()} to preserve the <code>onAttach</code> contract.
    */
-  protected void onAttach() {
+  protected void onLoad() {
     // Sets the event listener on the inputElem, as in this case that's the
     // element we want so input on.
     DOM.setEventListener(inputElem, this);
-    super.onAttach();
   }
 
   /**
@@ -175,11 +174,10 @@
    * document. Overridden because of IE bug that throws away checked state and
    * in order to clear the event listener off of the <code>inputElem</code>.
    */
-  protected void onDetach() {
+  protected void onUnload() {
     // Clear out the inputElem's event listener (breaking the circular
     // reference between it and the widget).
     DOM.setEventListener(inputElem, null);
     setChecked(isChecked());
-    super.onDetach();
   }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/Composite.java b/user/src/com/google/gwt/user/client/ui/Composite.java
index 2e63f11..491fea1 100644
--- a/user/src/com/google/gwt/user/client/ui/Composite.java
+++ b/user/src/com/google/gwt/user/client/ui/Composite.java
@@ -48,6 +48,13 @@
     return super.getElement();
   }
 
+  public boolean isAttached() {
+    if (widget != null) {
+      return widget.isAttached();
+    }
+    return false;
+  }
+
   /**
    * Provides subclasses access to the topmost widget that defines this
    * composite.
@@ -84,17 +91,19 @@
   }
 
   protected void onAttach() {
-    super.onAttach();
-
-    // Call onAttach() on behalf of the contained widget.
     widget.onAttach();
+    onLoad();
   }
 
   protected void onDetach() {
-    super.onDetach();
-
-    // Call onDetach() on behalf of the contained widget.
-    widget.onDetach();
+    try {
+      onUnload();
+    } finally {
+      // We don't want an exception in user code to keep us from calling the
+      // super implementation (or event listeners won't get cleaned up and
+      // the attached flag will be wrong).
+      widget.onDetach();
+    }
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/client/ui/Panel.java b/user/src/com/google/gwt/user/client/ui/Panel.java
index 2b0b37b..8c755cc 100644
--- a/user/src/com/google/gwt/user/client/ui/Panel.java
+++ b/user/src/com/google/gwt/user/client/ui/Panel.java
@@ -71,15 +71,14 @@
       throw new IllegalArgumentException("w is not a child of this panel");
     }
 
-    // Remove it at the DOM and GWT levels.
+    // setParent() must be called before removeChild() to ensure that the
+    // element is still attached when onDetach()/onUnload() are called.
     Element elem = w.getElement();
     w.setParent(null);
     DOM.removeChild(DOM.getParent(elem), elem);
   }
 
-  protected void onAttach() {
-    super.onAttach();
-
+  protected void doAttachChildren() {
     // Ensure that all child widgets are attached.
     for (Iterator it = iterator(); it.hasNext();) {
       Widget child = (Widget) it.next();
@@ -87,13 +86,29 @@
     }
   }
 
-  protected void onDetach() {
-    super.onDetach();
-
+  protected void doDetachChildren() {
     // Ensure that all child widgets are detached.
     for (Iterator it = iterator(); it.hasNext();) {
       Widget child = (Widget) it.next();
       child.onDetach();
     }
   }
+
+  /**
+   * A Panel's onLoad method will be called after all of its children are
+   * attached.
+   * 
+   * @see Widget#onLoad()
+   */
+  protected void onLoad() {
+  }
+
+  /**
+   * A Panel's onUnload method will be called before its children become
+   * detached themselves.
+   * 
+   * @see Widget#onLoad()
+   */
+  protected void onUnload() {
+  }
 }
diff --git a/user/src/com/google/gwt/user/client/ui/Tree.java b/user/src/com/google/gwt/user/client/ui/Tree.java
index 74acaa6..103c02d 100644
--- a/user/src/com/google/gwt/user/client/ui/Tree.java
+++ b/user/src/com/google/gwt/user/client/ui/Tree.java
@@ -597,6 +597,24 @@
     return accum.iterator();
   }
 
+  protected void doAttachChildren() {
+    // Ensure that all child widgets are attached.
+    for (Iterator it = iterator(); it.hasNext();) {
+      Widget child = (Widget) it.next();
+      child.onAttach();
+    }
+    DOM.setEventListener(focusable, this);
+  }
+
+  protected void doDetachChildren() {
+    // Ensure that all child widgets are detached.
+    for (Iterator it = iterator(); it.hasNext();) {
+      Widget child = (Widget) it.next();
+      child.onDetach();
+    }
+    DOM.setEventListener(focusable, null);
+  }
+
   /**
    * Indicates if keyboard navigation is enabled for the Tree and for a given
    * TreeItem. Subclasses of Tree can override this function to selectively
@@ -610,28 +628,6 @@
     return true;
   }
 
-  protected void onAttach() {
-    super.onAttach();
-
-    // Ensure that all child widgets are attached.
-    for (Iterator it = iterator(); it.hasNext();) {
-      Widget child = (Widget) it.next();
-      child.onAttach();
-    }
-    DOM.setEventListener(focusable, this);
-  }
-
-  protected void onDetach() {
-    super.onDetach();
-
-    // Ensure that all child widgets are detached.
-    for (Iterator it = iterator(); it.hasNext();) {
-      Widget child = (Widget) it.next();
-      child.onDetach();
-    }
-    DOM.setEventListener(focusable, null);
-  }
-
   protected void onLoad() {
     root.updateStateRecursive();
   }
diff --git a/user/src/com/google/gwt/user/client/ui/Widget.java b/user/src/com/google/gwt/user/client/ui/Widget.java
index d80321d..caa6855 100644
--- a/user/src/com/google/gwt/user/client/ui/Widget.java
+++ b/user/src/com/google/gwt/user/client/ui/Widget.java
@@ -72,32 +72,49 @@
   }
 
   /**
+   * If a widget implements HasWidgets, it must override this method and
+   * call onAttach() for each of its child widgets.
+   * 
+   * @see Panel#onAttach()
+   */
+  protected void doAttachChildren() {
+  }
+
+  /**
+   * If a widget implements HasWidgets, it must override this method and
+   * call onDetach() for each of its child widgets.
+   * 
+   * @see Panel#onDetach()
+   */
+  protected void doDetachChildren() {
+  }
+
+  /**
    * This method is called when a widget is attached to the browser's document.
-   * To receive notification after a Widget has been added from the
+   * To receive notification after a Widget has been added to the
    * document, override the {@link #onLoad} method.
    * 
    * <p>
    * Subclasses that override this method must call
    * <code>super.onAttach()</code> to ensure that the Widget has been
-   * attached to the underlying Element.
+   * attached to its underlying Element.
    * </p>
    * 
    * @throws IllegalStateException if this widget is already attached
    */
   protected void onAttach() {
-    if (attached) {
+    if (isAttached()) {
       throw new IllegalStateException(
           "Should only call onAttach when the widget is detached from the browser's document");
     }
 
     attached = true;
-
-    // Set the main element's event listener. This should only be set
-    // while the widget is attached, because it creates a circular
-    // reference between JavaScript and the DOM.
     DOM.setEventListener(getElement(), this);
+    doAttachChildren();
 
-    // Now that the widget is attached, call onLoad().
+    // onLoad() gets called only *after* all of the children are attached and
+    // the attached flag is set. This allows widgets to be notified when they
+    // are fully attached, and panels when all of their children are attached.
     onLoad();
   }
 
@@ -110,27 +127,27 @@
    * Subclasses that override this method must call
    * <code>super.onDetach()</code> to ensure that the Widget has been
    * detached from the underlying Element.  Failure to do so will result
-   * in application memeroy leaks due to circular references between DOM
+   * in application memory leaks due to circular references between DOM
    * Elements and JavaScript objects.
    * </p>
    * 
    * @throws IllegalStateException if this widget is already detached
    */
   protected void onDetach() {
-    if (!attached) {
+    if (!isAttached()) {
       throw new IllegalStateException(
           "Should only call onDetach when the widget is attached to the browser's document");
     }
-    
-    // Give the user a chance to clean up, but don't trust the code to not throw
+
     try {
+      // onUnload() gets called *before* everything else (the opposite of
+      // onLoad()).
       onUnload();
     } finally {
-      attached = false;
-  
-      // Clear out the element's event listener (breaking the circular
-      // reference between it and the widget).
+      // Put this in a finally, just in case onUnload throws an exception.
+      doDetachChildren();
       DOM.setEventListener(getElement(), null);
+      attached = false;
     }
   }
 
@@ -140,7 +157,7 @@
    */
   protected void onLoad() {
   }
-  
+
   /**
    * This method is called immediately before a widget will be detached from the
    * browser's document.
@@ -159,7 +176,7 @@
    * @param elem the object's new element
    */
   protected void setElement(Element elem) {
-    if (attached) {
+    if (isAttached()) {
       // Remove old event listener to avoid leaking. onDetach will not do this
       // for us, because it is only called when the widget itself is detached from
       // the document.
@@ -167,7 +184,7 @@
     }
 
     super.setElement(elem);
-    if (attached) {
+    if (isAttached()) {
       // Hook the event listener back up on the new element. onAttach will not
       // do this for us, because it is only called when the widget itself is
       // attached to the document.
diff --git a/user/test/com/google/gwt/user/client/ui/WidgetOnLoadTest.java b/user/test/com/google/gwt/user/client/ui/WidgetOnLoadTest.java
new file mode 100644
index 0000000..430deda
--- /dev/null
+++ b/user/test/com/google/gwt/user/client/ui/WidgetOnLoadTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2007 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.client.ui;
+
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+
+/**
+ * Tests the semantics and ordering of onAttach/onDetach/onLoad/onUnload.
+ */
+public class WidgetOnLoadTest extends GWTTestCase {
+
+  public String getModuleName() {
+    return "com.google.gwt.user.User";
+  }
+
+  static int orderIndex;
+
+  static boolean isElementAttached(Element elem) {
+    return DOM.isOrHasChild(RootPanel.getBodyElement(), elem);
+  }
+
+  static class TestPanel extends FlowPanel {
+    int onAttachOrder;
+    int onLoadOrder;
+    int onDetachOrder;
+    int onUnloadOrder;
+    boolean domAttachedOnLoad;
+    boolean domAttachedOnUnload;
+
+    protected void onAttach() {
+      onAttachOrder = ++orderIndex;
+      super.onAttach();
+    }
+
+    protected void onLoad() {
+      onLoadOrder = ++orderIndex;
+      domAttachedOnLoad = isElementAttached(getElement());
+      super.onLoad();
+    }
+
+    protected void onDetach() {
+      onDetachOrder = ++orderIndex;
+      super.onDetach();
+    }
+
+    protected void onUnload() {
+      onUnloadOrder = ++orderIndex;
+      domAttachedOnUnload = isElementAttached(getElement());
+      super.onUnload();
+    }
+  }
+
+  static class TestWidget extends Label {
+    int onAttachOrder;
+    int onLoadOrder;
+    int onDetachOrder;
+    int onUnloadOrder;
+    boolean domAttachedOnLoad;
+    boolean domAttachedOnUnload;
+
+    protected void onAttach() {
+      onAttachOrder = ++orderIndex;
+      super.onAttach();
+    }
+
+    protected void onLoad() {
+      domAttachedOnLoad = isElementAttached(getElement());
+      onLoadOrder = ++orderIndex;
+      super.onLoad();
+    }
+
+    protected void onDetach() {
+      onDetachOrder = ++orderIndex;
+      super.onDetach();
+    }
+
+    protected void onUnload() {
+      onUnloadOrder = ++orderIndex;
+      domAttachedOnUnload = isElementAttached(getElement());
+      super.onUnload();
+    }
+  }
+
+  public void testOnLoadAndUnloadOrder() {
+    TestPanel tp = new TestPanel();
+    TestWidget tw = new TestWidget();
+
+    tp.add(tw);
+    RootPanel.get().add(tp);
+    RootPanel.get().remove(tp);
+
+    // Trivial tests. Ensure that each panel/widget's onAttach/onDetach are
+    // called before their associated onLoad/onUnload.
+    assertTrue(tp.onAttachOrder < tp.onLoadOrder);
+    assertTrue(tp.onDetachOrder < tp.onUnloadOrder);
+    assertTrue(tw.onAttachOrder < tw.onLoadOrder);
+    assertTrue(tw.onDetachOrder < tw.onUnloadOrder);
+
+    // Also trivial. Ensure that the panel's onAttach/onDetach is called before
+    // its child's onAttach/onDetach.
+    assertTrue(tp.onAttachOrder < tw.onAttachOrder);
+
+    // Ensure that the panel's onLoad is only called after its widgets are
+    // attached/loaded.
+    assertTrue(tp.onLoadOrder > tw.onLoadOrder);
+
+    // Ensure that the panel's onUnload is called before its widgets are
+    // detached/unloaded.
+    assertTrue(tp.onUnloadOrder < tw.onUnloadOrder);
+
+    // Make sure each widget/panel's elements are actually still attached to the
+    // DOM during onLoad/onUnload.
+    assertTrue(tp.domAttachedOnLoad);
+    assertTrue(tp.domAttachedOnUnload);
+    assertTrue(tw.domAttachedOnLoad);
+    assertTrue(tw.domAttachedOnUnload);
+  }
+}