Replacing PagingListView.setPageStart/Size with PagingListView.setRange.  Replacing CellListImpl with PagingListViewPresenter, which makes the implementation easier to test and reuse.  Adding lots of tests.

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

Review by: jgw@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@8357 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MailRecipe.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MailRecipe.java
index 9397f2d..9a5349b 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MailRecipe.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/MailRecipe.java
@@ -21,6 +21,7 @@
 import com.google.gwt.cell.client.ClickableTextCell;
 import com.google.gwt.cell.client.DatePickerCell;
 import com.google.gwt.cell.client.FieldUpdater;
+import com.google.gwt.cell.client.ListBoxCell;
 import com.google.gwt.cell.client.TextCell;
 import com.google.gwt.cell.client.ValueUpdater;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -30,6 +31,7 @@
 import com.google.gwt.user.cellview.client.CellTable;
 import com.google.gwt.user.cellview.client.Column;
 import com.google.gwt.user.cellview.client.Header;
+import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -42,6 +44,7 @@
 import com.google.gwt.view.client.ListViewAdapter;
 import com.google.gwt.view.client.ProvidesKey;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
@@ -308,12 +311,19 @@
 
   @Override
   protected Widget createWidget() {
-    ListViewAdapter<Message> adapter = new ListViewAdapter<Message>();
+    final ListViewAdapter<Message> adapter = new ListViewAdapter<Message>();
     messages = adapter.getList();
 
     addMessages(10);
 
     table = new CellTable<Message>(10);
+    new Timer() {
+      @Override
+      public void run() {
+        table.redraw();
+        schedule(4000);
+      }
+    }.schedule(4000);
     table.setSelectionModel(selectionModel);
     adapter.addView(table);
 
@@ -342,12 +352,11 @@
     });
     table.addColumn(selectedColumn, selectedHeader);
 
-    addColumn(table, "ID", new TextCell(),
-        new GetValue<Message, String>() {
-          public String getValue(Message object) {
-            return "" + object.id;
-          }
-        }, idComparator);
+    addColumn(table, "ID", new TextCell(), new GetValue<Message, String>() {
+      public String getValue(Message object) {
+        return "" + object.id;
+      }
+    }, idComparator);
 
     addColumn(table, "Read", new GetValue<Message, String>() {
       public String getValue(Message object) {
@@ -365,7 +374,7 @@
       public void update(int index, Message object, Date value) {
         Window.alert("Changed date from " + object.date + " to " + value);
         object.date = value;
-        table.refresh();
+        table.redraw();
       }
     });
 
@@ -396,6 +405,40 @@
     });
     table.addColumn(toggleColumn, "Toggle Read/Unread");
 
+    final ListViewAdapter<String> monthAdapter = new ListViewAdapter<String>();
+    final List<String> monthList = monthAdapter.getList();
+    monthList.add("January");
+    monthList.add("February");
+    monthList.add("March");
+    monthList.add("April");
+    monthList.add("May");
+    monthList.add("June");
+    monthList.add("July");
+    monthList.add("August");
+    monthList.add("September");
+    monthList.add("October");
+    monthList.add("November");
+    monthList.add("December");
+    ListBoxCell<String> listBoxCell = new ListBoxCell<String>(new TextCell());
+    monthAdapter.addView(listBoxCell);
+    Column<Message, List<String>> monthColumn = new Column<Message, List<String>>(
+        listBoxCell) {
+      @Override
+      public List<String> getValue(Message object) {
+        List<String> l = new ArrayList<String>();
+        l.add(monthList.get(object.getDate().getMonth()));
+        return l;
+      }
+    };
+    monthColumn.setFieldUpdater(new FieldUpdater<Message, List<String>>() {
+      public void update(int index, Message object, List<String> values) {
+        int month = monthList.indexOf(values.get(0));
+        object.getDate().setMonth(month);
+        adapter.refresh();
+      }
+    });
+    table.addColumn(monthColumn, "Month");
+
     ScrollbarPager<Message> pager = new ScrollbarPager<Message>(table);
 
     Label searchLabel = new Label("Search Sender or Subject:");
@@ -434,9 +477,8 @@
   }
 
   private <C extends Comparable<C>> Column<Message, C> addColumn(
-      CellTable<Message> table, final String text,
-      final Cell<C> cell, final GetValue<Message, C> getter,
-      final Comparator<Message> comparator) {
+      CellTable<Message> table, final String text, final Cell<C> cell,
+      final GetValue<Message, C> getter, final Comparator<Message> comparator) {
     Column<Message, C> column = new Column<Message, C>(cell) {
       @Override
       public C getValue(Message object) {
@@ -469,9 +511,8 @@
     return column;
   }
 
-  private Column<Message, String> addColumn(
-      CellTable<Message> table, final String text,
-      final GetValue<Message, String> getter) {
+  private Column<Message, String> addColumn(CellTable<Message> table,
+      final String text, final GetValue<Message, String> getter) {
     return addColumn(table, text, new TextCell(), getter, null);
   }
 
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/ScrollbarPager.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/ScrollbarPager.java
index 927afb4..d9091c0 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/ScrollbarPager.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/ScrollbarPager.java
@@ -73,7 +73,7 @@
   }
 
   public void onRangeOrSizeChanged(PagingListView<T> listView) {
-    this.pageSize = listView.getPageSize();
+    this.pageSize = listView.getRange().getLength();
     this.dataSize = listView.getDataSize();
     
     this.height = view.getBodyHeight();
diff --git a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SimplePager.java b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SimplePager.java
index 388b3a5..ef11024 100644
--- a/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SimplePager.java
+++ b/bikeshed/src/com/google/gwt/sample/bikeshed/cookbook/client/SimplePager.java
@@ -22,6 +22,7 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.view.client.PagingListView;
+import com.google.gwt.view.client.Range;
 
 /**
  * A pager for controlling a PagingListView that uses a series of buttons for
@@ -58,7 +59,7 @@
    * additional rows to be displayed.
    */
   public boolean canAddRows(int rows) {
-    return view.getDataSize() - view.getPageSize() >= rows;
+    return view.getDataSize() - getPageSize() >= rows;
   }
 
   /**
@@ -66,7 +67,7 @@
    * to be removed.
    */
   public boolean canRemoveRows(int rows) {
-    return view.getPageSize() > rows;
+    return getPageSize() > rows;
   }
 
   public void onClick(ClickEvent event) {
@@ -91,7 +92,7 @@
   }
 
   private void addRows(int rows) {
-    view.setPageSize(view.getPageSize() + rows);
+    setPageSize(getPageSize() + rows);
   }
 
   private Button makeButton(String label, String id) {
@@ -102,7 +103,7 @@
   }
 
   private void removeRows(int rows) {
-    view.setPageSize(view.getPageSize() - rows);
+    setPageSize(getPageSize() - rows);
   }
 
   private void updateButtons() {
@@ -111,11 +112,13 @@
     prevPageButton.setEnabled(hasPreviousPage());
     nextPageButton.setEnabled(hasNextPage());
 
-    int page = (view.getPageStart() / view.getPageSize()) + 1;
-    int numPages = (view.getDataSize() + view.getPageSize() - 1)
-        / view.getPageSize();
+    Range range = view.getRange();
+    int pageStart = range.getStart();
+    int pageSize = range.getLength();
+    int page = (pageStart / pageSize) + 1;
+    int numPages = (view.getDataSize() + pageSize - 1) / pageSize;
     infoLabel.setText("Page " + page + " of " + numPages + ": Page Start = "
-        + view.getPageStart() + ", Page Size = " + view.getPageSize()
-        + ", Data Size = " + view.getDataSize());
+        + pageStart + ", Page Size = " + pageSize + ", Data Size = "
+        + view.getDataSize());
   }
 }
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/ExpenseDetails.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/ExpenseDetails.java
index 0ffda82..d23547f 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/ExpenseDetails.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/ExpenseDetails.java
@@ -548,7 +548,7 @@
     }
     allHeaders.get(0).setSorted(true);
     allHeaders.get(0).setReverseSort(false);
-    table.refreshHeaders();
+    table.redrawHeaders();
 
     // Request the expenses.
     requestExpenses();
@@ -731,7 +731,7 @@
 
         sortExpenses(items.getList(), header.getReverseSort() ? descComparator
             : ascComparator);
-        table.refreshHeaders();
+        table.redrawHeaders();
       }
     });
     table.addColumn(column, header);
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/ExpenseList.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/ExpenseList.java
index d2423c0..1f3f8ec 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/ExpenseList.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/ExpenseList.java
@@ -372,7 +372,7 @@
 
     // Refresh the table.
     pager.setPageStart(0);
-    table.refresh();
+    requestReports(false);
   }
 
   public void setListener(Listener listener) {
@@ -432,7 +432,7 @@
             otherHeader.setReverseSort(true);
           }
         }
-        table.refreshHeaders();
+        table.redrawHeaders();
 
         // Request sorted rows.
         orderBy = property.getName();
@@ -441,7 +441,7 @@
         }
         searchBox.resetDefaultText();
         searchRegExp = null;
-        
+
         // Go to the first page of the newly-sorted results
         pager.firstPage();
         requestReports(false);
@@ -560,7 +560,7 @@
         header.setSorted(false);
         header.setReverseSort(false);
       }
-      table.refreshHeaders();
+      table.redrawHeaders();
     }
 
     // Request the total data size.
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/MobileExpenseList.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/MobileExpenseList.java
index e330bc5..13083ea 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/MobileExpenseList.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/MobileExpenseList.java
@@ -180,7 +180,7 @@
     if (clear) {
       expenseAdapter.updateDataSize(0, true);
     }
-    expenseList.refresh();
+    requestExpenses();
   }
 
   public void show(ReportRecord report) {
diff --git a/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/MobileReportList.java b/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/MobileReportList.java
index d05bfa3..75d3927 100644
--- a/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/MobileReportList.java
+++ b/bikeshed/src/com/google/gwt/sample/expenses/gwt/client/MobileReportList.java
@@ -125,7 +125,7 @@
     if (clear) {
       reportAdapter.updateDataSize(0, true);
     }
-    reportList.refresh();
+    requestReports();
   }
 
   private Collection<Property<?>> getReportColumns() {
diff --git a/user/src/com/google/gwt/app/place/AbstractRecordListActivity.java b/user/src/com/google/gwt/app/place/AbstractRecordListActivity.java
index 63f78f4..c1c613d 100644
--- a/user/src/com/google/gwt/app/place/AbstractRecordListActivity.java
+++ b/user/src/com/google/gwt/app/place/AbstractRecordListActivity.java
@@ -180,12 +180,12 @@
         PagingListView<R> table = getView().asPagingListView();
         int rows = response.intValue();
         table.setDataSize(rows, true);
-        int pageSize = table.getPageSize();
+        int pageSize = table.getRange().getLength();
         int remnant = rows % pageSize;
         if (remnant == 0) {
-          table.setPageStart(rows - pageSize);
+          table.setRange(rows - pageSize, pageSize);
         } else {
-          table.setPageStart(rows - remnant);
+          table.setRange(rows - remnant, pageSize);
         }
         onRangeChanged(table);
       }
diff --git a/user/src/com/google/gwt/user/cellview/client/AbstractPager.java b/user/src/com/google/gwt/user/cellview/client/AbstractPager.java
index ade5370..db2b8a4 100644
--- a/user/src/com/google/gwt/user/cellview/client/AbstractPager.java
+++ b/user/src/com/google/gwt/user/cellview/client/AbstractPager.java
@@ -17,6 +17,7 @@
 
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.view.client.PagingListView;
+import com.google.gwt.view.client.Range;
 import com.google.gwt.view.client.PagingListView.Pager;
 
 /**
@@ -32,15 +33,21 @@
   private boolean isRangeLimited = true;
 
   /**
+   * The last data size.
+   */
+  private int lastDataSize;
+
+  /**
    * The {@link PagingListView} being paged.
    */
   private final PagingListView<T> view;
 
   public AbstractPager(PagingListView<T> view) {
     this.view = view;
+    this.lastDataSize = view.getDataSize();
     view.setPager(this);
   }
-  
+
   /**
    * Go to the first page.
    */
