Adding TreeNode API to CellTree and CellBrowser, which allows users to programmatically open/close tree nodes. Similar to the way CellList works, a TreeNode primarily operates on its children, and it is only accessible if the node is open.  That is important because it means that implementors do not need to create a TreeNode for every leaf, as is the case with CellBrowser.  TreeNodes are destroyed when they are closed or otherwise removed from the tree.  However, if new data is pushed into the tree, a TreeNode will not be destroyed unless the new data set does not contain the nodes value.  CellTree and CellBrowser now implement HasOpen/CloseHandlers.  TreeNodes can return the value of the node itself, allowing users to respond to open/close events appropriately.  This patch also adds a bunch of unit tests, and fixes some subtle bugs.


Design Doc:
https://wave.google.com/wave/#restored:wave:googlewave.com%252Fw%252BAuahvuinA

Review at http://gwt-code-reviews.appspot.com/796801

Review by: rice@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8642 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactTreeViewModel.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactTreeViewModel.java
index e3c3f75..6df28dc 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactTreeViewModel.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactTreeViewModel.java
@@ -102,7 +102,7 @@
     public int compareTo(LetterCount o) {
       return (o == null) ? -1 : (firstLetter - o.firstLetter);
     }
- 
+
     @Override
     public boolean equals(Object o) {
       return compareTo((LetterCount) o) == 0;
@@ -201,6 +201,9 @@
       @Override
       public void onBrowserEvent(Element parent, ContactInfo value, Object key,
           NativeEvent event, ValueUpdater<ContactInfo> valueUpdater) {
+        // Make sure that the composition cells see the event.
+        super.onBrowserEvent(parent, value, key, event, valueUpdater);
+
         if ("keyup".equals(event.getType())
             && event.getKeyCode() == KeyCodes.KEY_ENTER) {
           selectionModel.setSelected(value, !selectionModel.isSelected(value));
diff --git a/user/src/com/google/gwt/user/cellview/client/AbstractCellTree.java b/user/src/com/google/gwt/user/cellview/client/AbstractCellTree.java
new file mode 100644
index 0000000..649bf0e
--- /dev/null
+++ b/user/src/com/google/gwt/user/cellview/client/AbstractCellTree.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2010 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.cellview.client;
+
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.logical.shared.HasCloseHandlers;
+import com.google.gwt.event.logical.shared.HasOpenHandlers;
+import com.google.gwt.event.logical.shared.OpenEvent;
+import com.google.gwt.event.logical.shared.OpenHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.view.client.TreeViewModel;
+import com.google.gwt.view.client.TreeViewModel.NodeInfo;
+
+/**
+ * An abstract representation of a tree widget that renders items using
+ * {@link com.google.gwt.cell.client.Cell}s.
+ */
+public abstract class AbstractCellTree extends Composite
+    implements HasOpenHandlers<TreeNode>, HasCloseHandlers<TreeNode> {
+
+  /**
+   * The {@link TreeViewModel} that backs the tree.
+   */
+  private final TreeViewModel viewModel;
+
+  /**
+   * Construct a new {@link CellTree} with the specified {@link TreeViewModel}
+   * and root value.
+   *
+   * @param viewModel the {@link TreeViewModel} that backs the tree
+   */
+  public AbstractCellTree(TreeViewModel viewModel) {
+    this.viewModel = viewModel;
+  }
+
+  public HandlerRegistration addCloseHandler(CloseHandler<TreeNode> handler) {
+    return addHandler(handler, CloseEvent.getType());
+  }
+
+  public HandlerRegistration addOpenHandler(OpenHandler<TreeNode> handler) {
+    return addHandler(handler, OpenEvent.getType());
+  }
+
+  /**
+   * Get the root {@link TreeNode}.
+   *
+   * @return the {@link TreeNode} at the root of the tree
+   */
+  public abstract TreeNode getRootTreeNode();
+
+  /**
+   * Get the {@link TreeViewModel} that backs this tree.
+   *
+   * @return the {@link TreeViewModel}
+   */
+  public TreeViewModel getTreeViewModel() {
+    return viewModel;
+  }
+
+  /**
+   * Get the {@link NodeInfo} that will provide the information to retrieve and
+   * display the children of the specified value.
+   *
+   * @param value the value in the parent node
+   * @return the {@link NodeInfo}
+   */
+  protected <T> NodeInfo<?> getNodeInfo(T value) {
+    return viewModel.getNodeInfo(value);
+  }
+
+  /**
+   * Check if the value is known to be a leaf node.
+   *
+   * @param value the value at the node
+   *
+   * @return true if the node is known to be a leaf node, false otherwise
+   */
+  protected boolean isLeaf(Object value) {
+    return viewModel.isLeaf(value);
+  }
+}
diff --git a/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java b/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
index efd7e5c..13ca6d0 100644
--- a/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
+++ b/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
@@ -17,6 +17,8 @@
 
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.EventHandler;
 import com.google.gwt.event.shared.GwtEvent.Type;
 import com.google.gwt.event.shared.HandlerRegistration;
@@ -85,10 +87,12 @@
 
     public void replaceAllChildren(List<T> values, String html) {
       hasData.replaceAllChildren(values, html);
+      fireValueChangeEvent();
     }
 
     public void replaceChildren(List<T> values, int start, String html) {
       hasData.replaceChildren(values, start, html);
+      fireValueChangeEvent();
     }
 
     public void resetFocus() {
@@ -108,6 +112,18 @@
     public void setSelected(Element elem, boolean selected) {
       hasData.setSelected(elem, selected);
     }
+
+    /**
+     * Fire a value change event.
+     */
+    private void fireValueChangeEvent() {
+      // Use an anonymous class to override ValueChangeEvents's protected
+      // constructor. We can't call ValueChangeEvent.fire() because this class
+      // doesn't implement HasValueChangeHandlers.
+      hasData.fireEvent(
+          new ValueChangeEvent<List<T>>(hasData.getDisplayedItems()) {
+          });
+    }
   }
 
   /**
@@ -379,6 +395,19 @@
       int start, SelectionModel<? super T> selectionModel);
 
   /**
+   * Add a {@link ValueChangeHandler} that is called when the display values
+   * change. Used by {@link CellBrowser} to detect when the displayed data
+   * changes.
+   *
+   * @param handler the handler
+   * @return a {@link HandlerRegistration} to remove the handler
+   */
+  final HandlerRegistration addValueChangeHandler(
+      ValueChangeHandler<List<T>> handler) {
+    return addHandler(handler, ValueChangeEvent.getType());
+  }
+
+  /**
    * Convert the specified HTML into DOM elements and return the parent of the
    * DOM elements.
    *
@@ -410,6 +439,35 @@
   }
 
   /**
+   * Get the first index of a displayed item according to its key.
+   *
+   * @param value the value
+   * @return the index of the item, or -1 of not found
+   */
+  int indexOf(T value) {
+    ProvidesKey<T> keyProvider = getKeyProvider();
+    List<T> items = getDisplayedItems();
+
+    // If no key provider is present, just compare the objets.
+    if (keyProvider == null) {
+      return items.indexOf(value);
+    }
+
+    // Compare the keys of each object.
+    Object key = keyProvider.getKey(value);
+    if (key == null) {
+      return -1;
+    }
+    int itemCount = items.size();
+    for (int i = 0; i < itemCount; i++) {
+      if (key.equals(keyProvider.getKey(items.get(i)))) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  /**
    * Called when selection changes.
    */
   void onUpdateSelection() {
diff --git a/user/src/com/google/gwt/user/cellview/client/CellBrowser.java b/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
index 322dbf7..31d7727 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
@@ -26,7 +26,10 @@
 import com.google.gwt.dom.client.Style.Position;
 import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.dom.client.Style.Visibility;
-import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.OpenEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.resources.client.ClientBundle;
 import com.google.gwt.resources.client.CssResource;
@@ -35,7 +38,6 @@
 import com.google.gwt.resources.client.ImageResource.RepeatStyle;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HasAnimation;
 import com.google.gwt.user.client.ui.ProvidesResize;
@@ -45,9 +47,6 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.view.client.HasData;
 import com.google.gwt.view.client.ProvidesKey;
-import com.google.gwt.view.client.Range;
-import com.google.gwt.view.client.RangeChangeEvent;
-import com.google.gwt.view.client.RowCountChangeEvent;
 import com.google.gwt.view.client.SelectionModel;
 import com.google.gwt.view.client.TreeViewModel;
 import com.google.gwt.view.client.TreeViewModel.NodeInfo;
@@ -61,7 +60,7 @@
  * A "browsable" view of a tree in which only a single node per level may be
  * open at one time.
  */
-public class CellBrowser extends Composite
+public class CellBrowser extends AbstractCellTree
     implements ProvidesResize, RequiresResize, HasAnimation {
 
   /**
@@ -167,6 +166,11 @@
     private Object openKey;
 
     /**
+     * The value of the currently open item.
+     */
+    private C openValue;
+
+    /**
      * The key provider for the node.
      */
     private final ProvidesKey<C> providesKey;
@@ -221,30 +225,13 @@
 
       // Open child nodes.
       if (Event.getTypeInt(event.getType()) == Event.ONMOUSEDOWN) {
-        trimToLevel(level);
-
-        // Remove style from currently open item.
-        Element curOpenItem = Document.get().getElementById(getOpenId());
-        if (curOpenItem != null) {
-          setElementOpenState(curOpenItem.getParentElement(), false);
-        }
-        openKey = null;
-
-        // Save the key of the new open item and update the Element.
-        if (!viewModel.isLeaf(value)) {
-          NodeInfo<?> nodeInfo = viewModel.getNodeInfo(value);
-          if (nodeInfo != null) {
-            openKey = providesKey.getKey(value);
-            setElementOpenState(parent, true);
-            appendTreeNode(nodeInfo);
-          }
-        }
+        setChildState(this, value, true, true);
       }
     }
 
     public void render(C value, Object viewData, StringBuilder sb) {
       boolean isOpen = (openKey == null) ? false : openKey.equals(
-          providesKey.getKey(value));
+          getValueKey(value));
       boolean isSelected = (selectionModel == null)
           ? false : selectionModel.isSelected(value);
       sb.append("<div style='position:relative;padding-right:");
@@ -257,14 +244,10 @@
       if (isSelected) {
         sb.append(" ").append(style.selectedItem());
       }
-      sb.append("'");
-      if (isOpen) {
-        sb.append(" id='").append(getOpenId()).append("'");
-      }
-      sb.append(">");
+      sb.append("'>");
       if (isOpen) {
         sb.append(openImageHtml);
-      } else if (viewModel.isLeaf(value)) {
+      } else if (isLeaf(value)) {
         sb.append(LEAF_IMAGE);
       } else {
         sb.append(closedImageHtml);
@@ -289,49 +272,13 @@
     }
 
     /**
-     * Get the image element of the decorated cell.
+     * Get the key for the specified value.
      *
-     * @param parent the parent of this cell
-     * @return the image element
+     * @param value the value
+     * @return the key
      */
-    private Element getImageElement(Element parent) {
-      return parent.getFirstChildElement().getFirstChildElement();
-    }
-
-    /**
-     * Get the ID of the open element.
-     *
-     * @return the ID
-     */
-    private String getOpenId() {
-      return uniqueId + "-" + level;
-    }
-
-    /**
-     * Replace the image element of a cell and update the styles associated with
-     * the open state.
-     *
-     * @param parent the parent element of the cell
-     * @param open true if open, false if closed
-     */
-    private void setElementOpenState(Element parent, boolean open) {
-      // Update the style name and ID.
-      Element wrapper = parent.getFirstChildElement();
-      if (open) {
-        wrapper.addClassName(style.openItem());
-        wrapper.setId(getOpenId());
-      } else {
-        wrapper.removeClassName(style.openItem());
-        wrapper.setId("");
-      }
-
-      // Replace the image element.
-      String html = open ? openImageHtml : closedImageHtml;
-      Element tmp = Document.get().createDivElement();
-      tmp.setInnerHTML(html);
-      Element imageElem = tmp.getFirstChildElement();
-      Element oldImg = getImageElement(parent);
-      wrapper.replaceChild(imageElem, oldImg);
+    private Object getValueKey(C value) {
+      return (providesKey == null) ? value : providesKey.getKey(value);
     }
   }
 
@@ -381,43 +328,146 @@
    *
    * @param <C> the data type of the children of the node
    */
-  private class TreeNode<C> {
-    private CellDecorator<C> cell;
-    private HasData<C> view;
+  private class TreeNodeImpl<C> implements TreeNode {
+    private final CellDecorator<C> cell;
+    private final AbstractHasData<C> display;
     private NodeInfo<C> nodeInfo;
-    private Widget widget;
+    private final Object value;
+    private final HandlerRegistration valueChangeHandler;
+    private final Widget widget;
 
     /**
-     * Construct a new {@link TreeNode}.
+     * Construct a new {@link TreeNodeImpl}.
      *
      * @param nodeInfo the nodeInfo for the children nodes
-     * @param view the view associated with the node
+     * @param value the value of the node
+     * @param display the display associated with the node
+     * @param cell the {@link Cell} used to render the data
      * @param widget the widget that represents the list view
      */
-    public TreeNode(NodeInfo<C> nodeInfo, HasData<C> view,
-        CellDecorator<C> cell, Widget widget) {
+    public TreeNodeImpl(final NodeInfo<C> nodeInfo, Object value,
+        AbstractHasData<C> display, final CellDecorator<C> cell,
+        Widget widget) {
       this.cell = cell;
-      this.view = view;
+      this.display = display;
       this.nodeInfo = nodeInfo;
+      this.value = value;
       this.widget = widget;
+
+      // Trim to the current level if the open node disappears.
+      valueChangeHandler = display.addValueChangeHandler(
+          new ValueChangeHandler<List<C>>() {
+            public void onValueChange(ValueChangeEvent<List<C>> event) {
+              Object openKey = cell.openKey;
+              if (openKey != null) {
+                boolean stillExists = false;
+                List<C> displayValues = event.getValue();
+                for (C displayValue : displayValues) {
+                  if (openKey.equals(cell.getValueKey(displayValue))) {
+                    stillExists = true;
+                    break;
+                  }
+                }
+                if (!stillExists) {
+                  trimToLevel(cell.level);
+                }
+              }
+            }
+          });
+    }
+
+    public int getChildCount() {
+      assertNotDestroyed();
+      return display.getChildCount();
+    }
+
+    public C getChildValue(int index) {
+      assertNotDestroyed();
+      checkChildBounds(index);
+      return display.getDisplayedItem(index);
+    }
+
+    public int getIndex() {
+      assertNotDestroyed();
+      TreeNodeImpl<?> parent = getParent();
+      return (parent == null) ? 0 : parent.getOpenIndex();
+    }
+
+    public TreeNodeImpl<?> getParent() {
+      assertNotDestroyed();
+      return (cell.level == 0) ? null : treeNodes.get(cell.level - 1);
+    }
+
+    public Object getValue() {
+      return value;
+    }
+
+    public boolean isChildLeaf(int index) {
+      assertNotDestroyed();
+      checkChildBounds(index);
+      return isLeaf(getChildValue(index));
+    }
+
+    public boolean isChildOpen(int index) {
+      assertNotDestroyed();
+      checkChildBounds(index);
+      return (cell.openKey == null) ? false : cell.openKey.equals(
+          cell.getValueKey(getChildValue(index)));
+    }
+
+    public boolean isDestroyed() {
+      return nodeInfo == null;
+    }
+
+    public TreeNode setChildOpen(int index, boolean open) {
+      return setChildOpen(index, open, true);
+    }
+
+    public TreeNode setChildOpen(int index, boolean open, boolean fireEvents) {
+      assertNotDestroyed();
+      checkChildBounds(index);
+      return setChildState(cell, getChildValue(index), open, fireEvents);
     }
 
     /**
-     * Get the {@link CellDecorator} used to render the node.
-     *
-     * @return the cell decorator
+     * Assert that the node has not been destroyed.
      */
-    public CellDecorator<C> getCell() {
-      return cell;
+    private void assertNotDestroyed() {
+      if (isDestroyed()) {
+        throw new IllegalStateException("TreeNode no longer exists.");
+      }
+    }
+
+    /**
+     * Check the child bounds.
+     *
+     * @param index the index of the child
+     * @throws IndexOutOfBoundsException if the child is not in range
+     */
+    private void checkChildBounds(int index) {
+      if ((index < 0) || (index >= getChildCount())) {
+        throw new IndexOutOfBoundsException();
+      }
     }
 
     /**
      * Unregister the list view and remove it from the widget.
      */
-    void cleanup() {
-      view.setSelectionModel(null);
+    private void destroy() {
+      valueChangeHandler.removeHandler();
+      display.setSelectionModel(null);
       nodeInfo.unsetDataDisplay();
       getSplitLayoutPanel().remove(widget);
+      nodeInfo = null;
+    }
+
+    /**
+     * Get the index of the open item.
+     *
+     * @return the index of the open item, or -1 if not found
+     */
+    private int getOpenIndex() {
+      return display.indexOf(cell.openValue);
     }
   }
 
@@ -467,11 +517,6 @@
   private final String closedImageHtml;
 
   /**
-   * The unique ID assigned to this tree widget.
-   */
-  private final String uniqueId = Document.get().createUniqueId();
-
-  /**
    * A boolean indicating whether or not animations are enabled.
    */
   private boolean isAnimationEnabled;
@@ -502,14 +547,10 @@
   private Element scrollLock;
 
   /**
-   * The visible {@link TreeNode}.
+   * The visible {@link TreeNodeImpl}s.
    */
-  private final List<TreeNode<?>> treeNodes = new ArrayList<TreeNode<?>>();
-
-  /**
-   * The {@link TreeViewModel} that backs the tree.
-   */
-  private final TreeViewModel viewModel;
+  private final List<TreeNodeImpl<?>> treeNodes = new ArrayList<
+      TreeNodeImpl<?>>();
 
   /**
    * Construct a new {@link CellBrowser}.
@@ -532,7 +573,7 @@
    */
   public <T> CellBrowser(
       TreeViewModel viewModel, T rootValue, Resources resources) {
-    this.viewModel = viewModel;
+    super(viewModel);
     this.style = resources.cellBrowserStyle();
     this.style.ensureInjected();
     initWidget(new SplitLayoutPanel());
@@ -560,7 +601,7 @@
     getElement().appendChild(scrollLock);
 
     // Associate the first view with the rootValue.
-    appendTreeNode(viewModel.getNodeInfo(rootValue));
+    appendTreeNode(getNodeInfo(rootValue), rootValue);
 
     // Catch scroll events.
     sinkEvents(Event.ONSCROLL);
@@ -584,6 +625,11 @@
     return minWidth;
   }
 
+  @Override
+  public TreeNode getRootTreeNode() {
+    return treeNodes.get(0);
+  }
+
   public boolean isAnimationEnabled() {
     return isAnimationEnabled;
   }
@@ -634,8 +680,9 @@
    * @param cell the cell to use in the list view
    * @return the {@link HasData}
    */
-  // TODO(jlabanca): Move createView into constructor factory arg?
-  protected <C> HasData<C> createDisplay(NodeInfo<C> nodeInfo, Cell<C> cell) {
+  // TODO(jlabanca): Move createDisplay into constructor factory arg?
+  protected <C> AbstractHasData<C> createDisplay(
+      NodeInfo<C> nodeInfo, Cell<C> cell) {
     CellList<C> display = new CellList<C>(cell, getCellListResources());
     display.setValueUpdater(nodeInfo.getValueUpdater());
     display.setKeyProvider(nodeInfo.getProvidesKey());
@@ -671,123 +718,43 @@
   }
 
   /**
-   * Create a new {@link TreeNode} and append it to the end of the LayoutPanel.
+   * Create a new {@link TreeNodeImpl} and append it to the end of the
+   * LayoutPanel.
    *
    * @param <C> the data type of the children
    * @param nodeInfo the info about the node
+   * @param value the value of the open node
    */
-  private <C> void appendTreeNode(final NodeInfo<C> nodeInfo) {
+  private <C> void appendTreeNode(final NodeInfo<C> nodeInfo, Object value) {
     // Create the list view.
     final int level = treeNodes.size();
     final CellDecorator<C> cell = new CellDecorator<C>(nodeInfo, level);
-    final HasData<C> view = createDisplay(nodeInfo, cell);
-    assert (view instanceof Widget) : "createView() must return a widget";
+    final AbstractHasData<C> view = createDisplay(nodeInfo, cell);
 
     // Create a pager and wrap the components in a scrollable container.
     ScrollPanel scrollable = new ScrollPanel();
     final Widget pager = createPager(view);
     if (pager != null) {
       FlowPanel flowPanel = new FlowPanel();
-      flowPanel.add((Widget) view);
+      flowPanel.add(view);
       flowPanel.add(pager);
       scrollable.setWidget(flowPanel);
     } else {
-      scrollable.setWidget((Widget) view);
+      scrollable.setWidget(view);
     }
     scrollable.setStyleName(style.column());
     if (level == 0) {
       scrollable.addStyleName(style.firstColumn());
     }
 
-    // Create a delegate list view so we can trap data changes.
-    HasData<C> viewDelegate = new HasData<C>() {
-
-      public HandlerRegistration addRangeChangeHandler(
-          RangeChangeEvent.Handler handler) {
-        return view.addRangeChangeHandler(handler);
-      }
-
-      public HandlerRegistration addRowCountChangeHandler(
-          RowCountChangeEvent.Handler handler) {
-        return view.addRowCountChangeHandler(handler);
-      }
-
-      public void fireEvent(GwtEvent<?> event) {
-        view.fireEvent(event);
-      }
-
-      public int getRowCount() {
-        return view.getRowCount();
-      }
-
-      public SelectionModel<? super C> getSelectionModel() {
-        return view.getSelectionModel();
-      }
-
-      public Range getVisibleRange() {
-        return view.getVisibleRange();
-      }
-
-      public boolean isRowCountExact() {
-        return view.isRowCountExact();
-      }
-
-      public final void setRowCount(int count) {
-        view.setRowCount(count);
-      }
-
-      public void setRowCount(int count, boolean isExact) {
-        view.setRowCount(count, isExact);
-      }
-
-      public void setRowData(int start, List<C> values) {
-        // Trim to the current level if the open node no longer exists.
-        TreeNode<?> node = treeNodes.get(level);
-        Object openKey = node.getCell().openKey;
-        if (openKey != null) {
-          boolean stillExists = false;
-          ProvidesKey<C> keyProvider = nodeInfo.getProvidesKey();
-          for (C value : values) {
-            if (openKey.equals(keyProvider.getKey(value))) {
-              stillExists = true;
-              break;
-            }
-          }
-          if (!stillExists) {
-            trimToLevel(level);
-          }
-        }
-
-        // Refresh the list.
-        view.setRowData(start, values);
-      }
-
-      public void setSelectionModel(SelectionModel<? super C> selectionModel) {
-        view.setSelectionModel(selectionModel);
-      }
-
-      public void setVisibleRange(int start, int length) {
-        view.setVisibleRange(start, length);
-      }
-
-      public void setVisibleRange(Range range) {
-        view.setVisibleRange(range);
-      }
-
-      public void setVisibleRangeAndClearData(
-          Range range, boolean forceRangeChangeEvent) {
-        view.setVisibleRangeAndClearData(range, forceRangeChangeEvent);
-      }
-    };
-
     // Create a TreeNode.
-    TreeNode<C> treeNode = new TreeNode<C>(
-        nodeInfo, viewDelegate, cell, scrollable);
+    TreeNodeImpl<C> treeNode = new TreeNodeImpl<C>(
+        nodeInfo, value, view, cell, scrollable);
     treeNodes.add(treeNode);
 
     // Attach the view to the selection model and node info.
     view.setSelectionModel(nodeInfo.getSelectionModel());
-    nodeInfo.setDataDisplay(viewDelegate);
+    nodeInfo.setDataDisplay(view);
 
     // Add the view to the LayoutPanel.
     SplitLayoutPanel splitPanel = getSplitLayoutPanel();
@@ -830,6 +797,83 @@
   }
 
   /**
+   * Set the open state of a tree node.
+   *
+   * @param cell the Cell that changed state.
+   * @param value the value to open
+   * @param open true to open, false to close
+   * @param fireEvents true to fireEvents
+   * @return the open {@link TreeNode}, or null if not opened
+   */
+  private <C> TreeNode setChildState(
+      CellDecorator<C> cell, C value, boolean open, boolean fireEvents) {
+
+    // Early exit if the node is a leaf.
+    if (isLeaf(value)) {
+      return null;
+    }
+
+    // Get the key of the value to open.
+    Object newKey = cell.getValueKey(value);
+
+    if (open) {
+      if (newKey == null) {
+        // Early exit if opening but the specified node has no key.
+        return null;
+      } else if (newKey.equals(cell.openKey)) {
+        // Early exit if opening but the specified node is already open.
+        return treeNodes.get(cell.level + 1);
+      }
+
+      // Close the currently open node.
+      if (cell.openKey != null) {
+        setChildState(cell, cell.openValue, false, fireEvents);
+      }
+
+      // Get the child node info.
+      NodeInfo<?> childNodeInfo = getNodeInfo(value);
+      if (childNodeInfo == null) {
+        return null;
+      }
+
+      // Update the cell so it renders the styles correctly.
+      cell.openValue = value;
+      cell.openKey = cell.getValueKey(value);
+
+      // Refresh the display to update the styles for this node.
+      treeNodes.get(cell.level).display.redraw();
+
+      // Add the child node.
+      appendTreeNode(childNodeInfo, value);
+
+      if (fireEvents) {
+        OpenEvent.fire(this, treeNodes.get(cell.level + 1));
+      }
+      return treeNodes.get(cell.level + 1);
+    } else {
+      // Early exit if closing and the specified node or all nodes are closed.
+      if (cell.openKey == null || !cell.openKey.equals(newKey)) {
+        return null;
+      }
+
+      // Close the node.
+      TreeNode closedNode = treeNodes.get(cell.level + 1);
+      trimToLevel(cell.level);
+      cell.openKey = null;
+      cell.openValue = null;
+
+      // Refresh the display to update the styles for this node.
+      treeNodes.get(cell.level).display.redraw();
+
+      if (fireEvents) {
+        CloseEvent.fire(this, closedNode);
+      }
+    }
+
+    return null;
+  }
+
+  /**
    * Reduce the number of {@link HasData}s down to the specified level.
    *
    * @param level the level to trim to
@@ -841,8 +885,8 @@
     // Remove the views that are no longer needed.
     int curLevel = treeNodes.size() - 1;
     while (curLevel > level) {
-      TreeNode<?> removed = treeNodes.remove(curLevel);
-      removed.cleanup();
+      TreeNodeImpl<?> removed = treeNodes.remove(curLevel);
+      removed.destroy();
       curLevel--;
     }
   }
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTree.java b/user/src/com/google/gwt/user/cellview/client/CellTree.java
index 0ae1b54..1764aa2 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTree.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTree.java
@@ -28,7 +28,6 @@
 import com.google.gwt.resources.client.ImageResource.ImageOptions;
 import com.google.gwt.resources.client.ImageResource.RepeatStyle;
 import com.google.gwt.user.client.Event;
-import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.HasAnimation;
 import com.google.gwt.user.client.ui.SimplePanel;
 import com.google.gwt.view.client.TreeViewModel;
@@ -38,7 +37,7 @@
 /**
  * A view of a tree.
  */
-public class CellTree extends Composite implements HasAnimation {
+public class CellTree extends AbstractCellTree implements HasAnimation {
 
   /**
    * A cleaner version of the table that uses less graphics.
@@ -452,11 +451,6 @@
   private final Style style;
 
   /**
-   * The {@link TreeViewModel} that backs the tree.
-   */
-  private final TreeViewModel viewModel;
-
-  /**
    * Construct a new {@link CellTree}.
    *
    * @param <T> the type of data in the root node
@@ -475,9 +469,9 @@
    * @param rootValue the hidden root value of the tree
    * @param resources the resources used to render the tree
    */
-  public <T> CellTree(TreeViewModel viewModel, T rootValue,
-      Resources resources) {
-    this.viewModel = viewModel;
+  public <T> CellTree(
+      TreeViewModel viewModel, T rootValue, Resources resources) {
+    super(viewModel);
     this.style = resources.cellTreeStyle();
     this.style.ensureInjected();
     initWidget(new SimplePanel());
@@ -502,10 +496,10 @@
     sinkEvents(Event.ONCLICK | Event.ONKEYDOWN | Event.ONKEYUP);
 
     // Associate a view with the item.
-    CellTreeNodeView<T> root = new CellTreeNodeView<T>(this, null, null,
-        getElement(), rootValue);
+    CellTreeNodeView<T> root = new CellTreeNodeView<T>(
+        this, null, null, getElement(), rootValue);
     keyboardSelectedNode = rootNode = root;
-    root.setOpen(true);
+    root.setOpen(true, false);
     keyboardSelectedNode.keyboardEnter(0, false);
   }
 
@@ -529,8 +523,9 @@
     return defaultNodeSize;
   }
 
-  public TreeViewModel getTreeViewModel() {
-    return viewModel;
+  @Override
+  public TreeNode getRootTreeNode() {
+    return rootNode.getTreeNode();
   }
 
   public boolean isAnimationEnabled() {
@@ -577,7 +572,7 @@
         // Open the node when the open image is clicked.
         Element showMoreElem = nodeView.getShowMoreElement();
         if (nodeView.getImageElement().isOrHasChild(target)) {
-          nodeView.setOpen(!nodeView.isOpen());
+          nodeView.setOpen(!nodeView.isOpen(), true);
           return;
         } else if (showMoreElem != null && showMoreElem.isOrHasChild(target)) {
           nodeView.showMore();
@@ -631,6 +626,13 @@
   }
 
   /**
+   * Cancel a pending animation.
+   */
+  void cancelTreeNodeAnimation() {
+    animation.cancel();
+  }
+
+  /**
    * Get the HTML to render the closed image.
    *
    * @param isTop true if the top element, false if not
@@ -680,16 +682,16 @@
    */
   void maybeAnimateTreeNode(CellTreeNodeView<?> node) {
     if (animation != null) {
-      animation.animate(node, node.consumeAnimate() && isAnimationEnabled()
-          && !node.isRootNode());
+      animation.animate(node,
+          node.consumeAnimate() && isAnimationEnabled() && !node.isRootNode());
     }
   }
 
   /**
    * Collects parents going up the element tree, terminated at the tree root.
    */
-  private void collectElementChain(ArrayList<Element> chain, Element hRoot,
-      Element hElem) {
+  private void collectElementChain(
+      ArrayList<Element> chain, Element hRoot, Element hElem) {
     if ((hElem == null) || (hElem == hRoot)) {
       return;
     }
@@ -698,8 +700,8 @@
     chain.add(hElem);
   }
 
-  private CellTreeNodeView<?> findItemByChain(ArrayList<Element> chain,
-      int idx, CellTreeNodeView<?> parent) {
+  private CellTreeNodeView<?> findItemByChain(
+      ArrayList<Element> chain, int idx, CellTreeNodeView<?> parent) {
     if (idx == chain.size()) {
       return parent;
     }
@@ -815,7 +817,7 @@
         // TODO(rice) - try different key bahavior mappings such as
         // left=close, right=open, enter=toggle.
         if (child != null && !child.isLeaf()) {
-          child.setOpen(!child.isOpen());
+          child.setOpen(!child.isOpen(), true);
           return true;
         }
         break;
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
index ce91186..dc0fc1f 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
@@ -23,6 +23,8 @@
 import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.dom.client.Style.Overflow;
 import com.google.gwt.dom.client.Style.Position;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.OpenEvent;
 import com.google.gwt.event.shared.EventHandler;
 import com.google.gwt.event.shared.GwtEvent;
 import com.google.gwt.event.shared.GwtEvent.Type;
@@ -55,7 +57,10 @@
 class CellTreeNodeView<T> extends UIObject {
 
   /**
-   * The {@link com.google.gwt.view.client.HasData} used to show children.
+   * The {@link com.google.gwt.view.client.HasData} used to show children. This
+   * class is intentionally static because we might move it to a new
+   * {@link CellTreeNodeView}, and we don't want non-static references to the
+   * old {@link CellTreeNodeView}.
    *
    * @param <C> the child item type
    */
@@ -72,8 +77,8 @@
         this.childContainer = childContainer;
       }
 
-      public <H extends EventHandler> HandlerRegistration addHandler(H handler,
-          Type<H> type) {
+      public <H extends EventHandler> HandlerRegistration addHandler(
+          H handler, Type<H> type) {
         return handlerManger.addHandler(type, handler);
       }
 
@@ -86,8 +91,8 @@
       }
 
       public ElementIterator getChildIterator() {
-        return new HasDataPresenter.DefaultElementIterator(this,
-            childContainer.getFirstChildElement());
+        return new HasDataPresenter.DefaultElementIterator(
+            this, childContainer.getFirstChildElement());
       }
 
       public void onUpdateSelection() {
@@ -181,6 +186,16 @@
         // Replace the child nodes.
         Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, 0);
         AbstractHasData.replaceAllChildren(nodeView.tree, childContainer, html);
+
+        // Trim the list of children.
+        int size = values.size();
+        int childCount = nodeView.children.size();
+        while (childCount > size) {
+          childCount--;
+          nodeView.children.remove(childCount);
+        }
+
+        // Reattach the open nodes.
         loadChildState(values, 0, savedViews);
 
         // Animate the child container open.
@@ -192,18 +207,18 @@
       public void replaceChildren(List<C> values, int start, String html) {
         Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, 0);
 
-        Element newChildren = AbstractHasData.convertToElements(nodeView.tree,
-            getTmpElem(), html);
-        AbstractHasData.replaceChildren(nodeView.tree, childContainer,
-            newChildren, start, html);
+        Element newChildren = AbstractHasData.convertToElements(
+            nodeView.tree, getTmpElem(), html);
+        AbstractHasData.replaceChildren(
+            nodeView.tree, childContainer, newChildren, start, html);
 
         loadChildState(values, 0, savedViews);
       }
 
       public void resetFocus() {
         if (nodeView.keyboardSelectedIndex != -1) {
-          nodeView.keyboardEnter(nodeView.keyboardSelectedIndex,
-              nodeView.keyboardFocused);
+          nodeView.keyboardEnter(
+              nodeView.keyboardSelectedIndex, nodeView.keyboardFocused);
         }
       }
 
@@ -230,14 +245,15 @@
         int end = start + len;
         int childCount = nodeView.getChildCount();
         ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
-        Element childElem =
-          nodeView.ensureChildContainer().getFirstChildElement();
+
+        Element container = nodeView.ensureChildContainer();
+        Element childElem = container.getFirstChildElement();
         for (int i = start; i < end; i++) {
           C childValue = values.get(i - start);
-          CellTreeNodeView<C> child = nodeView.createTreeNodeView(nodeInfo,
-              childElem, childValue, null);
-          CellTreeNodeView<?> savedChild =
-            savedViews.remove(providesKey.getKey(childValue));
+          CellTreeNodeView<C> child = nodeView.createTreeNodeView(
+              nodeInfo, childElem, childValue, null);
+          CellTreeNodeView<?> savedChild = savedViews.remove(
+              providesKey.getKey(childValue));
           // Copy the saved child's state into the new child
           if (savedChild != null) {
             child.animationFrame = savedChild.animationFrame;
@@ -250,20 +266,25 @@
             child.open = savedChild.open;
             child.showMoreElem = savedChild.showMoreElem;
 
+            // Transfer the tree node so that if the user has a handle to it, it
+            // won't be destroyed.
+            child.treeNode = savedChild.treeNode;
+            if (child.treeNode != null) {
+              child.treeNode.nodeView = child;
+            }
+
             // Swap the node view in the child. We reuse the same NodeListView
             // so that we don't have to unset and register a new view with the
-            // NodeInfo.
-            savedChild.listView.setNodeView(child);
+            // NodeInfo, which would inevitably cause the NodeInfo to push
+            // new data.
+            child.listView = savedChild.listView;
+            child.listView.nodeView = child;
 
             // Copy the child container element to the new child
             child.getElement().appendChild(savedChild.ensureAnimationFrame());
           }
 
           if (childCount > i) {
-            if (savedChild == null) {
-              // Cleanup the child node if we aren't going to reuse it.
-              nodeView.children.get(i).cleanup();
-            }
             nodeView.children.set(i, child);
           } else {
             nodeView.children.add(child);
@@ -281,8 +302,8 @@
        * @param start the start index
        * @return the map of open nodes
        */
-      private Map<Object, CellTreeNodeView<?>> saveChildState(List<C> values,
-          int start) {
+      private Map<Object, CellTreeNodeView<?>> saveChildState(
+          List<C> values, int start) {
         // Ensure that we have a children array.
         if (nodeView.children == null) {
           nodeView.children = new ArrayList<CellTreeNodeView<?>>();
@@ -292,20 +313,23 @@
         int len = values.size();
         int end = start + len;
         int childCount = nodeView.getChildCount();
-        Map<Object, CellTreeNodeView<?>> openNodes =
-          new HashMap<Object, CellTreeNodeView<?>>();
+        Map<Object, CellTreeNodeView<?>> openNodes = new HashMap<
+            Object, CellTreeNodeView<?>>();
         for (int i = start; i < end && i < childCount; i++) {
           CellTreeNodeView<?> child = nodeView.getChildNode(i);
-          // Ignore child nodes that are closed.
           if (child.isOpen()) {
+            // Save child nodes that are open.
             openNodes.put(child.getValueKey(), child);
+          } else {
+            // Cleanup child nodes that are closed.
+            child.cleanup(true);
           }
         }
 
         // Trim the saved views down to the children that still exists.
         ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
-        Map<Object, CellTreeNodeView<?>> savedViews =
-          new HashMap<Object, CellTreeNodeView<?>>();
+        Map<Object, CellTreeNodeView<?>> savedViews = new HashMap<
+            Object, CellTreeNodeView<?>>();
         for (C childValue : values) {
           // Remove any child elements that correspond to prior children
           // so the call to setInnerHtml will not destroy them
@@ -316,6 +340,12 @@
             savedViews.put(key, savedView);
           }
         }
+
+        // Cleanup the remaining open nodes that are not in the new data set.
+        for (CellTreeNodeView<?> lostNode : openNodes.values()) {
+          lostNode.cleanup(true);
+        }
+
         return savedViews;
       }
     }
@@ -335,8 +365,8 @@
       this.nodeView = nodeView;
       cell = nodeInfo.getCell();
 
-      presenter = new HasDataPresenter<C>(this, new View(
-          nodeView.ensureChildContainer()), pageSize);
+      presenter = new HasDataPresenter<C>(
+          this, new View(nodeView.ensureChildContainer()), pageSize);
 
       // Use a pager to update buttons.
       presenter.addRowCountChangeHandler(new RowCountChangeEvent.Handler() {
@@ -419,23 +449,102 @@
         Range range, boolean forceRangeChangeEvent) {
       presenter.setVisibleRangeAndClearData(range, forceRangeChangeEvent);
     }
+  }
+
+  /**
+   * An implementation of {@link TreeNode} that delegates to a
+   * {@link CellTreeNodeView}. This class is intentionally static because we
+   * might move it to a new {@link CellTreeNodeView}, and we don't want
+   * non-static references to the old {@link CellTreeNodeView}.
+   */
+  private static class TreeNodeImpl implements TreeNode {
+
+    private CellTreeNodeView<?> nodeView;
+
+    public TreeNodeImpl(CellTreeNodeView<?> nodeView) {
+      this.nodeView = nodeView;
+    }
+
+    public int getChildCount() {
+      assertNotDestroyed();
+      return nodeView.getChildCount();
+    }
+
+    public Object getChildValue(int index) {
+      assertNotDestroyed();
+      checkChildBounds(index);
+      return nodeView.getChildNode(index).value;
+    }
+
+    public int getIndex() {
+      assertNotDestroyed();
+      return (nodeView.parentNode == null)
+          ? 0 : nodeView.parentNode.children.indexOf(nodeView);
+    }
+
+    public TreeNode getParent() {
+      assertNotDestroyed();
+      return nodeView.isRootNode() ? null : nodeView.parentNode.treeNode;
+    }
+
+    public Object getValue() {
+      return nodeView.value;
+    }
+
+    public boolean isChildLeaf(int index) {
+      assertNotDestroyed();
+      checkChildBounds(index);
+      return nodeView.getChildNode(index).isLeaf();
+    }
+
+    public boolean isChildOpen(int index) {
+      assertNotDestroyed();
+      checkChildBounds(index);
+      return nodeView.getChildNode(index).isOpen();
+    }
+
+    public boolean isDestroyed() {
+      return nodeView.isDestroyed || !nodeView.isOpen();
+    }
+
+    public TreeNode setChildOpen(int index, boolean open) {
+      return setChildOpen(index, open, true);
+    }
+
+    public TreeNode setChildOpen(int index, boolean open, boolean fireEvents) {
+      assertNotDestroyed();
+      checkChildBounds(index);
+      CellTreeNodeView<?> child = nodeView.getChildNode(index);
+      return child.setOpen(open, fireEvents) ? child.treeNode : null;
+    }
 
     /**
-     * Assign this {@link HasData} to a new {@link CellTreeNodeView}.
-     *
-     * @param nodeView the new node view
+     * Assert that the node has not been destroyed.
      */
-    private void setNodeView(CellTreeNodeView<?> nodeView) {
-      this.nodeView.listView = null;
-      this.nodeView = nodeView;
-      nodeView.listView = this;
+    private void assertNotDestroyed() {
+      if (isDestroyed()) {
+        throw new IllegalStateException("TreeNode no longer exists.");
+      }
+    }
+
+    /**
+     * Check the child bounds.
+     *
+     * @param index the index of the child
+     * @throws IndexOutOfBoundsException if the child is not in range
+     */
+    private void checkChildBounds(int index) {
+      if ((index < 0) || (index >= getChildCount())) {
+        throw new IndexOutOfBoundsException();
+      }
     }
   }
 
   /**
    * The element used in place of an image when a node has no children.
    */
-  private static final String LEAF_IMAGE = "<div style='position:absolute;display:none;'></div>";
+  private static final String LEAF_IMAGE =
+      "<div style='position:absolute;display:none;'></div>";
 
   /**
    * The temporary element used to render child items.
@@ -449,7 +558,8 @@
    * @return the cell parent within the node
    */
   private static Element getCellParent(Element nodeElem) {
-    return getSelectionElement(nodeElem).getFirstChildElement().getChild(1).cast();
+    return getSelectionElement(nodeElem).getFirstChildElement().getChild(
+        1).cast();
   }
 
   /**
@@ -535,6 +645,11 @@
   private Element emptyMessageElem;
 
   /**
+   * Set to true when the node is destroyed.
+   */
+  private boolean isDestroyed;
+
+  /**
    * True if the keyboard selection has focus.
    */
   private boolean keyboardFocused;
@@ -590,6 +705,13 @@
   private final CellTree tree;
 
   /**
+   * The publicly visible tree node. The {@link CellTreeNodeView} doesn't
+   * implement {@link TreeNode} directly because we want to transfer the user's
+   * handle to the {@link TreeNode} to the new {@link CellTreeNodeView}.
+   */
+  private TreeNodeImpl treeNode;
+
+  /**
    * This node's value.
    */
   private T value;
@@ -622,7 +744,7 @@
   }
 
   public boolean isLeaf() {
-    return tree.getTreeViewModel().isLeaf(value);
+    return tree.isLeaf(value);
   }
 
   /**
@@ -638,19 +760,31 @@
    * Sets whether this item's children are displayed.
    *
    * @param open whether the item is open
+   * @param fireEvents true to fire events if the state changes
+   * @return true if successfully opened, false otherwise.
    */
-  public void setOpen(boolean open) {
+  public boolean setOpen(boolean open, boolean fireEvents) {
     // Early out.
     if (this.open == open) {
-      return;
+      return this.open;
     }
 
+    // If this node is a leaf node, do not call TreeViewModel.getNodeInfo().
+    if (open && isLeaf()) {
+      return false;
+    }
+
+    // The animation clears the innerHtml of the childContainer. If we reopen a
+    // node as its closing, it is possible that the new data will be set
+    // synchronously, so we have to cancel the animation before attaching the
+    // data display to the node info.
+    tree.cancelTreeNodeAnimation();
     this.animate = true;
     this.open = open;
     if (open) {
       if (!nodeInfoLoaded) {
         nodeInfoLoaded = true;
-        nodeInfo = tree.getTreeViewModel().getNodeInfo(value);
+        nodeInfo = tree.getNodeInfo(value);
 
         // Sink events for the new node.
         if (nodeInfo != null) {
@@ -678,22 +812,39 @@
         }
         ensureAnimationFrame().getStyle().setProperty("display", "");
         onOpen(nodeInfo);
+
+        // Fire an event.
+        if (fireEvents) {
+          OpenEvent.fire(tree, getTreeNode());
+        }
+      } else {
+        this.open = false;
       }
     } else {
       if (!isRootNode()) {
         setStyleName(getCellParent(), tree.getStyle().openItem(), false);
       }
-      cleanup();
+      cleanup(false);
       tree.maybeAnimateTreeNode(this);
       updateImage(false);
       keyboardExit();
+      keyboardSelectedIndex = -1;
+
+      // Fire an event.
+      if (fireEvents) {
+        CloseEvent.fire(tree, getTreeNode());
+      }
     }
+
+    return this.open;
   }
 
   /**
    * Unregister the list handler and destroy all child nodes.
+   *
+   * @param destroy true to destroy this node
    */
-  protected void cleanup() {
+  protected void cleanup(boolean destroy) {
     // Unregister the list handler.
     if (listView != null) {
       listView.cleanup();
@@ -701,13 +852,18 @@
       listView = null;
     }
 
-    // Recursively kill children.
+    // Recursively destroy children.
     if (children != null) {
       for (CellTreeNodeView<?> child : children) {
-        child.cleanup();
+        child.cleanup(true);
       }
       children = null;
     }
+
+    // Destroy this node.
+    if (destroy) {
+      isDestroyed = true;
+    }
   }
 
   protected boolean consumeAnimate() {
@@ -727,8 +883,8 @@
    * @param viewData view data associated with the node
    * @return a TreeNodeView of suitable type
    */
-  protected <C> CellTreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo,
-      Element childElem, C childValue, Object viewData) {
+  protected <C> CellTreeNodeView<C> createTreeNodeView(
+      NodeInfo<C> nodeInfo, Element childElem, C childValue, Object viewData) {
     return new CellTreeNodeView<C>(tree, this, nodeInfo, childElem, childValue);
   }
 
@@ -741,8 +897,7 @@
     if (parentNodeInfo != null) {
       Cell<T> parentCell = parentNodeInfo.getCell();
       String eventType = event.getType();
-      SelectionModel<? super T> selectionModel =
-        parentNodeInfo.getSelectionModel();
+          SelectionModel<? super T> selectionModel = parentNodeInfo.getSelectionModel();
 
       // Update selection.
       if (selectionModel != null && "click".equals(eventType)
@@ -756,8 +911,8 @@
       Object key = getValueKey();
       Set<String> consumedEvents = parentCell.getConsumedEvents();
       if (consumedEvents != null && consumedEvents.contains(eventType)) {
-        parentCell.onBrowserEvent(cellParent, value, key, event,
-            parentNodeInfo.getValueUpdater());
+        parentCell.onBrowserEvent(
+            cellParent, value, key, event, parentNodeInfo.getValueUpdater());
       }
     }
   }
@@ -794,8 +949,8 @@
    * @param <C> the child data type of the node
    */
   protected <C> void onOpen(final NodeInfo<C> nodeInfo) {
-    NodeCellList<C> view = new NodeCellList<C>(nodeInfo, this,
-        tree.getDefaultNodeSize());
+    NodeCellList<C> view = new NodeCellList<C>(
+        nodeInfo, this, tree.getDefaultNodeSize());
     listView = view;
     view.setSelectionModel(nodeInfo.getSelectionModel());
     nodeInfo.setDataDisplay(view);
@@ -872,6 +1027,18 @@
     return showMoreElem;
   }
 
+  /**
+   * Get a {@link TreeNode} with a public API for this node view.
+   *
+   * @return the {@link TreeNode}
+   */
+  TreeNode getTreeNode() {
+    if (treeNode == null) {
+      treeNode = new TreeNodeImpl(this);
+    }
+    return treeNode;
+  }
+
   int indexOf(CellTreeNodeView<?> child) {
     return children.indexOf(child);
   }
@@ -977,8 +1144,7 @@
    * @return true if the selection moved
    */
   boolean keyboardUp() {
-    Element prev =
-      keyboardSelection.getParentElement().getFirstChildElement();
+    Element prev = keyboardSelection.getParentElement().getFirstChildElement();
     Element next = prev.getNextSiblingElement();
     while (next != null && next != keyboardSelection) {
       prev = next;
@@ -997,8 +1163,8 @@
   void showFewer() {
     Range range = listView.getVisibleRange();
     int defaultPageSize = listView.getDefaultPageSize();
-    int maxSize = Math.max(defaultPageSize,
-        range.getLength() - defaultPageSize);
+    int maxSize = Math.max(
+        defaultPageSize, range.getLength() - defaultPageSize);
     listView.setVisibleRange(range.getStart(), maxSize);
   }
 
@@ -1036,8 +1202,8 @@
     boolean isTopLevel = parentNode.isRootNode();
     String html = tree.getClosedImageHtml(isTopLevel);
     if (open) {
-      html = isLoading ? tree.getLoadingImageHtml()
-          : tree.getOpenImageHtml(isTopLevel);
+      html = isLoading ? tree.getLoadingImageHtml() : tree.getOpenImageHtml(
+          isTopLevel);
     }
     if (nodeInfoLoaded && nodeInfo == null) {
       html = LEAF_IMAGE;
diff --git a/user/src/com/google/gwt/user/cellview/client/TreeNode.java b/user/src/com/google/gwt/user/cellview/client/TreeNode.java
new file mode 100644
index 0000000..c7eedab
--- /dev/null
+++ b/user/src/com/google/gwt/user/cellview/client/TreeNode.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2010 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.cellview.client;
+
+/**
+ * A representation of a node in a tree.
+ */
+public interface TreeNode {
+
+  /**
+   * Get the number of children of the node.
+   *
+   * @return the child count
+   */
+  int getChildCount();
+
+  /**
+   * Get the value associated with a child node.
+   *
+   * @return the value
+   */
+  Object getChildValue(int index);
+
+  /**
+   * Get the index of the current node relative to its parent.
+   *
+   * @return the index of the current node
+   */
+  int getIndex();
+
+  /**
+   * Get the parent node of this node.
+   *
+   * @return the parent node, or null if this node is the root node
+   */
+  TreeNode getParent();
+
+  /**
+   * Get the value associated with this node. This method can be called on
+   * destroyed nodes.
+   *
+   * @return the value
+   */
+  Object getValue();
+
+  /**
+   * Check whether or not a child node is a leaf node.
+   *
+   * @param index the index of the child
+   * @return true if a leaf node, false if not
+   */
+  boolean isChildLeaf(int index);
+
+  /**
+   * Check whether or not a child node is open.
+   *
+   * @param index the index of the child
+   * @return true if open, false if closed
+   */
+  boolean isChildOpen(int index);
+
+  /**
+   * Check whether or not the current node is destroyed. The node is destroyed
+   * when it is closed, even if it still appears in the tree as an unopened
+   * non-leaf node. Once a node is destroyed, calling most methods on the node
+   * results in an {@link IllegalStateException}.
+   *
+   * @return true if destroyed, false if active
+   */
+  boolean isDestroyed();
+
+  /**
+   * Open or close a child node and fire an event. If <code>open</code> is true
+   * and the {@link TreeNode} successfully opens, returns the child
+   * {@link TreeNode}. Delegates to {@link #setChildOpen(int,boolean, boolean)}.
+   *
+   * @param index the index of the child
+   * @param open true to open, false to close
+   * @return the {@link TreeNode} that was opened, or null if the node was
+   *         closed or could not be opened
+   */
+  TreeNode setChildOpen(int index, boolean open);
+
+  /**
+   * Open or close the node, optionally firing an event. If <code>open</code> is
+   * true and the {@link TreeNode} successfully opens, returns the child
+   * {@link TreeNode}.
+   *
+   * @param index the index of the child
+   * @param open true to open, false to flose
+   * @param fireEvents true to fire an event, false not to
+   * @return the {@link TreeNode} that was opened, or null if the node was
+   *         closed or could not be opened
+   */
+  TreeNode setChildOpen(int index, boolean open, boolean fireEvents);
+}
diff --git a/user/test/com/google/gwt/user/cellview/CellViewSuite.java b/user/test/com/google/gwt/user/cellview/CellViewSuite.java
index 836bf4d..2fe796b 100644
--- a/user/test/com/google/gwt/user/cellview/CellViewSuite.java
+++ b/user/test/com/google/gwt/user/cellview/CellViewSuite.java
@@ -17,6 +17,9 @@
 
 import com.google.gwt.junit.tools.GWTTestSuite;
 import com.google.gwt.user.cellview.client.AbstractPagerTest;
+import com.google.gwt.user.cellview.client.AnimatedCellTreeTest;
+import com.google.gwt.user.cellview.client.CellBrowserTest;
+import com.google.gwt.user.cellview.client.CellTreeTest;
 import com.google.gwt.user.cellview.client.ColumnTest;
 import com.google.gwt.user.cellview.client.HasDataPresenterTest;
 import com.google.gwt.user.cellview.client.PageSizePagerTest;
@@ -33,6 +36,9 @@
         "Test suite for all cellview classes");
 
     suite.addTestSuite(AbstractPagerTest.class);
+    suite.addTestSuite(AnimatedCellTreeTest.class);
+    suite.addTestSuite(CellBrowserTest.class);
+    suite.addTestSuite(CellTreeTest.class);
     suite.addTestSuite(ColumnTest.class);
     suite.addTestSuite(HasDataPresenterTest.class);
     suite.addTestSuite(PageSizePagerTest.class);
diff --git a/user/test/com/google/gwt/user/cellview/client/AbstractCellTreeTestBase.java b/user/test/com/google/gwt/user/cellview/client/AbstractCellTreeTestBase.java
new file mode 100644
index 0000000..1f57b59
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/client/AbstractCellTreeTestBase.java
@@ -0,0 +1,606 @@
+/*
+ * Copyright 2010 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.cellview.client;
+
+import com.google.gwt.cell.client.Cell;
+import com.google.gwt.cell.client.TextCell;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.logical.shared.OpenEvent;
+import com.google.gwt.event.logical.shared.OpenHandler;
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.view.client.AbstractDataProvider;
+import com.google.gwt.view.client.ListDataProvider;
+import com.google.gwt.view.client.TreeViewModel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base tests for subclasses of {@link AbstractCellTree}.
+ */
+public abstract class AbstractCellTreeTestBase extends GWTTestCase {
+
+  /**
+   * The root value.
+   */
+  private static final Object ROOT_VALUE = new Object();
+
+  /**
+   * A mock {@link TreeViewModel} used for testing. Each child ads a character
+   * to the parent string. The longest string in the tree is 4 characters.
+   */
+  protected class MockTreeViewModel implements TreeViewModel {
+
+    /**
+     * The cell used to render all nodes in the tree.
+     */
+    private final Cell<String> cell = new TextCell();
+
+    /**
+     * The root data provider.
+     */
+    private final ListDataProvider<String> rootDataProvider =
+        createDataProvider("");
+
+    public <T> NodeInfo<?> getNodeInfo(T value) {
+      if (value == ROOT_VALUE) {
+        return new DefaultNodeInfo<String>(rootDataProvider, cell);
+      } else if (value instanceof String) {
+        String prefix = (String) value;
+        if (prefix.length() > 3) {
+          throw new IllegalStateException(
+              "Prefix should never exceed four characters.");
+        }
+        return new DefaultNodeInfo<String>(createDataProvider(prefix), cell);
+      }
+      throw new IllegalArgumentException("Unrecognized value type");
+    }
+
+    public boolean isLeaf(Object value) {
+      if (value == ROOT_VALUE) {
+        return false;
+      } else if (value instanceof String) {
+        String s = (String) value;
+        if (s.length() > 4) {
+          throw new IllegalStateException(
+              "value should never exceed five characters.");
+        }
+        return ((String) value).length() == 4;
+      }
+      throw new IllegalArgumentException("Unrecognized value type");
+    }
+
+    public AbstractDataProvider<String> getRootDataProvider() {
+      return rootDataProvider;
+    }
+
+    /**
+     * Create a data provider that extends the prefix by one letter.
+     *
+     * @param prefix the prefix string
+     * @return a data provider
+     */
+    private ListDataProvider<String> createDataProvider(String prefix) {
+      ListDataProvider<String> provider = new ListDataProvider<String>();
+      List<String> list = provider.getList();
+      for (int i = 0; i < 10; i++) {
+        list.add(prefix + ((char) ('a' + i)));
+      }
+      provider.flush();
+      return provider;
+    }
+  }
+
+  /**
+   * A mock {@link CloseHandler} used for testing.
+   */
+  private class MockCloseHandler implements CloseHandler<TreeNode> {
+
+    private CloseEvent<TreeNode> lastEvent;
+
+    public CloseEvent<TreeNode> getLastEventAndClear() {
+      CloseEvent<TreeNode> toRet = lastEvent;
+      lastEvent = null;
+      return toRet;
+    }
+
+    public void onClose(CloseEvent<TreeNode> event) {
+      assertNull(lastEvent);
+      this.lastEvent = event;
+    }
+  }
+
+  /**
+   * A mock {@link OpenHandler} used for testing.
+   */
+  private class MockOpenHandler implements OpenHandler<TreeNode> {
+
+    private OpenEvent<TreeNode> lastEvent;
+
+    public OpenEvent<TreeNode> getLastEventAndClear() {
+      OpenEvent<TreeNode> toRet = lastEvent;
+      lastEvent = null;
+      return toRet;
+    }
+
+    public void onOpen(OpenEvent<TreeNode> event) {
+      assertNull(lastEvent);
+      this.lastEvent = event;
+    }
+  }
+
+  /**
+   * The model that backs the tree.
+   */
+  protected MockTreeViewModel model;
+
+  /**
+   * The current tree being tested.
+   */
+  protected AbstractCellTree tree;
+
+  /**
+   * If true, the tree only supports opening a single path.
+   */
+  private final boolean singlePathOnly;
+
+  /**
+   * Construct a new {@link AbstractCellTreeTestBase}.
+   *
+   * @param singlePathOnly true if the tree only supports a single open path
+   */
+  public AbstractCellTreeTestBase(boolean singlePathOnly) {
+    this.singlePathOnly = singlePathOnly;
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.user.cellview.CellView";
+  }
+
+  public void testGetRootNode() {
+    TreeNode root = tree.getRootTreeNode();
+    assertEquals(10, root.getChildCount());
+    assertEquals(0, root.getIndex());
+    assertNull(root.getParent());
+    assertEquals(ROOT_VALUE, root.getValue());
+    testTreeNode(root, null, 0, ROOT_VALUE, false);
+  }
+
+  public void testIsLeaf() {
+    assertFalse(tree.isLeaf(ROOT_VALUE));
+    assertFalse(tree.isLeaf("a"));
+    assertFalse(tree.isLeaf("ab"));
+    assertFalse(tree.isLeaf("ab"));
+    assertFalse(tree.isLeaf("abc"));
+    assertTrue(tree.isLeaf("abcd"));
+  }
+
+  /**
+   * Test that opening a sibling node works.
+   */
+  public void testOpenSiblingNode() {
+    MockOpenHandler openHandler = new MockOpenHandler();
+    MockCloseHandler closeHandler = new MockCloseHandler();
+    tree.addOpenHandler(openHandler);
+    tree.addCloseHandler(closeHandler);
+    TreeNode root = tree.getRootTreeNode();
+
+    // Open a node.
+    TreeNode b = root.setChildOpen(1, true);
+    assertEquals(b, openHandler.getLastEventAndClear().getTarget());
+
+    // Open a sibling node.
+    TreeNode d = root.setChildOpen(3, true);
+    if (singlePathOnly) {
+      assertFalse(root.isChildOpen(1));
+      assertEquals(b, closeHandler.getLastEventAndClear().getTarget());
+    } else {
+      assertTrue(root.isChildOpen(1));
+      assertNull(closeHandler.getLastEventAndClear());
+    }
+    assertEquals(d, openHandler.getLastEventAndClear().getTarget());
+    assertTrue(root.isChildOpen(3));
+  }
+
+  /**
+   * Test a {@link TreeNode} at the leaf. We access the leaf nodes with the
+   * {@link TreeNode} that is the parent of the leaf nodes.
+   */
+  public void testTreeNodeAtLeaf() {
+    MockOpenHandler openHandler = new MockOpenHandler();
+    MockCloseHandler closeHandler = new MockCloseHandler();
+    tree.addOpenHandler(openHandler);
+    tree.addCloseHandler(closeHandler);
+    TreeNode root = tree.getRootTreeNode();
+
+    // Walk to a parent of leaf nodes.
+    TreeNode b = root.setChildOpen(1, true);
+    assertEquals(b, openHandler.getLastEventAndClear().getTarget());
+    TreeNode bc = b.setChildOpen(2, true);
+    assertEquals(bc, openHandler.getLastEventAndClear().getTarget());
+    TreeNode bce = bc.setChildOpen(4, true);
+    assertEquals(bce, openHandler.getLastEventAndClear().getTarget());
+
+    // Try to open the leaf.
+    assertNull(bce.setChildOpen(0, true));
+    assertNull(openHandler.getLastEventAndClear());
+    assertNull(openHandler.getLastEventAndClear());
+
+    // Test the values associated with the node.
+    testTreeNode(bce, bc, 4, "bce", true);
+  }
+
+  /**
+   * Test a {@link TreeNode} in the middle of the tree.
+   */
+  public void testTreeNodeAtMiddle() {
+    MockOpenHandler openHandler = new MockOpenHandler();
+    MockCloseHandler closeHandler = new MockCloseHandler();
+    tree.addOpenHandler(openHandler);
+    tree.addCloseHandler(closeHandler);
+    TreeNode root = tree.getRootTreeNode();
+
+    // Walk to a parent of leaf nodes.
+    TreeNode b = root.setChildOpen(1, true);
+    assertEquals(b, openHandler.getLastEventAndClear().getTarget());
+    TreeNode bc = b.setChildOpen(2, true);
+    assertEquals(bc, openHandler.getLastEventAndClear().getTarget());
+
+    // Test the values associated with the node.
+    testTreeNode(bc, b, 2, "bc", false);
+  }
+
+  /**
+   * Test that closing a branch closes all open nodes recursively.
+   */
+  public void testTreeNodeCloseBranch() {
+    MockOpenHandler openHandler = new MockOpenHandler();
+    MockCloseHandler closeHandler = new MockCloseHandler();
+    tree.addOpenHandler(openHandler);
+    tree.addCloseHandler(closeHandler);
+    TreeNode root = tree.getRootTreeNode();
+
+    // Walk down a branch.
+    TreeNode b = root.setChildOpen(1, true);
+    assertEquals(b, openHandler.getLastEventAndClear().getTarget());
+    TreeNode bc = b.setChildOpen(2, true);
+    assertEquals(bc, openHandler.getLastEventAndClear().getTarget());
+    TreeNode bce = bc.setChildOpen(4, true);
+    assertEquals(bce, openHandler.getLastEventAndClear().getTarget());
+
+    // Close the node at the top of the branch.
+    assertNull(root.setChildOpen(1, false));
+    assertFalse(root.isChildOpen(1));
+    assertTrue(b.isDestroyed());
+    assertTrue(bc.isDestroyed());
+    assertTrue(bce.isDestroyed());
+    assertNull(openHandler.getLastEventAndClear());
+    assertEquals(b, closeHandler.getLastEventAndClear().getTarget());
+  }
+
+  public void testTreeNodeCloseChild() {
+    MockOpenHandler openHandler = new MockOpenHandler();
+    MockCloseHandler closeHandler = new MockCloseHandler() {
+      @Override
+      public void onClose(CloseEvent<TreeNode> event) {
+        super.onClose(event);
+
+        // The node should be destroyed when the close event is fired.
+        TreeNode node = event.getTarget();
+        assertTrue(node.isDestroyed());
+      }
+    };
+    tree.addOpenHandler(openHandler);
+    tree.addCloseHandler(closeHandler);
+    TreeNode root = tree.getRootTreeNode();
+
+    // Open a node.
+    TreeNode child = root.setChildOpen(2, true);
+    assertEquals(child, openHandler.getLastEventAndClear().getTarget());
+    assertNull(closeHandler.getLastEventAndClear());
+    assertTrue(root.isChildOpen(2));
+    assertFalse(child.isDestroyed());
+    assertEquals("c", child.getValue());
+    assertEquals(2, child.getIndex());
+    assertEquals(root, child.getParent());
+
+    // Close the child.
+    assertNull(root.setChildOpen(2, false));
+    assertNull(openHandler.getLastEventAndClear());
+    assertEquals(child, closeHandler.getLastEventAndClear().getTarget());
+    assertFalse(root.isChildOpen(2));
+    assertFalse(root.isDestroyed());
+    assertTrue(child.isDestroyed());
+  }
+
+  public void testTreeNodeCloseChildAlreadyClosed() {
+    MockOpenHandler openHandler = new MockOpenHandler();
+    MockCloseHandler closeHandler = new MockCloseHandler();
+    tree.addOpenHandler(openHandler);
+    tree.addCloseHandler(closeHandler);
+    TreeNode root = tree.getRootTreeNode();
+
+    // Open a node.
+    TreeNode child = root.setChildOpen(2, true);
+    assertEquals(child, openHandler.getLastEventAndClear().getTarget());
+    assertNull(closeHandler.getLastEventAndClear());
+    assertTrue(root.isChildOpen(2));
+    assertFalse(child.isDestroyed());
+    assertEquals("c", child.getValue());
+    assertEquals(2, child.getIndex());
+    assertEquals(root, child.getParent());
+
+    // Close the child.
+    assertNull(root.setChildOpen(2, false));
+    assertNull(openHandler.getLastEventAndClear());
+    assertEquals(child, closeHandler.getLastEventAndClear().getTarget());
+    assertFalse(root.isChildOpen(2));
+    assertFalse(root.isDestroyed());
+    assertTrue(child.isDestroyed());
+
+    // Close the child again.
+    assertNull(root.setChildOpen(2, false));
+    assertNull(openHandler.getLastEventAndClear());
+    assertNull(closeHandler.getLastEventAndClear());
+    assertFalse(root.isChildOpen(2));
+    assertFalse(root.isDestroyed());
+    assertTrue(child.isDestroyed());
+  }
+
+  /**
+   * Test that a tree node is destroyed if its associated data is lost when new
+   * data is provided to the node.
+   */
+  public void testTreeNodeDataLost() {
+    MockOpenHandler openHandler = new MockOpenHandler();
+    MockCloseHandler closeHandler = new MockCloseHandler();
+    tree.addOpenHandler(openHandler);
+    tree.addCloseHandler(closeHandler);
+    TreeNode root = tree.getRootTreeNode();
+
+    // Get a node.
+    TreeNode b = root.setChildOpen(1, true);
+    assertEquals(b, openHandler.getLastEventAndClear().getTarget());
+
+    // Replace the data without the old node.
+    List<String> list = new ArrayList<String>();
+    list.add("x");
+    list.add("y");
+    list.add("z");
+    model.rootDataProvider.setList(list);
+
+    // Verify the node is destroyed.
+    assertTrue(b.isDestroyed());
+  }
+
+  /**
+   * Test that a tree node continues to exist when new data is pushed to the
+   * node.
+   */
+  public void testTreeNodeDataReplaced() {
+    MockOpenHandler openHandler = new MockOpenHandler();
+    MockCloseHandler closeHandler = new MockCloseHandler();
+    tree.addOpenHandler(openHandler);
+    tree.addCloseHandler(closeHandler);
+    TreeNode root = tree.getRootTreeNode();
+
+    // Get a node.
+    TreeNode b = root.setChildOpen(1, true);
+    assertEquals(b, openHandler.getLastEventAndClear().getTarget());
+
+    // Replace the data and include the old node at a different location.
+    List<String> list = new ArrayList<String>();
+    list.add("x");
+    list.add("y");
+    list.add("b");
+    list.add("z");
+    model.rootDataProvider.setList(list);
+
+    // Verify the node still exists.
+    assertFalse(root.isChildOpen(1));
+    assertTrue(root.isChildOpen(2));
+    testTreeNode(b, root, 2, "b", false);
+  }
+
+  public void testTreeNodeIsDestroyed() {
+    TreeNode root = tree.getRootTreeNode();
+
+    // Open a node.
+    TreeNode c = root.setChildOpen(2, true);
+    assertFalse(c.isDestroyed());
+
+    // Close the node.
+    assertNull(root.setChildOpen(2, false));
+    assertFalse(root.isDestroyed());
+    assertTrue(c.isDestroyed());
+
+    // Verify we can still get the value.
+    assertEquals("c", c.getValue());
+
+    try {
+      c.getChildCount();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected;
+    }
+    try {
+      c.getChildValue(0);
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected;
+    }
+    try {
+      c.getIndex();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected;
+    }
+    try {
+      c.getParent();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected;
+    }
+    try {
+      c.isChildLeaf(0);
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected;
+    }
+    try {
+      c.setChildOpen(0, true);
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected;
+    }
+    try {
+      c.setChildOpen(0, true, true);
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected;
+    }
+  }
+
+  /**
+   * Try to open a child that is already open.
+   */
+  public void testTreeNodeOpenChildAlreadyOpen() {
+    MockOpenHandler openHandler = new MockOpenHandler();
+    MockCloseHandler closeHandler = new MockCloseHandler();
+    tree.addOpenHandler(openHandler);
+    tree.addCloseHandler(closeHandler);
+    TreeNode root = tree.getRootTreeNode();
+
+    // Open a node.
+    TreeNode child = root.setChildOpen(2, true);
+    assertEquals(child, openHandler.getLastEventAndClear().getTarget());
+    assertNull(closeHandler.getLastEventAndClear());
+    assertTrue(root.isChildOpen(2));
+    assertFalse(child.isDestroyed());
+    assertEquals("c", child.getValue());
+    assertEquals(2, child.getIndex());
+    assertEquals(root, child.getParent());
+
+    // Open the same node.
+    assertEquals(child, root.setChildOpen(2, true));
+    assertNull(openHandler.getLastEventAndClear());
+    assertNull(closeHandler.getLastEventAndClear());
+    assertTrue(root.isChildOpen(2));
+    assertFalse(child.isDestroyed());
+  }
+
+  /**
+   * Create an {@link AbstractCellTree} to test.
+   *
+   * @param <T> the data type of the root value
+   * @param model the {@link TreeViewModel} that backs the tree
+   * @param rootValue the root value
+   * @return a new {@link AbstractCellTree}
+   */
+  protected abstract <T> AbstractCellTree createAbstractCellTree(
+      TreeViewModel model, T rootValue);
+
+  @Override
+  protected void gwtSetUp() throws Exception {
+    model = new MockTreeViewModel();
+    tree = createAbstractCellTree(model, ROOT_VALUE);
+    RootPanel.get().add(tree);
+  }
+
+  @Override
+  protected void gwtTearDown() throws Exception {
+    RootPanel.get().remove(tree);
+  }
+
+  /**
+   * Test the state of a {@link TreeNode}.
+   *
+   * @param node the node to test
+   * @param parent the expected parent
+   * @param index the expected index within the parent
+   * @param value the expected value
+   * @param isChildLeaf true if the node only contains leaf nodes
+   */
+  private void testTreeNode(TreeNode node, TreeNode parent, int index,
+      Object value, boolean isChildLeaf) {
+    assertEquals(10, node.getChildCount());
+    assertEquals(index, node.getIndex());
+    assertEquals(parent, node.getParent());
+    assertEquals(value, node.getValue());
+
+    // Test child values.
+    String prefix = (value == ROOT_VALUE) ? "" : value.toString();
+    assertEquals(prefix + "a", node.getChildValue(0));
+    assertEquals(prefix + "j", node.getChildValue(9));
+    for (int i = 0; i < 10; i++) {
+      assertEquals(isChildLeaf, node.isChildLeaf(i));
+      assertFalse(node.isChildOpen(i));
+    }
+
+    // Test children out of range.
+    try {
+      node.getChildValue(-1);
+      fail("Expected IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException e) {
+      // Expected.
+    }
+    try {
+      node.getChildValue(10);
+      fail("Expected IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException e) {
+      // Expected.
+    }
+    try {
+      node.isChildLeaf(-1);
+      fail("Expected IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException e) {
+      // Expected.
+    }
+    try {
+      node.isChildLeaf(10);
+      fail("Expected IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException e) {
+      // Expected.
+    }
+    try {
+      node.isChildOpen(-1);
+      fail("Expected IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException e) {
+      // Expected.
+    }
+    try {
+      node.isChildOpen(10);
+      fail("Expected IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException e) {
+      // Expected.
+    }
+    try {
+      node.setChildOpen(-1, true);
+      fail("Expected IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException e) {
+      // Expected.
+    }
+    try {
+      node.setChildOpen(10, true);
+      fail("Expected IndexOutOfBoundsException");
+    } catch (IndexOutOfBoundsException e) {
+      // Expected.
+    }
+  }
+}
diff --git a/user/test/com/google/gwt/user/cellview/client/AnimatedCellTreeTest.java b/user/test/com/google/gwt/user/cellview/client/AnimatedCellTreeTest.java
new file mode 100644
index 0000000..b636acf
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/client/AnimatedCellTreeTest.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2010 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.cellview.client;
+
+import com.google.gwt.view.client.TreeViewModel;
+
+/**
+ * Tests for {@link CellTree} with animations enabled.
+ */
+public class AnimatedCellTreeTest extends CellTreeTest {
+
+  @Override
+  protected <T> CellTree createAbstractCellTree(
+      TreeViewModel model, T rootValue) {
+    CellTree tree = super.createAbstractCellTree(model, rootValue);
+    tree.setAnimationEnabled(true);
+    return tree;
+  }
+}
diff --git a/user/test/com/google/gwt/user/cellview/client/CellBrowserTest.java b/user/test/com/google/gwt/user/cellview/client/CellBrowserTest.java
new file mode 100644
index 0000000..afe60a7
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/client/CellBrowserTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2010 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.cellview.client;
+
+import com.google.gwt.view.client.TreeViewModel;
+
+/**
+ * Tests for {@link CellBrowser}.
+ */
+public class CellBrowserTest extends AbstractCellTreeTestBase {
+
+  public CellBrowserTest() {
+    super(true);
+  }
+
+  @Override
+  protected <T> CellBrowser createAbstractCellTree(
+      TreeViewModel model, T rootValue) {
+    CellBrowser browser = new CellBrowser(model, rootValue);
+    browser.setHeight("500px");
+    return browser;
+  }
+}
diff --git a/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
new file mode 100644
index 0000000..07f5886
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/client/CellTreeTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2010 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.cellview.client;
+
+import com.google.gwt.view.client.TreeViewModel;
+
+/**
+ * Tests for {@link CellTree}.
+ */
+public class CellTreeTest extends AbstractCellTreeTestBase {
+
+  public CellTreeTest() {
+    super(false);
+  }
+
+  public void testSetDefaultNodeSize() {
+    CellTree cellTree = (CellTree) tree;
+    TreeNode root = cellTree.getRootTreeNode();
+    assertEquals(10, root.getChildCount());
+
+    TreeNode b = root.setChildOpen(1, true);
+    assertEquals(10, b.getChildCount());
+
+    // Change the default size.
+    cellTree.setDefaultNodeSize(5);
+    assertEquals(5, cellTree.getDefaultNodeSize());
+    assertEquals(10, b.getChildCount());
+    TreeNode d = root.setChildOpen(3, true);
+    assertEquals(5, d.getChildCount());
+  }
+
+  @Override
+  protected <T> CellTree createAbstractCellTree(
+      TreeViewModel model, T rootValue) {
+    return new CellTree(model, rootValue);
+  }
+}