Replaced HasKey with ProvidesKey and moved it from Column/NodeInfo to ListModel. Updating SelectionModel to use handlers and have only one setSelected method, firing events in a finally command to avoid multiple view updates.
http://gwt-code-reviews.appspot.com/307801/show

Review by: jgw@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7866 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/Column.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/Column.java
index 976d8a1..f296f4f 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/Column.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/Column.java
@@ -18,6 +18,7 @@
 import com.google.gwt.bikeshed.cells.client.Cell;
 import com.google.gwt.bikeshed.cells.client.FieldUpdater;
 import com.google.gwt.bikeshed.cells.client.ValueUpdater;
+import com.google.gwt.bikeshed.list.shared.ProvidesKey;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
 
@@ -37,28 +38,16 @@
 // TODO - when can we get rid of a view data object?
 // TODO - should viewData implement some interface? (e.g., with commit/rollback/dispose)
 // TODO - have a ViewDataColumn superclass / SimpleColumn subclass
-public abstract class Column<T, C, V> implements HasCell<T, C, V> {
+public abstract class Column<T, C, V> {
 
   protected final Cell<C, V> cell;
 
   protected FieldUpdater<T, C, V> fieldUpdater;
 
-  /**
-   * An instance of HasKey<T> that is used to provide a key for a row object
-   * for the purpose of retrieving view data.  A value of null means that
-   * the row object itself will be used as the key.
-   */
-  protected HasKey<T> hasKey;
-
   protected Map<Object, V> viewDataMap = new HashMap<Object, V>();
 
-  public Column(Cell<C, V> cell, HasKey<T> hasKey) {
-    this.cell = cell;
-    this.hasKey = hasKey;
-  }
-
   public Column(Cell<C, V> cell) {
-    this(cell, null);
+    this.cell = cell;
   }
 
   public boolean consumesEvents() {
@@ -76,8 +65,8 @@
   public abstract C getValue(T object);
 
   public void onBrowserEvent(Element elem, final int index, final T object,
-      NativeEvent event) {
-    Object key = hasKey == null ? object : hasKey.getKey(object);
+      NativeEvent event, ProvidesKey<T> providesKey) {
+    Object key = providesKey.getKey(object);
     V viewData = viewDataMap.get(key);
     V newViewData = cell.onBrowserEvent(elem,
         getValue(object), viewData, event, fieldUpdater == null ? null
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/HasKey.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/HasKey.java
deleted file mode 100644
index d910a52..0000000
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/HasKey.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * 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.bikeshed.list.client;
-
-/**
- * An interface for extracting a key from a value. The extracted key must
- * contain suitable implementations of hashCode() and equals().
- *
- * @param <C> the value type for which keys are to be returned
- */
-public interface HasKey<C> {
-
-  /**
-   * Return a key that may be used to identify values that should be treated as
-   * the same in UI views.
-   *
-   * @param value a value of type C.
-   * @return an Object that implements appropriate hashCode() and equals()
-   *         methods.
-   */
-  Object getKey(C value);
-}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
index 599c9ec..339845b 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/PagingTableListView.java
@@ -21,7 +21,8 @@
 import com.google.gwt.bikeshed.list.shared.ListRegistration;
 import com.google.gwt.bikeshed.list.shared.SelectionModel;
 import com.google.gwt.bikeshed.list.shared.SizeChangeEvent;
-import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionListener;
+import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeEvent;
+import com.google.gwt.bikeshed.list.shared.SelectionModel.SelectionChangeHandler;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.EventTarget;
@@ -33,6 +34,7 @@
 import com.google.gwt.dom.client.TableRowElement;
 import com.google.gwt.dom.client.TableSectionElement;
 import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Widget;
 
@@ -46,15 +48,23 @@
  */
 public class PagingTableListView<T> extends Widget {
 
+  private class TableSelectionHandler implements SelectionChangeHandler {
+    public void onSelectionChange(SelectionChangeEvent event) {
+      refresh();
+    }
+  }
+
   protected int curPage;
   private List<Column<T, ?, ?>> columns = new ArrayList<Column<T, ?, ?>>();
   private ArrayList<T> data = new ArrayList<T>();
   private List<Header<?>> footers = new ArrayList<Header<?>>();
   private List<Header<?>> headers = new ArrayList<Header<?>>();
   private ListRegistration listReg;
+  private ListModel<T> listModel;
   private int numPages;
 
   private int pageSize;
+  private HandlerRegistration selectionHandler;
   private SelectionModel<T> selectionModel;
 
   private TableElement table;
@@ -64,13 +74,8 @@
   private TableSectionElement thead;
   private int totalSize;
 
-  private SelectionListener listener = new SelectionListener() {
-    public void selectionChanged() {
-      refresh();
-    }
-  };
-  
   public PagingTableListView(ListModel<T> listModel, final int pageSize) {
+    this.listModel = listModel;
     this.pageSize = pageSize;
     setElement(table = Document.get().createTableElement());
     thead = table.createTHead();
@@ -82,24 +87,6 @@
     // those events actually needed by cells.
     sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.KEYEVENTS
         | Event.ONCHANGE);
-
-    // Attach to the list model.
-    listReg = listModel.addListHandler(new ListHandler<T>() {
-      public void onDataChanged(ListEvent<T> event) {
-        render(event.getStart(), event.getLength(), event.getValues());
-      }
-
-      public void onSizeChanged(SizeChangeEvent event) {
-        totalSize = event.getSize();
-        if (totalSize <= 0) {
-          numPages = 0;
-        } else {
-          numPages = 1 + (totalSize - 1) / pageSize;
-        }
-        setPage(curPage);
-      }
-    });
-    listReg.setRangeOfInterest(0, pageSize);
   }
 
   public void addColumn(Column<T, ?, ?> col) {
@@ -118,7 +105,7 @@
   public void addColumn(Column<T, ?, ?> col, Header<?> header, Header<?> footer) {
     headers.add(header);
     footers.add(footer);
-    createHeadersAndFooters();  // TODO: defer header recreation
+    createHeadersAndFooters(); // TODO: defer header recreation
     columns.add(col);
     createRows();
     setPage(curPage); // TODO: better way to refresh?
@@ -131,11 +118,11 @@
     }
     return data.get(indexOnPage);
   }
-  
+
   public List<T> getDisplayedItems() {
     return new ArrayList<T>(data);
   }
-  
+
   public int getNumDisplayedItems() {
     return Math.min(getPageSize(), totalSize - curPage * pageSize);
   }
@@ -148,11 +135,11 @@
   public int getPage() {
     return curPage;
   }
-  
+
   public int getPageSize() {
     return pageSize;
   }
-  
+
   public void nextPage() {
     setPage(curPage + 1);
   }
@@ -184,14 +171,15 @@
       T value = data.get(row);
       Column<T, ?, ?> column = columns.get(col);
 
-      column.onBrowserEvent(cell, curPage * pageSize + row, value, event);
+      column.onBrowserEvent(cell, curPage * pageSize + row, value, event,
+          listModel);
     }
   }
 
   public void previousPage() {
     setPage(curPage - 1);
   }
-  
+
   public void refresh() {
     listReg.setRangeOfInterest(curPage * pageSize, pageSize);
     updateRowVisibility();
@@ -233,11 +221,55 @@
   }
 
   public void setSelectionModel(SelectionModel<T> selectionModel) {
-    if (this.selectionModel != null) {
-      this.selectionModel.removeListener(listener);
+    if (selectionHandler != null) {
+      selectionHandler.removeHandler();
+      selectionHandler = null;
     }
     this.selectionModel = selectionModel;
-    selectionModel.addListener(listener);
+    if (selectionModel != null && isAttached()) {
+      selectionHandler = selectionModel.addSelectionChangeHandler(new TableSelectionHandler());
+    }
+  }
+
+  @Override
+  protected void onLoad() {
+    // Attach a selection handler.
+    if (selectionModel != null) {
+      selectionHandler = selectionModel.addSelectionChangeHandler(new TableSelectionHandler());
+    }
+
+    // Attach to the list model.
+    listReg = listModel.addListHandler(new ListHandler<T>() {
+      public void onDataChanged(ListEvent<T> event) {
+        render(event.getStart(), event.getLength(), event.getValues());
+      }
+
+      public void onSizeChanged(SizeChangeEvent event) {
+        totalSize = event.getSize();
+        if (totalSize <= 0) {
+          numPages = 0;
+        } else {
+          numPages = 1 + (totalSize - 1) / pageSize;
+        }
+        setPage(curPage);
+      }
+    });
+    listReg.setRangeOfInterest(curPage * pageSize, pageSize);
+  }
+
+  @Override
+  protected void onUnload() {
+    // Detach the selection handler.
+    if (selectionHandler != null) {
+      selectionHandler.removeHandler();
+      selectionHandler = null;
+    }
+
+    // Detach from the list model.
+    if (listReg != null) {
+      listReg.removeHandler();
+      listReg = null;
+    }
   }
 
   protected void render(int start, int length, List<T> values) {
@@ -252,8 +284,8 @@
       if (selectionModel != null && selectionModel.isSelected(q)) {
         row.setClassName("pagingTableListView selected");
       } else {
-        row.setClassName("pagingTableListView " +
-            ((indexOnPage & 0x1) == 0 ? "evenRow" : "oddRow"));
+        row.setClassName("pagingTableListView "
+            + ((indexOnPage & 0x1) == 0 ? "evenRow" : "oddRow"));
       }
 
       data.set(indexOnPage, q);
@@ -272,7 +304,8 @@
     }
   }
 
-  private void createHeaders(List<Header<?>> headers, TableSectionElement section) {
+  private void createHeaders(List<Header<?>> headers,
+      TableSectionElement section) {
     StringBuilder sb = new StringBuilder();
     sb.append("<tr>");
     for (Header<?> header : headers) {
@@ -303,7 +336,8 @@
 
     for (int r = 0; r < pageSize; ++r) {
       TableRowElement row = tbody.insertRow(0);
-      row.setClassName("pagingTableListView " + ((r & 0x1) == 0 ? "evenRow" : "oddRow"));
+      row.setClassName("pagingTableListView "
+          + ((r & 0x1) == 0 ? "evenRow" : "oddRow"));
 
       // TODO: use cloneNode() to make this even faster.
       for (int c = 0; c < numCols; ++c) {
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleColumn.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleColumn.java
index 9ba3632..b750820 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleColumn.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/SimpleColumn.java
@@ -19,15 +19,11 @@
 
 /**
  * A column that does not make use of view data.
- *
+ * 
  * @param <T> the row type
  * @param <C> the column type
  */
-public abstract class SimpleColumn<T, C>  extends Column<T, C, Void> {
-
-  public SimpleColumn(Cell<C, Void> cell, HasKey<T> hasKey) {
-    super(cell, hasKey);
-  }
+public abstract class SimpleColumn<T, C> extends Column<T, C, Void> {
 
   public SimpleColumn(Cell<C, Void> cell) {
     super(cell);
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/client/TextColumn.java b/bikeshed/src/com/google/gwt/bikeshed/list/client/TextColumn.java
index f053998..d3dd015 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/client/TextColumn.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/client/TextColumn.java
@@ -20,15 +20,11 @@
 /**
  * A column that displays its contents with a {@link TextCell} and does not make
  * use of view data.
-
+ * 
  * @param <T> the row type
  */
 public abstract class TextColumn<T> extends Column<T, String, Void> {
 
-  public TextColumn(HasKey<T> hasKey) {
-    super(TextCell.getInstance(), hasKey);
-  }
-
   public TextColumn() {
     super(TextCell.getInstance());
   }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/shared/AbstractListModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/AbstractListModel.java
index b66db76..66e4a49 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/shared/AbstractListModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/AbstractListModel.java
@@ -99,6 +99,11 @@
   }
 
   /**
+   * The provider of keys for list items.
+   */
+  private ProvidesKey<T> keyProvider;
+
+  /**
    * The handlers that are listening to this model.
    */
   private List<DefaultListRegistration> registrations = new ArrayList<DefaultListRegistration>();
@@ -110,6 +115,26 @@
   }
 
   /**
+   * Get the key for a list item. The default implementation returns the item
+   * itself.
+   * 
+   * @param item the list item
+   * @return the key that represents the item
+   */
+  public Object getKey(T item) {
+    return keyProvider == null ? item : keyProvider.getKey(item);
+  }
+
+  /**
+   * Get the {@link ProvidesKey} that provides keys for list items.
+   * 
+   * @return the {@link ProvidesKey}
+   */
+  public ProvidesKey<T> getKeyProvider() {
+    return keyProvider;
+  }
+
+  /**
    * Get the current ranges of all views.
    * 
    * @return the ranges
@@ -124,6 +149,15 @@
   }
 
   /**
+   * Set the {@link ProvidesKey} that provides keys for list items.
+   * 
+   * @param keyProvider the {@link ProvidesKey}
+   */
+  public void setKeyProvider(ProvidesKey<T> keyProvider) {
+    this.keyProvider = keyProvider;
+  }
+
+  /**
    * Called when a view changes its range of interest.
    */
   protected abstract void onRangeChanged(int start, int length);
@@ -159,8 +193,8 @@
         int realStart = curStart < start ? start : curStart;
         int realEnd = curEnd > end ? end : curEnd;
         int realLength = realEnd - realStart;
-        List<T> realValues = values.subList(realStart - start,
-            realStart - start + realLength);
+        List<T> realValues = values.subList(realStart - start, realStart
+            - start + realLength);
         ListEvent<T> event = new ListEvent<T>(realStart, realLength, realValues);
         reg.getHandler().onDataChanged(event);
       }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListModel.java
index 6bf9ee5..d16f659 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/ListModel.java
@@ -20,7 +20,7 @@
  * 
  * @param <T> the data type of records in the list
  */
-public interface ListModel<T> {
+public interface ListModel<T> extends ProvidesKey<T> {
 
   /**
    * Add a {@link ListHandler} to the model.
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/shared/ProvidesKey.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/ProvidesKey.java
new file mode 100644
index 0000000..19cda3c
--- /dev/null
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/ProvidesKey.java
@@ -0,0 +1,39 @@
+/*
+ * 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.bikeshed.list.shared;
+
+/**
+ * <p>
+ * Implementors of {@link ProvidesKey} provide a key for list items.
+ * </p>
+ * <p>
+ * The key must implement a coherent set of {@link #equals(Object)} and
+ * {@link #hashCode()} methods. If the item type is a not uniquely identifiable,
+ * such as a list of {@link String}, the index can be used a the key.
+ * </p>
+ * 
+ * @param <T> the data type of records in the list
+ */
+public interface ProvidesKey<T> {
+
+  /**
+   * Get the key for a list item.
+   * 
+   * @param item the list item
+   * @return the key that represents the item
+   */
+  Object getKey(T item);
+}
diff --git a/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java
index 491ac00..e68e925 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/list/shared/SelectionModel.java
@@ -15,60 +15,153 @@
  */
 package com.google.gwt.bikeshed.list.shared;
 
-import java.util.ArrayList;
-import java.util.List;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.shared.EventHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerManager;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.event.shared.HasHandlers;
 
 /**
  * A model for selection within a list.
  * 
  * @param <T> the data type of records in the list
  */
-public interface SelectionModel<T> {
-  
+public interface SelectionModel<T> extends HasHandlers {
+
   /**
-   * A listener who will be updated when the selection changes.
+   * Handler interface for {@link SelectionChangeEvent} events.
    */
-  interface SelectionListener {
-    void selectionChanged();
+  public interface SelectionChangeHandler extends EventHandler {
+
+    /**
+     * Called when {@link SelectionChangeEvent} is fired.
+     * 
+     * @param event the {@link SelectionChangeEvent} that was fired
+     */
+    void onSelectionChange(SelectionChangeEvent event);
   }
-  
+
   /**
-   * A default implementation of SelectionModel that provides listener
-   * addition and removal.
-   *
+   * Represents a selection change event.
+   */
+  public static class SelectionChangeEvent extends
+      GwtEvent<SelectionChangeHandler> {
+
+    /**
+     * Handler type.
+     */
+    private static Type<SelectionChangeHandler> TYPE;
+
+    /**
+     * Fires a selection change event on all registered handlers in the handler
+     * manager. If no such handlers exist, this method will do nothing.
+     * 
+     * @param source the source of the handlers
+     */
+    static void fire(SelectionModel<?> source) {
+      if (TYPE != null) {
+        SelectionChangeEvent event = new SelectionChangeEvent();
+        source.fireEvent(event);
+      }
+    }
+
+    /**
+     * Gets the type associated with this event.
+     * 
+     * @return returns the handler type
+     */
+    public static Type<SelectionChangeHandler> getType() {
+      if (TYPE == null) {
+        TYPE = new Type<SelectionChangeHandler>();
+      }
+      return TYPE;
+    }
+
+    /**
+     * Creates a selection change event.
+     */
+    SelectionChangeEvent() {
+    }
+
+    @Override
+    public final Type<SelectionChangeHandler> getAssociatedType() {
+      return TYPE;
+    }
+
+    @Override
+    protected void dispatch(SelectionChangeHandler handler) {
+      handler.onSelectionChange(this);
+    }
+  }
+
+  /**
+   * A default implementation of SelectionModel that provides listener addition
+   * and removal.
+   * 
    * @param <T> the data type of records in the list
    */
-  abstract class DefaultSelectionModel<T> implements SelectionModel<T> {
+  abstract class AbstractSelectionModel<T> implements SelectionModel<T> {
 
-    protected List<SelectionListener> listeners = new ArrayList<SelectionListener>();
+    private final HandlerManager handlerManager = new HandlerManager(this);
 
-    public void addListener(SelectionListener listener) {
-      if (!listeners.contains(listener)) {
-        listeners.add(listener);
-      }
+    /**
+     * Set to true if an event is scheduled to be fired.
+     */
+    private boolean isEventScheduled;
+
+    public HandlerRegistration addSelectionChangeHandler(
+        SelectionChangeHandler handler) {
+      return handlerManager.addHandler(SelectionChangeEvent.getType(), handler);
     }
-    
-    public void removeListener(SelectionListener listener) {
-      if (listeners.contains(listener)) {
-        listeners.remove(listener);
-      }
+
+    public void fireEvent(GwtEvent<?> event) {
+      handlerManager.fireEvent(event);
     }
-    
-    public void updateListeners() {
-      // Inform the listeners
-      for (SelectionListener listener : listeners) {
-        listener.selectionChanged();
+
+    public void setSelected(T object, boolean selected) {
+      scheduleSelectionChangeEvent();
+    }
+
+    /**
+     * Schedules a {@link SelectionModel.SelectionChangeEvent} to fire at the
+     * end of the current event loop.
+     */
+    protected void scheduleSelectionChangeEvent() {
+      if (!isEventScheduled) {
+        isEventScheduled = true;
+        Scheduler.get().scheduleFinally(new ScheduledCommand() {
+          public void execute() {
+            isEventScheduled = false;
+            SelectionChangeEvent.fire(AbstractSelectionModel.this);
+          }
+        });
       }
     }
   }
-  
-  void addListener(SelectionListener listener);
 
+  /**
+   * Adds a {@link SelectionChangeEvent} handler.
+   * 
+   * @param handler the handler
+   * @return the registration for the event
+   */
+  HandlerRegistration addSelectionChangeHandler(SelectionChangeHandler handler);
+
+  /**
+   * Check if an object is selected.
+   * 
+   * @param object the object
+   * @return true if selected, false if not
+   */
   boolean isSelected(T object);
-  
-  void removeListener(SelectionListener listener);
 
+  /**
+   * Set the selected state of an object.
+   * 
+   * @param object the object to select or deselect
+   * @param selected true to select, false to deselect
+   */
   void setSelected(T object, boolean selected);
-  
-  void setSelected(List<T> objects, boolean selected);
 }
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java
index 9f4aae9..3b3ceb3 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeNodeView.java
@@ -336,7 +336,7 @@
    * implementation of NodeInfo.getKey().
    */
   protected Object getValueKey() {
-    return getParentNodeInfo().getKey(getValue());
+    return getParentNodeInfo().getListModel().getKey(getValue());
   }
 
   /**
@@ -353,7 +353,7 @@
     ensureAnimationFrame().getStyle().setProperty("display", "");
 
     // Get the node info.
-    ListModel<C> listModel = nodeInfo.getListModel();
+    final ListModel<C> listModel = nodeInfo.getListModel();
     listReg = listModel.addListHandler(new ListHandler<C>() {
       public void onDataChanged(ListEvent<C> event) {
         // TODO - handle event start and length
@@ -364,8 +364,7 @@
           for (TreeNodeView<?> child : children) {
             // Ignore child nodes that are closed
             if (child.getState()) {
-              Object key = child.getValueKey();
-              map.put(key, child);
+              map.put(child.getValueKey(), child);
             }
           }
         }
@@ -379,7 +378,7 @@
         for (C childValue : event.getValues()) {
           // Remove any child elements that correspond to prior children
           // so the call to setInnerHtml will not destroy them
-          TreeNodeView<?> savedView = map.get(nodeInfo.getKey(childValue));
+          TreeNodeView<?> savedView = map.get(listModel.getKey(childValue));
           if (savedView != null) {
             savedView.getContainer().getFirstChild().removeFromParent();
           }
@@ -398,7 +397,7 @@
         for (C childValue : event.getValues()) {
           TreeNodeView<C> child = createTreeNodeView(nodeInfo, childElem,
               childValue, null, idx);
-          TreeNodeView<?> savedChild = map.get(nodeInfo.getKey(childValue));
+          TreeNodeView<?> savedChild = map.get(listModel.getKey(childValue));
           // Copy the saved child's state into the new child
           if (savedChild != null) {
             child.children = savedChild.children;
diff --git a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java
index ce54371..3699a1b 100644
--- a/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/bikeshed/tree/client/TreeViewModel.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -19,7 +19,6 @@
 import com.google.gwt.bikeshed.cells.client.FieldUpdater;
 import com.google.gwt.bikeshed.cells.client.ValueUpdater;
 import com.google.gwt.bikeshed.list.client.HasCell;
-import com.google.gwt.bikeshed.list.client.HasKey;
 import com.google.gwt.bikeshed.list.shared.ListModel;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
@@ -35,20 +34,20 @@
   /**
    * The info needed to create a {@link TreeNodeView}.
    */
-  interface NodeInfo<T> extends HasKey<T> {
+  interface NodeInfo<T> {
 
     List<HasCell<T, ?, Void>> getHasCells();
 
     /**
      * Get the {@link ListModel} used to retrieve child node values.
-     *
+     * 
      * @return the list model
      */
     ListModel<T> getListModel();
 
     /**
      * Handle an event that is fired on one of the children of this item.
-     *
+     * 
      * @param elem the parent element of the item
      * @param object the data value of the item
      * @param event the event that was fired
@@ -66,7 +65,7 @@
     /**
      * Construct a new {@link DefaultNodeInfo} with a single cell and a
      * {@link ValueUpdater}.
-     *
+     * 
      * @param listModel the {@link ListModel} that provides the child values
      * @param cell the {@link Cell} used to render the child values
      * @param valueUpdater the {@link ValueUpdater}
@@ -95,7 +94,7 @@
 
     /**
      * Construct a new {@link DefaultNodeInfo}.
-     *
+     * 
      * @param listModel the {@link ListModel} that provides the child values
      * @param cell the {@link Cell} used to render the child values
      */
@@ -103,7 +102,8 @@
       this(listModel, cell, null);
     }
 
-    public DefaultNodeInfo(ListModel<T> listModel, List<HasCell<T, ?, Void>> hasCells) {
+    public DefaultNodeInfo(ListModel<T> listModel,
+        List<HasCell<T, ?, Void>> hasCells) {
       this.hasCells.addAll(hasCells);
       this.listModel = listModel;
     }
@@ -112,10 +112,6 @@
       return hasCells;
     }
 
-    public Object getKey(T value) {
-      return value;
-    }
-
     public ListModel<T> getListModel() {
       return listModel;
     }
@@ -134,23 +130,22 @@
       }
     }
 
-    private <X> void dispatch(Element target, final T object, NativeEvent event,
-        HasCell<T, X, Void> hc) {
+    private <X> void dispatch(Element target, final T object,
+        NativeEvent event, HasCell<T, X, Void> hc) {
       final FieldUpdater<T, X, Void> fieldUpdater = hc.getFieldUpdater();
       hc.getCell().onBrowserEvent(target, hc.getValue(object), null, event,
-          fieldUpdater == null ? null
-              : new ValueUpdater<X, Void>() {
-                public void update(X value, Void viewData) {
-                  fieldUpdater.update(0, object, value, viewData);
-                }
-              });
+          fieldUpdater == null ? null : new ValueUpdater<X, Void>() {
+            public void update(X value, Void viewData) {
+              fieldUpdater.update(0, object, value, viewData);
+            }
+          });
     }
   }
 
   /**
    * Get the {@link NodeInfo} that will provide the {@link ListModel} and
    * {@link Cell} to retrieve the children of the specified value.
-   *
+   * 
    * @param value the value in the parent node
    * @param treeNode the {@link TreeNode} that contains the value
    * @return the {@link NodeInfo}
@@ -159,10 +154,10 @@
 
   /**
    * Check if the value is known to be a leaf node.
-   *
+   * 
    * @param value the value at the node
    * @param treeNode the {@link TreeNode} that contains the value
-   *
+   * 
    * @return true if the node is known to be a leaf node, false otherwise
    */
   boolean isLeaf(Object value, TreeNode<?> treeNode);
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/mail/client/MailSample.java b/bikeshed/src/com/google/gwt/sample/bikeshed/mail/client/MailSample.java
index 8e90367..d35798b 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/mail/client/MailSample.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/mail/client/MailSample.java
@@ -21,7 +21,7 @@
 import com.google.gwt.bikeshed.list.client.SimpleColumn;
 import com.google.gwt.bikeshed.list.client.TextColumn;
 import com.google.gwt.bikeshed.list.shared.ListListModel;
-import com.google.gwt.bikeshed.list.shared.SelectionModel.DefaultSelectionModel;
+import com.google.gwt.bikeshed.list.shared.SelectionModel.AbstractSelectionModel;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -44,7 +44,7 @@
  */
 public class MailSample implements EntryPoint, ClickHandler {
 
-  class MailSelectionModel extends DefaultSelectionModel<Message> {
+  class MailSelectionModel extends AbstractSelectionModel<Message> {
     private static final int ALL = 0;
     private static final int NONE = 1;
     private static final int READ = 2;
@@ -73,14 +73,8 @@
       this.search = canonicalize(search);
       updateListeners();
     }
-    
-    public void setSelected(List<Message> objects, boolean selected) {
-      for (Message object : objects) {
-        addException(object.id, selected);
-      }
-      updateListeners();
-    }
 
+    @Override
     public void setSelected(Message object, boolean selected) {
       addException(object.id, selected);
       updateListeners();
@@ -91,7 +85,8 @@
       exceptions.clear();
       updateListeners();
     }
-    
+
+    @Override
     public String toString() {
       StringBuilder sb = new StringBuilder();
       switch (type) {
@@ -124,6 +119,7 @@
         if (exceptions.get(i) != Boolean.TRUE) {
           continue;
         }
+
         if (first) {
           first = false;
           sb.append("+msg(s) ");
@@ -137,6 +133,7 @@
         if (exceptions.get(i) != Boolean.FALSE) {
           continue;
         }
+
         if (first) {
           first = false;
           sb.append("-msg(s) ");
@@ -148,18 +145,14 @@
       return sb.toString();
     }
 
-    public void updateListeners() {
-      super.updateListeners();
-      selectionLabel.setText("Selected " + this.toString());
-    }
-
     private void addException(int id, boolean selected) {
       Boolean currentlySelected = exceptions.get(id);
-      if (currentlySelected != null && currentlySelected.booleanValue() != selected) {
+      if (currentlySelected != null
+          && currentlySelected.booleanValue() != selected) {
         exceptions.remove(id);
       } else {
         exceptions.put(id, selected);
-      }      
+      }
     }
 
     private String canonicalize(String input) {
@@ -190,6 +183,11 @@
           throw new IllegalStateException("type = " + type);
       }
     }
+
+    private void updateListeners() {
+      selectionLabel.setText("Selected " + this.toString());
+      scheduleSelectionChangeEvent();
+    }
   }
 
   class Message {
@@ -249,14 +247,12 @@
       "Kaan Boulier", "Emilee Naoma", "Atino Alice", "Debby Renay",
       "Versie Nereida", "Ramon Erikson", "Karole Crissy", "Nelda Olsen",
       "Mariana Dann", "Reda Cheyenne", "Edelmira Jody", "Agueda Shante",
-      "Marla Dorris"
-  };
+      "Marla Dorris"};
 
   private static final String[] subjects = {
       "GWT rocks", "What's a widget?", "Money in Nigeria",
       "Impress your colleagues with bling-bling", "Degree available",
-      "Rolex Watches", "Re: Re: yo bud", "Important notice"
-  };
+      "Rolex Watches", "Re: Re: yo bud", "Important notice"};
 
   private Button allButton = new Button("Select All");
   private Button allOnPageButton = new Button("Select All On This Page");
@@ -278,7 +274,11 @@
     if (source == noneButton) {
       selectionModel.setType(MailSelectionModel.NONE);
     } else if (source == allOnPageButton) {
-      selectionModel.setSelected(table.getDisplayedItems(), true);
+      selectionModel.setType(MailSelectionModel.NONE);
+      List<Message> selectedItems = table.getDisplayedItems();
+      for (Message item : selectedItems) {
+        selectionModel.setSelected(item, true);
+      }
     } else if (source == allButton) {
       selectionModel.setType(MailSelectionModel.ALL);
     } else if (source == readButton) {
@@ -346,7 +346,7 @@
       }
     };
     table.addColumn(subjectColumn, "Subject");
-    
+
     Label searchLabel = new Label("Search Sender or Subject:");
     final TextBox searchBox = new TextBox();
     searchBox.addKeyUpHandler(new KeyUpHandler() {
@@ -362,11 +362,11 @@
     unreadButton.addClickHandler(this);
     senderButton.addClickHandler(this);
     subjectButton.addClickHandler(this);
-    
+
     HorizontalPanel panel = new HorizontalPanel();
     panel.add(searchLabel);
     panel.add(searchBox);
-    
+
     RootPanel.get().add(panel);
     RootPanel.get().add(new HTML("<br>"));
     RootPanel.get().add(table);
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java
index 6739ad4..006d166 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksDesktop.java
@@ -117,6 +117,7 @@
         update();
       }
     };
+    searchListModel.setKeyProvider(StockQuote.KEY_PROVIDER);
 
     favoritesListModel = new AsyncListModel<StockQuote>() {
       @Override
@@ -124,6 +125,7 @@
         update();
       }
     };
+    favoritesListModel.setKeyProvider(StockQuote.KEY_PROVIDER);
 
     playerScoresListModel = new AsyncListModel<PlayerInfo>() {
       @Override
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksMobile.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksMobile.java
index 2242c84..01ca04e 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksMobile.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/StocksMobile.java
@@ -78,6 +78,7 @@
         update();
       }
     };
+    favoritesListModel.setKeyProvider(StockQuote.KEY_PROVIDER);
 
     // Now create the UI.
     RootPanel.get().add(binder.createAndBindUi(this));
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java
index 5536256..d1d151e 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/client/TransactionTreeViewModel.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -32,9 +32,8 @@
 import java.util.Map;
 
 /**
- * A TreeViewModel for a tree with a hidden root node of null,
- * a first level containing ticker symbol Strings, and a second
- * level containing Transactions.
+ * A TreeViewModel for a tree with a hidden root node of null, a first level
+ * containing ticker symbol Strings, and a second level containing Transactions.
  */
 class TransactionTreeViewModel implements TreeViewModel {
 
@@ -44,6 +43,7 @@
 
     public SectorListModel(String sector) {
       this.sector = sector;
+      setKeyProvider(StockQuote.KEY_PROVIDER);
     }
 
     public String getSector() {
@@ -70,19 +70,18 @@
     }
   };
 
-  private static final Cell<Transaction, Void> TRANSACTION_CELL =
-    new TransactionCell();
+  private static final Cell<Transaction, Void> TRANSACTION_CELL = new TransactionCell();
 
   private Map<String, SectorListModel> sectorListModels = new HashMap<String, SectorListModel>();
   private ListModel<StockQuote> stockQuoteListModel;
-  private ListListModel<String> topLevelListListModel =
-    new ListListModel<String>();
+  private ListListModel<String> topLevelListListModel = new ListListModel<String>();
 
   private Map<String, ListListModel<Transaction>> transactionListListModelsByTicker;
 
   private Updater updater;
 
-  public TransactionTreeViewModel(Updater updater, ListModel<StockQuote> stockQuoteListModel,
+  public TransactionTreeViewModel(Updater updater,
+      ListModel<StockQuote> stockQuoteListModel,
       Map<String, ListListModel<Transaction>> transactionListListModelsByTicker) {
     this.updater = updater;
     this.stockQuoteListModel = stockQuoteListModel;
@@ -92,19 +91,14 @@
     topLevelList.add("S&P 500");
     this.transactionListListModelsByTicker = transactionListListModelsByTicker;
   }
-  
+
   public <T> NodeInfo<?> getNodeInfo(T value, final TreeNode<T> treeNode) {
     if (value == null) {
       return new TreeViewModel.DefaultNodeInfo<String>(topLevelListListModel,
           TextCell.getInstance());
     } else if ("Favorites".equals(value)) {
       return new TreeViewModel.DefaultNodeInfo<StockQuote>(stockQuoteListModel,
-          STOCK_QUOTE_CELL) {
-        @Override
-        public Object getKey(StockQuote value) {
-          return value.getTicker();
-        }
-      };
+          STOCK_QUOTE_CELL);
     } else if ("History".equals(value)) {
       String ticker = ((StockQuote) treeNode.getParentNode().getValue()).getTicker();
       ListListModel<Transaction> listModel = transactionListListModelsByTicker.get(ticker);
@@ -119,8 +113,8 @@
       List<String> list = listModel.getList();
       list.add("Buy");
       list.add("Sell");
-      return new TreeViewModel.DefaultNodeInfo<String>(listModel, ButtonCell.getInstance(),
-          new ValueUpdater<String, Void>() {
+      return new TreeViewModel.DefaultNodeInfo<String>(listModel,
+          ButtonCell.getInstance(), new ValueUpdater<String, Void>() {
             public void update(String value, Void viewData) {
               StockQuote stockQuote = (StockQuote) treeNode.getParentNode().getValue();
               if ("Buy".equals(value)) {
@@ -133,18 +127,15 @@
     } else if (value instanceof String) {
       SectorListModel listModel = new SectorListModel((String) value);
       sectorListModels.put((String) value, listModel);
-      return new TreeViewModel.DefaultNodeInfo<StockQuote>(listModel, STOCK_QUOTE_CELL) {
-        @Override
-        public Object getKey(StockQuote value) {
-          return value.getTicker();
-        }
-      };
+      return new TreeViewModel.DefaultNodeInfo<StockQuote>(listModel,
+          STOCK_QUOTE_CELL);
     } else if (value instanceof StockQuote) {
       ListListModel<String> listModel = new ListListModel<String>();
       List<String> list = listModel.getList();
       list.add("Actions");
       list.add("History");
-      return new TreeViewModel.DefaultNodeInfo<String>(listModel, TextCell.getInstance());
+      return new TreeViewModel.DefaultNodeInfo<String>(listModel,
+          TextCell.getInstance());
     }
 
     throw new IllegalArgumentException(value.toString());
@@ -155,11 +146,11 @@
   }
 
   public boolean isLeaf(Object value, final TreeNode<?> parentNode) {
-    if (value instanceof Transaction ||
-      "Buy".equals(value) || "Sell".equals(value)) {
+    if (value instanceof Transaction || "Buy".equals(value)
+        || "Sell".equals(value)) {
       return true;
     }
-    
+
     return false;
   }
 }
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/shared/StockQuote.java b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/shared/StockQuote.java
index c279d09..7f2605a 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/shared/StockQuote.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/stocks/shared/StockQuote.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.sample.bikeshed.stocks.shared;
 
+import com.google.gwt.bikeshed.list.shared.ProvidesKey;
+
 import java.io.Serializable;
 
 /**
@@ -22,6 +24,15 @@
  */
 public class StockQuote implements Serializable {
 
+  /**
+   * Provides the key for {@link StockQuote}.
+   */
+  public static final ProvidesKey<StockQuote> KEY_PROVIDER = new ProvidesKey<StockQuote>() {
+    public Object getKey(StockQuote item) {
+      return item.getTicker();
+    }
+  };
+
   private boolean favorite;
   private String name;
   private int price;
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/tree/client/TreeSample.java b/bikeshed/src/com/google/gwt/sample/bikeshed/tree/client/TreeSample.java
index af14b87..63c1fef 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/tree/client/TreeSample.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/tree/client/TreeSample.java
@@ -15,7 +15,7 @@
  */
 package com.google.gwt.sample.bikeshed.tree.client;
 
-import com.google.gwt.bikeshed.list.shared.SelectionModel.DefaultSelectionModel;
+import com.google.gwt.bikeshed.list.shared.SelectionModel.AbstractSelectionModel;
 import com.google.gwt.bikeshed.tree.client.SideBySideTreeView;
 import com.google.gwt.bikeshed.tree.client.StandardTreeView;
 import com.google.gwt.core.client.EntryPoint;
@@ -23,7 +23,6 @@
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.RootPanel;
 
-import java.util.List;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -32,7 +31,7 @@
  */
 public class TreeSample implements EntryPoint {
 
-  class MySelectionModel extends DefaultSelectionModel<Object> {
+  class MySelectionModel extends AbstractSelectionModel<Object> {
 
     private Label label;
     private Set<Object> selectedSet = new TreeSet<Object>();
@@ -45,24 +44,15 @@
       return selectedSet.contains(object);
     }
 
+    @Override
     public void setSelected(Object object, boolean selected) {
-      setSelectedHelper(object, selected);
-      label.setText("Selected " + selectedSet.toString());
-    }
-
-    public void setSelected(List<Object> objects, boolean selected) {
-      for (Object object : objects) {
-        setSelectedHelper(object, selected);
-      }
-      label.setText("Selected " + selectedSet.toString());
-    }
-
-    public void setSelectedHelper(Object object, boolean selected) {
       if (selected) {
         selectedSet.add(object);
       } else {
         selectedSet.remove(object);
       }
+      label.setText("Selected " + selectedSet.toString());
+      super.setSelected(object, selected);
     }
   }
 
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/validation/client/Validation.java b/bikeshed/src/com/google/gwt/sample/bikeshed/validation/client/Validation.java
index 2635a48..dbe519c 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/validation/client/Validation.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/validation/client/Validation.java
@@ -17,10 +17,10 @@
 
 import com.google.gwt.bikeshed.cells.client.FieldUpdater;
 import com.google.gwt.bikeshed.list.client.Column;
-import com.google.gwt.bikeshed.list.client.HasKey;
 import com.google.gwt.bikeshed.list.client.PagingTableListView;
 import com.google.gwt.bikeshed.list.client.TextColumn;
 import com.google.gwt.bikeshed.list.shared.ListListModel;
+import com.google.gwt.bikeshed.list.shared.ProvidesKey;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.RootPanel;
@@ -80,6 +80,11 @@
       String zip = "300" + i;
       list.add(new Address("GA", zip));
     }
+    listModel.setKeyProvider(new ProvidesKey<Address>() {
+      public Object getKey(Address object) {
+        return object.key;
+      }
+    });
 
     PagingTableListView<Address> table = new PagingTableListView<Address>(
         listModel, 10);
@@ -90,14 +95,9 @@
       }
     };
 
-    HasKey<Address> key = new HasKey<Address>() {
-      public Object getKey(Address object) {
-        return object.key;
-      }
-    };
     Column<Address, String, ValidatableField<String>> zipColumn =
       new Column<Address, String, ValidatableField<String>>(
-        new ValidatableInputCell(), key) {
+        new ValidatableInputCell()) {
       @Override
       public String getValue(Address object) {
         return object.zip;