@@ -58,8 +65,9 @@
    * @return the page index
    */
   public int getPage() {
-    int pageSize = view.getPageSize();
-    return (view.getPageStart() + pageSize - 1) / pageSize;
+    Range range = view.getRange();
+    int pageSize = range.getLength();
+    return (range.getStart() + pageSize - 1) / pageSize;
   }
 
   /**
@@ -68,11 +76,29 @@
    * @return the page count
    */
   public int getPageCount() {
-    int pageSize = view.getPageSize();
+    int pageSize = getPageSize();
     return (view.getDataSize() + pageSize - 1) / pageSize;
   }
 
   /**
+   * Get the page size.
+   * 
+   * @return the page size
+   */
+  public int getPageSize() {
+    return view.getRange().getLength();
+  }
+
+  /**
+   * Get the page start index.
+   * 
+   * @return the page start index
+   */
+  public int getPageStart() {
+    return view.getRange().getStart();
+  }
+
+  /**
    * Get the {@link PagingListView} being paged.
    * 
    * @return the {@link PagingListView}
@@ -80,7 +106,7 @@
   public PagingListView<T> getPagingListView() {
     return view;
   }
-  
+
   /**
    * Returns true if there is enough data such that a call to
    * {@link #nextPage()} will succeed in moving the starting point of the table
@@ -90,7 +116,8 @@
     if (!view.isDataSizeExact()) {
       return true;
     }
-    return view.getPageStart() + view.getPageSize() < view.getDataSize();
+    Range range = view.getRange();
+    return range.getStart() + range.getLength() < view.getDataSize();
   }
 
   /**
@@ -98,7 +125,8 @@
    * additional pages.
    */
   public boolean hasNextPages(int pages) {
-    return view.getPageStart() + pages * view.getPageSize() < view.getDataSize();
+    Range range = view.getRange();
+    return range.getStart() + pages * range.getLength() < view.getDataSize();
   }
 
   /**
@@ -106,7 +134,7 @@
    * range.
    */
   public boolean hasPage(int index) {
-    return view.getPageSize() * index < view.getDataSize();
+    return getPageSize() * index < view.getDataSize();
   }
 
   /**
@@ -115,7 +143,16 @@
    * table backward.
    */
   public boolean hasPreviousPage() {
-    return view.getPageStart() > 0 && view.getDataSize() > 0;
+    return getPageStart() > 0 && view.getDataSize() > 0;
+  }
+
+  /**
+   * Returns true if there is enough data to display a given number of previous
+   * pages.
+   */
+  public boolean hasPreviousPages(int pages) {
+    Range range = view.getRange();
+    return (pages - 1) * range.getLength() < range.getStart();
   }
 
   /**
@@ -139,19 +176,25 @@
    * Set the page start to the last index that will still show a full page.
    */
   public void lastPageStart() {
-    setPageStart(view.getDataSize() - view.getPageSize());
+    setPageStart(view.getDataSize() - getPageSize());
   }
 
   /**
    * Advance the starting row by 'pageSize' rows.
    */
   public void nextPage() {
-    setPageStart(view.getPageStart() + view.getPageSize());
+    Range range = view.getRange();
+    setPageStart(range.getStart() + range.getLength());
   }
 
   public void onRangeOrSizeChanged(PagingListView<T> listView) {
-    if (isRangeLimited) {
-      setPageStart(view.getPageStart());
+    int oldDataSize = lastDataSize;
+    lastDataSize = listView.getDataSize();
+
+    // If the data size has changed, limit the range. If the page start or size
+    // was changed through the pager, it will already be limited.
+    if (isRangeLimited && oldDataSize != lastDataSize) {
+      setPageStart(getPageStart());
     }
   }
 
@@ -159,7 +202,8 @@
    * Move the starting row back by 'pageSize' rows.
    */
   public void previousPage() {
-    setPageStart(view.getPageStart() - view.getPageSize());
+    Range range = view.getRange();
+    setPageStart(range.getStart() - range.getLength());
   }
 
   /**
@@ -169,24 +213,43 @@
    */
   public void setPage(int index) {
     if (!isRangeLimited || !view.isDataSizeExact() || hasPage(index)) {
-      // We don't use the local version of setPageStart because the user
-      // probably wants to use absolute page indexes.
-      view.setPageStart(view.getPageSize() * index);
+      // We don't use the local version of setPageStart because it would
+      // constrain the index, but the user probably wants to use absolute page
+      // indexes.
+      int pageSize = getPageSize();
+      view.setRange(pageSize * index, pageSize);
     }
   }
 
   /**
+   * Set the page size of the view.
+   * 
+   * @param pageSize the new page size
+   */
+  public void setPageSize(int pageSize) {
+    Range range = view.getRange();
+    int pageStart = range.getStart();
+    if (isRangeLimited && view.isDataSizeExact()) {
+      pageStart = Math.min(pageStart, view.getDataSize() - pageSize);
+    }
+    pageStart = Math.max(0, pageStart);
+    view.setRange(pageStart, pageSize);
+  }
+
+  /**
    * Set the page start index.
    * 
    * @param index the index
    */
   public void setPageStart(int index) {
+    Range range = view.getRange();
+    int pageSize = range.getLength();
     if (isRangeLimited && view.isDataSizeExact()) {
-      index = Math.min(index, view.getDataSize() - view.getPageSize());
+      index = Math.min(index, view.getDataSize() - pageSize);
     }
     index = Math.max(0, index);
-    if (index != view.getPageStart()) {
-      view.setPageStart(index);
+    if (index != range.getStart()) {
+      view.setRange(index, pageSize);
     }
   }
 
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 37cd213..2fb3685 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellBrowser.java
@@ -617,7 +617,7 @@
    * @return the {@link Pager}
    */
   protected <C> Pager<C> createPager(PagingListView<C> listView) {
-    return new PageSizePager<C>(listView, listView.getPageSize());
+    return new PageSizePager<C>(listView, listView.getRange().getLength());
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/cellview/client/CellList.java b/user/src/com/google/gwt/user/cellview/client/CellList.java
index 0c8c7f2..b41fcdb 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellList.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellList.java
@@ -27,6 +27,7 @@
 import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.resources.client.ImageResource.ImageOptions;
 import com.google.gwt.resources.client.ImageResource.RepeatStyle;
+import com.google.gwt.user.cellview.client.PagingListViewPresenter.LoadingState;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.view.client.PagingListView;
@@ -83,6 +84,51 @@
   }
 
   /**
+   * The view used by the presenter.
+   */
+  private class View extends PagingListViewPresenter.DefaultView<T> {
+
+    public View(Element childContainer) {
+      super(childContainer);
+    }
+
+    public boolean dependsOnSelection() {
+      return cell.dependsOnSelection();
+    }
+
+    public void render(StringBuilder sb, List<T> values, int start,
+        SelectionModel<? super T> selectionModel) {
+      int length = values.size();
+      int end = start + length;
+      for (int i = start; i < end; i++) {
+        T value = values.get(i - start);
+        boolean isSelected = selectionModel == null ? false
+            : selectionModel.isSelected(value);
+        // TODO(jlabanca): Factor out __idx because rows can move.
+        sb.append("<div onclick='' __idx='").append(i).append("'");
+        sb.append(" class='");
+        sb.append(i % 2 == 0 ? style.evenItem() : style.oddItem());
+        if (isSelected) {
+          sb.append(" ").append(style.selectedItem());
+        }
+        sb.append("'>");
+        cell.render(value, null, sb);
+        sb.append("</div>");
+      }
+    }
+
+    public void setLoadingState(LoadingState state) {
+      showOrHide(emptyMessageElem, state == LoadingState.EMPTY);
+      // TODO(jlabanca): Add a loading icon.
+    }
+
+    @Override
+    protected void setSelected(Element elem, boolean selected) {
+      setStyleName(elem, style.selectedItem(), selected);
+    }
+  }
+
+  /**
    * The default page size.
    */
   private static final int DEFAULT_PAGE_SIZE = 25;
@@ -100,9 +146,10 @@
   private final Element childContainer;
   private String emptyListMessage = "";
   private final Element emptyMessageElem;
-  private final CellListImpl<T> impl;
+  private final PagingListViewPresenter<T> presenter;
   private final Style style;
   private ValueUpdater<T> valueUpdater;
+  private final View view;
 
   /**
    * Construct a new {@link CellList}.
@@ -139,49 +186,12 @@
     sinkEvents(Event.ONCLICK | Event.ONCHANGE | Event.MOUSEEVENTS);
 
     // Create the implementation.
-    impl = new CellListImpl<T>(this, DEFAULT_PAGE_SIZE, childContainer) {
-
-      @Override
-      protected boolean dependsOnSelection() {
-        return cell.dependsOnSelection();
-      }
-
-      @Override
-      protected void emitHtml(StringBuilder sb, List<T> values, int start,
-          SelectionModel<? super T> selectionModel) {
-        int length = values.size();
-        int end = start + length;
-        for (int i = start; i < end; i++) {
-          T value = values.get(i - start);
-          boolean isSelected = selectionModel == null ? false
-              : selectionModel.isSelected(value);
-          sb.append("<div onclick='' __idx='").append(i).append("'");
-          sb.append(" class='");
-          sb.append(i % 2 == 0 ? style.evenItem() : style.oddItem());
-          if (isSelected) {
-            sb.append(" ").append(style.selectedItem());
-          }
-          sb.append("'>");
-          cell.render(value, null, sb);
-          sb.append("</div>");
-        }
-      }
-
-      @Override
-      protected void onSizeChanged() {
-        super.onSizeChanged();
-        showOrHide(emptyMessageElem, impl.getDataSize() == 0);
-      }
-
-      @Override
-      protected void setSelected(Element elem, boolean selected) {
-        setStyleName(elem, style.selectedItem(), selected);
-      }
-    };
+    view = new View(childContainer);
+    presenter = new PagingListViewPresenter<T>(this, view, DEFAULT_PAGE_SIZE);
   }
 
   public int getDataSize() {
-    return impl.getDataSize();
+    return presenter.getDataSize();
   }
 
   /**
@@ -192,11 +202,11 @@
    */
   public T getDisplayedItem(int indexOnPage) {
     checkRowBounds(indexOnPage);
-    return impl.getData().get(indexOnPage);
+    return presenter.getData().get(indexOnPage);
   }
 
   public List<T> getDisplayedItems() {
-    return new ArrayList<T>(impl.getData());
+    return new ArrayList<T>(presenter.getData());
   }
 
   /**
@@ -208,16 +218,16 @@
     return emptyListMessage;
   }
 
-  public int getPageSize() {
-    return impl.getPageSize();
+  public final int getPageSize() {
+    return getRange().getLength();
   }
 
-  public int getPageStart() {
-    return impl.getPageStart();
+  public final int getPageStart() {
+    return getRange().getStart();
   }
 
   public Range getRange() {
-    return impl.getRange();
+    return presenter.getRange();
   }
 
   /**
@@ -238,7 +248,7 @@
   }
 
   public boolean isDataSizeExact() {
-    return impl.dataSizeIsExact();
+    return presenter.isDataSizeExact();
   }
 
   @Override
@@ -254,10 +264,10 @@
     }
     if (idxString.length() > 0) {
       int idx = Integer.parseInt(idxString);
-      T value = impl.getData().get(idx - impl.getPageStart());
+      T value = presenter.getData().get(idx - getPageStart());
       cell.onBrowserEvent(target, value, null, event, valueUpdater);
       if (event.getTypeInt() == Event.ONCLICK && !cell.consumesEvents()) {
-        SelectionModel<? super T> selectionModel = impl.getSelectionModel();
+        SelectionModel<? super T> selectionModel = presenter.getSelectionModel();
         if (selectionModel != null) {
           selectionModel.setSelected(value, true);
         }
@@ -269,26 +279,19 @@
    * Redraw the list using the existing data.
    */
   public void redraw() {
-    impl.redraw();
-  }
-
-  /**
-   * Redraw the list, requesting data from the delegate.
-   */
-  public void refresh() {
-    impl.refresh();
+    presenter.redraw();
   }
 
   public void setData(int start, int length, List<T> values) {
-    impl.setData(values, start);
+    presenter.setData(start, length, values);
   }
 
   public void setDataSize(int size, boolean isExact) {
-    impl.setDataSize(size, isExact);
+    presenter.setDataSize(size, isExact);
   }
 
   public void setDelegate(Delegate<T> delegate) {
-    impl.setDelegate(delegate);
+    presenter.setDelegate(delegate);
   }
 
   /**
@@ -302,19 +305,33 @@
   }
 
   public void setPager(Pager<T> pager) {
-    impl.setPager(pager);
+    presenter.setPager(pager);
   }
 
-  public void setPageSize(int pageSize) {
-    impl.setPageSize(pageSize);
+  /**
+   * Set the page size.
+   * 
+   * @param pageSize the new page size
+   */
+  public final void setPageSize(int pageSize) {
+    setRange(getPageStart(), pageSize);
   }
 
-  public void setPageStart(int pageStart) {
-    impl.setPageStart(pageStart);
+  /**
+   * Set the page start index.
+   * 
+   * @param pageStart the new page start
+   */
+  public final void setPageStart(int pageStart) {
+    setRange(pageStart, getPageSize());
+  }
+
+  public void setRange(int start, int length) {
+    presenter.setRange(start, length);
   }
 
   public void setSelectionModel(final SelectionModel<? super T> selectionModel) {
-    impl.setSelectionModel(selectionModel, true);
+    presenter.setSelectionModel(selectionModel);
   }
 
   /**
@@ -333,10 +350,10 @@
    * @throws IndexOutOfBoundsException
    */
   protected void checkRowBounds(int row) {
-    int rowSize = impl.getDisplayedItemCount();
-    if ((row >= rowSize) || (row < 0)) {
+    int rowCount = view.getChildCount();
+    if ((row >= rowCount) || (row < 0)) {
       throw new IndexOutOfBoundsException("Row index: " + row + ", Row size: "
-          + rowSize);
+          + rowCount);
     }
   }
 
diff --git a/user/src/com/google/gwt/user/cellview/client/CellListImpl.java b/user/src/com/google/gwt/user/cellview/client/CellListImpl.java
deleted file mode 100644
index 6ab24d4..0000000
--- a/user/src/com/google/gwt/user/cellview/client/CellListImpl.java
+++ /dev/null
@@ -1,574 +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.user.cellview.client;
-
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.dom.client.Document;
-import com.google.gwt.dom.client.Element;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.view.client.PagingListView;
-import com.google.gwt.view.client.Range;
-import com.google.gwt.view.client.SelectionModel;
-import com.google.gwt.view.client.ListView.Delegate;
-import com.google.gwt.view.client.PagingListView.Pager;
-import com.google.gwt.view.client.SelectionModel.SelectionChangeEvent;
-import com.google.gwt.view.client.SelectionModel.SelectionChangeHandler;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Implementation of {@link com.google.gwt.user.cellview.client.CellList}. This
- * class is subject to change or deletion. Do not rely on this class.
- * 
- * @param <T> the data type of items in the list
- */
-public abstract class CellListImpl<T> {
-
-  /**
-   * The Element that holds the rendered child items.
-   */
-  private Element childContainer;
-
-  /**
-   * The local cache of data in the view. The 0th index in the list corresponds
-   * to the data at pageStart.
-   */
-  private final List<T> data = new ArrayList<T>();
-
-  private int dataSize;
-
-  /**
-   * A boolean indicating whether or not the data size has ever been set. If the
-   * data size has never been set, then we will always pass it along to the
-   * view.
-   */
-  private boolean dataSizeInitialized;
-
-  private boolean dataSizeIsExact;
-
-  private Delegate<T> delegate;
-
-  /**
-   * As an optimization, keep track of the last HTML string that we rendered. If
-   * the contents do not change the next time we render, then we don't have to
-   * set inner html.
-   */
-  private String lastContents = null;
-  private final PagingListView<T> listView;
-
-  private Pager<T> pager;
-
-  /**
-   * The number of elements to show on the page.
-   */
-  private int pageSize;
-
-  /**
-   * The start index of the current page.
-   */
-  private int pageStart = 0;
-
-  /**
-   * Set to true when the page start changes, and we need to do a full refresh.
-   */
-  private boolean pageStartChanged;
-
-  /**
-   * Indicates whether or not a redraw is scheduled.
-   */
-  private boolean redrawScheduled;
-
-  /**
-   * The command used to refresh or redraw the page. If both are scheduled, the
-   * refresh will take priority.
-   */
-  private final Scheduler.ScheduledCommand refreshCommand = new Scheduler.ScheduledCommand() {
-    public void execute() {
-      // We clear the variables before making the refresh/redraw call so another
-      // refresh/redraw can be scheduled synchronously.
-      boolean wasRefreshScheduled = refreshScheduled;
-      boolean wasRedrawScheduled = redrawScheduled;
-      refreshScheduled = false;
-      redrawScheduled = false;
-      if (wasRefreshScheduled && delegate != null) {
-        // Refresh takes priority over redraw.
-        delegate.onRangeChanged(listView);
-      } else if (wasRedrawScheduled) {
-        setData(data, pageStart);
-      }
-    }
-  };
-
-  /**
-   * Indicates whether or not a refresh is scheduled.
-   */
-  private boolean refreshScheduled;
-
-  /**
-   * A local cache of the currently selected rows. We cannot track selected keys
-   * instead because we might end up in an inconsistent state where we render a
-   * subset of a list with duplicate values, styling a value in the subset but
-   * not styling the duplicate value outside of the subset.
-   */
-  private final Set<Integer> selectedRows = new HashSet<Integer>();
-
-  private HandlerRegistration selectionHandler;
-
-  private SelectionModel<? super T> selectionModel;
-
-  /**
-   * The temporary element use to convert HTML to DOM.
-   */
-  private final Element tmpElem;
-
-  public CellListImpl(PagingListView<T> listView, int pageSize,
-      Element childContainer) {
-    this.childContainer = childContainer;
-    this.listView = listView;
-    this.pageSize = pageSize;
-    tmpElem = Document.get().createDivElement();
-  }
-
-  public boolean dataSizeIsExact() {
-    return dataSizeIsExact;
-  }
-
-  /**
-   * Get the list of data within the current range. The data may not be
-   * complete.
-   * 
-   * @return the list of data
-   */
-  public List<T> getData() {
-    return data;
-  }
-
-  /**
-   * Get the overall data size.
-   * 
-   * @return the data size
-   */
-  public int getDataSize() {
-    return dataSize;
-  }
-
-  /**
-   * Get the number of items that are within the current page and data range.
-   * 
-   * @return the number of displayed items
-   */
-  public int getDisplayedItemCount() {
-    return Math.min(pageSize, dataSize - pageStart);
-  }
-
-  /**
-   * @return the page size
-   */
-  public int getPageSize() {
-    return pageSize;
-  }
-
-  /**
-   * @return the start index of the current page (inclusive)
-   */
-  public int getPageStart() {
-    return pageStart;
-  }
-
-  /**
-   * @return the range of data being displayed
-   */
-  public Range getRange() {
-    return new Range(pageStart, pageSize);
-  }
-
-  public SelectionModel<? super T> getSelectionModel() {
-    return selectionModel;
-  }
-
-  /**
-   * Redraw the list with the current data.
-   */
-  public void redraw() {
-    lastContents = null;
-    scheduleRefresh(true);
-  }
-
-  /**
-   * Request data from the delegate.
-   */
-  public void refresh() {
-    scheduleRefresh(false);
-  }
-
-  /**
-   * Set the data in the list.
-   * 
-   * @param values the new data
-   * @param valuesStart the start index of the values
-   */
-  public void setData(List<T> values, int valuesStart) {
-    int valuesLength = values.size();
-    int valuesEnd = valuesStart + valuesLength;
-
-    // Calculate the bounded start (inclusive) and end index (exclusive).
-    int pageEnd = pageStart + pageSize;
-    int boundedStart = Math.max(valuesStart, pageStart);
-    int boundedEnd = Math.min(valuesEnd, pageEnd);
-    if (boundedStart >= boundedEnd) {
-      // The data is out of range for the current page.
-      return;
-    }
-
-    // The data size must be at least as large as the data.
-    if (valuesEnd > dataSize) {
-      dataSize = valuesEnd;
-      onSizeChanged();
-    }
-
-    // Create placeholders up to the specified index.
-    int lastCacheIndex = pageStart + data.size();
-    while (lastCacheIndex < boundedStart) {
-      data.add(null);
-      lastCacheIndex++;
-    }
-
-    // Insert the new values into the data array.
-    for (int i = boundedStart; i < boundedEnd; i++) {
-      T value = values.get(i - valuesStart);
-      int dataIndex = i - pageStart;
-      if (dataIndex < data.size()) {
-        data.set(dataIndex, value);
-      } else {
-        data.add(value);
-      }
-
-      // Update our local cache of selected rows.
-      if (selectionModel != null) {
-        if (value != null && selectionModel.isSelected(value)) {
-          selectedRows.add(i);
-        } else {
-          selectedRows.remove(i);
-        }
-      }
-    }
-
-    // Construct a run of elements within the range of the data and the page.
-    boundedStart = pageStartChanged ? pageStart : boundedStart;
-    List<T> boundedValues = data.subList(boundedStart - pageStart, boundedEnd
-        - pageStart);
-    int boundedSize = boundedValues.size();
-    StringBuilder sb = new StringBuilder();
-    emitHtml(sb, boundedValues, boundedStart, selectionModel);
-
-    // Replace the DOM elements with the new rendered cells.
-    int childCount = childContainer.getChildCount();
-    if (boundedStart == pageStart
-        && (boundedSize >= childCount || boundedSize >= getDisplayedItemCount())) {
-      // If the contents have changed, we're done.
-      String newContents = sb.toString();
-      if (!newContents.equals(lastContents)) {
-        lastContents = newContents;
-        childContainer = renderChildContents(newContents);
-      }
-    } else {
-      lastContents = null;
-      Element container = convertToElements(sb.toString());
-      Element toReplace = null;
-      int realStart = boundedStart - pageStart;
-      if (realStart < childCount) {
-        toReplace = childContainer.getChild(realStart).cast();
-      }
-      for (int i = boundedStart; i < boundedEnd; i++) {
-        if (toReplace == null) {
-          // The child will be removed from tmpElem, so always use index 0.
-          childContainer.appendChild(container.getChild(0));
-        } else {
-          Element nextSibling = toReplace.getNextSiblingElement();
-          childContainer.replaceChild(container.getChild(0), toReplace);
-          toReplace = nextSibling;
-        }
-      }
-    }
-
-    // Reset the pageStartChanged boolean.
-    pageStartChanged = false;
-  }
-
-  /**
-   * Set the overall size of the list.
-   * 
-   * @param size the overall size
-   */
-  public void setDataSize(int size, boolean isExact) {
-    if (dataSizeInitialized && size == this.dataSize) {
-      return;
-    }
-    dataSizeInitialized = true;
-    this.dataSize = size;
-    this.dataSizeIsExact = isExact;
-    this.lastContents = null;
-    updateDataAndView();
-    onSizeChanged();
-  }
-
-  public void setDelegate(Delegate<T> delegate) {
-    this.delegate = delegate;
-  }
-
-  public void setPager(PagingListView.Pager<T> pager) {
-    this.pager = pager;
-  }
-
-  /**
-   * Set the number of items to show on each page.
-   * 
-   * @param pageSize the page size
-   */
-  public void setPageSize(int pageSize) {
-    if (pageSize == this.pageSize) {
-      return;
-    }
-    this.pageSize = pageSize;
-    updateDataAndView();
-    onSizeChanged();
-    refresh();
-  }
-
-  /**
-   * Set the start index of the range.
-   * 
-   * @param pageStart the start index
-   */
-  public void setPageStart(int pageStart) {
-    if (pageStart == this.pageStart) {
-      return;
-    } else if (pageStart > this.pageStart) {
-      if (data.size() > pageStart - this.pageStart) {
-        // Remove the data we no longer need.
-        for (int i = this.pageStart; i < pageStart; i++) {
-          data.remove(0);
-        }
-      } else {
-        // We have no overlapping data, so just clear it.
-        data.clear();
-      }
-    } else {
-      if ((data.size() > 0) && (this.pageStart - pageStart < pageSize)) {
-        // Insert null data at the beginning.
-        for (int i = pageStart; i < this.pageStart; i++) {
-          data.add(0, null);
-        }
-      } else {
-        // We have no overlapping data, so just clear it.
-        data.clear();
-      }
-    }
-
-    // Update the start index.
-    this.pageStart = pageStart;
-    this.pageStartChanged = true;
-    updateDataAndView();
-    onSizeChanged();
-
-    // Refresh the view with the data that is currently available.
-    setData(data, pageStart);
-
-    // Send a request for new data in the range.
-    refresh();
-  }
-
-  /**
-   * Set the {@link SelectionModel}, optionally triggering an update.
-   * 
-   * @param selectionModel the new {@link SelectionModel}
-   * @param updateSelection true to update selection
-   */
-  public void setSelectionModel(final SelectionModel<? super T> selectionModel,
-      boolean updateSelection) {
-    // Remove the old selection model.
-    if (selectionHandler != null) {
-      selectionHandler.removeHandler();
-      selectionHandler = null;
-    }
-
-    // Set the new selection model.
-    this.selectionModel = selectionModel;
-    if (selectionModel != null) {
-      selectionHandler = selectionModel.addSelectionChangeHandler(new SelectionChangeHandler() {
-        public void onSelectionChange(SelectionChangeEvent event) {
-          updateSelection();
-        }
-      });
-    }
-
-    // Update the current selection state based on the new model.
-    if (updateSelection) {
-      updateSelection();
-    }
-  }
-
-  /**
-   * Convert the specified HTML into DOM elements and return the parent of the
-   * DOM elements.
-   * 
-   * @param html the HTML to convert
-   * @return the parent element
-   */
-  protected Element convertToElements(String html) {
-    tmpElem.setInnerHTML(html);
-    return tmpElem;
-  }
-
-  /**
-   * Check whether or not the cells in the list depend on the selection state.
-   * 
-   * @return true if cells depend on selection, false if not
-   */
-  protected abstract boolean dependsOnSelection();
-
-  /**
-   * Construct the HTML that represents the list of items.
-   * 
-   * @param sb the {@link StringBuilder} to build into
-   * @param values the values to render
-   * @param start the start index
-   * @param selectionModel the {@link SelectionModel}
-   */
-  protected abstract void emitHtml(StringBuilder sb, List<T> values, int start,
-      SelectionModel<? super T> selectionModel);
-
-  /**
-   * Called when pageStart, pageSize, or data size changes.
-   */
-  protected void onSizeChanged() {
-    // Inform the pager about a change in page start, page size, or data size
-    if (pager != null) {
-      pager.onRangeOrSizeChanged(listView);
-    }
-  }
-
-  /**
-   * Remove the last element from the list.
-   */
-  protected void removeLastItem() {
-    childContainer.getLastChild().removeFromParent();
-  }
-
-  /**
-   * Set the contents of the child container.
-   * 
-   * @param html the html to render in the child
-   * @return the new child container
-   */
-  protected Element renderChildContents(String html) {
-    childContainer.setInnerHTML(html);
-    return childContainer;
-  }
-
-  /**
-   * Mark an element as selected or unselected. This is called when a cells
-   * selection state changes, but the cell does not depend on selection.
-   * 
-   * @param elem the element to modify
-   * @param selected true if selected, false if not
-   */
-  protected abstract void setSelected(Element elem, boolean selected);
-
-  /**
-   * Update the table based on the current selection.
-   */
-  protected void updateSelection() {
-    // Determine if our selection states are stale.
-    boolean dependsOnSelection = dependsOnSelection();
-    boolean refreshRequired = false;
-    Element cellElem = childContainer.getFirstChildElement();
-    int row = pageStart;
-    for (T value : data) {
-      boolean selected = selectionModel == null ? false
-          : selectionModel.isSelected(value);
-      if (selected != selectedRows.contains(row)) {
-        refreshRequired = true;
-        if (selected) {
-          selectedRows.add(row);
-        } else {
-          selectedRows.remove(row);
-        }
-        if (!dependsOnSelection) {
-          if (cellElem != null) {
-            // TODO: do a better check?
-            // The cell doesn't depend on selection, so we only need to update
-            // the style.
-            setSelected(cellElem, selected);
-          }
-        }
-      }
-      if (cellElem == null) {
-        // TODO: do a better check?
-        break;
-      }
-      cellElem = cellElem.getNextSiblingElement();
-      row++;
-    }
-
-    // Refresh the entire list if needed.
-    if (refreshRequired && dependsOnSelection) {
-      setData(data, pageStart);
-    }
-  }
-
-  /**
-   * Schedule a redraw or refresh.
-   * 
-   * @param redrawOnly if true, only schedule a redraw
-   */
-  private void scheduleRefresh(boolean redrawOnly) {
-    if (!refreshScheduled && !redrawScheduled) {
-      Scheduler.get().scheduleDeferred(refreshCommand);
-    }
-    if (redrawOnly) {
-      redrawScheduled = true;
-    } else {
-      refreshScheduled = true;
-    }
-  }
-
-  /**
-   * Ensure that the data and the view are in a consistent state.
-   */
-  private void updateDataAndView() {
-    // Update the data size.
-    int expectedLastIndex = Math.max(0,
-        Math.min(pageSize, dataSize - pageStart));
-    int lastIndex = data.size() - 1;
-    while (lastIndex >= expectedLastIndex) {
-      data.remove(lastIndex);
-      selectedRows.remove(lastIndex + pageStart);
-      lastIndex--;
-    }
-
-    // Update the DOM.
-    int expectedChildCount = data.size();
-    int childCount = childContainer.getChildCount();
-    while (childCount > expectedChildCount) {
-      removeLastItem();
-      childCount--;
-    }
-  }
-}
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTable.java b/user/src/com/google/gwt/user/cellview/client/CellTable.java
index 59153d6..2abc00e 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTable.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTable.java
@@ -16,6 +16,7 @@
 package com.google.gwt.user.cellview.client;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.EventTarget;
@@ -30,6 +31,7 @@
 import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.resources.client.ImageResource.ImageOptions;
 import com.google.gwt.resources.client.ImageResource.RepeatStyle;
+import com.google.gwt.user.cellview.client.PagingListViewPresenter.LoadingState;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.view.client.PagingListView;
@@ -236,7 +238,7 @@
 
   /**
    * Implementation of {@link CellTable} used by IE. Table sections do not
-   * support setInnerHtml in IE, so we need to replace the entire elements.
+   * support setInnerHtml in IE, so we need to replace the entire element.
    */
   @SuppressWarnings("unused")
   private static class ImplTrident extends Impl {
@@ -252,6 +254,112 @@
   }
 
   /**
+   * The view used by the presenter.
+   */
+  private class View extends PagingListViewPresenter.DefaultView<T> {
+
+    public View(Element childContainer) {
+      super(childContainer);
+    }
+
+    public boolean dependsOnSelection() {
+      for (Column<T, ?> column : columns) {
+        if (column.dependsOnSelection()) {
+          return true;
+        }
+      }
+      return false;
+    }
+
+    @Override
+    public void onUpdateSelection() {
+      // Refresh headers.
+      for (Header<?> header : headers) {
+        if (header != null && header.dependsOnSelection()) {
+          createHeaders(false);
+          break;
+        }
+      }
+
+      // Refresh footers.
+      for (Header<?> footer : footers) {
+        if (footer != null && footer.dependsOnSelection()) {
+          createHeaders(true);
+          break;
+        }
+      }
+    }
+
+    public void render(StringBuilder sb, List<T> values, int start,
+        SelectionModel<? super T> selectionModel) {
+      createHeadersAndFooters();
+
+      String firstColumnStyle = style.firstColumn();
+      String lastColumnStyle = style.lastColumn();
+      int columnCount = columns.size();
+      int length = values.size();
+      int end = start + length;
+      for (int i = start; i < end; i++) {
+        T value = values.get(i - start);
+        boolean isSelected = (selectionModel == null || value == null) ? false
+            : selectionModel.isSelected(value);
+        sb.append("<tr onclick=''");
+        sb.append(" class='");
+        sb.append(i % 2 == 0 ? style.evenRow() : style.oddRow());
+        if (isSelected) {
+          sb.append(" ").append(style.selectedRow());
+        }
+        sb.append("'>");
+        int curColumn = 0;
+        for (Column<T, ?> column : columns) {
+          // TODO(jlabanca): How do we sink ONFOCUS and ONBLUR?
+          sb.append("<td class='").append(style.cell());
+          if (curColumn == 0) {
+            sb.append(" ").append(firstColumnStyle);
+          }
+          // The first and last column could be the same column.
+          if (curColumn == columnCount - 1) {
+            sb.append(" ").append(lastColumnStyle);
+          }
+          sb.append("'>");
+          int bufferLength = sb.length();
+          if (value != null) {
+            column.render(value, providesKey, sb);
+          }
+
+          // Add blank space to ensure empty rows aren't squished.
+          if (bufferLength == sb.length()) {
+            sb.append("&nbsp");
+          }
+          sb.append("</td>");
+          curColumn++;
+        }
+        sb.append("</tr>");
+      }
+    }
+
+    @Override
+    public void replaceAllChildren(List<T> values, String html) {
+      Element section = TABLE_IMPL.renderSectionContents(tbody, html);
+      setChildContainer(section);
+    }
+
+    public void setLoadingState(LoadingState state) {
+      setLoadingIconVisible(state == LoadingState.LOADING);
+    }
+
+    @Override
+    protected Element convertToElements(String html) {
+      return TABLE_IMPL.convertToSectionElement("tbody", html);
+    }
+
+    @Override
+    protected void setSelected(Element elem, boolean selected) {
+      setStyleName(elem, style.selectedRow(), selected);
+    }
+  }
+
+  /**
    * The default page size.
    */
   private static final int DEFAULT_PAGESIZE = 15;
@@ -281,7 +389,6 @@
   private boolean headersStale;
 
   private TableRowElement hoveringRow;
-  private final CellListImpl<T> impl;
 
   /**
    * If true, enable selection via the mouse.
@@ -289,16 +396,46 @@
   private boolean isSelectionEnabled;
 
   /**
+   * The presenter.
+   */
+  private final PagingListViewPresenter<T> presenter;
+
+  /**
    * If null, each T will be used as its own key.
    */
   private ProvidesKey<T> providesKey;
 
+  /**
+   * Indicates whether or not the scheduled redraw has been cancelled.
+   */
+  private boolean redrawCancelled;
+
+  /**
+   * The command used to redraw the table after adding columns.
+   */
+  private final Scheduler.ScheduledCommand redrawCommand = new Scheduler.ScheduledCommand() {
+    public void execute() {
+      redrawScheduled = false;
+      if (redrawCancelled) {
+        redrawCancelled = false;
+        return;
+      }
+      redraw();
+    }
+  };
+
+  /**
+   * Indicates whether or not a redraw is scheduled.
+   */
+  private boolean redrawScheduled;
+
   private final Style style;
   private final TableElement table;
   private TableSectionElement tbody;
   private final TableSectionElement tbodyLoading;
   private TableSectionElement tfoot;
   private TableSectionElement thead;
+  private final View view;
 
   /**
    * Constructs a table with a default page size of 15.
@@ -351,110 +488,8 @@
     }
 
     // Create the implementation.
-    this.impl = new CellListImpl<T>(this, pageSize, tbody) {
-
-      @Override
-      public void setData(List<T> values, int start) {
-        createHeadersAndFooters();
-        super.setData(values, start);
-      }
-
-      @Override
-      protected Element convertToElements(String html) {
-        return TABLE_IMPL.convertToSectionElement("tbody", html);
-      }
-
-      @Override
-      protected boolean dependsOnSelection() {
-        for (Column<T, ?> column : columns) {
-          if (column.dependsOnSelection()) {
-            return true;
-          }
-        }
-        return false;
-      }
-
-      @Override
-      protected void emitHtml(StringBuilder sb, List<T> values, int start,
-          SelectionModel<? super T> selectionModel) {
-        setLoadingIconVisible(false);
-
-        String firstColumnStyle = style.firstColumn();
-        String lastColumnStyle = style.lastColumn();
-        int columnCount = columns.size();
-        int length = values.size();
-        int end = start + length;
-        for (int i = start; i < end; i++) {
-          T value = values.get(i - start);
-          boolean isSelected = (selectionModel == null || value == null)
-              ? false : selectionModel.isSelected(value);
-          sb.append("<tr onclick='' __idx='").append(i).append("'");
-          sb.append(" class='");
-          sb.append(i % 2 == 0 ? style.evenRow() : style.oddRow());
-          if (isSelected) {
-            sb.append(" ").append(style.selectedRow());
-          }
-          sb.append("'>");
-          int curColumn = 0;
-          for (Column<T, ?> column : columns) {
-            // TODO(jlabanca): How do we sink ONFOCUS and ONBLUR?
-            sb.append("<td class='").append(style.cell());
-            if (curColumn == 0) {
-              sb.append(" ").append(firstColumnStyle);
-            }
-            // The first and last column could be the same column.
-            if (curColumn == columnCount - 1) {
-              sb.append(" ").append(lastColumnStyle);
-            }
-            sb.append("'>");
-            int bufferLength = sb.length();
-            if (value != null) {
-              column.render(value, providesKey, sb);
-            }
-
-            // Add blank space to ensure empty rows aren't squished.
-            if (bufferLength == sb.length()) {
-              sb.append("&nbsp");
-            }
-            sb.append("</td>");
-            curColumn++;
-          }
-          sb.append("</tr>");
-        }
-      }
-
-      @Override
-      protected Element renderChildContents(String html) {
-        return (tbody = TABLE_IMPL.renderSectionContents(tbody, html));
-      }
-
-      @Override
-      protected void setSelected(Element elem, boolean selected) {
-        setStyleName(elem, style.selectedRow(), selected);
-      }
-
-      @Override
-      protected void updateSelection() {
-        // Refresh headers.
-        for (Header<?> header : headers) {
-          if (header != null && header.dependsOnSelection()) {
-            createHeaders(false);
-            break;
-          }
-        }
-
-        // Refresh footers.
-        for (Header<?> footer : footers) {
-          if (footer != null && footer.dependsOnSelection()) {
-            createHeaders(true);
-            break;
-          }
-        }
-
-        // Update data.
-        super.updateSelection();
-      }
-    };
+    view = new View(tbody);
+    this.presenter = new PagingListViewPresenter<T>(this, view, pageSize);
 
     setPageSize(pageSize);
 
@@ -462,9 +497,6 @@
     // those events actually needed by cells.
     sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.KEYEVENTS
         | Event.ONCHANGE | Event.FOCUSEVENTS);
-
-    // Show the loading indicator by default.
-    setLoadingIconVisible(true);
   }
 
   /**
@@ -489,7 +521,7 @@
     footers.add(footer);
     columns.add(col);
     headersStale = true;
-    redraw();
+    scheduleRedraw();
   }
 
   /**
@@ -507,8 +539,6 @@
     addColumn(col, new TextHeader(headerString), new TextHeader(footerString));
   }
 
-  // TODO: remove(Column)
-
   /**
    * Add a style name to the {@link TableColElement} at the specified index,
    * creating it if necessary.
@@ -526,16 +556,16 @@
   }
 
   public int getDataSize() {
-    return impl.getDataSize();
+    return presenter.getDataSize();
   }
 
   public T getDisplayedItem(int indexOnPage) {
     checkRowBounds(indexOnPage);
-    return impl.getData().get(indexOnPage);
+    return presenter.getData().get(indexOnPage);
   }
 
   public List<T> getDisplayedItems() {
-    return new ArrayList<T>(impl.getData());
+    return new ArrayList<T>(presenter.getData());
   }
 
   public int getHeaderHeight() {
@@ -547,20 +577,16 @@
     return providesKey;
   }
 
-  public int getNumDisplayedItems() {
-    return impl.getDisplayedItemCount();
+  public final int getPageSize() {
+    return getRange().getLength();
   }
 
-  public int getPageSize() {
-    return impl.getPageSize();
-  }
-
-  public int getPageStart() {
-    return impl.getPageStart();
+  public final int getPageStart() {
+    return getRange().getStart();
   }
 
   public Range getRange() {
-    return impl.getRange();
+    return presenter.getRange();
   }
 
   /**
@@ -578,12 +604,8 @@
     return rows.getLength() > row ? rows.getItem(row) : null;
   }
 
-  public int getSize() {
-    return impl.getDataSize();
-  }
-
   public boolean isDataSizeExact() {
-    return impl.dataSizeIsExact();
+    return presenter.isDataSizeExact();
   }
 
   /**
@@ -637,14 +659,14 @@
         tr.removeClassName(style.hoveredRow());
       }
 
-      T value = impl.getData().get(row);
+      T value = presenter.getData().get(row);
       Column<T, ?> column = columns.get(col);
-      column.onBrowserEvent(cell, impl.getPageStart() + row, value, event,
+      column.onBrowserEvent(cell, getPageStart() + row, value, event,
           providesKey);
 
       // Update selection.
       if (isSelectionEnabled && event.getTypeInt() == Event.ONCLICK) {
-        SelectionModel<? super T> selectionModel = impl.getSelectionModel();
+        SelectionModel<? super T> selectionModel = presenter.getSelectionModel();
         if (selectionModel != null) {
           selectionModel.setSelected(value, true);
         }
@@ -656,27 +678,52 @@
    * Redraw the table using the existing data.
    */
   public void redraw() {
-    setLoadingIconVisible(false);
-    impl.redraw();
+    if (redrawScheduled) {
+      redrawCancelled = true;
+    }
+    presenter.redraw();
   }
 
-  /**
-   * Redraw the table, requesting data from the delegate.
-   */
-  public void refresh() {
-    setLoadingIconVisible(true);
-    impl.refresh();
-  }
-
-  public void refreshFooters() {
+  public void redrawFooters() {
     createHeaders(true);
   }
 
-  public void refreshHeaders() {
+  public void redrawHeaders() {
     createHeaders(false);
   }
 
   /**
+   * Remove a column.
+   * 
+   * @param index the column index
+   */
+  public void removeColumn(int index) {
+    if (index < 0 || index >= columns.size()) {
+      throw new IndexOutOfBoundsException(
+          "The specified column index is out of bounds.");
+    }
+    columns.remove(index);
+    headers.remove(index);
+    footers.remove(index);
+    headersStale = true;
+    scheduleRedraw();
+  }
+
+  /**
+   * Remove a column.
+   * 
+   * @param col the column to remove
+   */
+  public void removeColumn(Column<T, ?> col) {
+    int index = columns.indexOf(col);
+    if (index < 0) {
+      throw new IllegalArgumentException(
+          "The specified column is not part of this table.");
+    }
+    removeColumn(index);
+  }
+
+  /**
    * Remove a style from the {@link TableColElement} at the specified index.
    * 
    * @param index the column index
@@ -690,20 +737,15 @@
   }
 
   public void setData(int start, int length, List<T> values) {
-    impl.setData(values, start);
+    presenter.setData(start, length, values);
   }
 
   public void setDataSize(int size, boolean isExact) {
-    impl.setDataSize(size, isExact);
-
-    // If there is no data, then we are done loading.
-    if (size <= 0) {
-      setLoadingIconVisible(false);
-    }
+    presenter.setDataSize(size, isExact);
   }
 
   public void setDelegate(Delegate<T> delegate) {
-    impl.setDelegate(delegate);
+    presenter.setDelegate(delegate);
   }
 
   /**
@@ -719,7 +761,7 @@
   }
 
   public void setPager(PagingListView.Pager<T> pager) {
-    impl.setPager(pager);
+    presenter.setPager(pager);
   }
 
   /**
@@ -729,8 +771,8 @@
    * 
    * @throws IllegalArgumentException if pageSize is negative or 0
    */
-  public void setPageSize(int pageSize) {
-    impl.setPageSize(pageSize);
+  public final void setPageSize(int pageSize) {
+    setRange(getPageStart(), pageSize);
   }
 
   /**
@@ -740,9 +782,12 @@
    * @param pageStart the index of the row that should appear at the start of
    *          the page
    */
-  public void setPageStart(int pageStart) {
-    setLoadingIconVisible(true);
-    impl.setPageStart(pageStart);
+  public final void setPageStart(int pageStart) {
+    setRange(pageStart, getPageSize());
+  }
+
+  public void setRange(int start, int length) {
+    presenter.setRange(start, length);
   }
 
   /**
@@ -755,7 +800,7 @@
   }
 
   public void setSelectionModel(SelectionModel<? super T> selectionModel) {
-    impl.setSelectionModel(selectionModel, true);
+    presenter.setSelectionModel(selectionModel);
   }
 
   /**
@@ -765,10 +810,10 @@
    * @throws IndexOutOfBoundsException
    */
   protected void checkRowBounds(int row) {
-    int rowSize = impl.getDisplayedItemCount();
-    if ((row >= rowSize) || (row < 0)) {
+    int rowCount = view.getChildCount();
+    if ((row >= rowCount) || (row < 0)) {
       throw new IndexOutOfBoundsException("Row index: " + row + ", Row size: "
-          + rowSize);
+          + rowCount);
     }
   }
 
@@ -863,6 +908,17 @@
   }-*/;
 
   /**
+   * Schedule a redraw for the end of the event loop.
+   */
+  private void scheduleRedraw() {
+    redrawCancelled = false;
+    if (!redrawScheduled) {
+      redrawScheduled = true;
+      Scheduler.get().scheduleFinally(redrawCommand);
+    }
+  }
+
+  /**
    * Show or hide the loading icon.
    * 
    * @param visible true to show, false to hide.
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 3a72edd..0905387 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,7 @@
 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.user.cellview.client.PagingListViewPresenter.LoadingState;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.view.client.TreeViewModel;
 import com.google.gwt.view.client.PagingListView;
@@ -33,8 +34,10 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * A view of a tree node.
@@ -98,214 +101,258 @@
    * 
    * @param <C> the child item type
    */
-  private static class NodeListView<C> implements PagingListView<C> {
+  private static class NodeCellList<C> implements PagingListView<C> {
 
+    /**
+     * The view used by the NodeCellList.
+     */
+    private class View extends PagingListViewPresenter.DefaultView<C> {
+
+      public View(Element childContainer) {
+        super(childContainer);
+      }
+
+      public boolean dependsOnSelection() {
+        return cell.dependsOnSelection();
+      }
+
+      public void render(StringBuilder sb, List<C> values, int start,
+          SelectionModel<? super C> selectionModel) {
+        // Cache the style names that will be used for each child.
+        CellTree.Style style = nodeView.tree.getStyle();
+        String selectedStyle = style.selectedItem();
+        String itemStyle = style.item();
+        String itemImageValueStyle = style.itemImageValue();
+        String itemValueStyle = style.itemValue();
+        String openStyle = style.openItem();
+        String topStyle = style.topItem();
+        String topImageValueStyle = style.topItemImageValue();
+        boolean isRootNode = nodeView.isRootNode();
+        String openImage = nodeView.tree.getOpenImageHtml(isRootNode);
+        String closedImage = nodeView.tree.getClosedImageHtml(isRootNode);
+        int imageWidth = nodeView.tree.getImageWidth();
+        int paddingLeft = imageWidth * nodeView.depth;
+
+        // Create a set of currently open nodes.
+        Set<Object> openNodes = new HashSet<Object>();
+        int childCount = nodeView.getChildCount();
+        int end = start + values.size();
+        for (int i = start; i < end && i < childCount; i++) {
+          CellTreeNodeView<?> child = nodeView.getChildNode(i);
+          // Ignore child nodes that are closed.
+          if (child.isOpen()) {
+            openNodes.add(child.getValueKey());
+          }
+        }
+
+        // Render the child nodes.
+        ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
+        TreeViewModel model = nodeView.tree.getTreeViewModel();
+        for (C value : values) {
+          Object key = providesKey.getKey(value);
+          boolean isOpen = openNodes.contains(key);
+
+          // Outer div contains image, value, and children (when open).
+          sb.append("<div>");
+
+          // The selection pads the content based on the depth.
+          sb.append("<div style='padding-left:");
+          sb.append(paddingLeft);
+          sb.append("px;' class='").append(itemStyle);
+          if (isOpen) {
+            sb.append(" ").append(openStyle);
+          }
+          if (isRootNode) {
+            sb.append(" ").append(topStyle);
+          }
+          if (selectionModel != null && selectionModel.isSelected(value)) {
+            sb.append(" ").append(selectedStyle);
+          }
+          sb.append("'>");
+
+          // Inner div contains image and value.
+          sb.append("<div onclick='' style='position:relative;padding-left:");
+          sb.append(imageWidth);
+          sb.append("px;' class='").append(itemImageValueStyle);
+          if (isRootNode) {
+            sb.append(" ").append(topImageValueStyle);
+          }
+          sb.append("'>");
+
+          // Add the open/close icon.
+          if (isOpen) {
+            sb.append(openImage);
+          } else if (model.isLeaf(value)) {
+            sb.append(LEAF_IMAGE);
+          } else {
+            sb.append(closedImage);
+          }
+
+          // Content div contains value.
+          sb.append("<div class='").append(itemValueStyle).append("'>");
+          cell.render(value, null, sb);
+          sb.append("</div></div></div></div>");
+        }
+      }
+
+      @Override
+      public void replaceAllChildren(List<C> values, String html) {
+        // Hide the child container so we can animate it.
+        if (nodeView.tree.isAnimationEnabled()) {
+          nodeView.ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
+        }
+
+        // Replace the child nodes.
+        Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, 0);
+        super.replaceAllChildren(values, html);
+        loadChildState(values, 0, savedViews);
+
+        // Animate the child container open.
+        if (nodeView.tree.isAnimationEnabled()) {
+          nodeView.tree.maybeAnimateTreeNode(nodeView);
+        }
+      }
+
+      @Override
+      public void replaceChildren(List<C> values, int start, String html) {
+        Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, 0);
+        super.replaceChildren(values, start, html);
+        loadChildState(values, 0, savedViews);
+      }
+
+      public void setLoadingState(LoadingState state) {
+        nodeView.updateImage(state == LoadingState.LOADING);
+        showOrHide(nodeView.emptyMessageElem, state == LoadingState.EMPTY);
+      }
+
+      @Override
+      protected void setSelected(Element elem, boolean selected) {
+        setStyleName(getSelectionElement(elem),
+            nodeView.tree.getStyle().selectedItem(), selected);
+      }
+
+      /**
+       * Reload the open children after rendering new items in this node.
+       * 
+       * @param values the values being replaced
+       * @param start the start index
+       * @param savedViews the open nodes
+       */
+      private void loadChildState(List<C> values, int start,
+          Map<Object, CellTreeNodeView<?>> savedViews) {
+        int len = values.size();
+        int end = start + len;
+        int childCount = nodeView.getChildCount();
+        ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
+        Element childElem = nodeView.ensureChildContainer().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));
+          // Copy the saved child's state into the new child
+          if (savedChild != null) {
+            child.animationFrame = savedChild.animationFrame;
+            child.contentContainer = savedChild.contentContainer;
+            child.childContainer = savedChild.childContainer;
+            child.children = savedChild.children;
+            child.emptyMessageElem = savedChild.emptyMessageElem;
+            child.nodeInfo = savedChild.nodeInfo;
+            child.nodeInfoLoaded = savedChild.nodeInfoLoaded;
+            child.open = savedChild.open;
+            child.showMoreElem = savedChild.showMoreElem;
+
+            // 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);
+
+            // 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);
+          }
+          childElem = childElem.getNextSiblingElement();
+        }
+      }
+
+      /**
+       * Save the state of the open child nodes within the range of the
+       * specified values. Use {@link #loadChildState(List, int, Map)} to
+       * re-attach the open nodes after they have been replaced.
+       * 
+       * @param values the values being replaced
+       * @param start the start index
+       * @return the map of open nodes
+       */
+      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<?>>();
+        }
+
+        // Construct a map of former child views based on their value keys.
+        int len = values.size();
+        int end = start + len;
+        int childCount = nodeView.getChildCount();
+        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()) {
+            openNodes.put(child.getValueKey(), child);
+          }
+        }
+
+        // Trim the saved views down to the children that still exists.
+        ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
+        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
+          Object key = providesKey.getKey(childValue);
+          CellTreeNodeView<?> savedView = openNodes.remove(key);
+          if (savedView != null) {
+            savedView.ensureAnimationFrame().removeFromParent();
+            savedViews.put(key, savedView);
+          }
+        }
+        return savedViews;
+      }
+    }
+
+    private final Cell<C> cell;
     private final int defaultPageSize;
-    private final CellListImpl<C> impl;
+    private final NodeInfo<C> nodeInfo;
     private CellTreeNodeView<?> nodeView;
-    private Map<Object, CellTreeNodeView<?>> savedViews;
+    private final PagingListViewPresenter<C> presenter;
 
-    public NodeListView(final NodeInfo<C> nodeInfo,
+    public NodeCellList(final NodeInfo<C> nodeInfo,
         final CellTreeNodeView<?> nodeView, int pageSize) {
       this.defaultPageSize = pageSize;
+      this.nodeInfo = nodeInfo;
       this.nodeView = nodeView;
+      cell = nodeInfo.getCell();
 
-      final Cell<C> cell = nodeInfo.getCell();
-      impl = new CellListImpl<C>(this, pageSize,
-          nodeView.ensureChildContainer()) {
-
-        @Override
-        public void setData(List<C> values, int start) {
-          nodeView.updateImage(false);
-
-          // Ensure that we have a children array.
-          if (nodeView.children == null) {
-            nodeView.children = new ArrayList<CellTreeNodeView<?>>();
-          }
-
-          // Construct a map of former child views based on their value keys.
-          int len = values.size();
-          int end = start + len;
-          int childCount = nodeView.getChildCount();
-          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()) {
-              openNodes.put(child.getValueKey(), child);
-            }
-          }
-
-          // Hide the child container so we can animate it.
-          if (nodeView.tree.isAnimationEnabled()) {
-            nodeView.ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
-          }
-
-          // Trim the saved views down to the children that still exists.
-          ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
-          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
-            Object key = providesKey.getKey(childValue);
-            CellTreeNodeView<?> savedView = openNodes.remove(key);
-            if (savedView != null) {
-              savedView.ensureAnimationFrame().removeFromParent();
-              savedViews.put(key, savedView);
-            }
-          }
-
-          // Create the new cells.
-          super.setData(values, start);
-
-          // Create the child TreeNodeViews from the new elements.
-          Element childElem = nodeView.ensureChildContainer().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));
-            // Copy the saved child's state into the new child
-            if (savedChild != null) {
-              child.animationFrame = savedChild.animationFrame;
-              child.contentContainer = savedChild.contentContainer;
-              child.childContainer = savedChild.childContainer;
-              child.children = savedChild.children;
-              child.emptyMessageElem = savedChild.emptyMessageElem;
-              child.nodeInfo = savedChild.nodeInfo;
-              child.nodeInfoLoaded = savedChild.nodeInfoLoaded;
-              child.open = savedChild.open;
-              child.showMoreElem = savedChild.showMoreElem;
-
-              // 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);
-
-              // 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);
-            }
-            childElem = childElem.getNextSiblingElement();
-          }
-
-          // Clear temporary state.
-          savedViews = null;
-
-          // Animate the child container open.
-          if (nodeView.tree.isAnimationEnabled()) {
-            nodeView.tree.maybeAnimateTreeNode(nodeView);
-          }
-        }
-
-        @Override
-        protected boolean dependsOnSelection() {
-          return cell.dependsOnSelection();
-        }
-
-        @Override
-        protected void emitHtml(StringBuilder sb, List<C> values, int start,
-            SelectionModel<? super C> selectionModel) {
-          // Cache the style names that will be used for each child.
-          CellTree.Style style = nodeView.tree.getStyle();
-          String selectedStyle = style.selectedItem();
-          String itemStyle = style.item();
-          String itemImageValueStyle = style.itemImageValue();
-          String itemValueStyle = style.itemValue();
-          String openStyle = style.openItem();
-          String topStyle = style.topItem();
-          String topImageValueStyle = style.topItemImageValue();
-          boolean isRootNode = nodeView.isRootNode();
-          String openImage = nodeView.tree.getOpenImageHtml(isRootNode);
-          String closedImage = nodeView.tree.getClosedImageHtml(isRootNode);
-          int imageWidth = nodeView.tree.getImageWidth();
-          int paddingLeft = imageWidth * nodeView.depth;
-
-          // Render the child nodes.
-          ProvidesKey<C> providesKey = nodeInfo.getProvidesKey();
-          TreeViewModel model = nodeView.tree.getTreeViewModel();
-          for (C value : values) {
-            Object key = providesKey.getKey(value);
-            boolean isOpen = savedViews.containsKey(key);
-
-            // Outer div contains image, value, and children (when open).
-            sb.append("<div>");
-
-            // The selection pads the content based on the depth.
-            sb.append("<div style='padding-left:");
-            sb.append(paddingLeft);
-            sb.append("px;' class='").append(itemStyle);
-            if (isOpen) {
-              sb.append(" ").append(openStyle);
-            }
-            if (isRootNode) {
-              sb.append(" ").append(topStyle);
-            }
-            if (selectionModel != null && selectionModel.isSelected(value)) {
-              sb.append(" ").append(selectedStyle);
-            }
-            sb.append("'>");
-
-            // Inner div contains image and value.
-            sb.append("<div onclick='' style='position:relative;padding-left:");
-            sb.append(imageWidth);
-            sb.append("px;' class='").append(itemImageValueStyle);
-            if (isRootNode) {
-              sb.append(" ").append(topImageValueStyle);
-            }
-            sb.append("'>");
-
-            // Add the open/close icon.
-            if (isOpen) {
-              sb.append(openImage);
-            } else if (model.isLeaf(value)) {
-              sb.append(LEAF_IMAGE);
-            } else {
-              sb.append(closedImage);
-            }
-
-            // Content div contains value.
-            sb.append("<div class='").append(itemValueStyle).append("'>");
-            cell.render(value, null, sb);
-            sb.append("</div></div></div></div>");
-          }
-        }
-
-        @Override
-        protected void removeLastItem() {
-          CellTreeNodeView<?> child = nodeView.children.remove(nodeView.children.size() - 1);
-          child.cleanup();
-          super.removeLastItem();
-        }
-
-        @Override
-        protected void setSelected(Element elem, boolean selected) {
-          setStyleName(getSelectionElement(elem),
-              nodeView.tree.getStyle().selectedItem(), selected);
-        }
-      };
+      presenter = new PagingListViewPresenter<C>(this, new View(
+          nodeView.ensureChildContainer()), pageSize);
 
       // Use a pager to update buttons.
-      impl.setPager(new Pager<C>() {
+      presenter.setPager(new Pager<C>() {
         public void onRangeOrSizeChanged(PagingListView<C> listView) {
           // Assumes a page start of 0.
-          int dataSize = impl.getDataSize();
-          showOrHide(nodeView.showMoreElem, dataSize > impl.getPageSize());
-          if (dataSize == 0) {
-            showOrHide(nodeView.emptyMessageElem, true);
-            nodeView.updateImage(false);
-          } else {
-            showOrHide(nodeView.emptyMessageElem, false);
-          }
+          int dataSize = presenter.getDataSize();
+          int pageSize = getRange().getLength();
+          showOrHide(nodeView.showMoreElem, dataSize > pageSize);
         }
       });
     }
@@ -314,59 +361,47 @@
      * Cleanup this node view.
      */
     public void cleanup() {
-      impl.setSelectionModel(null, false);
+      presenter.clearSelectionModel();
     }
-    
+
     public int getDataSize() {
-      return impl.getDataSize();
+      return presenter.getDataSize();
     }
 
     public int getDefaultPageSize() {
       return defaultPageSize;
     }
 
-    public int getPageSize() {
-      return impl.getPageSize();
-    }
-
-    public int getPageStart() {
-      return impl.getPageStart();
-    }
-
     public Range getRange() {
-      return impl.getRange();
+      return presenter.getRange();
     }
 
     public boolean isDataSizeExact() {
-      return impl.dataSizeIsExact();
+      return presenter.isDataSizeExact();
     }
 
     public void setData(int start, int length, List<C> values) {
-      impl.setData(values, start);
+      presenter.setData(start, length, values);
     }
 
     public void setDataSize(int size, boolean isExact) {
-      impl.setDataSize(size, isExact);
+      presenter.setDataSize(size, isExact);
     }
 
     public void setDelegate(Delegate<C> delegate) {
-      impl.setDelegate(delegate);
+      presenter.setDelegate(delegate);
     }
 
     public void setPager(Pager<C> pager) {
-      impl.setPager(pager);
+      presenter.setPager(pager);
     }
 
-    public void setPageSize(int pageSize) {
-      impl.setPageSize(pageSize);
-    }
-
-    public void setPageStart(int pageStart) {
-      impl.setPageStart(pageStart);
+    public void setRange(int start, int length) {
+      presenter.setRange(start, length);
     }
 
     public void setSelectionModel(final SelectionModel<? super C> selectionModel) {
-      impl.setSelectionModel(selectionModel, true);
+      presenter.setSelectionModel(selectionModel);
     }
 
     /**
@@ -422,7 +457,7 @@
   /**
    * The list view used to display the nodes.
    */
-  private NodeListView<?> listView;
+  private NodeCellList<?> listView;
 
   /**
    * The info about children of this node.
@@ -539,7 +574,6 @@
           setStyleName(getCellParent(), tree.getStyle().openItem(), true);
         }
         ensureAnimationFrame().getStyle().setProperty("display", "");
-        updateImage(true);
         onOpen(nodeInfo);
       }
     } else {
@@ -643,7 +677,7 @@
    * @param <C> the child data type of the node
    */
   protected <C> void onOpen(final NodeInfo<C> nodeInfo) {
-    NodeListView<C> view = new NodeListView<C>(nodeInfo, this,
+    NodeCellList<C> view = new NodeCellList<C>(nodeInfo, this,
         tree.getDefaultNodeSize());
     listView = view;
     view.setSelectionModel(nodeInfo.getSelectionModel());
@@ -720,15 +754,17 @@
   }
 
   void showFewer() {
+    Range range = listView.getRange();
     int defaultPageSize = listView.getDefaultPageSize();
-    int maxSize = Math.max(defaultPageSize, listView.impl.getPageSize()
-        - defaultPageSize);
-    listView.impl.setPageSize(maxSize);
+    int maxSize = Math.max(defaultPageSize, range.getLength() - defaultPageSize);
+    listView.setRange(range.getStart(), maxSize);
   }
 
   void showMore() {
-    listView.impl.setPageSize(listView.impl.getPageSize()
-        + listView.getDefaultPageSize());
+    Range range = listView.getRange();
+    int pageSize = listView.getRange().getLength()
+        + listView.getDefaultPageSize();
+    listView.setRange(range.getStart(), pageSize);
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/cellview/client/PageSizePager.java b/user/src/com/google/gwt/user/cellview/client/PageSizePager.java
index 103fb43..3259577 100644
--- a/user/src/com/google/gwt/user/cellview/client/PageSizePager.java
+++ b/user/src/com/google/gwt/user/cellview/client/PageSizePager.java
@@ -22,6 +22,7 @@
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.view.client.PagingListView;
+import com.google.gwt.view.client.Range;
 import com.google.gwt.view.client.PagingListView.Pager;
 
 /**
@@ -55,15 +56,17 @@
     // Show more button.
     showMoreButton.addClickHandler(new ClickHandler() {
       public void onClick(ClickEvent event) {
-        int pageSize = Math.min(listView.getPageSize() + increment,
+        Range range = listView.getRange();
+        int pageSize = Math.min(range.getLength() + increment,
             listView.getDataSize());
-        listView.setPageSize(pageSize);
+        listView.setRange(range.getStart(), pageSize);
       }
     });
     showLessButton.addClickHandler(new ClickHandler() {
       public void onClick(ClickEvent event) {
-        int pageSize = Math.max(listView.getPageSize() - increment, increment);
-        listView.setPageSize(pageSize);
+        Range range = listView.getRange();
+        int pageSize = Math.max(range.getLength() - increment, increment);
+        listView.setRange(range.getStart(), pageSize);
       }
     });
 
@@ -79,8 +82,9 @@
 
   public void onRangeOrSizeChanged(PagingListView<T> listView) {
     // Assumes a page start index of 0.
-    boolean hasLess = listView.getPageSize() > increment;
-    boolean hasMore = listView.getPageSize() < listView.getDataSize();
+    int pageSize = listView.getRange().getLength();
+    boolean hasLess = pageSize > increment;
+    boolean hasMore = pageSize < listView.getDataSize();
     showLessButton.setVisible(hasLess);
     showMoreButton.setVisible(hasMore);
     layout.setText(0, 1, (hasLess && hasMore) ? " | " : "");
diff --git a/user/src/com/google/gwt/user/cellview/client/PagingListViewPresenter.java b/user/src/com/google/gwt/user/cellview/client/PagingListViewPresenter.java
new file mode 100644
index 0000000..59ddc35
--- /dev/null
+++ b/user/src/com/google/gwt/user/cellview/client/PagingListViewPresenter.java
@@ -0,0 +1,680 @@
+/*
+ * 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.dom.client.Document;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.view.client.PagingListView;
+import com.google.gwt.view.client.Range;
+import com.google.gwt.view.client.SelectionModel;
+import com.google.gwt.view.client.SelectionModel.SelectionChangeEvent;
+import com.google.gwt.view.client.SelectionModel.SelectionChangeHandler;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * <p>
+ * Presenter implementation of {@link PagingListView} that presents data for
+ * various cell based widgets. This class contains most of the shared logic used
+ * by these widgets, making it easier to test the common code.
+ * <p>
+ * <p>
+ * In proper MVP design, user code would interact with the presenter. However,
+ * that would complicate the widget code. Instead, each widget owns its own
+ * presenter and contains its own View. The widget forwards commands through to
+ * the presenter, which then updates the widget via the view. This keeps the
+ * user facing API simpler.
+ * <p>
+ * 
+ * @param <T> the data type of items in the list
+ */
+class PagingListViewPresenter<T> implements PagingListView<T> {
+
+  /**
+   * Default iterator over DOM elements.
+   */
+  static class DefaultElementIterator implements ElementIterator {
+    private Element current;
+    private Element next;
+    private final DefaultView<?> view;
+
+    public DefaultElementIterator(DefaultView<?> view, Element first) {
+      this.view = view;
+      next = first;
+    }
+
+    public boolean hasNext() {
+      return next != null;
+    }
+
+    public Element next() {
+      if (!hasNext()) {
+        throw new NoSuchElementException();
+      }
+      current = next;
+      next = next.getNextSiblingElement();
+      return current;
+    }
+
+    public void remove() {
+      throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Set the selection state of the current element.
+     * 
+     * @param selected the selection state
+     * @throws IllegalStateException if {@link #next()} has not been called
+     */
+    public void setSelected(boolean selected) throws IllegalStateException {
+      if (current == null) {
+        throw new IllegalStateException();
+      }
+      view.setSelected(current, selected);
+    }
+  }
+
+  /**
+   * The default implementation of View.
+   * 
+   * @param <T> the data type
+   */
+  abstract static class DefaultView<T> implements View<T> {
+
+    /**
+     * The Element that holds the rendered child items.
+     */
+    private Element childContainer;
+
+    /**
+     * The temporary element use to convert HTML to DOM.
+     */
+    private final Element tmpElem;
+
+    /**
+     * Construct a new View.
+     * 
+     * @param childContainer the element that contains the children
+     */
+    public DefaultView(Element childContainer) {
+      this.childContainer = childContainer;
+      tmpElem = Document.get().createDivElement();
+    }
+
+    public int getChildCount() {
+      return childContainer.getChildCount();
+    }
+
+    public ElementIterator getChildIterator() {
+      return new DefaultElementIterator(this,
+          childContainer.getFirstChildElement());
+    }
+
+    public void onUpdateSelection() {
+    }
+
+    public void replaceAllChildren(List<T> values, String html) {
+      childContainer.setInnerHTML(html);
+    }
+
+    public void replaceChildren(List<T> values, int start, String html) {
+      // Convert the html to DOM elements.
+      Element container = convertToElements(html);
+      int count = container.getChildCount();
+
+      // Get the first element to be replaced.
+      Element toReplace = null;
+      if (start < getChildCount()) {
+        toReplace = childContainer.getChild(start).cast();
+      }
+
+      // Replace the elements.
+      for (int i = 0; i < count; i++) {
+        if (toReplace == null) {
+          // The child will be removed from tmpElem, so always use index 0.
+          childContainer.appendChild(container.getChild(0));
+        } else {
+          Element nextSibling = toReplace.getNextSiblingElement();
+          childContainer.replaceChild(container.getChild(0), toReplace);
+          toReplace = nextSibling;
+        }
+      }
+    }
+
+    /**
+     * Convert the specified HTML into DOM elements and return the parent of the
+     * DOM elements.
+     * 
+     * @param html the HTML to convert
+     * @return the parent element
+     */
+    protected Element convertToElements(String html) {
+      tmpElem.setInnerHTML(html);
+      return tmpElem;
+    }
+
+    /**
+     * Update an element to reflect its selected state.
+     * 
+     * @param elem the element to update
+     * @param selected true if selected, false if not
+     */
+    protected abstract void setSelected(Element elem, boolean selected);
+
+    /**
+     * Replace the child container.
+     * 
+     * @param childContainer the new container
+     */
+    void setChildContainer(Element childContainer) {
+      this.childContainer = childContainer;
+    }
+  }
+
+  /**
+   * An iterator over DOM elements.
+   */
+  static interface ElementIterator extends Iterator<Element> {
+    /**
+     * Set the selection state of the current element.
+     * 
+     * @param selected the selection state
+     * @throws IllegalStateException if {@link #next()} has not been called
+     */
+    void setSelected(boolean selected) throws IllegalStateException;
+  }
+
+  /**
+   * The loading state of the data.
+   */
+  static enum LoadingState {
+    LOADING, // Waiting for data to load.
+    PARTIALLY_LOADED, // Partial page data loaded.
+    LOADED, // All page data loaded.
+    EMPTY; // The data size is 0.
+  }
+
+  /**
+   * The view that this presenter presents.
+   * 
+   * @param <T> the data type
+   */
+  static interface View<T> {
+
+    /**
+     * Check whether or not the cells in the view depend on the selection state.
+     * 
+     * @return true if cells depend on selection, false if not
+     */
+    boolean dependsOnSelection();
+
+    /**
+     * Get the physical child count.
+     * 
+     * @return the child count
+     */
+    int getChildCount();
+
+    /**
+     * Get an iterator over the children of the view.
+     * 
+     * @return the iterator
+     */
+    ElementIterator getChildIterator();
+
+    /**
+     * Called when selection changes.
+     */
+    void onUpdateSelection();
+
+    /**
+     * Construct the HTML that represents the list of values, taking the
+     * selection state into account.
+     * 
+     * @param sb the {@link StringBuilder} to build into
+     * @param values the values to render
+     * @param start the start index that is being rendered
+     * @param selectionModel the {@link SelectionModel}
+     */
+    void render(StringBuilder sb, List<T> values, int start,
+        SelectionModel<? super T> selectionModel);
+
+    /**
+     * Replace all children with the specified html.
+     * 
+     * @param values the values of the new children
+     * @param html the html to render in the child
+     */
+    void replaceAllChildren(List<T> values, String html);
+
+    /**
+     * Convert the specified HTML into DOM elements and replace the existing
+     * elements starting at the specified index. If the number of children
+     * specified exceeds the existing number of children, the remaining children
+     * should be appended.
+     * 
+     * @param values the values of the new children
+     * @param start the start index to be replaced
+     * @param html the HTML to convert
+     */
+    void replaceChildren(List<T> values, int start, String html);
+
+    /**
+     * Set the current loading state of the data.
+     * 
+     * @param state the loading state
+     */
+    void setLoadingState(LoadingState state);
+  }
+
+  /**
+   * The local cache of data in the view. The 0th index in the list corresponds
+   * to the value at pageStart.
+   */
+  private final List<T> data = new ArrayList<T>();
+
+  private int dataSize = Integer.MIN_VALUE;
+  private boolean dataSizeIsExact;
+  private Delegate<T> delegate;
+
+  /**
+   * As an optimization, keep track of the last HTML string that we rendered. If
+   * the contents do not change the next time we render, then we don't have to
+   * set inner html.
+   */
+  private String lastContents = null;
+
+  private final PagingListView<T> listView;
+  private Pager<T> pager;
+  private int pageSize;
+  private int pageStart = 0;
+
+  /**
+   * Set to true when the page start changes, and we need to do a full refresh.
+   */
+  private boolean pageStartChangedSinceRender;
+
+  /**
+   * A local cache of the currently selected rows. We cannot track selected keys
+   * instead because we might end up in an inconsistent state where we render a
+   * subset of a list with duplicate values, styling a value in the subset but
+   * not styling the duplicate value outside of the subset.
+   */
+  private final Set<Integer> selectedRows = new HashSet<Integer>();
+
+  private HandlerRegistration selectionHandler;
+  private SelectionModel<? super T> selectionModel;
+  private final View<T> view;
+
+  /**
+   * Construct a new {@link PagingListViewPresenter}.
+   * 
+   * @param listView the listView that is being presented
+   * @param view the view implementation
+   * @param pageSize the default page size
+   */
+  public PagingListViewPresenter(PagingListView<T> listView, View<T> view,
+      int pageSize) {
+    this.listView = listView;
+    this.view = view;
+    this.pageSize = pageSize;
+    updateLoadingState();
+  }
+
+  /**
+   * Clear the {@link SelectionModel} without updating the view.
+   */
+  public void clearSelectionModel() {
+    if (selectionHandler != null) {
+      selectionHandler.removeHandler();
+      selectionHandler = null;
+    }
+    selectionModel = null;
+  }
+
+  /**
+   * Get the current page size. This is usually the page size, but can be less
+   * if the data size cannot fill the current page.
+   * 
+   * @return the size of the current page
+   */
+  public int getCurrentPageSize() {
+    return Math.min(pageSize, dataSize - pageStart);
+  }
+
+  /**
+   * Get the list of data within the current range. The 0th index corresponds to
+   * the first value on the page. The data may not be complete or may contain
+   * null values.
+   * 
+   * @return the list of data for the current page
+   */
+  public List<T> getData() {
+    return data;
+  }
+
+  /**
+   * Get the overall data size.
+   * 
+   * @return the data size
+   */
+  public int getDataSize() {
+    return dataSize;
+  }
+
+  /**
+   * @return the range of data being displayed
+   */
+  public Range getRange() {
+    return new Range(pageStart, pageSize);
+  }
+
+  public SelectionModel<? super T> getSelectionModel() {
+    return selectionModel;
+  }
+
+  public boolean isDataSizeExact() {
+    return dataSizeIsExact;
+  }
+
+  /**
+   * Redraw the list with the current data.
+   */
+  public void redraw() {
+    lastContents = null;
+    setData(pageStart, data.size(), data);
+  }
+
+  public void setData(int start, int length, List<T> values) {
+    int valuesLength = values.size();
+    int valuesEnd = start + valuesLength;
+
+    // Calculate the bounded start (inclusive) and end index (exclusive).
+    int pageEnd = pageStart + pageSize;
+    int boundedStart = Math.max(start, pageStart);
+    int boundedEnd = Math.min(valuesEnd, pageEnd);
+    if (boundedStart >= boundedEnd) {
+      // The data is out of range for the current page.
+      return;
+    }
+
+    // The data size must be at least as large as the data.
+    if (valuesEnd > dataSize) {
+      dataSize = valuesEnd;
+      onSizeChanged();
+    }
+
+    // Create placeholders up to the specified index.
+    int cacheOffset = Math.max(0, boundedStart - pageStart - data.size());
+    for (int i = 0; i < cacheOffset; i++) {
+      data.add(null);
+    }
+
+    // Insert the new values into the data array.
+    for (int i = boundedStart; i < boundedEnd; i++) {
+      T value = values.get(i - start);
+      int dataIndex = i - pageStart;
+      if (dataIndex < data.size()) {
+        data.set(dataIndex, value);
+      } else {
+        data.add(value);
+      }
+
+      // Update our local cache of selected rows.
+      if (selectionModel != null) {
+        if (value != null && selectionModel.isSelected(value)) {
+          selectedRows.add(i);
+        } else {
+          selectedRows.remove(i);
+        }
+      }
+    }
+
+    // Construct a run of elements within the range of the data and the page.
+    boundedStart = pageStartChangedSinceRender ? pageStart : boundedStart;
+    boundedStart -= cacheOffset;
+    List<T> boundedValues = data.subList(boundedStart - pageStart, boundedEnd
+        - pageStart);
+    int boundedSize = boundedValues.size();
+    StringBuilder sb = new StringBuilder();
+    view.render(sb, boundedValues, boundedStart, selectionModel);
+
+    // Update the loading state.
+    updateLoadingState();
+
+    // Replace the DOM elements with the new rendered cells.
+    int childCount = view.getChildCount();
+    if (boundedStart == pageStart
+        && (boundedSize >= childCount || boundedSize >= getCurrentPageSize())) {
+      // If the contents have not changed, we're done.
+      String newContents = sb.toString();
+      if (!newContents.equals(lastContents)) {
+        lastContents = newContents;
+        view.replaceAllChildren(boundedValues, newContents);
+      }
+    } else {
+      lastContents = null;
+      view.replaceChildren(boundedValues, boundedStart - pageStart,
+          sb.toString());
+    }
+
+    // Reset the pageStartChanged boolean.
+    pageStartChangedSinceRender = false;
+  }
+
+  /**
+   * Set the overall size of the list.
+   * 
+   * @param size the overall size
+   */
+  public void setDataSize(int size, boolean isExact) {
+    if (size == this.dataSize && isExact == this.dataSizeIsExact) {
+      return;
+    }
+    this.dataSize = size;
+    this.dataSizeIsExact = isExact;
+    updateLoadingState();
+
+    // Redraw the current page if it is affected by the new data size.
+    if (updateCachedData()) {
+      redraw();
+    }
+
+    // Update the pager.
+    onSizeChanged();
+  }
+
+  public void setDelegate(Delegate<T> delegate) {
+    this.delegate = delegate;
+  }
+
+  public void setPager(PagingListView.Pager<T> pager) {
+    this.pager = pager;
+  }
+
+  public void setRange(int start, int length) {
+    // Update the page start.
+    boolean pageStartChanged = false;
+    if (pageStart != start) {
+      if (start > pageStart) {
+        int increase = start - pageStart;
+        if (data.size() > increase) {
+          // Remove the data we no longer need.
+          for (int i = 0; i < increase; i++) {
+            data.remove(0);
+          }
+        } else {
+          // We have no overlapping data, so just clear it.
+          data.clear();
+        }
+      } else {
+        int decrease = pageStart - start;
+        if ((data.size() > 0) && (decrease < pageSize)) {
+          // Insert null data at the beginning.
+          for (int i = 0; i < decrease; i++) {
+            data.add(0, null);
+          }
+        } else {
+          // We have no overlapping data, so just clear it.
+          data.clear();
+        }
+      }
+      pageStart = start;
+      pageStartChanged = true;
+      pageStartChangedSinceRender = true;
+    }
+
+    // Update the page size.
+    boolean pageSizeChanged = false;
+    if (pageSize != length) {
+      pageSize = length;
+      pageSizeChanged = true;
+    }
+
+    // Early exit if the range hasn't changed.
+    if (!pageStartChanged && !pageSizeChanged) {
+      return;
+    }
+
+    // Update the loading state.
+    updateLoadingState();
+
+    // Redraw with the existing data.
+    boolean dataStale = updateCachedData();
+    if (pageStartChanged || dataStale) {
+      redraw();
+    }
+
+    // Update the pager.
+    onSizeChanged();
+
+    // Update the delegate with the new range.
+    if (delegate != null) {
+      delegate.onRangeChanged(listView);
+    }
+  }
+
+  public void setSelectionModel(final SelectionModel<? super T> selectionModel) {
+    clearSelectionModel();
+
+    // Set the new selection model.
+    this.selectionModel = selectionModel;
+    if (selectionModel != null) {
+      selectionHandler = selectionModel.addSelectionChangeHandler(new SelectionChangeHandler() {
+        public void onSelectionChange(SelectionChangeEvent event) {
+          updateSelection();
+        }
+      });
+    }
+
+    // Update the current selection state based on the new model.
+    updateSelection();
+  }
+
+  /**
+   * Called when pageStart, pageSize, or data size changes.
+   */
+  private void onSizeChanged() {
+    if (pager != null) {
+      pager.onRangeOrSizeChanged(listView);
+    }
+  }
+
+  /**
+   * Ensure that the cached data is consistent with the data size.
+   * 
+   * @return true if the data was updated, false if not
+   */
+  private boolean updateCachedData() {
+    boolean updated = false;
+    int expectedLastIndex = Math.max(0,
+        Math.min(pageSize, dataSize - pageStart));
+    int lastIndex = data.size() - 1;
+    while (lastIndex >= expectedLastIndex) {
+      data.remove(lastIndex);
+      selectedRows.remove(lastIndex + pageStart);
+      lastIndex--;
+      updated = true;
+    }
+    return updated;
+  }
+
+  /**
+   * Update the loading state of the view based on the data size and page size.
+   */
+  private void updateLoadingState() {
+    int cacheSize = data.size();
+    int curPageSize = isDataSizeExact() ? getCurrentPageSize() : pageSize;
+    if (dataSize == 0) {
+      view.setLoadingState(LoadingState.EMPTY);
+    } else if (cacheSize >= curPageSize) {
+      view.setLoadingState(LoadingState.LOADED);
+    } else if (cacheSize == 0) {
+      view.setLoadingState(LoadingState.LOADING);
+    } else {
+      view.setLoadingState(LoadingState.PARTIALLY_LOADED);
+    }
+  }
+
+  /**
+   * Update the table based on the current selection.
+   */
+  private void updateSelection() {
+    view.onUpdateSelection();
+
+    // Determine if our selection states are stale.
+    boolean dependsOnSelection = view.dependsOnSelection();
+    boolean refreshRequired = false;
+    ElementIterator children = view.getChildIterator();
+    int row = pageStart;
+    for (T value : data) {
+      // Increment the child.
+      if (!children.hasNext()) {
+        break;
+      }
+      children.next();
+
+      // Update the selection state.
+      boolean selected = selectionModel == null ? false
+          : selectionModel.isSelected(value);
+      if (selected != selectedRows.contains(row)) {
+        refreshRequired = true;
+        if (selected) {
+          selectedRows.add(row);
+        } else {
+          selectedRows.remove(row);
+        }
+        if (!dependsOnSelection) {
+          // The cell doesn't depend on selection, so we only need to update
+          // the style.
+          children.setSelected(selected);
+        }
+      }
+      row++;
+    }
+
+    // Redraw the entire list if needed.
+    if (refreshRequired && dependsOnSelection) {
+      redraw();
+    }
+  }
+}
diff --git a/user/src/com/google/gwt/user/cellview/client/SimplePager.java b/user/src/com/google/gwt/user/cellview/client/SimplePager.java
index 1e00ea2..e4d1faa 100644
--- a/user/src/com/google/gwt/user/cellview/client/SimplePager.java
+++ b/user/src/com/google/gwt/user/cellview/client/SimplePager.java
@@ -28,6 +28,7 @@
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.view.client.PagingListView;
+import com.google.gwt.view.client.Range;
 
 /**
  * A pager for controlling a {@link PagingListView} that only supports simple
@@ -51,7 +52,7 @@
      * The disabled "fast forward" image.
      */
     ImageResource simplePagerFastForwardDisabled();
-    
+
     /**
      * The image used to go to the first page.
      */
@@ -135,7 +136,7 @@
     }
     return DEFAULT_RESOURCES;
   }
-  
+
   private final Image fastForward;
 
   private final int fastForwardPages;
@@ -196,7 +197,7 @@
   // Hack for Google I/O demo
   public SimplePager(PagingListView<T> view, TextLocation location) {
     this(view, location, getDefaultResources(), true,
-        1000 / view.getPageSize(), false);
+        1000 / view.getRange().getLength(), false);
   }
 
   /**
@@ -212,8 +213,7 @@
    */
   public SimplePager(final PagingListView<T> view, TextLocation location,
       Resources resources, boolean showFastForwardButton,
-      final int fastForwardPages,
-      boolean showLastPageButton) {
+      final int fastForwardPages, boolean showLastPageButton) {
     super(view);
     this.resources = resources;
     this.showFastForwardButton = showFastForwardButton;
@@ -232,7 +232,8 @@
     // Add handlers.
     fastForward.addClickHandler(new ClickHandler() {
       public void onClick(ClickEvent event) {
-        setPageStart(view.getPageStart() + view.getPageSize() * fastForwardPages);
+        Range range = view.getRange();
+        setPageStart(range.getStart() + range.getLength() * fastForwardPages);
       }
     });
     firstPage.addClickHandler(new ClickHandler() {
@@ -337,14 +338,14 @@
               style.disabledButton());
         }
       }
-      
+
       if (showFastForwardButton) {
         if (hasNextPages(fastForwardPages)) {
-          fastForward.setResource(resources.simplePagerFastForward()); 
+          fastForward.setResource(resources.simplePagerFastForward());
           fastForward.getElement().getParentElement().removeClassName(
-              style.disabledButton()); 
+              style.disabledButton());
         } else {
-          fastForward.setResource(resources.simplePagerFastForwardDisabled()); 
+          fastForward.setResource(resources.simplePagerFastForwardDisabled());
           fastForward.getElement().getParentElement().addClassName(
               style.disabledButton());
         }
@@ -371,8 +372,9 @@
     // Default text is 1 based.
     NumberFormat formatter = NumberFormat.getFormat("#,###");
     PagingListView<T> view = getPagingListView();
-    int pageStart = view.getPageStart() + 1;
-    int pageSize = view.getPageSize();
+    Range range = view.getRange();
+    int pageStart = range.getStart() + 1;
+    int pageSize = range.getLength();
     int dataSize = view.getDataSize();
     int endIndex = Math.min(dataSize, pageStart + pageSize - 1);
     endIndex = Math.max(pageStart, endIndex);
diff --git a/user/src/com/google/gwt/view/client/ListView.java b/user/src/com/google/gwt/view/client/ListView.java
index ef7f16e..56b2cc1 100644
--- a/user/src/com/google/gwt/view/client/ListView.java
+++ b/user/src/com/google/gwt/view/client/ListView.java
@@ -36,15 +36,17 @@
   public interface Delegate<T> {
     void onRangeChanged(ListView<T> listView);
   }
-  
+
   /**
-   * Returns the value of the 'isExact' parameter of the most recent call
-   * to {@link #setDataSize(int, boolean)}.
+   * Returns the value of the 'isExact' parameter of the most recent call to
+   * {@link #setDataSize(int, boolean)}.
    */
   boolean isDataSizeExact();
 
   /**
-   * TODO: doc.
+   * Get the range that this view is displaying.
+   * 
+   * @return the range
    */
   Range getRange();
 
@@ -58,17 +60,17 @@
   void setData(int start, int length, List<T> values);
 
   /**
-   * TODO: doc.
+   * Set the total data size of the underlying data.
    * 
-   * @param size
-   * @param isExact
+   * @param size the total data size
+   * @param isExact true if the size is exact, false if it is an estimate
    */
   void setDataSize(int size, boolean isExact);
 
   /**
-   * TODO: doc.
+   * Set the {@link Delegate} that responds to changes in the range.
    * 
-   * @param delegate
+   * @param delegate the {@link Delegate}
    */
   void setDelegate(Delegate<T> delegate);
 
diff --git a/user/src/com/google/gwt/view/client/PagingListView.java b/user/src/com/google/gwt/view/client/PagingListView.java
index 0d3e646..9392112 100644
--- a/user/src/com/google/gwt/view/client/PagingListView.java
+++ b/user/src/com/google/gwt/view/client/PagingListView.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
@@ -17,11 +17,11 @@
 
 /**
  * A list view that displays data in 'pages'.
- *
+ * 
  * <p>
  * Note: This class is new and its interface subject to change.
  * </p>
- *
+ * 
  * @param <T> the data type of each row
  */
 public interface PagingListView<T> extends ListView<T> {
@@ -29,7 +29,7 @@
   /**
    * A pager delegate, implemented by classes that depend on the start index,
    * number of visible rows, or data size of a view.
-   *
+   * 
    * @param <T> the data type of each row
    */
   public interface Pager<T> {
@@ -37,38 +37,22 @@
   }
 
   /**
-   * TODO: doc.
+   * Get the total data size.
    */
   int getDataSize();
 
   /**
-   * TODO: doc.
-   */
-  int getPageSize();
-
-  /**
-   * TODO: doc.
-   */
-  int getPageStart();
-
-  /**
-   * TODO: doc.
+   * Set the {@link Pager} that allows the user to change the range.
    * 
-   * @param pager
+   * @param pager the {@link Pager}
    */
   void setPager(Pager<T> pager);
 
   /**
-   * TODO: doc.
+   * Set a new range.
    * 
-   * @param pageSize
+   * @param start the new start index
+   * @param length the new page size
    */
-  void setPageSize(int pageSize);
-
-  /**
-   * TODO: doc.
-   * 
-   * @param pageStart
-   */
-  void setPageStart(int pageStart);
+  void setRange(int start, int length);
 }
diff --git a/user/test/com/google/gwt/user/cellview/CellViewSuite.java b/user/test/com/google/gwt/user/cellview/CellViewSuite.java
new file mode 100644
index 0000000..ec2a040
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/CellViewSuite.java
@@ -0,0 +1,37 @@
+/*
+ * 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;
+
+import com.google.gwt.junit.tools.GWTTestSuite;
+import com.google.gwt.user.cellview.client.AbstractPagerTest;
+import com.google.gwt.user.cellview.client.PagingListViewPresenterTest;
+import com.google.gwt.user.cellview.client.SimplePagerTest;
+
+import junit.framework.Test;
+
+/**
+ * Tests of the cellview package.
+ */
+public class CellViewSuite {
+  public static Test suite() {
+    GWTTestSuite suite = new GWTTestSuite("Test suite for all cellview classes");
+
+    suite.addTestSuite(AbstractPagerTest.class);
+    suite.addTestSuite(PagingListViewPresenterTest.class);
+    suite.addTestSuite(SimplePagerTest.class);
+    return suite;
+  }
+}
diff --git a/user/test/com/google/gwt/user/cellview/client/AbstractPagerTest.java b/user/test/com/google/gwt/user/cellview/client/AbstractPagerTest.java
new file mode 100644
index 0000000..a59fdef
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/client/AbstractPagerTest.java
@@ -0,0 +1,223 @@
+/*
+ * 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.junit.client.GWTTestCase;
+import com.google.gwt.view.client.MockPagingListView;
+import com.google.gwt.view.client.PagingListView;
+import com.google.gwt.view.client.Range;
+
+/**
+ * Tests for {@link AbstractPager}.
+ */
+public class AbstractPagerTest extends GWTTestCase {
+
+  /**
+   * Mock {@link PagingListView.Pager} used for testing.
+   */
+  private class MockPager<T> extends AbstractPager<T> {
+    public MockPager(PagingListView<T> view) {
+      super(view);
+    }
+  }
+
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.user.cellview.CellView";
+  }
+
+  public void testFirstPage() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setRange(14, 20);
+
+    pager.firstPage();
+    assertEquals(new Range(0, 20), view.getRange());
+  }
+
+  public void testGetPage() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+
+    // Exact page.
+    view.setRange(0, 20);
+    assertEquals(0, pager.getPage());
+    view.setRange(200, 20);
+    assertEquals(10, pager.getPage());
+
+    // Inexact page.
+    view.setRange(1, 20);
+    assertEquals(1, pager.getPage());
+    view.setRange(205, 20);
+    assertEquals(11, pager.getPage());
+  }
+
+  public void testGetPageCount() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setRange(0, 20);
+
+    // Perfect count.
+    view.setDataSize(100, true);
+    assertEquals(5, pager.getPageCount());
+
+    // Imperfect page.
+    view.setDataSize(105, true);
+    assertEquals(6, pager.getPageCount());
+  }
+
+  public void testHasNextPage() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setRange(0, 20);
+
+    view.setDataSize(20, true);
+    assertFalse(pager.hasNextPage());
+    assertFalse(pager.hasNextPages(1));
+
+    view.setDataSize(105, true);
+    assertTrue(pager.hasNextPage());
+    assertTrue(pager.hasNextPages(5));
+    assertFalse(pager.hasNextPages(6));
+  }
+
+  public void testHasPage() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setRange(0, 20);
+    view.setDataSize(105, true);
+
+    assertTrue(pager.hasPage(0));
+    assertTrue(pager.hasPage(5));
+    assertFalse(pager.hasPage(6));
+  }
+
+  public void testHasPreviousPage() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setDataSize(105, true);
+
+    view.setRange(0, 20);
+    assertFalse(pager.hasPreviousPage());
+    assertFalse(pager.hasPreviousPages(1));
+
+    view.setRange(40, 20);
+    assertTrue(pager.hasPreviousPage());
+    assertTrue(pager.hasPreviousPages(2));
+    assertFalse(pager.hasPreviousPages(3));
+
+    view.setRange(41, 20);
+    assertTrue(pager.hasPreviousPage());
+    assertTrue(pager.hasPreviousPages(3));
+    assertFalse(pager.hasPreviousPages(4));
+  }
+
+  public void testLastPage() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setRange(14, 20);
+    view.setDataSize(105, true);
+
+    pager.lastPage();
+    assertEquals(new Range(100, 20), view.getRange());
+  }
+
+  public void testLastPageStart() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    pager.setRangeLimited(false);
+    view.setRange(14, 20);
+    view.setDataSize(105, true);
+
+    pager.lastPageStart();
+    assertEquals(new Range(85, 20), view.getRange());
+  }
+
+  public void testNextPage() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setRange(10, 20);
+    view.setDataSize(105, true);
+
+    pager.nextPage();
+    assertEquals(new Range(30, 20), view.getRange());
+  }
+
+  public void testPreviousPage() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setRange(45, 20);
+    view.setDataSize(105, true);
+
+    pager.previousPage();
+    assertEquals(new Range(25, 20), view.getRange());
+  }
+
+  public void testSetPage() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setRange(10, 20);
+    view.setDataSize(105, true);
+
+    pager.setPage(0);
+    assertEquals(new Range(0, 20), view.getRange());
+
+    pager.setPage(3);
+    assertEquals(new Range(60, 20), view.getRange());
+
+    pager.setPage(5);
+    assertEquals(new Range(100, 20), view.getRange());
+  }
+
+  public void testSetPageStart() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setRange(10, 20);
+    view.setDataSize(105, true);
+
+    pager.setPageStart(0);
+    assertEquals(new Range(0, 20), view.getRange());
+
+    pager.setPageStart(45);
+    assertEquals(new Range(45, 20), view.getRange());
+
+    pager.setPageStart(100);
+    assertEquals(new Range(85, 20), view.getRange());
+  }
+
+  public void testSetRangeLimited() {
+    AbstractPager<Void> pager = createPager();
+    PagingListView<Void> view = pager.getPagingListView();
+    view.setDataSize(110, true);
+    view.setRange(70, 20);
+
+    // Invalid ranges should be constrained by default.
+    assertTrue(pager.isRangeLimited());
+    view.setDataSize(84, true);
+    assertEquals(new Range(64, 20), view.getRange());
+
+    // Allow invalid ranges.
+    pager.setRangeLimited(false);
+    assertFalse(pager.isRangeLimited());
+    view.setRange(50, 20);
+    view.setDataSize(10, true);
+    assertEquals(new Range(50, 20), view.getRange());
+  }
+
+  protected <R> AbstractPager<R> createPager() {
+    return new MockPager<R>(new MockPagingListView<R>());
+  }
+}
diff --git a/user/test/com/google/gwt/user/cellview/client/PagingListViewPresenterTest.java b/user/test/com/google/gwt/user/cellview/client/PagingListViewPresenterTest.java
new file mode 100644
index 0000000..7d4d182
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/client/PagingListViewPresenterTest.java
@@ -0,0 +1,803 @@
+/*
+ * 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.dom.client.Element;
+import com.google.gwt.user.cellview.client.PagingListViewPresenter.ElementIterator;
+import com.google.gwt.user.cellview.client.PagingListViewPresenter.LoadingState;
+import com.google.gwt.user.cellview.client.PagingListViewPresenter.View;
+import com.google.gwt.view.client.MockPagingListView;
+import com.google.gwt.view.client.MockSelectionModel;
+import com.google.gwt.view.client.PagingListView;
+import com.google.gwt.view.client.Range;
+import com.google.gwt.view.client.SelectionModel;
+import com.google.gwt.view.client.MockPagingListView.MockDelegate;
+import com.google.gwt.view.client.MockPagingListView.MockPager;
+
+import junit.framework.TestCase;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * Tests for {@link PagingListViewPresenter}.
+ */
+public class PagingListViewPresenterTest extends TestCase {
+
+  /**
+   * Mock iterator over DOM elements.
+   */
+  private static class MockElementIterator implements ElementIterator {
+
+    private final int count;
+    private int next = 0;
+    private final MockView<?> view;
+
+    public MockElementIterator(MockView<?> view, int count) {
+      this.view = view;
+      this.count = count;
+    }
+
+    public boolean hasNext() {
+      return next < count;
+    }
+
+    public Element next() {
+      if (!hasNext()) {
+        throw new NoSuchElementException();
+      }
+      next++;
+      return null;
+    }
+
+    public void remove() {
+      throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Set the selection state of the current element.
+     * 
+     * @param selected the selection state
+     * @throws IllegalStateException if {@link #next()} has not been called
+     */
+    public void setSelected(boolean selected) throws IllegalStateException {
+      if (next == 0) {
+        throw new IllegalStateException();
+      }
+      view.setSelected(next - 1, selected);
+    }
+  }
+
+  /**
+   * A mock view used for testing.
+   * 
+   * @param <T> the data type
+   */
+  private static class MockView<T> implements View<T> {
+
+    private int childCount;
+    private boolean dependsOnSelection;
+    private String lastHtml;
+    private LoadingState loadingState;
+    private boolean onUpdateSelectionFired;
+    private boolean replaceAllChildrenCalled;
+    private boolean replaceChildrenCalled;
+    private Set<Integer> selectedRows = new HashSet<Integer>();
+
+    public void assertLastHtml(String html) {
+      assertEquals(html, lastHtml);
+      lastHtml = null;
+    }
+
+    public void assertLoadingState(LoadingState expected) {
+      assertEquals(expected, loadingState);
+    }
+
+    public void assertOnUpdateSelectionFired(boolean expected) {
+      assertEquals(expected, onUpdateSelectionFired);
+      onUpdateSelectionFired = false;
+    }
+
+    public void assertReplaceAllChildrenCalled(boolean expected) {
+      assertEquals(expected, replaceAllChildrenCalled);
+      replaceAllChildrenCalled = false;
+    }
+
+    public void assertReplaceChildrenCalled(boolean expected) {
+      assertEquals(expected, replaceChildrenCalled);
+      replaceChildrenCalled = false;
+    }
+
+    /**
+     * Assert that {@link #setSelected(int, boolean)} was called for the
+     * specified rows.
+     * 
+     * @param rows the rows
+     */
+    public void assertSelectedRows(Integer... rows) {
+      assertEquals(rows.length, selectedRows.size());
+      for (Integer row : rows) {
+        assertTrue("Row " + row + "is not selected", selectedRows.contains(row));
+      }
+    }
+
+    public boolean dependsOnSelection() {
+      return dependsOnSelection;
+    }
+
+    public int getChildCount() {
+      return childCount;
+    }
+
+    public MockElementIterator getChildIterator() {
+      return new MockElementIterator(this, 10);
+    }
+
+    public void onUpdateSelection() {
+      onUpdateSelectionFired = true;
+    }
+
+    public void render(StringBuilder sb, List<T> values, int start,
+        SelectionModel<? super T> selectionModel) {
+      sb.append("start=").append(start);
+      sb.append(",size=").append(values.size());
+    }
+
+    public void replaceAllChildren(List<T> values, String html) {
+      childCount = values.size();
+      replaceAllChildrenCalled = true;
+      lastHtml = html;
+    }
+
+    public void replaceChildren(List<T> values, int start, String html) {
+      childCount = Math.max(childCount, start + values.size());
+      replaceChildrenCalled = true;
+      lastHtml = html;
+    }
+
+    public void setDependsOnSelection(boolean dependsOnSelection) {
+      this.dependsOnSelection = dependsOnSelection;
+    }
+
+    public void setLoadingState(LoadingState state) {
+      this.loadingState = state;
+    }
+
+    protected void setSelected(int index, boolean selected) {
+      if (selected) {
+        selectedRows.add(index);
+      } else {
+        selectedRows.remove(index);
+      }
+    }
+  }
+
+  public void testClearSelectionModel() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    view.setDependsOnSelection(true);
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    assertNull(presenter.getSelectionModel());
+
+    // Initialize some data.
+    presenter.setData(0, 10, createData(0, 10));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+
+    // Set the selection model.
+    SelectionModel<String> model = new MockSelectionModel<String>();
+    model.setSelected("test 0", true);
+    presenter.setSelectionModel(model);
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertOnUpdateSelectionFired(true);
+    view.assertSelectedRows();
+
+    // Clear the selection model without updating the view.
+    presenter.clearSelectionModel();
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertOnUpdateSelectionFired(false);
+    view.assertSelectedRows();
+  }
+
+  public void testGetCurrentPageSize() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    presenter.setDataSize(35, true);
+
+    // First page.
+    assertEquals(10, presenter.getCurrentPageSize());
+
+    // Last page.
+    presenter.setRange(30, 10);
+    assertEquals(5, presenter.getCurrentPageSize());
+  }
+
+  public void testRedraw() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+
+    // Initialize some data.
+    presenter.setDataSize(10, true);
+    presenter.setData(0, 10, createData(0, 10));
+    assertEquals(10, presenter.getData().size());
+    assertEquals("test 0", presenter.getData().get(0));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Redraw.
+    presenter.redraw();
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertLoadingState(LoadingState.LOADED);
+  }
+
+  public void testSetData() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    presenter.setRange(5, 10);
+    view.assertLoadingState(LoadingState.LOADING);
+
+    // Page range same as data range.
+    List<String> expectedData = createData(5, 10);
+    presenter.setData(5, 10, createData(5, 10));
+    assertEquals(10, presenter.getData().size());
+    assertEquals(expectedData, presenter.getData());
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=5,size=10");
+    assertEquals(10, view.getChildCount());
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Page range contains data range.
+    expectedData.set(2, "test 100");
+    expectedData.set(3, "test 101");
+    presenter.setData(7, 2, createData(100, 2));
+    assertEquals(10, presenter.getData().size());
+    assertEquals(expectedData, presenter.getData());
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(true);
+    view.assertLastHtml("start=7,size=2");
+    assertEquals(10, view.getChildCount());
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Data range overlaps page start.
+    expectedData.set(0, "test 202");
+    expectedData.set(1, "test 203");
+    presenter.setData(3, 4, createData(200, 4));
+    assertEquals(10, presenter.getData().size());
+    assertEquals(expectedData, presenter.getData());
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(true);
+    view.assertLastHtml("start=5,size=2");
+    assertEquals(10, view.getChildCount());
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Data range overlaps page end.
+    expectedData.set(8, "test 300");
+    expectedData.set(9, "test 301");
+    presenter.setData(13, 4, createData(300, 4));
+    assertEquals(10, presenter.getData().size());
+    assertEquals(expectedData, presenter.getData());
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(true);
+    view.assertLastHtml("start=13,size=2");
+    assertEquals(10, view.getChildCount());
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Data range contains page range.
+    expectedData = createData(400, 20).subList(2, 12);
+    presenter.setData(3, 20, createData(400, 20));
+    assertEquals(10, presenter.getData().size());
+    assertEquals(expectedData, presenter.getData());
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=5,size=10");
+    assertEquals(10, view.getChildCount());
+    view.assertLoadingState(LoadingState.LOADED);
+  }
+
+  /**
+   * Setting data outside of the data size should update the data size.
+   */
+  public void testSetDataChangesDataSize() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+
+    // Set the initial data size.
+    presenter.setDataSize(10, true);
+    view.assertLoadingState(LoadingState.LOADING);
+
+    // Set the data within the range.
+    presenter.setData(0, 10, createData(0, 10));
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Set the data past the range.
+    presenter.setData(5, 10, createData(5, 10));
+    assertEquals(15, presenter.getDataSize());
+    view.assertLoadingState(LoadingState.LOADED);
+  }
+
+  public void testSetDataOutsideRange() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    presenter.setRange(5, 10);
+    view.assertLoadingState(LoadingState.LOADING);
+
+    // Page range same as data range.
+    List<String> expectedData = createData(5, 10);
+    presenter.setData(5, 10, createData(5, 10));
+    assertEquals(10, presenter.getData().size());
+    assertEquals(expectedData, presenter.getData());
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=5,size=10");
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Data range past page end.
+    presenter.setData(15, 5, createData(15, 5));
+    assertEquals(10, presenter.getData().size());
+    assertEquals(expectedData, presenter.getData());
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Data range before page start.
+    presenter.setData(0, 5, createData(0, 5));
+    assertEquals(10, presenter.getData().size());
+    assertEquals(expectedData, presenter.getData());
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertLoadingState(LoadingState.LOADED);
+  }
+
+  /**
+   * As an optimization, the presenter does not replace the rendered string if
+   * the rendered string is identical to the previously rendered string. This is
+   * useful for tables that refresh on an interval.
+   */
+  public void testSetDataSameContents() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    view.assertLoadingState(LoadingState.LOADING);
+
+    // Initialize some data.
+    presenter.setRange(0, 10);
+    presenter.setData(0, 10, createData(0, 10));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Set the same data over the entire range.
+    presenter.setData(0, 10, createData(0, 10));
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertLoadingState(LoadingState.LOADED);
+  }
+
+  /**
+   * Set data at the end of the page only.
+   */
+  public void testSetDataSparse() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    view.assertLoadingState(LoadingState.LOADING);
+
+    List<String> expectedData = createData(5, 3);
+    expectedData.add(0, null);
+    expectedData.add(0, null);
+    expectedData.add(0, null);
+    expectedData.add(0, null);
+    expectedData.add(0, null);
+    presenter.setRange(0, 10);
+    presenter.setData(5, 3, createData(5, 3));
+    assertEquals(8, presenter.getData().size());
+    assertEquals(expectedData, presenter.getData());
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=8");
+    view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
+  }
+
+  public void testSetDataSize() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    view.assertLoadingState(LoadingState.LOADING);
+
+    // Set size to 100.
+    presenter.setDataSize(100, true);
+    assertEquals(100, presenter.getDataSize());
+    assertTrue(presenter.isDataSizeExact());
+    view.assertLoadingState(LoadingState.LOADING);
+
+    // Set size to 0.
+    presenter.setDataSize(0, false);
+    assertEquals(0, presenter.getDataSize());
+    assertFalse(presenter.isDataSizeExact());
+    view.assertLoadingState(LoadingState.EMPTY);
+  }
+
+  public void testSetDataSizeTrimsCurrentPage() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    view.assertLoadingState(LoadingState.LOADING);
+
+    // Initialize some data.
+    presenter.setDataSize(10, true);
+    presenter.setRange(0, 10);
+    assertEquals(new Range(0, 10), presenter.getRange());
+    presenter.setData(0, 10, createData(0, 10));
+    assertEquals(10, presenter.getData().size());
+    assertEquals("test 0", presenter.getData().get(0));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Trim the size.
+    presenter.setDataSize(8, true);
+    assertEquals(8, presenter.getDataSize());
+    assertTrue(presenter.isDataSizeExact());
+    assertEquals(new Range(0, 10), presenter.getRange());
+    assertEquals(8, presenter.getData().size());
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=8");
+    view.assertLoadingState(LoadingState.LOADED);
+  }
+
+  public void testSetDelegate() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    MockDelegate<String> delegate = new MockDelegate<String>();
+    presenter.setDelegate(delegate);
+
+    // Change the pageStart.
+    presenter.setRange(10, 10);
+    assertEquals(listView, delegate.getLastListView());
+    delegate.clearListView();
+
+    // Change the pageSize.
+    presenter.setRange(10, 20);
+    assertEquals(listView, delegate.getLastListView());
+    delegate.clearListView();
+
+    // Reuse the same range.
+    presenter.setRange(10, 20);
+    assertNull(delegate.getLastListView());
+
+    // Change the data size, which does not affect the delegate.
+    presenter.setDataSize(100, true);
+    assertNull(delegate.getLastListView());
+
+    // Unset the delegate.
+    presenter.setDelegate(null);
+    presenter.setRange(20, 100);
+    assertNull(delegate.getLastListView());
+  }
+
+  public void testSetPager() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    MockPager<String> pager = new MockPager<String>();
+    presenter.setPager(pager);
+
+    // Change the pageStart.
+    presenter.setRange(10, 10);
+    assertEquals(listView, pager.getLastListView());
+    pager.clearListView();
+
+    // Change the pageSize.
+    presenter.setRange(10, 20);
+    assertEquals(listView, pager.getLastListView());
+    pager.clearListView();
+
+    // Reuse the same range.
+    presenter.setRange(10, 20);
+    assertNull(pager.getLastListView());
+
+    // Change the data size.
+    presenter.setDataSize(100, true);
+    assertEquals(listView, pager.getLastListView());
+    pager.clearListView();
+
+    // Unset the delegate.
+    presenter.setPager(null);
+    presenter.setRange(20, 100);
+    assertNull(pager.getLastListView());
+  }
+
+  public void testSetRange() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+
+    // Set the range the first time.
+    presenter.setRange(0, 100);
+    assertEquals(new Range(0, 100), presenter.getRange());
+    assertEquals(0, presenter.getData().size());
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertLoadingState(LoadingState.LOADING);
+
+    // Set the range to the same value.
+    presenter.setRange(0, 100);
+    assertEquals(new Range(0, 100), presenter.getRange());
+    assertEquals(0, presenter.getData().size());
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertLoadingState(LoadingState.LOADING);
+  }
+
+  public void testSetRangeDecreasePageSize() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+
+    // Initialize some data.
+    presenter.setRange(0, 10);
+    assertEquals(new Range(0, 10), presenter.getRange());
+    presenter.setData(0, 10, createData(0, 10));
+    assertEquals(10, presenter.getData().size());
+    assertEquals("test 0", presenter.getData().get(0));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Decrease the page size.
+    presenter.setRange(0, 8);
+    assertEquals(new Range(0, 8), presenter.getRange());
+    assertEquals(8, presenter.getData().size());
+    assertEquals("test 0", presenter.getData().get(0));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=8");
+    view.assertLoadingState(LoadingState.LOADED);
+  }
+
+  public void testSetRangeDecreasePageStart() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+
+    // Initialize some data.
+    presenter.setRange(10, 30);
+    assertEquals(new Range(10, 30), presenter.getRange());
+    presenter.setData(10, 10, createData(0, 10));
+    assertEquals(10, presenter.getData().size());
+    assertEquals("test 0", presenter.getData().get(0));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=10,size=10");
+    view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
+
+    // Decrease the start index.
+    presenter.setRange(8, 30);
+    assertEquals(new Range(8, 30), presenter.getRange());
+    assertEquals(12, presenter.getData().size());
+    assertEquals(null, presenter.getData().get(0));
+    assertEquals(null, presenter.getData().get(1));
+    assertEquals("test 0", presenter.getData().get(2));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=8,size=12");
+    view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
+  }
+
+  public void testSetRangeIncreasePageSize() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+
+    // Initialize some data.
+    presenter.setRange(0, 10);
+    assertEquals(new Range(0, 10), presenter.getRange());
+    presenter.setData(0, 10, createData(0, 10));
+    assertEquals(10, presenter.getData().size());
+    assertEquals("test 0", presenter.getData().get(0));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertLoadingState(LoadingState.LOADED);
+
+    // Increase the page size.
+    presenter.setRange(0, 20);
+    assertEquals(new Range(0, 20), presenter.getRange());
+    assertEquals(10, presenter.getData().size());
+    assertEquals("test 0", presenter.getData().get(0));
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
+  }
+
+  public void testSetRangeIncreasePageStart() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+
+    // Initialize some data.
+    presenter.setRange(0, 20);
+    assertEquals(new Range(0, 20), presenter.getRange());
+    presenter.setData(0, 10, createData(0, 10));
+    assertEquals(10, presenter.getData().size());
+    assertEquals("test 0", presenter.getData().get(0));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
+
+    // Increase the start index.
+    presenter.setRange(2, 20);
+    assertEquals(new Range(2, 20), presenter.getRange());
+    assertEquals(8, presenter.getData().size());
+    assertEquals("test 2", presenter.getData().get(0));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=2,size=8");
+    view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
+  }
+
+  /**
+   * If the cells depend on selection, the cells should be replaced.
+   */
+  public void testSetSelectionModelDependOnSelection() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    view.setDependsOnSelection(true);
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    assertNull(presenter.getSelectionModel());
+
+    // Initialize some data.
+    presenter.setRange(0, 10);
+    presenter.setData(0, 10, createData(0, 10));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+
+    // Set the selection model.
+    SelectionModel<String> model = new MockSelectionModel<String>();
+    model.setSelected("test 0", true);
+    presenter.setSelectionModel(model);
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertOnUpdateSelectionFired(true);
+    view.assertSelectedRows();
+
+    // Select something.
+    model.setSelected("test 2", true);
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertOnUpdateSelectionFired(true);
+    view.assertSelectedRows();
+
+    // Set selection model to null.
+    presenter.setSelectionModel(null);
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+    view.assertOnUpdateSelectionFired(true);
+    view.assertSelectedRows();
+  }
+
+  /**
+   * If the cells do not depend on selection, the view should be told to update
+   * the cell container element.
+   */
+  public void testSetSelectionModelDoesNotDependOnSelection() {
+    PagingListView<String> listView = new MockPagingListView<String>();
+    MockView<String> view = new MockView<String>();
+    PagingListViewPresenter<String> presenter = new PagingListViewPresenter<String>(
+        listView, view, 10);
+    assertNull(presenter.getSelectionModel());
+
+    // Initialize some data.
+    presenter.setRange(0, 10);
+    presenter.setData(0, 10, createData(0, 10));
+    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml("start=0,size=10");
+
+    // Set the selection model.
+    SelectionModel<String> model = new MockSelectionModel<String>();
+    model.setSelected("test 0", true);
+    presenter.setSelectionModel(model);
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertOnUpdateSelectionFired(true);
+    view.assertSelectedRows(0);
+
+    // Select something.
+    model.setSelected("test 2", true);
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertOnUpdateSelectionFired(true);
+    view.assertSelectedRows(0, 2);
+
+    // Set selection model to null.
+    presenter.setSelectionModel(null);
+    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenCalled(false);
+    view.assertLastHtml(null);
+    view.assertOnUpdateSelectionFired(true);
+    view.assertSelectedRows();
+  }
+
+  /**
+   * Create a list of data for testing.
+   * 
+   * @param start the start index
+   * @param length the length
+   * @return a list of data
+   */
+  private List<String> createData(int start, int length) {
+    List<String> toRet = new ArrayList<String>();
+    for (int i = 0; i < length; i++) {
+      toRet.add("test " + (i + start));
+    }
+    return toRet;
+  }
+}
diff --git a/user/test/com/google/gwt/user/cellview/client/SimplePagerTest.java b/user/test/com/google/gwt/user/cellview/client/SimplePagerTest.java
new file mode 100644
index 0000000..f866bd0
--- /dev/null
+++ b/user/test/com/google/gwt/user/cellview/client/SimplePagerTest.java
@@ -0,0 +1,29 @@
+/*
+ * 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.MockPagingListView;
+
+/**
+ * Tests for {@link SimplePager}.
+ */
+public class SimplePagerTest extends AbstractPagerTest {
+
+  @Override
+  protected <R> AbstractPager<R> createPager() {
+    return new SimplePager<R>(new MockPagingListView<R>());
+  }
+}
diff --git a/user/test/com/google/gwt/view/ViewSuite.java b/user/test/com/google/gwt/view/ViewSuite.java
index 07fa087..688b460 100644
--- a/user/test/com/google/gwt/view/ViewSuite.java
+++ b/user/test/com/google/gwt/view/ViewSuite.java
@@ -21,6 +21,7 @@
 import com.google.gwt.view.client.AsyncListViewAdapterTest;
 import com.google.gwt.view.client.DefaultNodeInfoTest;
 import com.google.gwt.view.client.DefaultSelectionModelTest;
+import com.google.gwt.view.client.ListViewAdapterTest;
 import com.google.gwt.view.client.MultiSelectionModelTest;
 import com.google.gwt.view.client.RangeTest;
 import com.google.gwt.view.client.SingleSelectionModelTest;
@@ -39,6 +40,7 @@
     suite.addTestSuite(AsyncListViewAdapterTest.class);
     suite.addTestSuite(DefaultNodeInfoTest.class);
     suite.addTestSuite(DefaultSelectionModelTest.class);
+    suite.addTestSuite(ListViewAdapterTest.class);
     suite.addTestSuite(MultiSelectionModelTest.class);
     suite.addTestSuite(RangeTest.class);
     suite.addTestSuite(SingleSelectionModelTest.class);
diff --git a/user/test/com/google/gwt/view/client/ListViewAdapterTest.java b/user/test/com/google/gwt/view/client/ListViewAdapterTest.java
new file mode 100644
index 0000000..a15d187
--- /dev/null
+++ b/user/test/com/google/gwt/view/client/ListViewAdapterTest.java
@@ -0,0 +1,458 @@
+/*
+ * 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.view.client;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+
+/**
+ * Test cases for {@link ListViewAdapter}.
+ */
+public class ListViewAdapterTest extends AbstractListViewAdapterTest {
+
+  public void testConstructorList() {
+    List<String> list = new ArrayList<String>();
+    list.add("helloworld");
+    ListViewAdapter<String> adapter = new ListViewAdapter<String>(list);
+    assertEquals("helloworld", adapter.getList().get(0));
+  }
+
+  public void testFlush() {
+    ListViewAdapter<String> adapter = createListViewAdapter();
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    view.clearLastDataAndRange();
+    view.setDataSize(0, true);
+
+    // Add data to the list.
+    for (int i = 0; i < 10; i++) {
+      list.add("test " + i);
+    }
+    assertEquals(0, view.getDataSize());
+    assertNull(view.getLastData());
+    assertNull(view.getLastDataRange());
+
+    // Flush the data immediately.
+    adapter.flush();
+    assertEquals(10, view.getDataSize());
+    assertTrue(view.isDataSizeExact());
+    assertEquals(list, view.getLastData());
+    assertEquals(new Range(0, 10), view.getLastDataRange());
+  }
+
+  public void testListAdd() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+
+    // add(String).
+    list.add("added");
+    assertEquals("added", list.get(10));
+    adapter.flush();
+    assertEquals(new Range(10, 1), view.getLastDataRange());
+
+    // add(int, String).
+    list.add(2, "inserted");
+    assertEquals("inserted", list.get(2));
+    adapter.flush();
+    assertEquals(new Range(2, 10), view.getLastDataRange());
+  }
+
+  public void testListAddAll() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 25);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+
+    // addAll(Collection).
+    List<String> toAdd = createData(10, 3);
+    list.addAll(toAdd);
+    assertEquals("test 10", list.get(10));
+    assertEquals("test 11", list.get(11));
+    assertEquals("test 12", list.get(12));
+    adapter.flush();
+    assertEquals(toAdd, view.getLastData());
+    assertEquals(new Range(10, 3), view.getLastDataRange());
+
+    // addAll(int, Collection).
+    List<String> toInsert = createData(20, 3);
+    list.addAll(2, toInsert);
+    assertEquals("test 20", list.get(2));
+    assertEquals("test 21", list.get(3));
+    assertEquals("test 22", list.get(4));
+    adapter.flush();
+    assertEquals(new Range(2, 14), view.getLastDataRange());
+  }
+
+  public void testListClear() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    assertEquals(10, list.size());
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+
+    list.clear();
+    assertEquals(0, list.size());
+    adapter.flush();
+    assertEquals(0, view.getDataSize());
+  }
+
+  public void testListContains() {
+    List<String> list = createListViewAdapter(5).getList();
+
+    // contains(Object).
+    assertTrue(list.contains("test 0"));
+    assertFalse(list.contains("platypus"));
+
+    // containsAll(Collection).
+    assertTrue(list.containsAll(createData(1, 2)));
+    assertFalse(list.containsAll(createData(10, 2)));
+  }
+
+  public void testListEquals() {
+    List<String> list = createListViewAdapter(5).getList();
+    assertTrue(list.equals(createData(0, 5)));
+    assertFalse(list.equals(createData(0, 4)));
+  }
+
+  public void testListIndexOf() {
+    List<String> list = createListViewAdapter(5).getList();
+
+    // indexOf(Object).
+    assertEquals(3, list.indexOf("test 3"));
+    assertEquals(-1, list.indexOf("duck"));
+
+    // lastIndexOf(Object).
+    assertEquals(3, list.lastIndexOf("test 3"));
+    assertEquals(-1, list.lastIndexOf("duck"));
+    list.add("test 3");
+    assertEquals(5, list.lastIndexOf("test 3"));
+  }
+
+  public void testListIsEmpty() {
+    List<String> list = createListViewAdapter(0).getList();
+    assertTrue(list.isEmpty());
+
+    list.add("test");
+    assertFalse(list.isEmpty());
+  }
+
+  public void testListIterator() {
+    List<String> list = createListViewAdapter(3).getList();
+    Iterator<String> iterator = list.iterator();
+
+    // Modify before next.
+    try {
+      iterator.remove();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+
+    // next and hasNext.
+    assertTrue(iterator.hasNext());
+    assertEquals("test 0", iterator.next());
+    assertEquals("test 1", iterator.next());
+    assertEquals("test 2", iterator.next());
+    assertFalse(iterator.hasNext());
+
+    // remove.
+    iterator = list.iterator();
+    iterator.next();
+    iterator.remove();
+    assertEquals("test 1", list.get(0));
+    assertEquals(2, list.size());
+    try {
+      iterator.remove();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+    assertEquals("test 1", iterator.next());
+  }
+
+  public void testListListIterator() {
+    List<String> list = createListViewAdapter(3).getList();
+    ListIterator<String> iterator = list.listIterator();
+
+    // Modify before next.
+    try {
+      iterator.set("test");
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+    try {
+      iterator.add("test");
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+    try {
+      iterator.remove();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+
+    // next, hasNext, and nextIndex.
+    assertTrue(iterator.hasNext());
+    assertEquals(0, iterator.nextIndex());
+    assertEquals("test 0", iterator.next());
+    assertEquals("test 1", iterator.next());
+    assertEquals("test 2", iterator.next());
+    assertFalse(iterator.hasNext());
+    assertEquals(3, iterator.nextIndex());
+
+    // previo0us, hasPrevious, and previousIndex.
+    assertTrue(iterator.hasPrevious());
+    assertEquals(2, iterator.previousIndex());
+    assertEquals("test 2", iterator.previous());
+    assertEquals("test 1", iterator.previous());
+    assertEquals("test 0", iterator.previous());
+    assertFalse(iterator.hasPrevious());
+    assertEquals(-1, iterator.previousIndex());
+
+    // set.
+    iterator.set("set0");
+    assertEquals("set0", list.get(0));
+    iterator.set("set1");
+    assertEquals("set1", list.get(0));
+
+    // add.
+    iterator.add("added");
+    assertEquals("added", list.get(0));
+    assertEquals("set1", list.get(1));
+    assertEquals(4, list.size());
+    try {
+      iterator.add("double add");
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+    assertEquals("set1", iterator.next());
+
+    // remove.
+    iterator.remove();
+    assertEquals("test 1", list.get(1));
+    assertEquals(3, list.size());
+    try {
+      iterator.remove();
+      fail("Expected IllegalStateException");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+    assertEquals("added", iterator.previous());
+  }
+
+  public void testListListIteratorAtIndex() {
+    List<String> list = createListViewAdapter(3).getList();
+    ListIterator<String> iterator = list.listIterator(2);
+    assertEquals("test 2", iterator.next());
+  }
+
+  public void testListRemove() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+
+    // remove(int).
+    assertEquals("test 4", list.remove(4));
+    assertEquals("test 5", list.get(4));
+    adapter.flush();
+    assertEquals(new Range(4, 5), view.getLastDataRange());
+
+    // remove(String).
+    assertTrue(list.remove("test 2"));
+    assertEquals("test 3", list.get(2));
+    adapter.flush();
+    assertEquals(new Range(2, 6), view.getLastDataRange());
+
+    // remove(String)
+    assertFalse(list.remove("not in list"));
+  }
+
+  public void testListRemoveAll() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+
+    List<String> toRemove = createData(2, 3);
+    assertTrue(list.removeAll(toRemove));
+    assertEquals(7, list.size());
+    assertEquals("test 5", list.get(2));
+    adapter.flush();
+    assertEquals(new Range(0, 7), view.getLastDataRange());
+
+    assertFalse(list.removeAll(toRemove));
+  }
+
+  public void testListRetainAll() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+
+    List<String> toRetain = createData(2, 3);
+    assertTrue(list.retainAll(toRetain));
+    assertEquals(3, list.size());
+    assertEquals("test 2", list.get(0));
+    adapter.flush();
+    assertEquals(new Range(0, 3), view.getLastDataRange());
+  }
+
+  public void testListSet() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+
+    list.set(3, "newvalue");
+    assertEquals("newvalue", list.get(3));
+    adapter.flush();
+    assertEquals(new Range(3, 1), view.getLastDataRange());
+  }
+
+  public void testSubList() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+
+    List<String> subList = list.subList(2, 5);
+    assertEquals(3, subList.size());
+
+    subList.set(0, "test");
+    assertEquals("test", subList.get(0));
+    assertEquals("test", list.get(2));
+    adapter.flush();
+    assertEquals(new Range(2, 1), view.getLastDataRange());
+  }
+
+  public void testToArray() {
+    List<String> list = createListViewAdapter(3).getList();
+    String[] expected = new String[] {"test 0", "test 1", "test 2"};
+
+    Object[] objects = list.toArray();
+    String[] strings = list.toArray(new String[3]);
+    assertEquals(3, strings.length);
+    assertEquals(3, objects.length);
+    for (int i = 0; i < 3; i++) {
+      String s = expected[i];
+      assertEquals(s, objects[i]);
+      assertEquals(s, strings[i]);
+    }
+  }
+
+  public void testListSize() {
+    List<String> list = createListViewAdapter(10).getList();
+    assertEquals(10, list.size());
+  }
+
+  public void testOnRangeChanged() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view0 = new MockPagingListView<String>();
+    MockPagingListView<String> view1 = new MockPagingListView<String>();
+    view0.setRange(0, 15);
+    view1.setRange(0, 15);
+    adapter.addView(view0);
+    adapter.addView(view1);
+    adapter.flush();
+    view0.clearLastDataAndRange();
+    view1.clearLastDataAndRange();
+
+    // Change the range of view0.
+    view0.setRange(0, 12);
+    assertEquals(list, view0.getLastData());
+    assertEquals(new Range(0, 10), view0.getLastDataRange());
+    assertNull(view1.getLastData());
+    assertNull(view1.getLastDataRange());
+  }
+
+  public void testRefresh() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    List<String> list = adapter.getList();
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+
+    // Refresh the view.
+    adapter.refresh();
+    assertEquals(list, view.getLastData());
+    assertEquals(new Range(0, 10), view.getLastDataRange());
+  }
+
+  public void testSetList() {
+    ListViewAdapter<String> adapter = createListViewAdapter(10);
+    MockPagingListView<String> view = new MockPagingListView<String>();
+    view.setRange(0, 15);
+    adapter.addView(view);
+    adapter.flush();
+    view.clearLastDataAndRange();
+    assertEquals("test 0", adapter.getList().get(0));
+
+    List<String> replace = new ArrayList<String>();
+    replace.add("helloworld");
+    adapter.setList(replace);
+    assertEquals("helloworld", adapter.getList().get(0));
+    assertEquals(1, view.getDataSize());
+    assertEquals(replace, view.getLastData());
+    assertEquals(new Range(0, 1), view.getLastDataRange());
+  }
+
+  @Override
+  protected ListViewAdapter<String> createListViewAdapter() {
+    return createListViewAdapter(0);
+  }
+
+  private ListViewAdapter<String> createListViewAdapter(int size) {
+    return new ListViewAdapter<String>(createData(0, size));
+  }
+}
diff --git a/user/test/com/google/gwt/view/client/MockPagingListView.java b/user/test/com/google/gwt/view/client/MockPagingListView.java
index b916ae5..cc0234a 100644
--- a/user/test/com/google/gwt/view/client/MockPagingListView.java
+++ b/user/test/com/google/gwt/view/client/MockPagingListView.java
@@ -30,6 +30,66 @@
 
   private static final int DEFAULT_PAGE_SIZE = 10;
 
+  /**
+   * A mock delegate used for testing.
+   * 
+   * @param <T> the data type of each row
+   */
+  public static class MockDelegate<T> implements Delegate<T> {
+
+    private ListView<T> listView;
+
+    /**
+     * Clear the last list view.
+     */
+    public void clearListView() {
+      this.listView = null;
+    }
+
+    /**
+     * Get the last list view to use the delegate.
+     * 
+     * @return the last {@link ListView}
+     */
+    public ListView<T> getLastListView() {
+      return listView;
+    }
+
+    public void onRangeChanged(ListView<T> listView) {
+      this.listView = listView;
+    }
+  }
+
+  /**
+   * A mock pager used for testing.
+   * 
+   * @param <T> the data type of each row
+   */
+  public static class MockPager<T> implements Pager<T> {
+
+    private ListView<T> listView;
+
+    /**
+     * Clear the last list view.
+     */
+    public void clearListView() {
+      this.listView = null;
+    }
+
+    /**
+     * Get the last list view to use the pager.
+     * 
+     * @return the last {@link ListView}
+     */
+    public ListView<T> getLastListView() {
+      return listView;
+    }
+
+    public void onRangeOrSizeChanged(PagingListView<T> listView) {
+      this.listView = listView;
+    }
+  }
+
   private int dataSize;
   private boolean dataSizeExact;
   private Delegate<T> delegate;
@@ -71,14 +131,6 @@
     return lastRange;
   }
 
-  public int getPageSize() {
-    return pageSize;
-  }
-
-  public int getPageStart() {
-    return pageStart;
-  }
-
   public Range getRange() {
     return new Range(pageStart, pageSize);
   }
@@ -113,14 +165,6 @@
     this.pager = pager;
   }
 
-  public void setPageSize(int pageSize) {
-    setRange(pageStart, pageSize);
-  }
-
-  public void setPageStart(int pageStart) {
-    setRange(pageStart, pageSize);
-  }
-
   public void setRange(int start, int length) {
     if (this.pageStart == start && this.pageSize == length) {
       return;
diff --git a/user/test/com/google/gwt/view/client/MockSelectionModel.java b/user/test/com/google/gwt/view/client/MockSelectionModel.java
new file mode 100644
index 0000000..e612db0
--- /dev/null
+++ b/user/test/com/google/gwt/view/client/MockSelectionModel.java
@@ -0,0 +1,30 @@
+/*
+ * 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.view.client;
+
+/**
+ * A mock {@link SelectionModel} used for testing without used any GWT client
+ * code.
+ * 
+ * @param <T> the selection type
+ */
+public class MockSelectionModel<T> extends MultiSelectionModel<T> {
+
+  @Override
+  protected void scheduleSelectionChangeEvent() {
+    fireSelectionChangeEvent();
+  }
+}