Introducing a new CellTableBuilder API, which allows users to customize the structure of a CellTable by building zero or more table rows per row value and adding zero or more Cells per TD.  The table rows are built using the generic ElementBuilder API, which allows for colspans and rowspans.  The CellTableBuilder interface defines one method #buildRow(T, int, Utility) that takes the row value and row index to build, as well as a Utility class.  The Utility class defines a #startRow() method to append a DOM row and associate it with the row value.  Using this API, you can define multiple rows per row value, such as an "error row" that spans all of the columns, or a set of "child rows" to simulate a tree table.  The Utility also contains a method #renderCell(ElementBuilder, Context, Column, T rowValue) that renders a Cell into the row.  The TableBuilder can be swapped out using setTableBuilder(), and the default tableBuilder renders a grid based on the Columns defined in the CellTable.  This change only applies to the data portion of the CellTable.  A subsequent change will add a similar HeaderBuilder that applies to the header and footer.

This API change required a significant refactor of CellTable because CellTable assumed a grid structure with one row per row value, one cell per column, and a structure consisting of a div inside of a td for every Cell.  Now that the structure is open ended, CellTable sets the ID of the row value on the TR and assigns a unique ID to Cells that is set as a DOM attribute of the cell's parent element.  Instead of looking for the TD that contains the cell, we walk up from the event target looking for the cell ID attribute and the row attribute on the TR. We then look up the cell by its ID in a Map.

In addition, there is an internal concept of "subrows", which occurs when a row value is rendered into multiple rows.  The subrow is now included in the Context that is passed to Cells when they handle events. There is nothing special about rendering one row versus five, we just consider the one row to be the 0th subrow.  The entire set of subrows rendered with a row value are considered a block associated with the row value. If the row value changes, the entire block of subrows is re-rendered as well.

Keyboard selection is also affected, and in particular there are different ways that users might want to handle keyboard selection depending on the existance of subrows.  So, I abstracted the keyboard selection code out to a CellPreviewEvent.Handler, which is how normal Selection works.  This abstraction cleans up some of the code, and it allows users to override how keyboard selection works (say, by changing the keys or actions) using #setKeyboardSelectionHandler().  In addition, we now expose methods like get/setKeyboardSelectionRow() to allow users to programatically change the keyboard selected row.  The default implementation of the handler navigates between interactive cells in the first row of each row value.  It does not navigate into subrows unless the user actually clicks on the subrow.

Currently, TableBuilder is implemented using HtmlBuilder because it still seems faster than DOM manipulation.  I added a TODO to test DomBuilder in more depth.  The TableBuilder API is generic enough that we can switch out the implementation without affecting user code.

There is a legacy code path to provide a decent level of backward compatibility for users who are overriding the protected method AbstractCellTable#renderRowValues(SafeHtmlBuilder), which is now deprecated and no longer used. We still call #renderRowValues(), but we expect it to throw an UnsupportedOperationException, which we detect and continue.  If it does not throw an UnsupportedOperationException, then we pass the rendered html to replaceChildren().  The only way it would not throw an UnsupportedOperationException is if the user overrides #renderRowValues().  I also added an unused method AbstractCellTable#renderRowValuesLegacy() in case users want to revert to the old style.

I also added a sample to Showcase. Demo availabale at http://showcase2.jlabanca-testing.appspot.com/#!CwCustomDataGrid

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

Review by: pengzhuang@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10476 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource.properties b/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource.properties
index c4e88c5..48a6ce7 100644
--- a/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource.properties
+++ b/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource.properties
@@ -300,6 +300,14 @@
 cwCellValidationColumnAddress = Address
 cwCellValidationColumnName = Name
 cwCellValidationError = ERROR: Address must be of the form: ### <street name>
+cwCustomDataGridName = Custom Data Grid
+cwCustomDataGridDescription = Customize the structure of a DataGrid with expandable rows or messages that span the width table.
+cwCustomDataGridColumnAddress = Address
+cwCustomDataGridColumnAge = Age
+cwCustomDataGridColumnCategory = Category
+cwCustomDataGridColumnFirstName = First Name
+cwCustomDataGridColumnLastName = Last Name
+cwCustomDataGridEmpty = There is no data to display
 cwDataGridName = Data Grid
 cwDataGridDescription = Use DataGrid to render large amounts of data in your enterprise application. DataGrid has a fixed header and footer, with a scrollable content area.
 cwDataGridColumnAddress = Address
diff --git a/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_ar.properties b/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_ar.properties
index 4fd36f2..c8e9f9f 100644
--- a/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_ar.properties
+++ b/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_ar.properties
@@ -300,6 +300,14 @@
 cwCellValidationColumnAddress = العنوان
 cwCellValidationColumnName = اس
 cwCellValidationError = يجب أن خطأ : العنوان يكون من النموذج : ### <اسم الشار>
+cwCustomDataGridName = بيانات الشبكة
+cwCustomDataGridDescription = تخصيص بنية DataGrid مع الصفوف توسيع أو الرسائل التي تمتد على طاولة العرض.
+cwCustomDataGridColumnAddress = العنوان
+cwCustomDataGridColumnAge = السن
+cwCustomDataGridColumnCategory = عنوان
+cwCustomDataGridColumnFirstName = أول اسم
+cwCustomDataGridColumnLastName = اسم آخر
+cwCustomDataGridEmpty = لا توجد بيانات للعرض
 cwDataGridName = بيانات الشبكة
 cwDataGridDescription = استخدام DataGrid لتقديم كميات كبيرة من البيانات في تطبيق المؤسسة. DataGrid لديه رأس وتذييل ثابتة، مع تمرير ناحية المحتوى.
 cwDataGridColumnAddress = العنوان
diff --git a/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_fr.properties b/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_fr.properties
index f4b402d..b2fabb8 100644
--- a/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_fr.properties
+++ b/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_fr.properties
@@ -300,6 +300,14 @@
 cwCellValidationColumnAddress = Adresse
 cwCellValidationColumnName = Nom
 cwCellValidationError = Erreur: l'adresse doit être de la forme: ### <nom de la rue>
+cwCustomDataGridName = Data Grid
+cwCustomDataGridDescription = Personnaliser la structure d'un DataGrid avec des lignes extensibles ou des messages qui couvrent la table largeur.
+cwCustomDataGridColumnAddress = Adresse
+cwCustomDataGridColumnAge = Âge
+cwCustomDataGridColumnCategory = Catégorie
+cwCustomDataGridColumnFirstName = Prénom
+cwCustomDataGridColumnLastName = Nom de famille
+cwCustomDataGridEmpty = Il n'y a aucune donnée à afficher
 cwDataGridName = Data Grid
 cwDataGridDescription = Utilisez DataGrid pour rendre de grandes quantités de données dans votre application d'entreprise. DataGrid a un en-tête et pied de page fixe, avec une zone de contenu scrollable.
 cwDataGridColumnAddress = Adresse
diff --git a/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_zh.properties b/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_zh.properties
index b172f28..bf53643 100644
--- a/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_zh.properties
+++ b/samples/showcase/src/com/google/gwt/i18n/client/LocalizableResource_zh.properties
@@ -300,6 +300,14 @@
 cwCellValidationColumnAddress =地址
 cwCellValidationColumnName =名称
 cwCellValidationError =错误:地址必须是形式:### <街道名称>
+cwCustomDataGridName = 数据网格
+cwCustomDataGridDescription = 自定义一个DataGrid的可扩展行或消息跨越的宽度表结构。
+cwCustomDataGridColumnAddress = 地址
+cwCustomDataGridColumnAge = 年龄
+cwCustomDataGridColumnCategory = 分类
+cwCustomDataGridColumnFirstName = 名字
+cwCustomDataGridColumnLastName = 姓氏
+cwCustomDataGridEmpty = 有没有数据显示
 cwDataGridName = 数据网格
 cwDataGridDescription = 使用DataGrid呈现在你的企业应用的大量数据。 DataGrid中有一个固定的报头和页脚可滚动的内容区域。
 cwDataGridColumnAddress = 地址
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/Showcase.gwt.xml b/samples/showcase/src/com/google/gwt/sample/showcase/Showcase.gwt.xml
index e429c36..6dad235 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/Showcase.gwt.xml
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/Showcase.gwt.xml
@@ -20,9 +20,11 @@
 
   <!-- Internationalization support. -->
   <extend-property name="locale" values="en"/>
+  <!--
   <extend-property name="locale" values="ar"/>
   <extend-property name="locale" values="fr"/>
   <extend-property name="locale" values="zh"/>
+  -->
   <set-property-fallback name="locale" value="en"/>
   <set-configuration-property name="locale.cookie" value="SHOWCASE_LOCALE"/>
   <set-configuration-property name="locale.useragent" value="Y"/>
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/MainMenuTreeViewModel.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/MainMenuTreeViewModel.java
index 95e2a4d..06f6322 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/MainMenuTreeViewModel.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/MainMenuTreeViewModel.java
@@ -25,6 +25,7 @@
 import com.google.gwt.sample.showcase.client.content.cell.CwCellTable;
 import com.google.gwt.sample.showcase.client.content.cell.CwCellTree;
 import com.google.gwt.sample.showcase.client.content.cell.CwCellValidation;
+import com.google.gwt.sample.showcase.client.content.cell.CwCustomDataGrid;
 import com.google.gwt.sample.showcase.client.content.cell.CwDataGrid;
 import com.google.gwt.sample.showcase.client.content.i18n.CwBidiFormatting;
 import com.google.gwt.sample.showcase.client.content.i18n.CwBidiInput;
@@ -371,6 +372,8 @@
           RunAsyncCode.runAsyncCode(CwCellTable.class));
       category.addExample(new CwDataGrid(constants),
           RunAsyncCode.runAsyncCode(CwDataGrid.class));
+      category.addExample(new CwCustomDataGrid(constants),
+          RunAsyncCode.runAsyncCode(CwCustomDataGrid.class));
       category.addExample(new CwCellTree(constants),
           RunAsyncCode.runAsyncCode(CwCellTree.class));
       category.addExample(new CwCellBrowser(constants),
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/ShowcaseConstants.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/ShowcaseConstants.java
index be8694a..f676456 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/ShowcaseConstants.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/ShowcaseConstants.java
@@ -22,6 +22,7 @@
 import com.google.gwt.sample.showcase.client.content.cell.CwCellTable;
 import com.google.gwt.sample.showcase.client.content.cell.CwCellTree;
 import com.google.gwt.sample.showcase.client.content.cell.CwCellValidation;
+import com.google.gwt.sample.showcase.client.content.cell.CwCustomDataGrid;
 import com.google.gwt.sample.showcase.client.content.cell.CwDataGrid;
 import com.google.gwt.sample.showcase.client.content.i18n.CwBidiFormatting;
 import com.google.gwt.sample.showcase.client.content.i18n.CwBidiInput;
@@ -83,7 +84,7 @@
     CwPluralFormsExample.CwConstants, CwCellList.CwConstants, CwCellTable.CwConstants,
     CwDataGrid.CwConstants, CwCellTree.CwConstants, CwCellBrowser.CwConstants,
     CwCellValidation.CwConstants, CwCellSampler.CwConstants, CwSplitLayoutPanel.CwConstants,
-    CwStackLayoutPanel.CwConstants {
+    CwStackLayoutPanel.CwConstants, CwCustomDataGrid.CwConstants  {
 
   /**
    * The path to source code for examples, raw files, and style definitions.
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactDatabase.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactDatabase.java
index f375b9e..8ed8165 100644
--- a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactDatabase.java
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/ContactDatabase.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
@@ -24,7 +24,11 @@
 
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /**
  * The data source for contact information used in the sample.
@@ -56,6 +60,7 @@
      * The key provider that provides the unique ID of a contact.
      */
     public static final ProvidesKey<ContactInfo> KEY_PROVIDER = new ProvidesKey<ContactInfo>() {
+      @Override
       public Object getKey(ContactInfo item) {
         return item == null ? null : item.getId();
       }
@@ -64,6 +69,7 @@
     private static int nextId = 0;
 
     private String address;
+    private int age;
     private Date birthday;
     private Category category;
     private String firstName;
@@ -76,9 +82,9 @@
       setCategory(category);
     }
 
+    @Override
     public int compareTo(ContactInfo o) {
-      return (o == null || o.firstName == null) ? -1
-          : -o.firstName.compareTo(firstName);
+      return (o == null || o.firstName == null) ? -1 : -o.firstName.compareTo(firstName);
     }
 
     @Override
@@ -99,14 +105,7 @@
     /**
      * @return the contact's age
      */
-    @SuppressWarnings("deprecation")
     public int getAge() {
-      Date today = new Date();
-      int age = today.getYear() - birthday.getYear();
-      if (today.getMonth() > birthday.getMonth()
-          || (today.getMonth() == birthday.getMonth() && today.getDate() > birthday.getDate())) {
-        age--;
-      }
       return age;
     }
 
@@ -159,7 +158,7 @@
 
     /**
      * Set the contact's address.
-     *
+     * 
      * @param address the address
      */
     public void setAddress(String address) {
@@ -168,16 +167,25 @@
 
     /**
      * Set the contact's birthday.
-     *
+     * 
      * @param birthday the birthday
      */
+    @SuppressWarnings("deprecation")
     public void setBirthday(Date birthday) {
       this.birthday = birthday;
+
+      // Recalculate the age.
+      Date today = new Date();
+      this.age = today.getYear() - birthday.getYear();
+      if (today.getMonth() > birthday.getMonth()
+          || (today.getMonth() == birthday.getMonth() && today.getDate() > birthday.getDate())) {
+        this.age--;
+      }
     }
 
     /**
      * Set the contact's category.
-     *
+     * 
      * @param category the category to set
      */
     public void setCategory(Category category) {
@@ -187,7 +195,7 @@
 
     /**
      * Set the contact's first name.
-     *
+     * 
      * @param firstName the firstName to set
      */
     public void setFirstName(String firstName) {
@@ -196,7 +204,7 @@
 
     /**
      * Set the contact's last name.
-     *
+     * 
      * @param lastName the lastName to set
      */
     public void setLastName(String lastName) {
@@ -212,99 +220,83 @@
   }
 
   private static final String[] FEMALE_FIRST_NAMES = {
-      "Mary", "Patricia", "Linda", "Barbara", "Elizabeth", "Jennifer", "Maria",
-      "Susan", "Margaret", "Dorothy", "Lisa", "Nancy", "Karen", "Betty",
-      "Helen", "Sandra", "Donna", "Carol", "Ruth", "Sharon", "Michelle",
-      "Laura", "Sarah", "Kimberly", "Deborah", "Jessica", "Shirley", "Cynthia",
-      "Angela", "Melissa", "Brenda", "Amy", "Anna", "Rebecca", "Virginia",
-      "Kathleen", "Pamela", "Martha", "Debra", "Amanda", "Stephanie", "Carolyn",
-      "Christine", "Marie", "Janet", "Catherine", "Frances", "Ann", "Joyce",
-      "Diane", "Alice", "Julie", "Heather", "Teresa", "Doris", "Gloria",
-      "Evelyn", "Jean", "Cheryl", "Mildred", "Katherine", "Joan", "Ashley",
-      "Judith", "Rose", "Janice", "Kelly", "Nicole", "Judy", "Christina",
-      "Kathy", "Theresa", "Beverly", "Denise", "Tammy", "Irene", "Jane", "Lori",
-      "Rachel", "Marilyn", "Andrea", "Kathryn", "Louise", "Sara", "Anne",
-      "Jacqueline", "Wanda", "Bonnie", "Julia", "Ruby", "Lois", "Tina",
-      "Phyllis", "Norma", "Paula", "Diana", "Annie", "Lillian", "Emily",
-      "Robin", "Peggy", "Crystal", "Gladys", "Rita", "Dawn", "Connie",
-      "Florence", "Tracy", "Edna", "Tiffany", "Carmen", "Rosa", "Cindy",
-      "Grace", "Wendy", "Victoria", "Edith", "Kim", "Sherry", "Sylvia",
-      "Josephine", "Thelma", "Shannon", "Sheila", "Ethel", "Ellen", "Elaine",
-      "Marjorie", "Carrie", "Charlotte", "Monica", "Esther", "Pauline", "Emma",
-      "Juanita", "Anita", "Rhonda", "Hazel", "Amber", "Eva", "Debbie", "April",
-      "Leslie", "Clara", "Lucille", "Jamie", "Joanne", "Eleanor", "Valerie",
-      "Danielle", "Megan", "Alicia", "Suzanne", "Michele", "Gail", "Bertha",
-      "Darlene", "Veronica", "Jill", "Erin", "Geraldine", "Lauren", "Cathy",
-      "Joann", "Lorraine", "Lynn", "Sally", "Regina", "Erica", "Beatrice",
-      "Dolores", "Bernice", "Audrey", "Yvonne", "Annette", "June", "Samantha",
-      "Marion", "Dana", "Stacy", "Ana", "Renee", "Ida", "Vivian", "Roberta",
-      "Holly", "Brittany", "Melanie", "Loretta", "Yolanda", "Jeanette",
-      "Laurie", "Katie", "Kristen", "Vanessa", "Alma", "Sue", "Elsie", "Beth",
-      "Jeanne"};
+      "Mary", "Patricia", "Linda", "Barbara", "Elizabeth", "Jennifer", "Maria", "Susan",
+      "Margaret", "Dorothy", "Lisa", "Nancy", "Karen", "Betty", "Helen", "Sandra", "Donna",
+      "Carol", "Ruth", "Sharon", "Michelle", "Laura", "Sarah", "Kimberly", "Deborah", "Jessica",
+      "Shirley", "Cynthia", "Angela", "Melissa", "Brenda", "Amy", "Anna", "Rebecca", "Virginia",
+      "Kathleen", "Pamela", "Martha", "Debra", "Amanda", "Stephanie", "Carolyn", "Christine",
+      "Marie", "Janet", "Catherine", "Frances", "Ann", "Joyce", "Diane", "Alice", "Julie",
+      "Heather", "Teresa", "Doris", "Gloria", "Evelyn", "Jean", "Cheryl", "Mildred", "Katherine",
+      "Joan", "Ashley", "Judith", "Rose", "Janice", "Kelly", "Nicole", "Judy", "Christina",
+      "Kathy", "Theresa", "Beverly", "Denise", "Tammy", "Irene", "Jane", "Lori", "Rachel",
+      "Marilyn", "Andrea", "Kathryn", "Louise", "Sara", "Anne", "Jacqueline", "Wanda", "Bonnie",
+      "Julia", "Ruby", "Lois", "Tina", "Phyllis", "Norma", "Paula", "Diana", "Annie", "Lillian",
+      "Emily", "Robin", "Peggy", "Crystal", "Gladys", "Rita", "Dawn", "Connie", "Florence",
+      "Tracy", "Edna", "Tiffany", "Carmen", "Rosa", "Cindy", "Grace", "Wendy", "Victoria", "Edith",
+      "Kim", "Sherry", "Sylvia", "Josephine", "Thelma", "Shannon", "Sheila", "Ethel", "Ellen",
+      "Elaine", "Marjorie", "Carrie", "Charlotte", "Monica", "Esther", "Pauline", "Emma",
+      "Juanita", "Anita", "Rhonda", "Hazel", "Amber", "Eva", "Debbie", "April", "Leslie", "Clara",
+      "Lucille", "Jamie", "Joanne", "Eleanor", "Valerie", "Danielle", "Megan", "Alicia", "Suzanne",
+      "Michele", "Gail", "Bertha", "Darlene", "Veronica", "Jill", "Erin", "Geraldine", "Lauren",
+      "Cathy", "Joann", "Lorraine", "Lynn", "Sally", "Regina", "Erica", "Beatrice", "Dolores",
+      "Bernice", "Audrey", "Yvonne", "Annette", "June", "Samantha", "Marion", "Dana", "Stacy",
+      "Ana", "Renee", "Ida", "Vivian", "Roberta", "Holly", "Brittany", "Melanie", "Loretta",
+      "Yolanda", "Jeanette", "Laurie", "Katie", "Kristen", "Vanessa", "Alma", "Sue", "Elsie",
+      "Beth", "Jeanne"};
   private static final String[] MALE_FIRST_NAMES = {
-      "James", "John", "Robert", "Michael", "William", "David", "Richard",
-      "Charles", "Joseph", "Thomas", "Christopher", "Daniel", "Paul", "Mark",
-      "Donald", "George", "Kenneth", "Steven", "Edward", "Brian", "Ronald",
-      "Anthony", "Kevin", "Jason", "Matthew", "Gary", "Timothy", "Jose",
-      "Larry", "Jeffrey", "Frank", "Scott", "Eric", "Stephen", "Andrew",
-      "Raymond", "Gregory", "Joshua", "Jerry", "Dennis", "Walter", "Patrick",
-      "Peter", "Harold", "Douglas", "Henry", "Carl", "Arthur", "Ryan", "Roger",
-      "Joe", "Juan", "Jack", "Albert", "Jonathan", "Justin", "Terry", "Gerald",
-      "Keith", "Samuel", "Willie", "Ralph", "Lawrence", "Nicholas", "Roy",
-      "Benjamin", "Bruce", "Brandon", "Adam", "Harry", "Fred", "Wayne", "Billy",
-      "Steve", "Louis", "Jeremy", "Aaron", "Randy", "Howard", "Eugene",
-      "Carlos", "Russell", "Bobby", "Victor", "Martin", "Ernest", "Phillip",
-      "Todd", "Jesse", "Craig", "Alan", "Shawn", "Clarence", "Sean", "Philip",
-      "Chris", "Johnny", "Earl", "Jimmy", "Antonio", "Danny", "Bryan", "Tony",
-      "Luis", "Mike", "Stanley", "Leonard", "Nathan", "Dale", "Manuel",
-      "Rodney", "Curtis", "Norman", "Allen", "Marvin", "Vincent", "Glenn",
-      "Jeffery", "Travis", "Jeff", "Chad", "Jacob", "Lee", "Melvin", "Alfred",
-      "Kyle", "Francis", "Bradley", "Jesus", "Herbert", "Frederick", "Ray",
-      "Joel", "Edwin", "Don", "Eddie", "Ricky", "Troy", "Randall", "Barry",
-      "Alexander", "Bernard", "Mario", "Leroy", "Francisco", "Marcus",
-      "Micheal", "Theodore", "Clifford", "Miguel", "Oscar", "Jay", "Jim", "Tom",
-      "Calvin", "Alex", "Jon", "Ronnie", "Bill", "Lloyd", "Tommy", "Leon",
-      "Derek", "Warren", "Darrell", "Jerome", "Floyd", "Leo", "Alvin", "Tim",
-      "Wesley", "Gordon", "Dean", "Greg", "Jorge", "Dustin", "Pedro", "Derrick",
-      "Dan", "Lewis", "Zachary", "Corey", "Herman", "Maurice", "Vernon",
-      "Roberto", "Clyde", "Glen", "Hector", "Shane", "Ricardo", "Sam", "Rick",
-      "Lester", "Brent", "Ramon", "Charlie", "Tyler", "Gilbert", "Gene"};
+      "James", "John", "Robert", "Michael", "William", "David", "Richard", "Charles", "Joseph",
+      "Thomas", "Christopher", "Daniel", "Paul", "Mark", "Donald", "George", "Kenneth", "Steven",
+      "Edward", "Brian", "Ronald", "Anthony", "Kevin", "Jason", "Matthew", "Gary", "Timothy",
+      "Jose", "Larry", "Jeffrey", "Frank", "Scott", "Eric", "Stephen", "Andrew", "Raymond",
+      "Gregory", "Joshua", "Jerry", "Dennis", "Walter", "Patrick", "Peter", "Harold", "Douglas",
+      "Henry", "Carl", "Arthur", "Ryan", "Roger", "Joe", "Juan", "Jack", "Albert", "Jonathan",
+      "Justin", "Terry", "Gerald", "Keith", "Samuel", "Willie", "Ralph", "Lawrence", "Nicholas",
+      "Roy", "Benjamin", "Bruce", "Brandon", "Adam", "Harry", "Fred", "Wayne", "Billy", "Steve",
+      "Louis", "Jeremy", "Aaron", "Randy", "Howard", "Eugene", "Carlos", "Russell", "Bobby",
+      "Victor", "Martin", "Ernest", "Phillip", "Todd", "Jesse", "Craig", "Alan", "Shawn",
+      "Clarence", "Sean", "Philip", "Chris", "Johnny", "Earl", "Jimmy", "Antonio", "Danny",
+      "Bryan", "Tony", "Luis", "Mike", "Stanley", "Leonard", "Nathan", "Dale", "Manuel", "Rodney",
+      "Curtis", "Norman", "Allen", "Marvin", "Vincent", "Glenn", "Jeffery", "Travis", "Jeff",
+      "Chad", "Jacob", "Lee", "Melvin", "Alfred", "Kyle", "Francis", "Bradley", "Jesus", "Herbert",
+      "Frederick", "Ray", "Joel", "Edwin", "Don", "Eddie", "Ricky", "Troy", "Randall", "Barry",
+      "Alexander", "Bernard", "Mario", "Leroy", "Francisco", "Marcus", "Micheal", "Theodore",
+      "Clifford", "Miguel", "Oscar", "Jay", "Jim", "Tom", "Calvin", "Alex", "Jon", "Ronnie",
+      "Bill", "Lloyd", "Tommy", "Leon", "Derek", "Warren", "Darrell", "Jerome", "Floyd", "Leo",
+      "Alvin", "Tim", "Wesley", "Gordon", "Dean", "Greg", "Jorge", "Dustin", "Pedro", "Derrick",
+      "Dan", "Lewis", "Zachary", "Corey", "Herman", "Maurice", "Vernon", "Roberto", "Clyde",
+      "Glen", "Hector", "Shane", "Ricardo", "Sam", "Rick", "Lester", "Brent", "Ramon", "Charlie",
+      "Tyler", "Gilbert", "Gene"};
   private static final String[] LAST_NAMES = {
-      "Smith", "Johnson", "Williams", "Jones", "Brown", "Davis", "Miller",
-      "Wilson", "Moore", "Taylor", "Anderson", "Thomas", "Jackson", "White",
-      "Harris", "Martin", "Thompson", "Garcia", "Martinez", "Robinson", "Clark",
-      "Rodriguez", "Lewis", "Lee", "Walker", "Hall", "Allen", "Young",
-      "Hernandez", "King", "Wright", "Lopez", "Hill", "Scott", "Green", "Adams",
-      "Baker", "Gonzalez", "Nelson", "Carter", "Mitchell", "Perez", "Roberts",
-      "Turner", "Phillips", "Campbell", "Parker", "Evans", "Edwards", "Collins",
-      "Stewart", "Sanchez", "Morris", "Rogers", "Reed", "Cook", "Morgan",
-      "Bell", "Murphy", "Bailey", "Rivera", "Cooper", "Richardson", "Cox",
-      "Howard", "Ward", "Torres", "Peterson", "Gray", "Ramirez", "James",
-      "Watson", "Brooks", "Kelly", "Sanders", "Price", "Bennett", "Wood",
-      "Barnes", "Ross", "Henderson", "Coleman", "Jenkins", "Perry", "Powell",
-      "Long", "Patterson", "Hughes", "Flores", "Washington", "Butler",
-      "Simmons", "Foster", "Gonzales", "Bryant", "Alexander", "Russell",
-      "Griffin", "Diaz", "Hayes", "Myers", "Ford", "Hamilton", "Graham",
-      "Sullivan", "Wallace", "Woods", "Cole", "West", "Jordan", "Owens",
-      "Reynolds", "Fisher", "Ellis", "Harrison", "Gibson", "Mcdonald", "Cruz",
-      "Marshall", "Ortiz", "Gomez", "Murray", "Freeman", "Wells", "Webb",
-      "Simpson", "Stevens", "Tucker", "Porter", "Hunter", "Hicks", "Crawford",
-      "Henry", "Boyd", "Mason", "Morales", "Kennedy", "Warren", "Dixon",
-      "Ramos", "Reyes", "Burns", "Gordon", "Shaw", "Holmes", "Rice",
-      "Robertson", "Hunt", "Black", "Daniels", "Palmer", "Mills", "Nichols",
-      "Grant", "Knight", "Ferguson", "Rose", "Stone", "Hawkins", "Dunn",
-      "Perkins", "Hudson", "Spencer", "Gardner", "Stephens", "Payne", "Pierce",
-      "Berry", "Matthews", "Arnold", "Wagner", "Willis", "Ray", "Watkins",
-      "Olson", "Carroll", "Duncan", "Snyder", "Hart", "Cunningham", "Bradley",
-      "Lane", "Andrews", "Ruiz", "Harper", "Fox", "Riley", "Armstrong",
-      "Carpenter", "Weaver", "Greene", "Lawrence", "Elliott", "Chavez", "Sims",
-      "Austin", "Peters", "Kelley", "Franklin", "Lawson"};
-  private static final String[] STREET_NAMES = {
-      "Peachtree", "First", "Second", "Third", "Fourth", "Fifth", "Sixth",
-      "Tenth", "Fourteenth", "Spring", "Techwood", "West Peachtree", "Juniper",
-      "Cypress", "Fowler", "Piedmont", "Juniper", "Main", "Central", "Currier",
-      "Courtland", "Williams", "Centennial", "Olympic", "Baker", "Highland",
-      "Pryor", "Decatur", "Bell", "Edgewood", "Mitchell", "Forsyth", "Capital"};
+      "Smith", "Johnson", "Williams", "Jones", "Brown", "Davis", "Miller", "Wilson", "Moore",
+      "Taylor", "Anderson", "Thomas", "Jackson", "White", "Harris", "Martin", "Thompson", "Garcia",
+      "Martinez", "Robinson", "Clark", "Rodriguez", "Lewis", "Lee", "Walker", "Hall", "Allen",
+      "Young", "Hernandez", "King", "Wright", "Lopez", "Hill", "Scott", "Green", "Adams", "Baker",
+      "Gonzalez", "Nelson", "Carter", "Mitchell", "Perez", "Roberts", "Turner", "Phillips",
+      "Campbell", "Parker", "Evans", "Edwards", "Collins", "Stewart", "Sanchez", "Morris",
+      "Rogers", "Reed", "Cook", "Morgan", "Bell", "Murphy", "Bailey", "Rivera", "Cooper",
+      "Richardson", "Cox", "Howard", "Ward", "Torres", "Peterson", "Gray", "Ramirez", "James",
+      "Watson", "Brooks", "Kelly", "Sanders", "Price", "Bennett", "Wood", "Barnes", "Ross",
+      "Henderson", "Coleman", "Jenkins", "Perry", "Powell", "Long", "Patterson", "Hughes",
+      "Flores", "Washington", "Butler", "Simmons", "Foster", "Gonzales", "Bryant", "Alexander",
+      "Russell", "Griffin", "Diaz", "Hayes", "Myers", "Ford", "Hamilton", "Graham", "Sullivan",
+      "Wallace", "Woods", "Cole", "West", "Jordan", "Owens", "Reynolds", "Fisher", "Ellis",
+      "Harrison", "Gibson", "Mcdonald", "Cruz", "Marshall", "Ortiz", "Gomez", "Murray", "Freeman",
+      "Wells", "Webb", "Simpson", "Stevens", "Tucker", "Porter", "Hunter", "Hicks", "Crawford",
+      "Henry", "Boyd", "Mason", "Morales", "Kennedy", "Warren", "Dixon", "Ramos", "Reyes", "Burns",
+      "Gordon", "Shaw", "Holmes", "Rice", "Robertson", "Hunt", "Black", "Daniels", "Palmer",
+      "Mills", "Nichols", "Grant", "Knight", "Ferguson", "Rose", "Stone", "Hawkins", "Dunn",
+      "Perkins", "Hudson", "Spencer", "Gardner", "Stephens", "Payne", "Pierce", "Berry",
+      "Matthews", "Arnold", "Wagner", "Willis", "Ray", "Watkins", "Olson", "Carroll", "Duncan",
+      "Snyder", "Hart", "Cunningham", "Bradley", "Lane", "Andrews", "Ruiz", "Harper", "Fox",
+      "Riley", "Armstrong", "Carpenter", "Weaver", "Greene", "Lawrence", "Elliott", "Chavez",
+      "Sims", "Austin", "Peters", "Kelley", "Franklin", "Lawson"};
+  private static final String[] STREET_NAMES =
+      {
+          "Peachtree", "First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Tenth",
+          "Fourteenth", "Spring", "Techwood", "West Peachtree", "Juniper", "Cypress", "Fowler",
+          "Piedmont", "Juniper", "Main", "Central", "Currier", "Courtland", "Williams",
+          "Centennial", "Olympic", "Baker", "Highland", "Pryor", "Decatur", "Bell", "Edgewood",
+          "Mitchell", "Forsyth", "Capital"};
   private static final String[] STREET_SUFFIX = {
       "St", "Rd", "Ln", "Blvd", "Way", "Pkwy", "Cir", "Ave"};
 
@@ -315,7 +307,7 @@
 
   /**
    * Get the singleton instance of the contact database.
-   *
+   * 
    * @return the singleton instance
    */
   public static ContactDatabase get() {
@@ -333,6 +325,12 @@
   private final Category[] categories;
 
   /**
+   * The map of contacts to her friends.
+   */
+  private final Map<Integer, Set<ContactInfo>> friendsMap =
+      new HashMap<Integer, Set<ContactInfo>>();
+
+  /**
    * Construct a new contact database.
    */
   private ContactDatabase() {
@@ -350,7 +348,7 @@
 
   /**
    * Add a new contact.
-   *
+   * 
    * @param contact the contact to add.
    */
   public void addContact(ContactInfo contact) {
@@ -363,7 +361,7 @@
   /**
    * Add a display to the database. The current range of interest of the display
    * will be populated with data.
-   *
+   * 
    * @param display a {@Link HasData}.
    */
   public void addDataDisplay(HasData<ContactInfo> display) {
@@ -373,7 +371,7 @@
   /**
    * Generate the specified number of contacts and add them to the data
    * provider.
-   *
+   * 
    * @param count the number of contacts to generate.
    */
   public void generateContacts(int count) {
@@ -389,7 +387,7 @@
 
   /**
    * Get the categories in the database.
-   *
+   * 
    * @return the categories in the database
    */
   public Category[] queryCategories() {
@@ -398,7 +396,7 @@
 
   /**
    * Query all contacts for the specified category.
-   *
+   * 
    * @param category the category
    * @return the list of contacts in the category
    */
@@ -415,17 +413,16 @@
   /**
    * Query all contacts for the specified category that begin with the specified
    * first name prefix.
-   *
+   * 
    * @param category the category
    * @param firstNamePrefix the prefix of the first name
    * @return the list of contacts in the category
    */
-  public List<ContactInfo> queryContactsByCategoryAndFirstName(
-      Category category, String firstNamePrefix) {
+  public List<ContactInfo> queryContactsByCategoryAndFirstName(Category category,
+      String firstNamePrefix) {
     List<ContactInfo> matches = new ArrayList<ContactInfo>();
     for (ContactInfo contact : dataProvider.getList()) {
-      if (contact.getCategory() == category
-          && contact.getFirstName().startsWith(firstNamePrefix)) {
+      if (contact.getCategory() == category && contact.getFirstName().startsWith(firstNamePrefix)) {
         matches.add(contact);
       }
     }
@@ -433,6 +430,27 @@
   }
 
   /**
+   * Query the list of friends for the specified contact.
+   * 
+   * @param contact the contact
+   * @return the friends of the contact
+   */
+  public Set<ContactInfo> queryFriends(ContactInfo contact) {
+    Set<ContactInfo> friends = friendsMap.get(contact.getId());
+    if (friends == null) {
+      // Assign some random friends.
+      friends = new HashSet<ContactInfo>();
+      int numContacts = dataProvider.getList().size();
+      int friendCount = 2 + Random.nextInt(8);
+      for (int i = 0; i < friendCount; i++) {
+        friends.add(dataProvider.getList().get(Random.nextInt(numContacts)));
+      }
+      friendsMap.put(contact.getId(), friends);
+    }
+    return friends;
+  }
+
+  /**
    * Refresh all displays.
    */
   public void refreshDisplays() {
@@ -441,7 +459,7 @@
 
   /**
    * Create a new random {@link ContactInfo}.
-   *
+   * 
    * @return the new {@link ContactInfo}.
    */
   @SuppressWarnings("deprecation")
@@ -458,8 +476,7 @@
 
     // Create a birthday between 20-80 years ago.
     int year = (new Date()).getYear() - 21 - Random.nextInt(61);
-    contact.setBirthday(new Date(year, Random.nextInt(12),
-        1 + Random.nextInt(31)));
+    contact.setBirthday(new Date(year, Random.nextInt(12), 1 + Random.nextInt(31)));
 
     // Create an address.
     int addrNum = 1 + Random.nextInt(999);
@@ -471,7 +488,7 @@
 
   /**
    * Get the next random value from an array.
-   *
+   * 
    * @param array the array
    * @return a random value in the array
    */
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.css b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.css
new file mode 100644
index 0000000..aaaeb4d
--- /dev/null
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.css
@@ -0,0 +1,5 @@
+.childCell {
+  padding-left: 30px;
+  border: 2px solid #eee !important;
+  background: #eee !important;
+}
\ No newline at end of file
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.java b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.java
new file mode 100644
index 0000000..568af54
--- /dev/null
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.java
@@ -0,0 +1,603 @@
+/*
+ * Copyright 2011 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.sample.showcase.client.content.cell;
+
+import com.google.gwt.cell.client.CheckboxCell;
+import com.google.gwt.cell.client.ClickableTextCell;
+import com.google.gwt.cell.client.EditTextCell;
+import com.google.gwt.cell.client.FieldUpdater;
+import com.google.gwt.cell.client.NumberCell;
+import com.google.gwt.cell.client.SelectionCell;
+import com.google.gwt.cell.client.TextCell;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.RunAsyncCallback;
+import com.google.gwt.dom.builder.shared.DivBuilder;
+import com.google.gwt.dom.builder.shared.TableCellBuilder;
+import com.google.gwt.dom.builder.shared.TableRowBuilder;
+import com.google.gwt.dom.client.Style.OutlineStyle;
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.i18n.client.Constants;
+import com.google.gwt.i18n.client.NumberFormat;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.safehtml.shared.SafeHtml;
+import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.google.gwt.sample.showcase.client.ContentWidget;
+import com.google.gwt.sample.showcase.client.ShowcaseAnnotations.ShowcaseData;
+import com.google.gwt.sample.showcase.client.ShowcaseAnnotations.ShowcaseRaw;
+import com.google.gwt.sample.showcase.client.ShowcaseAnnotations.ShowcaseSource;
+import com.google.gwt.sample.showcase.client.content.cell.ContactDatabase.Category;
+import com.google.gwt.sample.showcase.client.content.cell.ContactDatabase.ContactInfo;
+import com.google.gwt.text.shared.AbstractSafeHtmlRenderer;
+import com.google.gwt.text.shared.SafeHtmlRenderer;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.cellview.client.AbstractCellTable.Style;
+import com.google.gwt.user.cellview.client.CellTableBuilder;
+import com.google.gwt.user.cellview.client.Column;
+import com.google.gwt.user.cellview.client.ColumnSortEvent.ListHandler;
+import com.google.gwt.user.cellview.client.DataGrid;
+import com.google.gwt.user.cellview.client.Header;
+import com.google.gwt.user.cellview.client.SafeHtmlHeader;
+import com.google.gwt.user.cellview.client.SimplePager;
+import com.google.gwt.user.cellview.client.SimplePager.TextLocation;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwt.view.client.DefaultSelectionEventManager;
+import com.google.gwt.view.client.MultiSelectionModel;
+import com.google.gwt.view.client.SelectionModel;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Example file.
+ */
+@ShowcaseRaw({"ContactDatabase.java", "CwCustomDataGrid.ui.xml", "CwCustomDataGrid.css"})
+public class CwCustomDataGrid extends ContentWidget {
+
+  /**
+   * The constants used in this Content Widget.
+   */
+  @ShowcaseSource
+  public static interface CwConstants extends Constants {
+    String cwCustomDataGridColumnAddress();
+
+    String cwCustomDataGridColumnAge();
+
+    String cwCustomDataGridColumnCategory();
+
+    String cwCustomDataGridColumnFirstName();
+
+    String cwCustomDataGridColumnLastName();
+
+    String cwCustomDataGridDescription();
+
+    String cwCustomDataGridEmpty();
+
+    String cwCustomDataGridName();
+  }
+
+  /**
+   * The UiBinder interface used by this example.
+   */
+  @ShowcaseSource
+  interface Binder extends UiBinder<Widget, CwCustomDataGrid> {
+  }
+
+  /**
+   * The resources used by this example.
+   */
+  @ShowcaseSource
+  interface Resources extends ClientBundle {
+
+    /**
+     * Get the styles used but this example.
+     */
+    @Source("CwCustomDataGrid.css")
+    Styles styles();
+  }
+
+  /**
+   * The CSS Resources used by this example.
+   */
+  @ShowcaseSource
+  interface Styles extends CssResource {
+    /**
+     * Indents cells in child rows.
+     */
+    String childCell();
+  }
+
+  /**
+   * A custom version of {@link CellTableBuilder}.
+   */
+  @ShowcaseSource
+  private class CustomTableBuilder implements CellTableBuilder<ContactInfo> {
+
+    private final int todayMonth;
+    private final Set<Integer> showingFriends = new HashSet<Integer>();
+
+    private final String childCell = " " + resources.styles().childCell();
+    private final String rowStyle;
+    private final String selectedRowStyle;
+    private final String cellStyle;
+    private final String selectedCellStyle;
+
+    @SuppressWarnings("deprecation")
+    public CustomTableBuilder(ListHandler<ContactInfo> sortHandler) {
+      // Cache styles for faster access.
+      Style style = dataGrid.getResources().style();
+      rowStyle = style.evenRow();
+      selectedRowStyle = " " + style.selectedRow();
+      cellStyle = style.cell() + " " + style.evenRowCell();
+      selectedCellStyle = " " + style.selectedRowCell();
+
+      // Record today's date.
+      Date today = new Date();
+      todayMonth = today.getMonth();
+
+      /*
+       * Checkbox column.
+       * 
+       * This table will uses a checkbox column for selection. Alternatively,
+       * you can call dataGrid.setSelectionEnabled(true) to enable mouse
+       * selection.
+       */
+      Column<ContactInfo, Boolean> checkboxColumn =
+          new Column<ContactInfo, Boolean>(new CheckboxCell(true, false)) {
+            @Override
+            public Boolean getValue(ContactInfo object) {
+              // Get the value from the selection model.
+              return dataGrid.getSelectionModel().isSelected(object);
+            }
+          };
+      dataGrid.addColumn(checkboxColumn, SafeHtmlUtils.fromSafeConstant("<br/>"));
+      dataGrid.setColumnWidth(0, 40, Unit.PX);
+
+      // View friends.
+      SafeHtmlRenderer<String> anchorRenderer = new AbstractSafeHtmlRenderer<String>() {
+        @Override
+        public SafeHtml render(String object) {
+          SafeHtmlBuilder sb = new SafeHtmlBuilder();
+          sb.appendHtmlConstant("(<a href=\"javascript:;\">").appendEscaped(object)
+              .appendHtmlConstant("</a>)");
+          return sb.toSafeHtml();
+        }
+      };
+      Column<ContactInfo, String> viewFriendsColumn =
+          new Column<ContactInfo, String>(new ClickableTextCell(anchorRenderer)) {
+            @Override
+            public String getValue(ContactInfo object) {
+              if (showingFriends.contains(object.getId())) {
+                return "hide friends";
+              } else {
+                return "show friends";
+              }
+            }
+          };
+      dataGrid.addColumn(viewFriendsColumn, SafeHtmlUtils.fromSafeConstant("<br/>"));
+      viewFriendsColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+        @Override
+        public void update(int index, ContactInfo object, String value) {
+          if (showingFriends.contains(object.getId())) {
+            showingFriends.remove(object.getId());
+          } else {
+            showingFriends.add(object.getId());
+          }
+
+          // Redraw the modified row.
+          dataGrid.redrawRow(index);
+        }
+      });
+      dataGrid.setColumnWidth(1, 10, Unit.EM);
+
+      // First name.
+      Column<ContactInfo, String> firstNameColumn =
+          new Column<ContactInfo, String>(new EditTextCell()) {
+            @Override
+            public String getValue(ContactInfo object) {
+              return object.getFirstName();
+            }
+          };
+      firstNameColumn.setSortable(true);
+      sortHandler.setComparator(firstNameColumn, new Comparator<ContactInfo>() {
+        @Override
+        public int compare(ContactInfo o1, ContactInfo o2) {
+          return o1.getFirstName().compareTo(o2.getFirstName());
+        }
+      });
+      dataGrid.addColumn(firstNameColumn, constants.cwCustomDataGridColumnFirstName());
+      firstNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+        @Override
+        public void update(int index, ContactInfo object, String value) {
+          // Called when the user changes the value.
+          object.setFirstName(value);
+          ContactDatabase.get().refreshDisplays();
+        }
+      });
+      dataGrid.setColumnWidth(2, 20, Unit.PCT);
+
+      // Last name.
+      Column<ContactInfo, String> lastNameColumn =
+          new Column<ContactInfo, String>(new EditTextCell()) {
+            @Override
+            public String getValue(ContactInfo object) {
+              return object.getLastName();
+            }
+          };
+      lastNameColumn.setSortable(true);
+      sortHandler.setComparator(lastNameColumn, new Comparator<ContactInfo>() {
+        @Override
+        public int compare(ContactInfo o1, ContactInfo o2) {
+          return o1.getLastName().compareTo(o2.getLastName());
+        }
+      });
+      dataGrid.addColumn(lastNameColumn, constants.cwCustomDataGridColumnLastName());
+      lastNameColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+        @Override
+        public void update(int index, ContactInfo object, String value) {
+          // Called when the user changes the value.
+          object.setLastName(value);
+          ContactDatabase.get().refreshDisplays();
+        }
+      });
+      dataGrid.setColumnWidth(3, 20, Unit.PCT);
+
+      // Age.
+      Column<ContactInfo, Number> ageColumn = new Column<ContactInfo, Number>(new NumberCell()) {
+        @Override
+        public Number getValue(ContactInfo object) {
+          return object.getAge();
+        }
+      };
+      ageColumn.setSortable(true);
+      sortHandler.setComparator(ageColumn, new Comparator<ContactInfo>() {
+        @Override
+        public int compare(ContactInfo o1, ContactInfo o2) {
+          return o1.getAge() - o2.getAge();
+        }
+      });
+      Header<String> ageFooter = new Header<String>(new TextCell()) {
+        @Override
+        public String getValue() {
+          List<ContactInfo> items = dataGrid.getVisibleItems();
+          if (items.size() == 0) {
+            return "";
+          } else {
+            int totalAge = 0;
+            for (ContactInfo item : items) {
+              totalAge += item.getAge();
+            }
+            return "Avg: " + totalAge / items.size();
+          }
+        }
+      };
+      dataGrid.addColumn(ageColumn, new SafeHtmlHeader(SafeHtmlUtils.fromSafeConstant(constants
+          .cwCustomDataGridColumnAge())), ageFooter);
+      dataGrid.setColumnWidth(4, 7, Unit.EM);
+
+      // Category.
+      final Category[] categories = ContactDatabase.get().queryCategories();
+      List<String> categoryNames = new ArrayList<String>();
+      for (Category category : categories) {
+        categoryNames.add(category.getDisplayName());
+      }
+      SelectionCell categoryCell = new SelectionCell(categoryNames);
+      Column<ContactInfo, String> categoryColumn = new Column<ContactInfo, String>(categoryCell) {
+        @Override
+        public String getValue(ContactInfo object) {
+          return object.getCategory().getDisplayName();
+        }
+      };
+      dataGrid.addColumn(categoryColumn, constants.cwCustomDataGridColumnCategory());
+      categoryColumn.setFieldUpdater(new FieldUpdater<ContactInfo, String>() {
+        @Override
+        public void update(int index, ContactInfo object, String value) {
+          for (Category category : categories) {
+            if (category.getDisplayName().equals(value)) {
+              object.setCategory(category);
+            }
+          }
+          ContactDatabase.get().refreshDisplays();
+        }
+      });
+      dataGrid.setColumnWidth(5, 130, Unit.PX);
+
+      // Address.
+      Column<ContactInfo, String> addressColumn = new Column<ContactInfo, String>(new TextCell()) {
+        @Override
+        public String getValue(ContactInfo object) {
+          return object.getAddress();
+        }
+      };
+      addressColumn.setSortable(true);
+      sortHandler.setComparator(addressColumn, new Comparator<ContactInfo>() {
+        @Override
+        public int compare(ContactInfo o1, ContactInfo o2) {
+          return o1.getAddress().compareTo(o2.getAddress());
+        }
+      });
+      dataGrid.addColumn(addressColumn, constants.cwCustomDataGridColumnAddress());
+      dataGrid.setColumnWidth(6, 60, Unit.PCT);
+    }
+
+    @SuppressWarnings("deprecation")
+    @Override
+    public void buildRow(ContactInfo rowValue, int absRowIndex,
+        CellTableBuilder.Utility<ContactInfo> utility) {
+      buildContactRow(rowValue, absRowIndex, utility, false);
+
+      // Display information about the user in another row that spans the entire
+      // table.
+      if (rowValue.getAge() > 65) {
+        TableRowBuilder row = utility.startRow();
+        TableCellBuilder td = row.startTD().colSpan(7).className(cellStyle);
+        td.style().trustedBackgroundColor("#ffa").outlineStyle(OutlineStyle.NONE).endStyle();
+        td.text(rowValue.getFirstName() + " is elegible for retirement benefits").endTD();
+        row.endTR();
+      }
+
+      // Display information about the user in another row that spans the entire
+      // table.
+      Date dob = rowValue.getBirthday();
+      if (dob.getMonth() == todayMonth) {
+        TableRowBuilder row = utility.startRow();
+        TableCellBuilder td = row.startTD().colSpan(7).className(cellStyle);
+        td.style().trustedBackgroundColor("#ccf").endStyle();
+        td.text(rowValue.getFirstName() + "'s birthday is this month!").endTD();
+        row.endTR();
+      }
+
+      // Display list of friends.
+      if (showingFriends.contains(rowValue.getId())) {
+        Set<ContactInfo> friends = ContactDatabase.get().queryFriends(rowValue);
+        for (ContactInfo friend : friends) {
+          buildContactRow(friend, absRowIndex, utility, true);
+        }
+      }
+    }
+
+    /**
+     * Build a row.
+     * 
+     * @param rowValue the contact info
+     * @param absRowIndex the absolute row index
+     * @param utility the utility used to add rows and Cells
+     * @param isFriend true if this is a subrow, false if a top level row
+     */
+    @SuppressWarnings("deprecation")
+    private void buildContactRow(ContactInfo rowValue, int absRowIndex,
+        CellTableBuilder.Utility<ContactInfo> utility, boolean isFriend) {
+      // Calculate the row styles.
+      SelectionModel<? super ContactInfo> selectionModel = dataGrid.getSelectionModel();
+      boolean isSelected =
+          (selectionModel == null || rowValue == null) ? false : selectionModel
+              .isSelected(rowValue);
+      boolean isEven = absRowIndex % 2 == 0;
+      StringBuilder trClasses = new StringBuilder(rowStyle);
+      if (isSelected) {
+        trClasses.append(selectedRowStyle);
+      }
+
+      // Calculate the cell styles.
+      String cellStyles = cellStyle;
+      if (isSelected) {
+        cellStyles += selectedCellStyle;
+      }
+      if (isFriend) {
+        cellStyles += childCell;
+      }
+
+      TableRowBuilder row = utility.startRow();
+      row.className(trClasses.toString());
+
+      /*
+       * Checkbox column.
+       * 
+       * This table will uses a checkbox column for selection. Alternatively,
+       * you can call dataGrid.setSelectionEnabled(true) to enable mouse
+       * selection.
+       */
+      TableCellBuilder td = row.startTD();
+      td.className(cellStyles);
+      td.style().outlineStyle(OutlineStyle.NONE).endStyle();
+      if (!isFriend) {
+        utility.renderCell(td, utility.createContext(0), dataGrid.getColumn(0), rowValue);
+      }
+      td.endTD();
+
+      /*
+       * View friends column.
+       * 
+       * Displays a link to "show friends". When clicked, the list of friends is
+       * displayed below the contact.
+       */
+      td = row.startTD();
+      td.className(cellStyles);
+      if (!isFriend) {
+        td.style().outlineStyle(OutlineStyle.NONE).endStyle();
+        utility.renderCell(td, utility.createContext(1), dataGrid.getColumn(1), rowValue);
+      }
+      td.endTD();
+
+      // First name column.
+      td = row.startTD();
+      td.className(cellStyles);
+      td.style().outlineStyle(OutlineStyle.NONE).endStyle();
+      if (isFriend) {
+        td.text(rowValue.getFirstName());
+      } else {
+        utility.renderCell(td, utility.createContext(2), dataGrid.getColumn(2), rowValue);
+      }
+      td.endTD();
+
+      // Last name column.
+      td = row.startTD();
+      td.className(cellStyles);
+      td.style().outlineStyle(OutlineStyle.NONE).endStyle();
+      if (isFriend) {
+        td.text(rowValue.getLastName());
+      } else {
+        utility.renderCell(td, utility.createContext(3), dataGrid.getColumn(3), rowValue);
+      }
+      td.endTD();
+
+      // Age column.
+      td = row.startTD();
+      td.className(cellStyles);
+      td.style().outlineStyle(OutlineStyle.NONE).endStyle();
+      td.text(NumberFormat.getDecimalFormat().format(rowValue.getAge())).endTD();
+
+      // Category column.
+      td = row.startTD();
+      td.className(cellStyles);
+      td.style().outlineStyle(OutlineStyle.NONE).endStyle();
+      if (isFriend) {
+        td.text(rowValue.getCategory().getDisplayName());
+      } else {
+        utility.renderCell(td, utility.createContext(5), dataGrid.getColumn(5), rowValue);
+      }
+      td.endTD();
+
+      // Address column.
+      td = row.startTD();
+      td.className(cellStyles);
+      DivBuilder div = td.startDiv();
+      div.style().outlineStyle(OutlineStyle.NONE).endStyle();
+      div.text(rowValue.getAddress()).endDiv();
+      td.endTD();
+
+      row.endTR();
+    }
+  }
+
+  /**
+   * The main DataGrid.
+   */
+  @ShowcaseData
+  @UiField(provided = true)
+  DataGrid<ContactInfo> dataGrid;
+
+  /**
+   * The pager used to change the range of data.
+   */
+  @ShowcaseData
+  @UiField(provided = true)
+  SimplePager pager;
+
+  /**
+   * An instance of the constants.
+   */
+  @ShowcaseData
+  private final CwConstants constants;
+
+  /**
+   * The resources used by this example.
+   */
+  @ShowcaseData
+  private Resources resources;
+
+  /**
+   * Constructor.
+   * 
+   * @param constants the constants
+   */
+  public CwCustomDataGrid(CwConstants constants) {
+    super(constants.cwCustomDataGridName(), constants.cwCustomDataGridDescription(), false,
+        "ContactDatabase.java", "CwCustomDataGrid.ui.xml", "CwCustomDataGrid.css");
+    this.constants = constants;
+  }
+
+  @Override
+  public boolean hasMargins() {
+    return false;
+  }
+
+  @Override
+  public boolean hasScrollableContent() {
+    return false;
+  }
+
+  /**
+   * Initialize this example.
+   */
+  @ShowcaseSource
+  @Override
+  public Widget onInitialize() {
+    resources = GWT.create(Resources.class);
+    resources.styles().ensureInjected();
+
+    // Create a DataGrid.
+
+    // Set a key provider that provides a unique key for each contact. If key is
+    // used to identify contacts when fields (such as the name and address)
+    // change.
+    dataGrid = new DataGrid<ContactInfo>(ContactDatabase.ContactInfo.KEY_PROVIDER);
+    dataGrid.setWidth("100%");
+
+    // Set the message to display when the table is empty.
+    dataGrid.setEmptyTableWidget(new Label(constants.cwCustomDataGridEmpty()));
+
+    // Attach a column sort handler to the ListDataProvider to sort the list.
+    ListHandler<ContactInfo> sortHandler =
+        new ListHandler<ContactInfo>(ContactDatabase.get().getDataProvider().getList());
+    dataGrid.addColumnSortHandler(sortHandler);
+
+    // Create a Pager to control the table.
+    SimplePager.Resources pagerResources = GWT.create(SimplePager.Resources.class);
+    pager = new SimplePager(TextLocation.CENTER, pagerResources, false, 0, true);
+    pager.setDisplay(dataGrid);
+
+    // Add a selection model so we can select cells.
+    final SelectionModel<ContactInfo> selectionModel =
+        new MultiSelectionModel<ContactInfo>(ContactDatabase.ContactInfo.KEY_PROVIDER);
+    dataGrid.setSelectionModel(selectionModel, DefaultSelectionEventManager
+        .<ContactInfo> createCheckboxManager());
+
+    // Specify a custom table.
+    dataGrid.setTableBuilder(new CustomTableBuilder(sortHandler));
+
+    // Add the CellList to the adapter in the database.
+    ContactDatabase.get().addDataDisplay(dataGrid);
+
+    // Create the UiBinder.
+    Binder uiBinder = GWT.create(Binder.class);
+    return uiBinder.createAndBindUi(this);
+  }
+
+  @Override
+  protected void asyncOnInitialize(final AsyncCallback<Widget> callback) {
+    GWT.runAsync(CwCustomDataGrid.class, new RunAsyncCallback() {
+
+      @Override
+      public void onFailure(Throwable caught) {
+        callback.onFailure(caught);
+      }
+
+      @Override
+      public void onSuccess() {
+        callback.onSuccess(onInitialize());
+      }
+    });
+  }
+}
diff --git a/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.ui.xml b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.ui.xml
new file mode 100644
index 0000000..4f154e9
--- /dev/null
+++ b/samples/showcase/src/com/google/gwt/sample/showcase/client/content/cell/CwCustomDataGrid.ui.xml
@@ -0,0 +1,32 @@
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder
+  xmlns:ui="urn:ui:com.google.gwt.uibinder"
+  xmlns:g="urn:import:com.google.gwt.user.client.ui"
+  xmlns:c="urn:import:com.google.gwt.user.cellview.client">
+
+  <g:DockLayoutPanel
+    unit="EM">
+    <!-- DataGrid. -->
+    <g:center>
+      <c:DataGrid
+        ui:field='dataGrid' />
+    </g:center>
+
+    <!-- Pager. -->
+    <g:south
+      size="3">
+      <g:HTMLPanel>
+        <table
+          style="width:100%">
+          <tr>
+            <td
+              align='center'>
+              <c:SimplePager
+                ui:field='pager' />
+            </td>
+          </tr>
+        </table>
+      </g:HTMLPanel>
+    </g:south>
+  </g:DockLayoutPanel>
+</ui:UiBinder>
diff --git a/tools/api-checker/config/gwt23_24userApi.conf b/tools/api-checker/config/gwt23_24userApi.conf
index 2801c61..ffa0848 100644
--- a/tools/api-checker/config/gwt23_24userApi.conf
+++ b/tools/api-checker/config/gwt23_24userApi.conf
@@ -169,3 +169,10 @@
 com.google.gwt.user.client.ui.Image::setUrl(Ljava/lang/String;) OVERLOADED_METHOD_CALL
 com.google.gwt.user.client.ui.Image::setUrlAndVisibleRect(Ljava/lang/String;IIII) OVERLOADED_METHOD_CALL
 
+# Overloading CellTable#clearColumnWidth(Column) to accept the column index
+com.google.gwt.user.cellview.client.CellTable::clearColumnWidth(Lcom/google/gwt/user/cellview/client/Column;) OVERLOADED_METHOD_CALL
+
+# Removing deprecated methods in Cell Widgets.
+com.google.gwt.user.cellview.client.AbstractHasData::onUpdateSelection() MISSING
+com.google.gwt.user.cellview.client.CellList::doSelection(Lcom/google/gwt/user/client/Event;Ljava/lang/Object;I) MISSING
+com.google.gwt.user.cellview.client.CellTable::doSelection(Lcom/google/gwt/user/client/Event;Ljava/lang/Object;II) MISSING
diff --git a/user/src/com/google/gwt/cell/client/Cell.java b/user/src/com/google/gwt/cell/client/Cell.java
index c8239c9..f5ea607 100644
--- a/user/src/com/google/gwt/cell/client/Cell.java
+++ b/user/src/com/google/gwt/cell/client/Cell.java
@@ -41,6 +41,7 @@
     private final int column;
     private final int index;
     private final Object key;
+    private final int subindex;
 
     /**
      * Create a new {@link Context}.
@@ -50,9 +51,22 @@
      * @param key the unique key that represents the row value
      */
     public Context(int index, int column, Object key) {
+      this(index, column, key, 0);
+    }
+
+    /**
+     * Create a new {@link Context}.
+     * 
+     * @param index the absolute index of the value
+     * @param column the column index of the cell, or 0
+     * @param key the unique key that represents the row value
+     * @param subindex the child index
+     */
+    public Context(int index, int column, Object key, int subindex) {
       this.index = index;
       this.column = column;
       this.key = key;
+      this.subindex = subindex;
     }
 
     /**
@@ -82,6 +96,15 @@
     public Object getKey() {
       return key;
     }
+
+    /**
+     * Get the sub index of the rendered row value. If the row value renders to
+     * a single row element, the sub index is 0. If the row value renders to
+     * more than one row element, the sub index may be greater than zero.
+     */
+    public int getSubIndex() {
+      return subindex;
+    }
   }
 
   /**
@@ -137,8 +160,8 @@
    * @param event the native browser event
    * @param valueUpdater a {@link ValueUpdater}, or null if not specified
    */
-  void onBrowserEvent(Context context, Element parent, C value,
-      NativeEvent event, ValueUpdater<C> valueUpdater);
+  void onBrowserEvent(Context context, Element parent, C value, NativeEvent event,
+      ValueUpdater<C> valueUpdater);
 
   /**
    * Render a cell as HTML into a {@link SafeHtmlBuilder}, suitable for passing
diff --git a/user/src/com/google/gwt/dom/builder/client/DomStylesBuilder.java b/user/src/com/google/gwt/dom/builder/client/DomStylesBuilder.java
index b8d01e3..1d0c4e1 100644
--- a/user/src/com/google/gwt/dom/builder/client/DomStylesBuilder.java
+++ b/user/src/com/google/gwt/dom/builder/client/DomStylesBuilder.java
@@ -24,6 +24,7 @@
 import com.google.gwt.dom.client.Style.FontStyle;
 import com.google.gwt.dom.client.Style.FontWeight;
 import com.google.gwt.dom.client.Style.ListStyleType;
+import com.google.gwt.dom.client.Style.OutlineStyle;
 import com.google.gwt.dom.client.Style.Overflow;
 import com.google.gwt.dom.client.Style.Position;
 import com.google.gwt.dom.client.Style.TableLayout;
@@ -265,6 +266,18 @@
   }
 
   @Override
+  public StylesBuilder outlineStyle(OutlineStyle value) {
+    delegate.assertCanAddStyleProperty().setOutlineStyle(value);
+    return this;
+  }
+
+  @Override
+  public StylesBuilder outlineWidth(double value, Unit unit) {
+    delegate.assertCanAddStyleProperty().setOutlineWidth(value, unit);
+    return this;
+  }
+
+  @Override
   public StylesBuilder overflow(Overflow value) {
     delegate.assertCanAddStyleProperty().setOverflow(value);
     return this;
@@ -367,6 +380,12 @@
   }
 
   @Override
+  public StylesBuilder trustedOutlineColor(String value) {
+    delegate.assertCanAddStyleProperty().setOutlineColor(value);
+    return this;
+  }
+
+  @Override
   public StylesBuilder trustedProperty(String name, double value, Unit unit) {
     name = toCamelCaseForm(name);
     delegate.assertCanAddStyleProperty().setProperty(name, value, unit);
diff --git a/user/src/com/google/gwt/dom/builder/shared/AbstractElementBuilderBase.java b/user/src/com/google/gwt/dom/builder/shared/AbstractElementBuilderBase.java
index ff2b658..e74f7d1 100644
--- a/user/src/com/google/gwt/dom/builder/shared/AbstractElementBuilderBase.java
+++ b/user/src/com/google/gwt/dom/builder/shared/AbstractElementBuilderBase.java
@@ -396,6 +396,11 @@
   }
 
   @Override
+  public int getDepth() {
+    return delegate.getDepth();
+  }
+
+  @Override
   public R html(SafeHtml html) {
     delegate.html(html);
     return getReturnBuilder();
diff --git a/user/src/com/google/gwt/dom/builder/shared/ElementBuilderBase.java b/user/src/com/google/gwt/dom/builder/shared/ElementBuilderBase.java
index 29e8966..4b0e53e 100644
--- a/user/src/com/google/gwt/dom/builder/shared/ElementBuilderBase.java
+++ b/user/src/com/google/gwt/dom/builder/shared/ElementBuilderBase.java
@@ -551,6 +551,11 @@
   Element finish();
 
   /**
+   * Get the element depth of the current builder.
+   */
+  int getDepth();
+
+  /**
    * Append html within the node.
    * 
    * <p>
diff --git a/user/src/com/google/gwt/dom/builder/shared/ElementBuilderImpl.java b/user/src/com/google/gwt/dom/builder/shared/ElementBuilderImpl.java
index 5591afa..131e4de 100644
--- a/user/src/com/google/gwt/dom/builder/shared/ElementBuilderImpl.java
+++ b/user/src/com/google/gwt/dom/builder/shared/ElementBuilderImpl.java
@@ -70,6 +70,7 @@
      * The top item in the stack.
      */
     private StackNode top;
+    private int size = 0;
 
     public boolean isEmpty() {
       return (top == null);
@@ -93,6 +94,7 @@
       assertNotEmpty();
       StackNode toRet = top;
       top = top.next;
+      size--;
       return toRet;
     }
 
@@ -100,6 +102,11 @@
       StackNode node = new StackNode(tagName, builder);
       node.next = top;
       top = node;
+      size++;
+    }
+
+    public int size() {
+      return size;
     }
 
     /**
@@ -203,6 +210,10 @@
     return doFinishImpl();
   }
 
+  public int getDepth() {
+    return stack.size();
+  }
+
   public void html(SafeHtml html) {
     assertStartTagOpen("html cannot be set on an element that already "
         + "contains other content or elements.");
diff --git a/user/src/com/google/gwt/dom/builder/shared/HtmlStylesBuilder.java b/user/src/com/google/gwt/dom/builder/shared/HtmlStylesBuilder.java
index 2af1e83..4746468 100644
--- a/user/src/com/google/gwt/dom/builder/shared/HtmlStylesBuilder.java
+++ b/user/src/com/google/gwt/dom/builder/shared/HtmlStylesBuilder.java
@@ -24,6 +24,7 @@
 import com.google.gwt.dom.client.Style.FontStyle;
 import com.google.gwt.dom.client.Style.FontWeight;
 import com.google.gwt.dom.client.Style.ListStyleType;
+import com.google.gwt.dom.client.Style.OutlineStyle;
 import com.google.gwt.dom.client.Style.Overflow;
 import com.google.gwt.dom.client.Style.Position;
 import com.google.gwt.dom.client.Style.TableLayout;
@@ -293,6 +294,16 @@
   }
 
   @Override
+  public StylesBuilder outlineStyle(OutlineStyle value) {
+    return delegate.styleProperty(SafeStylesUtils.forOutlineStyle(value));
+  }
+
+  @Override
+  public StylesBuilder outlineWidth(double value, Unit unit) {
+    return delegate.styleProperty(SafeStylesUtils.forOutlineWidth(value, unit));
+  }
+
+  @Override
   public StylesBuilder overflow(Overflow value) {
     return delegate.styleProperty(SafeStylesUtils.forOverflow(value));
   }
@@ -378,6 +389,11 @@
   }
 
   @Override
+  public StylesBuilder trustedOutlineColor(String value) {
+    return delegate.styleProperty(SafeStylesUtils.forTrustedOutlineColor(value));
+  }
+
+  @Override
   public StylesBuilder trustedProperty(String name, double value, Unit unit) {
     name = toHyphenatedForm(name);
     return delegate.styleProperty(SafeStylesUtils.fromTrustedNameAndValue(name, value, unit));
diff --git a/user/src/com/google/gwt/dom/builder/shared/StylesBuilder.java b/user/src/com/google/gwt/dom/builder/shared/StylesBuilder.java
index 7562ae9..7e34f41 100644
--- a/user/src/com/google/gwt/dom/builder/shared/StylesBuilder.java
+++ b/user/src/com/google/gwt/dom/builder/shared/StylesBuilder.java
@@ -22,6 +22,7 @@
 import com.google.gwt.dom.client.Style.FontStyle;
 import com.google.gwt.dom.client.Style.FontWeight;
 import com.google.gwt.dom.client.Style.ListStyleType;
+import com.google.gwt.dom.client.Style.OutlineStyle;
 import com.google.gwt.dom.client.Style.Overflow;
 import com.google.gwt.dom.client.Style.Position;
 import com.google.gwt.dom.client.Style.TableLayout;
@@ -149,6 +150,16 @@
   StylesBuilder opacity(double value);
 
   /**
+   * Sets the outline-style CSS property.
+   */
+  StylesBuilder outlineStyle(OutlineStyle value);
+
+  /**
+   * Set the outline-width css property.
+   */
+  StylesBuilder outlineWidth(double value, Unit unit);
+
+  /**
    * Sets the overflow CSS property.
    */
   StylesBuilder overflow(Overflow value);
@@ -283,6 +294,23 @@
 
   /**
    * <p>
+   * Sets the "outline-color" style property to the specified color string. Does
+   * not check or escape the color string. The calling code should be carefully
+   * reviewed to ensure that the provided color string won't cause a security
+   * issue if included in a style attribute.
+   * </p>
+   * 
+   * <p>
+   * For details and constraints, see
+   * {@link com.google.gwt.safecss.shared.SafeStyles}.
+   * </p>
+   * 
+   * @return this {@link StylesBuilder}
+   */
+  StylesBuilder trustedOutlineColor(String value);
+
+  /**
+   * <p>
    * Set a style property from a trusted name and a trusted value, i.e., without
    * escaping the name and value. No checks are performed. The calling code
    * should be carefully reviewed to ensure the argument will satisfy the
diff --git a/user/src/com/google/gwt/dom/client/Style.java b/user/src/com/google/gwt/dom/client/Style.java
index 11a1261..9cb36ae 100644
--- a/user/src/com/google/gwt/dom/client/Style.java
+++ b/user/src/com/google/gwt/dom/client/Style.java
@@ -129,6 +129,7 @@
         return BORDER_STYLE_SOLID;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -244,6 +245,7 @@
         return CURSOR_ROW_RESIZE;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -275,6 +277,7 @@
         return DISPLAY_INLINE_BLOCK;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -300,6 +303,7 @@
         return FLOAT_NONE;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -325,6 +329,7 @@
         return FONT_STYLE_OBLIQUE;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -356,6 +361,7 @@
         return FONT_WEIGHT_LIGHTER;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -417,6 +423,69 @@
         return LIST_STYLE_TYPE_UPPER_ROMAN;
       }
     };
+    @Override
+    public abstract String getCssName();
+  }
+
+  /**
+   * Enum for the outline-style property.
+   */
+  public enum OutlineStyle implements HasCssName {
+    NONE {
+      @Override
+      public String getCssName() {
+        return OUTLINE_STYLE_NONE;
+      }
+    },
+    DASHED {
+      @Override
+      public String getCssName() {
+        return OUTLINE_STYLE_DASHED;
+      }
+    },
+    DOTTED {
+      @Override
+      public String getCssName() {
+        return OUTLINE_STYLE_DOTTED;
+      }
+    },
+    DOUBLE {
+      @Override
+      public String getCssName() {
+        return OUTLINE_STYLE_DOUBLE;
+      }
+    },
+    GROOVE {
+      @Override
+      public String getCssName() {
+        return OUTLINE_STYLE_GROOVE;
+      }
+    },
+    INSET {
+      @Override
+      public String getCssName() {
+        return OUTLINE_STYLE_INSET;
+      }
+    },
+    OUTSET {
+      @Override
+      public String getCssName() {
+        return OUTLINE_STYLE_OUTSET;
+      }
+    },
+    RIDGE {
+      @Override
+      public String getCssName() {
+        return OUTLINE_STYLE_RIDGE;
+      }
+    },
+    SOLID {
+      @Override
+      public String getCssName() {
+        return OUTLINE_STYLE_SOLID;
+      }
+    };
+    @Override
     public abstract String getCssName();
   }
 
@@ -448,6 +517,7 @@
         return OVERFLOW_AUTO;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -479,6 +549,7 @@
         return POSITION_FIXED;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -498,6 +569,7 @@
         return TABLE_LAYOUT_FIXED;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -529,6 +601,7 @@
         return TEXT_DECORATION_LINE_THROUGH;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -584,6 +657,7 @@
         return VERTICAL_ALIGN_TEXT_BOTTOM;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -603,6 +677,7 @@
         return VISIBILITY_HIDDEN;
       }
     };
+    @Override
     public abstract String getCssName();
   }
 
@@ -659,6 +734,16 @@
   private static final String LIST_STYLE_TYPE_DISC = "disc";
   private static final String LIST_STYLE_TYPE_NONE = "none";
 
+  private static final String OUTLINE_STYLE_DASHED = "dashed";
+  private static final String OUTLINE_STYLE_DOTTED = "dotted";
+  private static final String OUTLINE_STYLE_DOUBLE = "double";
+  private static final String OUTLINE_STYLE_GROOVE = "groove";
+  private static final String OUTLINE_STYLE_INSET = "inset";
+  private static final String OUTLINE_STYLE_NONE = "none";
+  private static final String OUTLINE_STYLE_OUTSET = "outset";
+  private static final String OUTLINE_STYLE_RIDGE = "ridge";
+  private static final String OUTLINE_STYLE_SOLID = "solid";
+
   private static final String OVERFLOW_AUTO = "auto";
   private static final String OVERFLOW_SCROLL = "scroll";
   private static final String OVERFLOW_HIDDEN = "hidden";
@@ -707,6 +792,9 @@
   private static final String STYLE_BACKGROUND_COLOR = "backgroundColor";
   private static final String STYLE_VERTICAL_ALIGN = "verticalAlign";
   private static final String STYLE_TABLE_LAYOUT = "tableLayout";
+  private static final String STYLE_OUTLINE_WIDTH = "outlineWidth";
+  private static final String STYLE_OUTLINE_STYLE = "outlineStyle";
+  private static final String STYLE_OUTLINE_COLOR = "outlineColor";
 
   private static final String TABLE_LAYOUT_AUTO = "auto";
   private static final String TABLE_LAYOUT_FIXED = "fixed";
@@ -774,7 +862,7 @@
    */
   public final void clearBorderWidth() {
      clearProperty(STYLE_BORDER_WIDTH);
-   }
+  }
 
   /**
    * Clear the bottom css property.
@@ -896,6 +984,27 @@
   }
 
   /**
+   * Clear the outline-color css property.
+   */
+  public final void clearOutlineColor() {
+     clearProperty(STYLE_OUTLINE_COLOR);
+   }
+
+  /**
+   * Clears the outline-style CSS property.
+   */
+  public final void clearOutlineStyle() {
+    clearProperty(STYLE_OUTLINE_STYLE);
+  }
+
+  /**
+   * Clear the outline-width css property.
+   */
+  public final void clearOutlineWidth() {
+     clearProperty(STYLE_OUTLINE_WIDTH);
+  }
+
+  /**
    * Clears the overflow CSS property.
    */
   public final void clearOverflow() {
@@ -1443,6 +1552,27 @@
   }
 
   /**
+   * Set the outline-color css property.
+   */
+  public final void setOutlineColor(String value) {
+    setProperty(STYLE_OUTLINE_COLOR, value);
+  }
+
+  /**
+   * Sets the outline-style CSS property.
+   */
+  public final void setOutlineStyle(OutlineStyle value) {
+    setProperty(STYLE_OUTLINE_STYLE, value.getCssName());
+  }
+
+  /**
+   * Set the outline-width css property.
+   */
+  public final void setOutlineWidth(double value, Unit unit) {
+    setProperty(STYLE_OUTLINE_WIDTH, value, unit);
+  }
+
+  /**
    * Sets the overflow CSS property.
    */
   public final void setOverflow(Overflow value) {
diff --git a/user/src/com/google/gwt/safecss/shared/SafeStylesUtils.java b/user/src/com/google/gwt/safecss/shared/SafeStylesUtils.java
index 8c402ca..cb81640 100644
--- a/user/src/com/google/gwt/safecss/shared/SafeStylesUtils.java
+++ b/user/src/com/google/gwt/safecss/shared/SafeStylesUtils.java
@@ -23,6 +23,7 @@
 import com.google.gwt.dom.client.Style.FontStyle;
 import com.google.gwt.dom.client.Style.FontWeight;
 import com.google.gwt.dom.client.Style.ListStyleType;
+import com.google.gwt.dom.client.Style.OutlineStyle;
 import com.google.gwt.dom.client.Style.Overflow;
 import com.google.gwt.dom.client.Style.Position;
 import com.google.gwt.dom.client.Style.TableLayout;
@@ -218,6 +219,20 @@
   }
 
   /**
+   * Sets the outline-style CSS property.
+   */
+  public static SafeStyles forOutlineStyle(OutlineStyle value) {
+    return fromTrustedNameAndValue("outline-style", value.getCssName());
+  }
+
+  /**
+   * Set the outline-width css property.
+   */
+  public static SafeStyles forOutlineWidth(double value, Unit unit) {
+    return fromTrustedNameAndValue("outline-width", value, unit);
+  }
+
+  /**
    * Sets the overflow CSS property.
    */
   public static SafeStyles forOverflow(Overflow value) {
@@ -410,6 +425,31 @@
   }
 
   /**
+   * <p>
+   * Returns a {@link SafeStyles} constructed from a trusted outline color,
+   * i.e., without escaping the value. No checks are performed. The calling code
+   * should be carefully reviewed to ensure the argument will satisfy the
+   * {@link SafeStyles} contract when they are composed into the form:
+   * "&lt;name&gt;:&lt;value&gt;;".
+   * 
+   * <p>
+   * {@link SafeStyles} may never contain literal angle brackets. Otherwise, it
+   * could be unsafe to place a {@link SafeStyles} into a &lt;style&gt; tag
+   * (where it can't be HTML escaped). For example, if the {@link SafeStyles}
+   * containing "
+   * <code>font: 'foo &lt;style&gt;&lt;script&gt;evil&lt;/script&gt;</code>'" is
+   * used in a style sheet in a &lt;style&gt; tag, this could then break out of
+   * the style context into HTML.
+   * </p>
+   * 
+   * @param value the property value
+   * @return a {@link SafeStyles} instance
+   */
+  public static SafeStyles forTrustedOutlineColor(String value) {
+    return fromTrustedNameAndValue("outline-color", value);
+  }
+
+  /**
    * Sets the vertical-align CSS property.
    */
   public static SafeStyles forVerticalAlign(double value, Unit unit) {
diff --git a/user/src/com/google/gwt/user/cellview/client/AbstractCellTable.java b/user/src/com/google/gwt/user/cellview/client/AbstractCellTable.java
index 01e9179..c8b3331 100644
--- a/user/src/com/google/gwt/user/cellview/client/AbstractCellTable.java
+++ b/user/src/com/google/gwt/user/cellview/client/AbstractCellTable.java
@@ -17,14 +17,25 @@
 
 import com.google.gwt.cell.client.Cell;
 import com.google.gwt.cell.client.Cell.Context;
+import com.google.gwt.cell.client.FieldUpdater;
+import com.google.gwt.cell.client.HasCell;
 import com.google.gwt.cell.client.IconCellDecorator;
 import com.google.gwt.cell.client.SafeHtmlCell;
+import com.google.gwt.cell.client.ValueUpdater;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.dom.builder.shared.DivBuilder;
+import com.google.gwt.dom.builder.shared.ElementBuilderBase;
+import com.google.gwt.dom.builder.shared.HtmlBuilderFactory;
+import com.google.gwt.dom.builder.shared.HtmlTableSectionBuilder;
+import com.google.gwt.dom.builder.shared.TableCellBuilder;
+import com.google.gwt.dom.builder.shared.TableRowBuilder;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.dom.client.NodeList;
+import com.google.gwt.dom.client.Style.OutlineStyle;
 import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.dom.client.TableCellElement;
 import com.google.gwt.dom.client.TableElement;
@@ -79,6 +90,278 @@
 public abstract class AbstractCellTable<T> extends AbstractHasData<T> {
 
   /**
+   * Default implementation of a keyboard navigation handler for tables that
+   * supports navigation between cells.
+   * 
+   * @param <T> the data type of each row
+   */
+  public static class CellTableKeyboardSelectionHandler<T> extends
+      DefaultKeyboardSelectionHandler<T> {
+
+    private final AbstractCellTable<T> table;
+
+    /**
+     * Construct a new keyboard selection handler for the specified table.
+     * 
+     * @param table the display being handled
+     */
+    public CellTableKeyboardSelectionHandler(AbstractCellTable<T> table) {
+      super(table);
+      this.table = table;
+    }
+
+    @Override
+    public AbstractCellTable<T> getDisplay() {
+      return table;
+    }
+
+    @Override
+    public void onCellPreview(CellPreviewEvent<T> event) {
+      NativeEvent nativeEvent = event.getNativeEvent();
+      String eventType = event.getNativeEvent().getType();
+      if ("keydown".equals(eventType) && !event.isCellEditing()) {
+        /*
+         * Handle keyboard navigation, unless the cell is being edited. If the
+         * cell is being edited, we do not want to change rows.
+         * 
+         * Prevent default on navigation events to prevent default scrollbar
+         * behavior.
+         */
+        int oldRow = table.getKeyboardSelectedRow();
+        int oldColumn = table.getKeyboardSelectedColumn();
+        boolean isRtl = LocaleInfo.getCurrentLocale().isRTL();
+        int keyCodeLineEnd = isRtl ? KeyCodes.KEY_LEFT : KeyCodes.KEY_RIGHT;
+        int keyCodeLineStart = isRtl ? KeyCodes.KEY_RIGHT : KeyCodes.KEY_LEFT;
+        int keyCode = nativeEvent.getKeyCode();
+        if (keyCode == keyCodeLineEnd) {
+          int nextColumn = findInteractiveColumn(oldColumn, false);
+          if (nextColumn <= oldColumn) {
+            // Wrap to the next row.
+            table.setKeyboardSelectedRow(oldRow + 1);
+            if (table.getKeyboardSelectedRow() != oldRow) {
+              // If the row didn't change, we are at the end of the table.
+              table.setKeyboardSelectedColumn(nextColumn);
+              handledEvent(event);
+              return;
+            }
+          } else {
+            table.setKeyboardSelectedColumn(nextColumn);
+            handledEvent(event);
+            return;
+          }
+        } else if (keyCode == keyCodeLineStart) {
+          int prevColumn = findInteractiveColumn(oldColumn, true);
+          if (prevColumn >= oldColumn) {
+            // Wrap to the previous row.
+            table.setKeyboardSelectedRow(oldRow - 1);
+            if (table.getKeyboardSelectedRow() != oldRow) {
+              // If the row didn't change, we are at the start of the table.
+              table.setKeyboardSelectedColumn(prevColumn);
+              handledEvent(event);
+              return;
+            }
+          } else {
+            table.setKeyboardSelectedColumn(prevColumn);
+            handledEvent(event);
+            return;
+          }
+        }
+      } else if ("click".equals(eventType) || "focus".equals(eventType)) {
+        /*
+         * Move keyboard focus to the clicked column, even if the cell is being
+         * edited. Unlike key events, we aren't moving the currently selected
+         * row, just updating it based on where the user clicked.
+         * 
+         * Since the user clicked, allow focus to go to a non-interactive
+         * column.
+         */
+        int col = event.getColumn();
+        int relRow = event.getIndex() - table.getPageStart();
+        int subrow = event.getContext().getSubIndex();
+        if ((table.getKeyboardSelectedColumn() != col)
+            || (table.getKeyboardSelectedRow() != relRow)
+            || (table.getKeyboardSelectedSubRow() != subrow)) {
+          boolean stealFocus = false;
+          if ("click".equals(eventType)) {
+            // If a natively focusable element was just clicked, then do not
+            // steal focus.
+            Element target = Element.as(event.getNativeEvent().getEventTarget());
+            stealFocus = !CellBasedWidgetImpl.get().isFocusable(target);
+          }
+
+          // Update the row and subrow.
+          table.setKeyboardSelectedRow(relRow, subrow, stealFocus);
+
+          // Update the column index.
+          table.setKeyboardSelectedColumn(col, stealFocus);
+        }
+
+        // Do not cancel the event as the click may have occurred on a Cell.
+        return;
+      }
+
+      // Let the parent class handle the event.
+      super.onCellPreview(event);
+    }
+
+    /**
+     * Find and return the index of the next interactive column. If no column is
+     * interactive, 0 is returned. If the start index is the only interactive
+     * column, it is returned.
+     * 
+     * @param start the start index, exclusive unless it is the only option
+     * @param reverse true to do a reverse search
+     * @return the interactive column index, or 0 if not interactive
+     */
+    private int findInteractiveColumn(int start, boolean reverse) {
+      if (!table.isInteractive) {
+        return 0;
+      } else if (reverse) {
+        for (int i = start - 1; i >= 0; i--) {
+          if (isColumnInteractive(table.getColumn(i))) {
+            return i;
+          }
+        }
+        // Wrap to the end.
+        for (int i = table.getColumnCount() - 1; i >= start; i--) {
+          if (isColumnInteractive(table.getColumn(i))) {
+            return i;
+          }
+        }
+      } else {
+        for (int i = start + 1; i < table.getColumnCount(); i++) {
+          if (isColumnInteractive(table.getColumn(i))) {
+            return i;
+          }
+        }
+        // Wrap to the start.
+        for (int i = 0; i <= start; i++) {
+          if (isColumnInteractive(table.getColumn(i))) {
+            return i;
+          }
+        }
+      }
+      return 0;
+    }
+  }
+
+  /**
+   * Default cell table builder that renders row values into a grid of columns.
+   * 
+   * @param <T> the data type of the rows.
+   */
+  public static class DefaultCellTableBuilder<T> implements CellTableBuilder<T> {
+
+    private AbstractCellTable<T> cellTable;
+
+    private final String evenRowStyle;
+    private final String oddRowStyle;
+    private final String selectedRowStyle;
+    private final String cellStyle;
+    private final String evenCellStyle;
+    private final String oddCellStyle;
+    private final String firstColumnStyle;
+    private final String lastColumnStyle;
+    private final String selectedCellStyle;
+
+    public DefaultCellTableBuilder(AbstractCellTable<T> cellTable) {
+      this.cellTable = cellTable;
+
+      // Cache styles for faster access.
+      Style style = cellTable.getResources().style();
+      evenRowStyle = style.evenRow();
+      oddRowStyle = style.oddRow();
+      selectedRowStyle = " " + style.selectedRow();
+      cellStyle = style.cell();
+      evenCellStyle = " " + style.evenRowCell();
+      oddCellStyle = " " + style.oddRowCell();
+      firstColumnStyle = " " + style.firstColumn();
+      lastColumnStyle = " " + style.lastColumn();
+      selectedCellStyle = " " + style.selectedRowCell();
+    }
+
+    @Override
+    public void buildRow(T rowValue, int absRowIndex, CellTableBuilder.Utility<T> utility) {
+
+      // Calculate the row styles.
+      SelectionModel<? super T> selectionModel = cellTable.getSelectionModel();
+      boolean isSelected =
+          (selectionModel == null || rowValue == null) ? false : selectionModel
+              .isSelected(rowValue);
+      boolean isEven = absRowIndex % 2 == 0;
+      StringBuilder trClasses = new StringBuilder(isEven ? evenRowStyle : oddRowStyle);
+      if (isSelected) {
+        trClasses.append(selectedRowStyle);
+      }
+
+      // Add custom row styles.
+      RowStyles<T> rowStyles = cellTable.getRowStyles();
+      if (rowStyles != null) {
+        String extraRowStyles = rowStyles.getStyleNames(rowValue, absRowIndex);
+        if (extraRowStyles != null) {
+          trClasses.append(" ").append(extraRowStyles);
+        }
+      }
+
+      // Build the row.
+      TableRowBuilder tr = utility.startRow();
+      tr.className(trClasses.toString());
+
+      // Build the columns.
+      int columnCount = cellTable.getColumnCount();
+      for (int curColumn = 0; curColumn < columnCount; curColumn++) {
+        Column<T, ?> column = cellTable.getColumn(curColumn);
+        // Create the cell styles.
+        StringBuilder tdClasses = new StringBuilder(cellStyle);
+        tdClasses.append(isEven ? evenCellStyle : oddCellStyle);
+        if (curColumn == 0) {
+          tdClasses.append(firstColumnStyle);
+        }
+        if (isSelected) {
+          tdClasses.append(selectedCellStyle);
+        }
+        // The first and last column could be the same column.
+        if (curColumn == columnCount - 1) {
+          tdClasses.append(lastColumnStyle);
+        }
+
+        // Add class names specific to the cell.
+        Context context = new Context(absRowIndex, curColumn, cellTable.getValueKey(rowValue));
+        String cellStyles = column.getCellStyleNames(context, rowValue);
+        if (cellStyles != null) {
+          tdClasses.append(" " + cellStyles);
+        }
+
+        // Builder the cell.
+        HorizontalAlignmentConstant hAlign = column.getHorizontalAlignment();
+        VerticalAlignmentConstant vAlign = column.getVerticalAlignment();
+        TableCellBuilder td = tr.startTD();
+        td.className(tdClasses.toString());
+        if (hAlign != null) {
+          td.align(hAlign.getTextAlignString());
+        }
+        if (vAlign != null) {
+          td.vAlign(vAlign.getVerticalAlignString());
+        }
+
+        // Add the inner div.
+        DivBuilder div = td.startDiv();
+        div.style().outlineStyle(OutlineStyle.NONE).endStyle();
+
+        // Render the cell into the div.
+        utility.renderCell(div, context, column, rowValue);
+
+        // End the cell.
+        div.endDiv();
+        td.endTD();
+      }
+
+      // End the row.
+      tr.endTR();
+    }
+  }
+
+  /**
    * A ClientBundle that provides images for this widget.
    */
   public interface Resources {
@@ -227,12 +510,6 @@
     @SafeHtmlTemplates.Template("<div style=\"outline:none;\">{0}</div>")
     SafeHtml div(SafeHtml contents);
 
-    @SafeHtmlTemplates.Template("<div style=\"outline:none;\" tabindex=\"{0}\">{1}</div>")
-    SafeHtml divFocusable(int tabIndex, SafeHtml contents);
-
-    @SafeHtmlTemplates.Template("<div style=\"outline:none;\" tabindex=\"{0}\" accessKey=\"{1}\">{2}</div>")
-    SafeHtml divFocusableWithKey(int tabIndex, char accessKey, SafeHtml contents);
-
     @SafeHtmlTemplates.Template("<div class=\"{0}\"></div>")
     SafeHtml loading(String loading);
 
@@ -280,7 +557,7 @@
      * @param rowHtml the Html for the rows
      * @return the section element
      */
-    protected TableSectionElement convertToSectionElement(AbstractCellTable<?> table,
+    public TableSectionElement convertToSectionElement(AbstractCellTable<?> table,
         String sectionTag, SafeHtml rowHtml) {
       // Attach an event listener so we can catch synchronous load events from
       // cached images.
@@ -321,6 +598,109 @@
     }
 
     /**
+     * Render a table section in the table.
+     * 
+     * @param table the {@link AbstractCellTable}
+     * @param section the {@link TableSectionElement} to replace
+     * @param html the html of a table section element containing the rows
+     */
+    public final void replaceAllRows(AbstractCellTable<?> table, TableSectionElement section,
+        SafeHtml html) {
+      // If the widget is not attached, attach an event listener so we can catch
+      // synchronous load events from cached images.
+      if (!table.isAttached()) {
+        DOM.setEventListener(table.getElement(), table);
+      }
+
+      // Remove the section from the tbody.
+      Element parent = section.getParentElement();
+      Element nextSection = section.getNextSiblingElement();
+      detachSectionElement(section);
+
+      // Render the html.
+      replaceAllRowsImpl(table, section, html);
+
+      /*
+       * Reattach the section. If next section is null, the section will be
+       * appended instead.
+       */
+      reattachSectionElement(parent, section, nextSection);
+
+      // Detach the event listener.
+      if (!table.isAttached()) {
+        DOM.setEventListener(table.getElement(), null);
+      }
+    }
+
+    /**
+     * Replace a set of row values with newly rendered values.
+     * 
+     * This method does not necessarily perform a one to one replacement. Some
+     * row values may be rendered as multiple row elements, while others are
+     * rendered as only one row element.
+     * 
+     * @param table the {@link AbstractCellTable}
+     * @param section the {@link TableSectionElement} to replace
+     * @param html the html of a table section element containing the rows
+     * @param startIndex the start index to replace
+     * @param childCount the number of row values to replace
+     */
+    public final void replaceChildren(AbstractCellTable<?> table, TableSectionElement section,
+        SafeHtml html, int startIndex, int childCount) {
+      // If the widget is not attached, attach an event listener so we can catch
+      // synchronous load events from cached images.
+      if (!table.isAttached()) {
+        DOM.setEventListener(table.getElement(), table);
+      }
+
+      // Remove the section from the tbody.
+      Element parent = section.getParentElement();
+      Element nextSection = section.getNextSiblingElement();
+      detachSectionElement(section);
+
+      // Remove all children in the range.
+      final int absEndIndex = table.getPageStart() + startIndex + childCount;
+      boolean done = false;
+      Element insertBefore = table.getChildElement(startIndex);
+      if (table.legacyRenderRowValues) {
+        int count = 0;
+        while (insertBefore != null && count < childCount) {
+          Element next = insertBefore.getNextSiblingElement();
+          section.removeChild(insertBefore);
+          insertBefore = next;
+          count++;
+        }
+      } else {
+        while (insertBefore != null
+            && table.getRowValueIndex(insertBefore.<TableRowElement> cast()) < absEndIndex) {
+          Element next = insertBefore.getNextSiblingElement();
+          section.removeChild(insertBefore);
+          insertBefore = next;
+        }
+      }
+
+      // Add new child elements.
+      TableSectionElement newSection = convertToSectionElement(table, section.getTagName(), html);
+      Element newChild = newSection.getFirstChildElement();
+      while (newChild != null) {
+        Element next = newChild.getNextSiblingElement();
+        section.insertBefore(newChild, insertBefore);
+        newChild = next;
+      }
+
+      /*
+       * Reattach the section. If next section is null, the section will be
+       * appended instead.
+       */
+      reattachSectionElement(parent, section, nextSection);
+
+      // Detach the event listener.
+      if (!table.isAttached()) {
+        DOM.setEventListener(table.getElement(), null);
+      }
+    }
+
+    /**
      * Detach a table section element from its parent.
      * 
      * @param section the element to detach
@@ -346,34 +726,11 @@
      * 
      * @param table the {@link AbstractCellTable}
      * @param section the {@link TableSectionElement} to replace
-     * @param html the html to render
+     * @param html the html of a table section element containing the rows
      */
-    protected void replaceAllRows(AbstractCellTable<?> table, TableSectionElement section,
+    protected void replaceAllRowsImpl(AbstractCellTable<?> table, TableSectionElement section,
         SafeHtml html) {
-      // If the widget is not attached, attach an event listener so we can catch
-      // synchronous load events from cached images.
-      if (!table.isAttached()) {
-        DOM.setEventListener(table.getElement(), table);
-      }
-
-      // Remove the section from the tbody.
-      Element parent = section.getParentElement();
-      Element nextSection = section.getNextSiblingElement();
-      detachSectionElement(section);
-
-      // Render the html.
       section.setInnerHTML(html.asString());
-
-      /*
-       * Reattach the section. If next section is null, the section will be
-       * appended instead.
-       */
-      reattachSectionElement(parent, section, nextSection);
-
-      // Detach the event listener.
-      if (!table.isAttached()) {
-        DOM.setEventListener(table.getElement(), null);
-      }
     }
   }
 
@@ -418,12 +775,26 @@
   private static class ImplTrident extends Impl {
 
     /**
+     * Detaching a tbody in IE throws an error.
+     */
+    @Override
+    protected void detachSectionElement(TableSectionElement section) {
+      return;
+    }
+
+    @Override
+    protected void reattachSectionElement(Element parent, TableSectionElement section,
+        Element nextSection) {
+      return;
+    }
+
+    /**
      * IE doesn't support innerHTML on tbody, nor does it support removing or
      * replacing a tbody. The only solution is to remove and replace the rows
      * themselves.
      */
     @Override
-    protected void replaceAllRows(AbstractCellTable<?> table, TableSectionElement section,
+    protected void replaceAllRowsImpl(AbstractCellTable<?> table, TableSectionElement section,
         SafeHtml html) {
       // Remove all children.
       Element child = section.getFirstChildElement();
@@ -445,15 +816,134 @@
   }
 
   /**
+   * Implementation for {@link CellTableBuilder.Utility}.
+   */
+  private class UtilityImpl extends CellTableBuilder.Utility<T> {
+
+    private int rowIndex;
+    private int subrowIndex;
+    private Object rowValueKey;
+    private final HtmlTableSectionBuilder tbody;
+
+    private UtilityImpl() {
+      /*
+       * TODO(jlabanca): Test with DomBuilder.
+       * 
+       * DOM manipulation is sometimes faster than String concatenation and
+       * innerHTML, but not when mixing the two. Cells render as HTML strings,
+       * so its faster to render the entire table as a string.
+       */
+      tbody = HtmlBuilderFactory.get().createTBodyBuilder();
+    }
+
+    @Override
+    public Context createContext(int column) {
+      return new Context(rowIndex, column, rowValueKey);
+    }
+
+    @Override
+    public <C> void renderCell(ElementBuilderBase<?> builder, Context context,
+        HasCell<T, C> column, T rowValue) {
+      // Generate a unique ID for the cell.
+      String cellId = cellToIdMap.get(column);
+      if (cellId == null) {
+        cellId = "cell-" + Document.get().createUniqueId();
+        idToCellMap.put(cellId, column);
+        cellToIdMap.put(column, cellId);
+      }
+      builder.attribute(CELL_ATTRIBUTE, cellId);
+
+      // Render the cell into the builder.
+      SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
+      column.getCell().render(context, column.getValue(rowValue), cellBuilder);
+      builder.html(cellBuilder.toSafeHtml());
+    }
+
+    @Override
+    public TableRowBuilder startRow() {
+      // End any dangling rows.
+      while (tbody.getDepth() > 1) {
+        tbody.end();
+      }
+
+      // Verify the depth.
+      if (tbody.getDepth() < 1) {
+        throw new IllegalStateException(
+            "Cannot start a row.  Did you call TableRowBuilder.end() too many times?");
+      }
+
+      // Start the next row.
+      TableRowBuilder row = tbody.startTR();
+      row.attribute(ROW_ATTRIBUTE, rowIndex);
+      row.attribute(SUBROW_ATTRIBUTE, subrowIndex);
+      subrowIndex++;
+      return row;
+    }
+
+    /**
+     * Get the {@link TableSectionElement} containing the children.
+     */
+    private SafeHtml asSafeHtml() {
+      // End dangling elements.
+      while (tbody.getDepth() > 0) {
+        tbody.endTBody();
+      }
+
+      // Strip the table section tags off of the tbody.
+      String rawHtml = tbody.asSafeHtml().asString();
+      assert rawHtml.startsWith("<tbody>") : "Malformed html";
+      assert rawHtml.endsWith("</tbody>") : "Malformed html";
+      rawHtml = rawHtml.substring(7, rawHtml.length() - 8);
+      return SafeHtmlUtils.fromTrustedString(rawHtml);
+    }
+
+    private void setRowInfo(int rowIndex, T rowValue) {
+      this.rowIndex = rowIndex;
+      this.rowValueKey = getValueKey(rowValue);
+      this.subrowIndex = 0; // Reset the subrow.
+    }
+  }
+
+  /**
+   * The attribute used to indicate that an element contains a cell.
+   */
+  private static final String CELL_ATTRIBUTE = "__gwt_cell";
+
+  /**
+   * The attribute used to specify the logical row index.
+   */
+  private static final String ROW_ATTRIBUTE = "__gwt_row";
+
+  /**
+   * The attribute used to specify the subrow within a logical row value.
+   */
+  private static final String SUBROW_ATTRIBUTE = "__gwt_subrow";
+
+  /**
    * The table specific {@link Impl}.
    */
   private static Impl TABLE_IMPL;
 
-  static Template template;
+  private static Template template;
+
+  /**
+   * Check if a column consumes events.
+   */
+  private static boolean isColumnInteractive(HasCell<?, ?> column) {
+    Set<String> consumedEvents = column.getCell().getConsumedEvents();
+    return consumedEvents != null && consumedEvents.size() > 0;
+  }
+
+  /**
+   * A mapping of unique cell IDs to the cell.
+   */
+  private final Map<String, HasCell<T, ?>> idToCellMap = new HashMap<String, HasCell<T, ?>>();
+  private final Map<HasCell<T, ?>, String> cellToIdMap = new HashMap<HasCell<T, ?>, String>();
 
   private boolean cellIsEditing;
   private final List<Column<T, ?>> columns = new ArrayList<Column<T, ?>>();
   private final Map<Column<T, ?>, String> columnWidths = new HashMap<Column<T, ?>, String>();
+  private final Map<Integer, String> columnWidthsByIndex = new HashMap<Integer, String>();
 
   /**
    * Indicates that at least one column depends on selection.
@@ -478,7 +968,10 @@
   private boolean isInteractive;
 
   private int keyboardSelectedColumn = 0;
+  private int keyboardSelectedSubrow = 0;
+  private int lastKeyboardSelectedSubrow = 0;
   private Widget loadingIndicator;
+  private boolean legacyRenderRowValues = true;
   private final Resources resources;
   private RowStyles<T> rowStyles;
   private IconCellDecorator<SafeHtml> sortAscDecorator;
@@ -492,6 +985,7 @@
     }
   });
   private final Style style;
+  private CellTableBuilder<T> tableBuilder;
   private boolean updatingSortList;
 
   /**
@@ -635,6 +1129,16 @@
   }
 
   /**
+   * Clear the width of the specified {@link Column}.
+   * 
+   * @param column the column index
+   */
+  public void clearColumnWidth(Integer column) {
+    columnWidthsByIndex.remove(column);
+    refreshColumnWidths();
+  }
+
+  /**
    * Flush all pending changes to the table and render immediately.
    * 
    * <p>
@@ -710,6 +1214,27 @@
   }
 
   /**
+   * Get the index of the column that is currently selected via the keyboard.
+   * 
+   * @return the currently selected column, or -1 if none selected
+   */
+  public int getKeyboardSelectedColumn() {
+    return KeyboardSelectionPolicy.DISABLED == getKeyboardSelectionPolicy() ? -1
+        : keyboardSelectedColumn;
+  }
+
+  /**
+   * Get the index of the sub row that is currently selected via the keyboard.
+   * If the row value maps to one rendered row element, the subrow is 0.
+   * 
+   * @return the currently selected subrow, or -1 if none selected
+   */
+  public int getKeyboardSelectedSubRow() {
+    return KeyboardSelectionPolicy.DISABLED == getKeyboardSelectionPolicy() ? -1
+        : keyboardSelectedSubrow;
+  }
+
+  /**
    * Get the widget displayed when the data is loading.
    * 
    * @return the loading indicator
@@ -719,6 +1244,13 @@
   }
 
   /**
+   * Get the resources used by this table.
+   */
+  public Resources getResources() {
+    return resources;
+  }
+
+  /**
    * Get the {@link TableRowElement} for the specified row. If the row element
    * has not been created, null is returned.
    * 
@@ -729,9 +1261,16 @@
    */
   public TableRowElement getRowElement(int row) {
     flush();
-    checkRowBounds(row);
-    NodeList<TableRowElement> rows = getTableBodyElement().getRows();
-    return rows.getLength() > row ? rows.getItem(row) : null;
+    return getChildElement(row);
+  }
+
+  /**
+   * Gets the object used to determine how a row is styled.
+   * 
+   * @return the {@link RowStyles} object if set, null if not
+   */
+  public RowStyles<T> getRowStyles() {
+    return this.rowStyles;
   }
 
   /**
@@ -775,12 +1314,17 @@
     headers.add(beforeIndex, header);
     footers.add(beforeIndex, footer);
     columns.add(beforeIndex, col);
-    boolean wasinteractive = isInteractive;
-    coalesceCellProperties();
+
+    // Increment the keyboard selected column.
+    if (beforeIndex <= keyboardSelectedColumn) {
+      keyboardSelectedColumn = Math.min(keyboardSelectedColumn + 1, columns.size() - 1);
+    }
 
     // Move the keyboard selected column if the current column is not
     // interactive.
-    if (!wasinteractive && isInteractive) {
+    if (isColumnInteractive(col)
+        && ((keyboardSelectedColumn >= columns.size()) || !isColumnInteractive(columns
+            .get(keyboardSelectedColumn)))) {
       keyboardSelectedColumn = beforeIndex;
     }
 
@@ -906,19 +1450,10 @@
     columns.remove(index);
     headers.remove(index);
     footers.remove(index);
-    coalesceCellProperties();
 
-    // Find an interactive column. Stick with 0 if no column is interactive.
-    if (index <= keyboardSelectedColumn) {
-      keyboardSelectedColumn = 0;
-      if (isInteractive) {
-        for (int i = 0; i < columns.size(); i++) {
-          if (isColumnInteractive(columns.get(i))) {
-            keyboardSelectedColumn = i;
-            break;
-          }
-        }
-      }
+    // Decrement the keyboard selected column.
+    if (index <= keyboardSelectedColumn && keyboardSelectedColumn > 0) {
+      keyboardSelectedColumn--;
     }
 
     // Redraw the table asynchronously.
@@ -937,7 +1472,9 @@
   public abstract void removeColumnStyleName(int index, String styleName);
 
   /**
-   * Set the width of a {@link Column}.
+   * Set the width of a {@link Column}. The width will persist with the column
+   * and takes precedence of any width set via
+   * {@link #setColumnWidth(int, String)}.
    * 
    * @param column the column
    * @param width the width of the column
@@ -948,7 +1485,9 @@
   }
 
   /**
-   * Set the width of a {@link Column}.
+   * Set the width of a {@link Column}. The width will persist with the column
+   * and takes precedence of any width set via
+   * {@link #setColumnWidth(int, double, Unit)}.
    * 
    * @param column the column
    * @param width the width of the column
@@ -959,6 +1498,28 @@
   }
 
   /**
+   * Set the width of a {@link Column}.
+   * 
+   * @param column the column
+   * @param width the width of the column
+   * @param unit the {@link Unit} of measurement
+   */
+  public void setColumnWidth(int column, double width, Unit unit) {
+    setColumnWidth(column, width + unit.getType());
+  }
+
+  /**
+   * Set the width of a {@link Column}.
+   * 
+   * @param column the column
+   * @param width the width of the column
+   */
+  public void setColumnWidth(int column, String width) {
+    columnWidthsByIndex.put(column, width);
+    refreshColumnWidths();
+  }
+
+  /**
    * Set the widget to display when the table has no rows.
    * 
    * @param widget the empty table widget, or null to disable
@@ -968,6 +1529,63 @@
   }
 
   /**
+   * Set the keyboard selected column index.
+   * 
+   * <p>
+   * If keyboard selection is disabled, this method does nothing.
+   * </p>
+   * 
+   * <p>
+   * If the keyboard selected column is greater than the number of columns in
+   * the keyboard selected row, the last column in the row is selected, but the
+   * column index is remembered.
+   * </p>
+   * 
+   * @param column the column index, greater than or equal to zero
+   */
+  public final void setKeyboardSelectedColumn(int column) {
+    setKeyboardSelectedColumn(column, true);
+  }
+
+  /**
+   * Set the keyboard selected column index and optionally focus on the new
+   * cell.
+   * 
+   * @param column the column index, greater than or equal to zero
+   * @param stealFocus true to focus on the new column
+   * @see #setKeyboardSelectedColumn(int)
+   */
+  public void setKeyboardSelectedColumn(int column, boolean stealFocus) {
+    assert column >= 0 : "Column must be zero or greater";
+    if (KeyboardSelectionPolicy.DISABLED == getKeyboardSelectionPolicy()) {
+      return;
+    }
+
+    this.keyboardSelectedColumn = column;
+
+    // Reselect the row to move the selected column.
+    setKeyboardSelectedRow(getKeyboardSelectedRow(), keyboardSelectedSubrow, stealFocus);
+  }
+
+  @Override
+  public void setKeyboardSelectedRow(int row, boolean stealFocus) {
+    setKeyboardSelectedRow(row, 0, stealFocus);
+  }
+
+  /**
+   * Set the keyboard selected row and subrow, optionally focus on the new row.
+   * 
+   * @param row the row index relative to the page start
+   * @param subrow the row index of the child row
+   * @param stealFocus true to focus on the new row
+   * @see #setKeyboardSelectedRow(int)
+   */
+  public void setKeyboardSelectedRow(int row, int subrow, boolean stealFocus) {
+    this.keyboardSelectedSubrow = subrow;
+    super.setKeyboardSelectedRow(row, stealFocus);
+  }
+
+  /**
    * Set the widget to display when the data is loading.
    * 
    * @param widget the loading indicator, or null to disable
@@ -986,6 +1604,16 @@
     this.rowStyles = rowStyles;
   }
 
+  /**
+   * Specify the {@link CellTableBuilder} that will be used to render the row
+   * values into the table.
+   */
+  public void setTableBuilder(CellTableBuilder<T> tableBuilder) {
+    assert tableBuilder != null : "tableBuilder cannot be null";
+    this.tableBuilder = tableBuilder;
+    redraw();
+  }
+
   @Override
   protected Element convertToElements(SafeHtml html) {
     return TABLE_IMPL.convertToSectionElement(AbstractCellTable.this, "tbody", html);
@@ -997,21 +1625,6 @@
   }
 
   /**
-   * Called when a user action triggers selection.
-   * 
-   * @param event the event that triggered selection
-   * @param value the value that was selected
-   * @param row the row index of the value on the page
-   * @param column the column index where the event occurred
-   * @deprecated use
-   *             {@link #addCellPreviewHandler(com.google.gwt.view.client.CellPreviewEvent.Handler)}
-   *             instead
-   */
-  @Deprecated
-  protected void doSelection(Event event, T value, int row, int column) {
-  }
-
-  /**
    * Set the width of a column.
    * 
    * @param column the column index
@@ -1032,17 +1645,27 @@
     return getTableBodyElement();
   }
 
+  /**
+   * {@inheritDoc}
+   * 
+   * <p>
+   * The row element may not be the same as the TR element at the specified
+   * index if some row values are rendered with additional rows.
+   * </p>
+   * 
+   * @param row the row index, relative to the page start
+   * @return the row element, or null if it doesn't exists
+   * @throws IndexOutOfBoundsException if the row index is outside of the
+   *           current page
+   */
+  @Override
+  protected TableRowElement getChildElement(int row) {
+    return getSubRowElement(row + getPageStart(), 0);
+  }
+
   @Override
   protected Element getKeyboardSelectedElement() {
-    // Do not use getRowElement() because that will flush the presenter.
-    int rowIndex = getKeyboardSelectedRow();
-    NodeList<TableRowElement> rows = getTableBodyElement().getRows();
-    if (rowIndex >= 0 && rowIndex < rows.getLength() && columns.size() > 0) {
-      TableRowElement tr = rows.getItem(rowIndex);
-      TableCellElement td = tr.getCells().getItem(keyboardSelectedColumn);
-      return getCellParent(td);
-    }
-    return null;
+    return getKeyboardSelectedElement(getKeyboardSelectedTableCellElement());
   }
 
   /**
@@ -1067,9 +1690,8 @@
 
   @Override
   protected void onBlur() {
-    Element elem = getKeyboardSelectedElement();
-    if (elem != null) {
-      TableCellElement td = elem.getParentElement().cast();
+    TableCellElement td = getKeyboardSelectedTableCellElement();
+    if (td != null) {
       TableRowElement tr = td.getParentElement().cast();
       td.removeClassName(style.keyboardSelectedCell());
       setRowStyleName(tr, style.keyboardSelectedRow(), style.keyboardSelectedRowCell(), false);
@@ -1086,117 +1708,151 @@
     }
     final Element target = event.getEventTarget().cast();
 
-    // Ignore keydown events unless the cell is in edit mode
-    String eventType = event.getType();
-    if ("keydown".equals(eventType) && !isKeyboardNavigationSuppressed()
-        && KeyboardSelectionPolicy.DISABLED != getKeyboardSelectionPolicy()) {
-      if (handleKey(event)) {
-        return;
+    // Find the cell where the event occurred.
+    TableSectionElement tbody = getTableBodyElement();
+    TableSectionElement tfoot = getTableFootElement();
+    TableSectionElement thead = getTableHeadElement();
+    TableSectionElement targetTableSection = null;
+    TableCellElement targetTableCell = null;
+    Element cellParent = null;
+    String cellId = null;
+    {
+      Element maybeTableCell = null;
+      Element cur = target;
+      while (cur != null && targetTableSection == null) {
+        /*
+         * Found the table section. Return the most recent cell element that we
+         * discovered.
+         */
+        if (cur == tbody || cur == tfoot || cur == thead) {
+          targetTableSection = cur.cast(); // We found the table section.
+          if (maybeTableCell != null) {
+            targetTableCell = maybeTableCell.cast();
+            break;
+          }
+        }
+
+        // Look for a table cell.
+        String tagName = cur.getTagName();
+        if (TableCellElement.TAG_TD.equalsIgnoreCase(tagName)
+            || TableCellElement.TAG_TH.equalsIgnoreCase(tagName)) {
+          /*
+           * Found a table cell, but we can't return yet because it may be part
+           * of a sub table within the a CellTable cell.
+           */
+          maybeTableCell = cur;
+        }
+
+        // Look for the most immediate cell parent if not already found.
+        String curCellId = isCellParent(cur);
+        if (cellParent == null && curCellId != null) {
+          cellId = curCellId;
+          cellParent = cur;
+        }
+
+        // Iterate.
+        cur = cur.getParentElement();
       }
     }
-
-    // Find the cell where the event occurred.
-    TableCellElement tableCell = findNearestParentCell(target);
-    if (tableCell == null) {
+    if (targetTableCell == null) {
       return;
     }
 
-    // Determine if we are in the header, footer, or body. Its possible that
-    // the table has been refreshed before the current event fired (ex. change
-    // event refreshes before mouseup fires), so we need to check each parent
-    // element.
-    Element trElem = tableCell.getParentElement();
-    if (trElem == null) {
-      return;
+    // Support the legacy mode where the div inside of the TD is the cell
+    // parent.
+    if (legacyRenderRowValues) {
+      cellParent = targetTableCell.getFirstChildElement();
     }
-    TableRowElement tr = TableRowElement.as(trElem);
-    Element sectionElem = tr.getParentElement();
-    if (sectionElem == null) {
-      return;
-    }
-    TableSectionElement section = TableSectionElement.as(sectionElem);
 
-    // Forward the event to the associated header, footer, or column.
+    /*
+     * Forward the event to the associated header, footer, or column.
+     */
+    TableRowElement targetTableRow = targetTableCell.getParentElement().cast();
+    String eventType = event.getType();
     boolean isClick = "click".equals(eventType);
-    int col = tableCell.getCellIndex();
-    if (section == getTableHeadElement()) {
+    int col = targetTableCell.getCellIndex();
+    if (targetTableSection == thead) {
       Header<?> header = headers.get(col);
       if (header != null) {
         // Fire the event to the header.
         if (cellConsumesEventType(header.getCell(), eventType)) {
           Context context = new Context(0, col, header.getKey());
-          header.onBrowserEvent(context, tableCell, event);
+          header.onBrowserEvent(context, targetTableCell, event);
         }
 
         // Sort the header.
-        Column<T, ?> column = columns.get(col);
-        if (isClick && column.isSortable()) {
-          updatingSortList = true;
-          sortList.push(column);
-          updatingSortList = false;
-          ColumnSortEvent.fire(this, sortList);
+        if (isClick) {
+          // TODO(jlabanca): Get visible col when custom headers are supported.
+          Column<T, ?> column = col < columns.size() ? columns.get(col) : null;
+          if (column != null && column.isSortable()) {
+            updatingSortList = true;
+            sortList.push(column);
+            updatingSortList = false;
+            ColumnSortEvent.fire(this, sortList);
+          }
         }
       }
-    } else if (section == getTableFootElement()) {
+    } else if (targetTableSection == tfoot) {
       Header<?> footer = footers.get(col);
       if (footer != null && cellConsumesEventType(footer.getCell(), eventType)) {
         Context context = new Context(0, col, footer.getKey());
-        footer.onBrowserEvent(context, tableCell, event);
+        footer.onBrowserEvent(context, targetTableCell, event);
       }
-    } else if (section == getTableBodyElement()) {
-      // Update the hover state.
-      int row = tr.getSectionRowIndex();
+    } else if (targetTableSection == tbody) {
+      /*
+       * Get the row index of the data value. This may not correspond to the DOM
+       * row index if the user specifies multiple table rows per row object.
+       */
+      int absRow = getRowValueIndex(targetTableRow);
+      int relRow = absRow - getPageStart();
+      int subrow = getSubrowValueIndex(targetTableRow);
       if ("mouseover".equals(eventType)) {
         // Unstyle the old row if it is still part of the table.
         if (hoveringRow != null && getTableBodyElement().isOrHasChild(hoveringRow)) {
           setRowStyleName(hoveringRow, style.hoveredRow(), style.hoveredRowCell(), false);
         }
-        hoveringRow = tr;
+        hoveringRow = targetTableRow;
         setRowStyleName(hoveringRow, style.hoveredRow(), style.hoveredRowCell(), true);
       } else if ("mouseout".equals(eventType) && hoveringRow != null) {
         setRowStyleName(hoveringRow, style.hoveredRow(), style.hoveredRowCell(), false);
         hoveringRow = null;
-      } else if (isClick
-          && ((getPresenter().getKeyboardSelectedRowInView() != row) || (keyboardSelectedColumn != col))) {
-        // Move keyboard focus. Since the user clicked, allow focus to go to a
-        // non-interactive column.
-        boolean isFocusable = CellBasedWidgetImpl.get().isFocusable(target);
-        isFocused = isFocused || isFocusable;
-        keyboardSelectedColumn = col;
-        getPresenter().setKeyboardSelectedRow(row, !isFocusable, true);
       }
 
-      // Update selection. Selection occurs before firing the event to the cell
-      // in case the cell operates on the currently selected item.
-      if (!isRowWithinBounds(row)) {
-        // If the event causes us to page, then the physical index will be out
-        // of bounds of the underlying data.
+      // If the event causes us to page, then the physical index will be out
+      // of bounds of the underlying data.
+      if (!isRowWithinBounds(relRow)) {
         return;
       }
+
+      /*
+       * Fire a preview event. The preview event is fired even if the TD does
+       * not contain a cell so the selection handler and keyboard handler have a
+       * chance to act.
+       */
       boolean isSelectionHandled =
           handlesSelection
               || KeyboardSelectionPolicy.BOUND_TO_SELECTION == getKeyboardSelectionPolicy();
-      T value = getVisibleItem(row);
-      Context context = new Context(row + getPageStart(), col, getValueKey(value));
+      T value = getVisibleItem(relRow);
+      Context context = new Context(absRow, col, getValueKey(value), subrow);
       CellPreviewEvent<T> previewEvent =
           CellPreviewEvent.fire(this, event, this, context, value, cellIsEditing,
               isSelectionHandled);
-      if (isClick && !cellIsEditing && !isSelectionHandled) {
-        doSelection(event, value, row, col);
-      }
 
       // Pass the event to the cell.
-      if (!previewEvent.isCanceled()) {
-        fireEventToCell(event, eventType, tableCell, value, context, columns.get(col));
+      if (cellParent != null && !previewEvent.isCanceled()) {
+        HasCell<T, ?> column = idToCellMap.get(cellId);
+        if (legacyRenderRowValues) {
+          column = columns.get(col);
+        }
+        fireEventToCell(event, eventType, cellParent, value, context, column);
       }
     }
   }
 
   @Override
   protected void onFocus() {
-    Element elem = getKeyboardSelectedElement();
-    if (elem != null) {
-      TableCellElement td = elem.getParentElement().cast();
+    TableCellElement td = getKeyboardSelectedTableCellElement();
+    if (td != null) {
       TableRowElement tr = td.getParentElement().cast();
       td.addClassName(style.keyboardSelectedCell());
       setRowStyleName(tr, style.keyboardSelectedRow(), style.keyboardSelectedRowCell(), true);
@@ -1204,19 +1860,52 @@
   }
 
   protected void refreshColumnWidths() {
+    // TODO(jlabanca): Set size without looking at column count when custom
+    // headers added?
     int columnCount = getColumnCount();
     for (int i = 0; i < columnCount; i++) {
       Column<T, ?> column = columns.get(i);
       String width = columnWidths.get(column);
+      if (width == null) {
+        width = columnWidthsByIndex.get(i);
+      }
       doSetColumnWidth(i, width);
     }
   }
 
+  /**
+   * Throws an {@link UnsupportedOperationException}.
+   * 
+   * @deprecated as of GWT 2.5, use a {@link TableCellBuilder} to customize the
+   *             table structure instead
+   * @see #renderRowValuesLegacy(SafeHtmlBuilder, List, int, SelectionModel)
+   */
   @Override
+  @Deprecated
   protected void renderRowValues(SafeHtmlBuilder sb, List<T> values, int start,
       SelectionModel<? super T> selectionModel) {
-    createHeadersAndFooters();
+    legacyRenderRowValues = false;
+    throw new UnsupportedOperationException();
+  }
 
+  /**
+   * Render all row values into the specified {@link SafeHtmlBuilder}.
+   * 
+   * <p>
+   * This method is here for legacy reasons, to support subclasses that call
+   * {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}.
+   * </p>
+   * 
+   * @param sb the {@link SafeHtmlBuilder} to render into
+   * @param values the row values
+   * @param start the absolute start index of the values
+   * @param selectionModel the {@link SelectionModel}
+   * @deprecated as of GWT 2.5, use a {@link TableCellBuilder} to customize the
+   *             table structure instead
+   */
+  @Deprecated
+  protected final void renderRowValuesLegacy(SafeHtmlBuilder sb, List<T> values, int start,
+      SelectionModel<? super T> selectionModel) {
     int keyboardSelectedRow = getKeyboardSelectedRow() + getPageStart();
     String evenRowStyle = style.evenRow();
     String oddRowStyle = style.oddRow();
@@ -1227,9 +1916,6 @@
     String lastColumnStyle = " " + style.lastColumn();
     String selectedRowStyle = " " + style.selectedRow();
     String selectedCellStyle = " " + style.selectedRowCell();
-    String keyboardRowStyle = " " + style.keyboardSelectedRow();
-    String keyboardRowCellStyle = " " + style.keyboardSelectedRowCell();
-    String keyboardCellStyle = " " + style.keyboardSelectedCell();
     int columnCount = columns.size();
     int length = values.size();
     int end = start + length;
@@ -1238,14 +1924,10 @@
       boolean isSelected =
           (selectionModel == null || value == null) ? false : selectionModel.isSelected(value);
       boolean isEven = i % 2 == 0;
-      boolean isKeyboardSelected = i == keyboardSelectedRow && isFocused;
       String trClasses = isEven ? evenRowStyle : oddRowStyle;
       if (isSelected) {
         trClasses += selectedRowStyle;
       }
-      if (isKeyboardSelected) {
-        trClasses += keyboardRowStyle;
-      }
 
       if (rowStyles != null) {
         String extraRowStyles = rowStyles.getStyleNames(value, i);
@@ -1266,9 +1948,6 @@
         if (isSelected) {
           tdClasses += selectedCellStyle;
         }
-        if (isKeyboardSelected) {
-          tdClasses += keyboardRowCellStyle;
-        }
         // The first and last column could be the same column.
         if (curColumn == columnCount - 1) {
           tdClasses += lastColumnStyle;
@@ -1288,21 +1967,7 @@
 
         // Build the contents.
         SafeHtml contents = SafeHtmlUtils.EMPTY_SAFE_HTML;
-        if (i == keyboardSelectedRow && curColumn == keyboardSelectedColumn) {
-          // This is the focused cell.
-          if (isFocused) {
-            tdClasses += keyboardCellStyle;
-          }
-          char accessKey = getAccessKey();
-          if (accessKey != 0) {
-            contents =
-                template.divFocusableWithKey(getTabIndex(), accessKey, cellBuilder.toSafeHtml());
-          } else {
-            contents = template.divFocusable(getTabIndex(), cellBuilder.toSafeHtml());
-          }
-        } else {
-          contents = template.div(cellBuilder.toSafeHtml());
-        }
+        contents = template.div(cellBuilder.toSafeHtml());
 
         // Build the cell.
         HorizontalAlignmentConstant hAlign = column.getHorizontalAlignment();
@@ -1329,66 +1994,201 @@
 
   @Override
   protected void replaceAllChildren(List<T> values, SafeHtml html) {
-    TABLE_IMPL.replaceAllRows(AbstractCellTable.this, getTableBodyElement(), CellBasedWidgetImpl
-        .get().processHtml(html));
+    // Render the headers and footers.
+    createHeadersAndFooters();
+
+    /*
+     * If html is not null, then the user overrode renderRowValues() and
+     * rendered directly into a SafeHtmlBuilder. The legacy method is deprecated
+     * but still supported.
+     */
+    if (html == null) {
+      cellToIdMap.clear();
+      idToCellMap.clear();
+      html = buildRowValues(values, getPageStart());
+    }
+
+    TABLE_IMPL.replaceAllRows(this, getTableBodyElement(), CellBasedWidgetImpl.get().processHtml(
+        html));
+  }
+
+  @SuppressWarnings("deprecation")
+  @Override
+  protected void replaceChildren(List<T> values, int start, SafeHtml html) {
+    createHeadersAndFooters();
+
+    /*
+     * If html is not null, then the user override renderRowValues() and
+     * rendered directly into a SafeHtmlBuilder. The legacy method is deprecated
+     * but still supported.
+     */
+    if (html == null) {
+      html = buildRowValues(values, getPageStart() + start);
+    }
+
+    TABLE_IMPL.replaceChildren(this, getTableBodyElement(), CellBasedWidgetImpl.get().processHtml(
+        html), start, values.size());
   }
 
   @Override
   protected boolean resetFocusOnCell() {
-    int row = getKeyboardSelectedRow();
-    if (isRowWithinBounds(row) && columns.size() > 0) {
-      Column<T, ?> column = columns.get(keyboardSelectedColumn);
-      return resetFocusOnCellImpl(row, keyboardSelectedColumn, column);
+    Element elem = getKeyboardSelectedElement();
+    if (elem == null) {
+      // There is no selected element.
+      return false;
     }
+
+    String cellId = isCellParent(elem);
+    if (cellId == null) {
+      // The selected element does not contain a Cell.
+      return false;
+    }
+
+    HasCell<T, ?> column = idToCellMap.get(cellId);
+    if (column != null) {
+      resetFocusOnCellImpl(getKeyboardSelectedRow(), getKeyboardSelectedColumn(), column, elem);
+    }
+
     return false;
   }
 
   @Override
   protected void setKeyboardSelected(int index, boolean selected, boolean stealFocus) {
     if (KeyboardSelectionPolicy.DISABLED == getKeyboardSelectionPolicy()
-        || !isRowWithinBounds(index) || columns.size() == 0) {
+        || !isRowWithinBounds(index)) {
       return;
     }
 
-    TableRowElement tr = getRowElement(index);
+    // If deselecting, we deselect the previous subrow.
+    int subrow = lastKeyboardSelectedSubrow;
+    if (selected) {
+      subrow = keyboardSelectedSubrow;
+      lastKeyboardSelectedSubrow = keyboardSelectedSubrow;
+    }
+
+    // Deselect the row.
+    TableRowElement tr = getSubRowElement(index + getPageStart(), subrow);
+    if (tr == null) {
+      // The row does not exist.
+      return;
+    }
     String cellStyle = style.keyboardSelectedCell();
     boolean updatedSelection = !selected || isFocused || stealFocus;
     setRowStyleName(tr, style.keyboardSelectedRow(), style.keyboardSelectedRowCell(), selected);
     NodeList<TableCellElement> cells = tr.getCells();
+    int keyboardColumn = Math.min(getKeyboardSelectedColumn(), cells.getLength() - 1);
     for (int i = 0; i < cells.getLength(); i++) {
       TableCellElement td = cells.getItem(i);
+      boolean isKeyboardSelected = (i == keyboardColumn);
 
       // Update the selected style.
-      setStyleName(td, cellStyle, updatedSelection && selected && i == keyboardSelectedColumn);
+      setStyleName(td, cellStyle, updatedSelection && selected && isKeyboardSelected);
 
       // Mark as focusable.
-      final com.google.gwt.user.client.Element cellParent = getCellParent(td).cast();
-      setFocusable(cellParent, selected && i == keyboardSelectedColumn);
-    }
+      final Element focusable = getKeyboardSelectedElement(td);
+      setFocusable(focusable, selected && isKeyboardSelected);
 
-    // Move focus to the cell.
-    if (selected && stealFocus && !cellIsEditing) {
-      TableCellElement td = tr.getCells().getItem(keyboardSelectedColumn);
-      final com.google.gwt.user.client.Element cellParent = getCellParent(td).cast();
-      CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
-        @Override
-        public void execute() {
-          cellParent.focus();
-        }
-      });
+      // Move focus to the cell.
+      if (selected && stealFocus && !cellIsEditing && isKeyboardSelected) {
+        CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
+          @Override
+          public void execute() {
+            focusable.focus();
+          }
+        });
+      }
     }
   }
 
   /**
-   * @deprecated this method is never called by AbstractHasData, render the
-   *             selected styles in
-   *             {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
+   * Get a subrow element given the index of the row value and the sub row
+   * index.
+   * 
+   * @param absRow the absolute row value index
+   * @param subrow the index of the subrow beneath the row.
+   * @return the row element, or null if not found
    */
-  @Override
-  @Deprecated
-  protected void setSelected(Element elem, boolean selected) {
-    TableRowElement tr = elem.cast();
-    setRowStyleName(tr, style.selectedRow(), style.selectedRowCell(), selected);
+  // Visible for testing.
+  TableRowElement getSubRowElement(int absRow, int subrow) {
+    int relRow = absRow - getPageStart();
+    checkRowBounds(relRow);
+
+    /*
+     * In most tables, the row element that represents the row object at the
+     * specified index will be at the same index in the DOM. However, if the
+     * user provides a TableBuilder that renders multiple rows per row value,
+     * that will not be the case.
+     * 
+     * We use a binary search to find the row, but we start at the index as that
+     * is the most likely location.
+     */
+    NodeList<TableRowElement> rows = getTableBodyElement().getRows();
+    int rowCount = rows.getLength();
+    if (rowCount == 0) {
+      return null;
+    }
+
+    int frameStart = 0;
+    int frameEnd = rowCount - 1;
+    int domIndex = Math.min(relRow, frameEnd);
+    while (domIndex >= frameStart && domIndex <= frameEnd) {
+      TableRowElement curRow = rows.getItem(domIndex);
+      int rowValueIndex = getRowValueIndex(curRow);
+      if (rowValueIndex == absRow) {
+        // Found a subrow in the row index.
+        int subrowValueIndex = getSubrowValueIndex(curRow);
+        if (subrow != subrowValueIndex) {
+          // Shift to the correct subrow.
+          int offset = subrow - subrowValueIndex;
+          int subrowIndex = domIndex + offset;
+          if (subrowIndex >= rows.getLength()) {
+            // The subrow is out of range of the table.
+            return null;
+          }
+          curRow = rows.getItem(subrowIndex);
+          if (getRowValueIndex(curRow) != absRow) {
+            // The "subrow" is actually part of the next row.
+            return null;
+          }
+        }
+        return curRow;
+      } else if (rowValueIndex > absRow) {
+        // Shift the frame to lower indexes.
+        frameEnd = domIndex - 1;
+      } else {
+        // Shift the frame to higher indexes.
+        frameStart = domIndex + 1;
+      }
+
+      // Move the dom index.
+      domIndex = (frameStart + frameEnd) / 2;
+    }
+
+    // The element wasn't found.
+    return null;
+  }
+
+  /**
+   * Build a list of row values.
+   * 
+   * @param values the row values to render
+   * @param start the absolute start index
+   * @return a {@link SafeHtml} string containing the row values
+   */
+  private SafeHtml buildRowValues(List<T> values, int start) {
+    UtilityImpl utility = new UtilityImpl();
+    int length = values.size();
+    int end = start + length;
+    for (int i = start; i < end; i++) {
+      T value = values.get(i - start);
+      utility.setRowInfo(i, value);
+      tableBuilder.buildRow(value, i, utility);
+    }
+
+    // Update the properties of the table.
+    coalesceCellProperties();
+
+    return utility.asSafeHtml();
   }
 
   /**
@@ -1411,7 +2211,7 @@
     dependsOnSelection = false;
     handlesSelection = false;
     isInteractive = false;
-    for (Column<T, ?> column : columns) {
+    for (HasCell<T, ?> column : idToCellMap.values()) {
       Cell<?> cell = column.getCell();
       if (cell.dependsOnSelection()) {
         dependsOnSelection = true;
@@ -1560,97 +2360,118 @@
   }
 
   /**
-   * Find and return the index of the next interactive column. If no column is
-   * interactive, 0 is returned. If the start index is the only interactive
-   * column, it is returned.
-   * 
-   * @param start the start index, exclusive unless it is the only option
-   * @param reverse true to do a reverse search
-   * @return the interactive column index, or 0 if not interactive
+   * Fire an event to the Cell within the specified {@link TableCellElement}.
    */
-  private int findInteractiveColumn(int start, boolean reverse) {
-    if (!isInteractive) {
-      return 0;
-    } else if (reverse) {
-      for (int i = start - 1; i >= 0; i--) {
-        if (isColumnInteractive(columns.get(i))) {
-          return i;
-        }
-      }
-      // Wrap to the end.
-      for (int i = columns.size() - 1; i >= start; i--) {
-        if (isColumnInteractive(columns.get(i))) {
-          return i;
-        }
-      }
-    } else {
-      for (int i = start + 1; i < columns.size(); i++) {
-        if (isColumnInteractive(columns.get(i))) {
-          return i;
-        }
-      }
-      // Wrap to the start.
-      for (int i = 0; i <= start; i++) {
-        if (isColumnInteractive(columns.get(i))) {
-          return i;
-        }
-      }
+  private <C> void fireEventToCell(Event event, String eventType, Element parentElem,
+      final T rowValue, Context context, HasCell<T, C> column) {
+    // Check if the cell consumes the event.
+    Cell<C> cell = column.getCell();
+    if (!cellConsumesEventType(cell, eventType)) {
+      return;
     }
-    return 0;
+
+    // Create a FieldUpdater.
+    final FieldUpdater<T, C> fieldUpdater = column.getFieldUpdater();
+    final int index = context.getIndex();
+    ValueUpdater<C> valueUpdater = (fieldUpdater == null) ? null : new ValueUpdater<C>() {
+      @Override
+      public void update(C value) {
+        fieldUpdater.update(index, rowValue, value);
+      }
+    };
+
+    // Fire the event to the cell.
+    C cellValue = column.getValue(rowValue);
+    boolean cellWasEditing = cell.isEditing(context, parentElem, cellValue);
+    cell.onBrowserEvent(context, parentElem, column.getValue(rowValue), event, valueUpdater);
+
+    // Reset focus if needed.
+    cellIsEditing = cell.isEditing(context, parentElem, cellValue);
+    if (cellWasEditing && !cellIsEditing) {
+      CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
+        @Override
+        public void execute() {
+          setFocus(true);
+        }
+      });
+    }
   }
 
   /**
-   * Find the cell that contains the element. Note that the TD element is not
-   * the parent. The parent is the div inside the TD cell.
+   * Get the keyboard selected element from the selected table cell.
    * 
-   * @param elem the element
-   * @return the parent cell
+   * @return the keyboard selected element, or null if there is none
    */
-  private TableCellElement findNearestParentCell(Element elem) {
-    while ((elem != null) && (elem != getElement())) {
-      // TODO: We need is() implementations in all Element subclasses.
-      // This would allow us to use TableCellElement.is() -- much cleaner.
-      String tagName = elem.getTagName();
-      if ("td".equalsIgnoreCase(tagName) || "th".equalsIgnoreCase(tagName)) {
-        return elem.cast();
+  private Element getKeyboardSelectedElement(TableCellElement td) {
+    if (td == null) {
+      return null;
+    }
+
+    /*
+     * The TD itself is a cell parent, which means its internal structure
+     * (including the tabIndex that we set) could be modified by its Cell. We
+     * return the TD to be safe.
+     */
+    if (isCellParent(td) != null) {
+      return td;
+    }
+
+    /*
+     * The default table builder adds a focusable div to the table cell because
+     * TDs aren't focusable in all browsers. If the user defines a custom table
+     * builder with a different structure, we must assume the keyboard selected
+     * element is the TD itself.
+     */
+    Element firstChild = td.getFirstChildElement();
+    if (firstChild != null && td.getChildCount() == 1
+        && "div".equalsIgnoreCase(firstChild.getTagName())) {
+      return firstChild;
+    }
+
+    return td;
+  }
+
+  /**
+   * Get the {@link TableCellElement} that is currently keyboard selected.
+   * 
+   * @return the table cell element, or null if not selected
+   */
+  private TableCellElement getKeyboardSelectedTableCellElement() {
+    int colIndex = getKeyboardSelectedColumn();
+    if (colIndex < 0) {
+      return null;
+    }
+
+    // Do not use getRowElement() because that will flush the presenter.
+    int rowIndex = getKeyboardSelectedRow();
+    if (rowIndex < 0 || rowIndex >= getTableBodyElement().getRows().getLength()) {
+      return null;
+    }
+    TableRowElement tr = getSubRowElement(rowIndex + getPageStart(), keyboardSelectedSubrow);
+    if (tr != null) {
+      int cellCount = tr.getCells().getLength();
+      if (cellCount > 0) {
+        int column = Math.min(colIndex, cellCount - 1);
+        return tr.getCells().getItem(column);
       }
-      elem = elem.getParentElement();
     }
     return null;
   }
 
   /**
-   * Fire an event to the Cell within the specified {@link TableCellElement}.
-   */
-  private <C> void fireEventToCell(Event event, String eventType, TableCellElement tableCell,
-      T value, Context context, Column<T, C> column) {
-    Cell<C> cell = column.getCell();
-    if (cellConsumesEventType(cell, eventType)) {
-      C cellValue = column.getValue(value);
-      Element parentElem = getCellParent(tableCell);
-      boolean cellWasEditing = cell.isEditing(context, parentElem, cellValue);
-      column.onBrowserEvent(context, parentElem, value, event);
-      cellIsEditing = cell.isEditing(context, parentElem, cellValue);
-      if (cellWasEditing && !cellIsEditing) {
-        CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
-          @Override
-          public void execute() {
-            setFocus(true);
-          }
-        });
-      }
-    }
-  }
-
-  /**
-   * Get the parent element that is passed to the {@link Cell} from the table
-   * cell element.
+   * Get the index of the row value from the associated {@link TableRowElement}.
    * 
-   * @param td the table cell
-   * @return the parent of the {@link Cell}
+   * @param row the row element
+   * @return the row value index
    */
-  private Element getCellParent(TableCellElement td) {
-    return td.getFirstChildElement();
+  private int getRowValueIndex(TableRowElement row) {
+    try {
+      return Integer.parseInt(row.getAttribute(ROW_ATTRIBUTE));
+    } catch (NumberFormatException e) {
+      // The attribute doesn't exist. Maybe the user is overriding
+      // renderRowValues().
+      return row.getSectionRowIndex() + getPageStart();
+    }
   }
 
   /**
@@ -1675,50 +2496,22 @@
     }
   }
 
-  private boolean handleKey(Event event) {
-    HasDataPresenter<T> presenter = getPresenter();
-    int oldRow = getKeyboardSelectedRow();
-    boolean isRtl = LocaleInfo.getCurrentLocale().isRTL();
-    int keyCodeLineEnd = isRtl ? KeyCodes.KEY_LEFT : KeyCodes.KEY_RIGHT;
-    int keyCodeLineStart = isRtl ? KeyCodes.KEY_RIGHT : KeyCodes.KEY_LEFT;
-    int keyCode = event.getKeyCode();
-    if (keyCode == keyCodeLineEnd) {
-      int nextColumn = findInteractiveColumn(keyboardSelectedColumn, false);
-      if (nextColumn <= keyboardSelectedColumn) {
-        // Wrap to the next row.
-        if (presenter.hasKeyboardNext()) {
-          keyboardSelectedColumn = nextColumn;
-          presenter.keyboardNext();
-          event.preventDefault();
-          return true;
-        }
-      } else {
-        // Reselect the row to move the selected column.
-        keyboardSelectedColumn = nextColumn;
-        getPresenter().setKeyboardSelectedRow(oldRow, true, true);
-        event.preventDefault();
-        return true;
-      }
-    } else if (keyCode == keyCodeLineStart) {
-      int prevColumn = findInteractiveColumn(keyboardSelectedColumn, true);
-      if (prevColumn >= keyboardSelectedColumn) {
-        // Wrap to the previous row.
-        if (presenter.hasKeyboardPrev()) {
-          keyboardSelectedColumn = prevColumn;
-          presenter.keyboardPrev();
-          event.preventDefault();
-          return true;
-        }
-      } else {
-        // Reselect the row to move the selected column.
-        keyboardSelectedColumn = prevColumn;
-        getPresenter().setKeyboardSelectedRow(oldRow, true, true);
-        event.preventDefault();
-        return true;
-      }
+  /**
+   * Get the index of the subrow value from the associated
+   * {@link TableRowElement}. The sub row value starts at 0 for the first row
+   * that represents a row value.
+   * 
+   * @param row the row element
+   * @return the subrow value index, or 0 if not found
+   */
+  private int getSubrowValueIndex(TableRowElement row) {
+    try {
+      return Integer.parseInt(row.getAttribute(SUBROW_ATTRIBUTE));
+    } catch (NumberFormatException e) {
+      // The attribute doesn't exist. Maybe the user is overriding
+      // renderRowValues().
+      return 0;
     }
-
-    return false;
   }
 
   /**
@@ -1737,24 +2530,36 @@
     eventTypes.add("mouseover");
     eventTypes.add("mouseout");
     CellBasedWidgetImpl.get().sinkEvents(this, eventTypes);
+
+    // Set the table builder.
+    tableBuilder = new DefaultCellTableBuilder<T>(this);
+
+    // Set the keyboard handler.
+    setKeyboardSelectionHandler(new CellTableKeyboardSelectionHandler<T>(this));
   }
 
   /**
-   * Check if a column consumes events.
+   * Check if an element is the parent of a rendered cell.
+   * 
+   * @param elem the element to check
+   * @return the cellId if a cell parent, null if not
    */
-  private boolean isColumnInteractive(Column<T, ?> column) {
-    Set<String> consumedEvents = column.getCell().getConsumedEvents();
-    return consumedEvents != null && consumedEvents.size() > 0;
+  private String isCellParent(Element elem) {
+    if (elem == null) {
+      return null;
+    }
+    String cellId = elem.getAttribute(CELL_ATTRIBUTE);
+    return (cellId == null) || (cellId.length() == 0) ? null : cellId;
   }
 
-  private <C> boolean resetFocusOnCellImpl(int row, int col, Column<T, C> column) {
-    Element parent = getKeyboardSelectedElement();
+  private <C> boolean resetFocusOnCellImpl(int row, int col, HasCell<T, C> column,
+      Element cellParent) {
     T value = getVisibleItem(row);
     Object key = getValueKey(value);
     C cellValue = column.getValue(value);
     Cell<C> cell = column.getCell();
     Context context = new Context(row + getPageStart(), col, key);
-    return cell.resetFocus(context, parent, cellValue);
+    return cell.resetFocus(context, cellParent, cellValue);
   }
 
   /**
diff --git a/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java b/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
index b33d8de..21dc3c9 100644
--- a/user/src/com/google/gwt/user/cellview/client/AbstractHasData.java
+++ b/user/src/com/google/gwt/user/cellview/client/AbstractHasData.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
@@ -20,6 +20,7 @@
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.EventTarget;
+import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
@@ -46,21 +47,176 @@
 import com.google.gwt.view.client.RowCountChangeEvent;
 import com.google.gwt.view.client.SelectionModel;
 
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 /**
  * An abstract {@link Widget} that implements {@link HasData}.
- *
+ * 
  * @param <T> the data type of each row
  */
 public abstract class AbstractHasData<T> extends Composite implements HasData<T>,
     HasKeyProvider<T>, Focusable, HasKeyboardPagingPolicy {
 
   /**
+   * Default implementation of a keyboard navigation handler.
+   * 
+   * @param <T> the data type of each row
+   */
+  public static class DefaultKeyboardSelectionHandler<T> implements CellPreviewEvent.Handler<T> {
+
+    /**
+     * The number of rows to jump when PAGE_UP or PAGE_DOWN is pressed and the
+     * {@link HasKeyboardPagingPolicy.KeyboardPagingPolicy} is
+     * {@link HasKeyboardPagingPolicy.KeyboardPagingPolicy#INCREASE_RANGE}.
+     */
+    private static final int PAGE_INCREMENT = 30;
+
+    private final AbstractHasData<T> display;
+
+    /**
+     * Construct a new keyboard selection handler for the specified view.
+     * 
+     * @param display the display being handled
+     */
+    public DefaultKeyboardSelectionHandler(AbstractHasData<T> display) {
+      this.display = display;
+    }
+
+    public AbstractHasData<T> getDisplay() {
+      return display;
+    }
+
+    @Override
+    public void onCellPreview(CellPreviewEvent<T> event) {
+      NativeEvent nativeEvent = event.getNativeEvent();
+      String eventType = event.getNativeEvent().getType();
+      if ("keydown".equals(eventType) && !event.isCellEditing()) {
+        /*
+         * Handle keyboard navigation, unless the cell is being edited. If the
+         * cell is being edited, we do not want to change rows.
+         * 
+         * Prevent default on navigation events to prevent default scrollbar
+         * behavior.
+         */
+        switch (nativeEvent.getKeyCode()) {
+          case KeyCodes.KEY_DOWN:
+            nextRow();
+            handledEvent(event);
+            return;
+          case KeyCodes.KEY_UP:
+            prevRow();
+            handledEvent(event);
+            return;
+          case KeyCodes.KEY_PAGEDOWN:
+            nextPage();
+            handledEvent(event);
+            return;
+          case KeyCodes.KEY_PAGEUP:
+            prevPage();
+            handledEvent(event);
+            return;
+          case KeyCodes.KEY_HOME:
+            home();
+            handledEvent(event);
+            return;
+          case KeyCodes.KEY_END:
+            end();
+            handledEvent(event);
+            return;
+          case 32:
+            // Prevent the list box from scrolling.
+            handledEvent(event);
+            return;
+        }
+      } else if ("click".equals(eventType)) {
+        /*
+         * Move keyboard focus to the clicked row, even if the Cell is being
+         * edited. Unlike key events, we aren't moving the currently selected
+         * row, just updating it based on where the user clicked.
+         */
+        int relRow = event.getIndex() - display.getPageStart();
+        if (display.getKeyboardSelectedRow() != relRow) {
+          // If a natively focusable element was just clicked, then do not steal
+          // focus.
+          boolean isFocusable = false;
+          Element target = Element.as(event.getNativeEvent().getEventTarget());
+          isFocusable = CellBasedWidgetImpl.get().isFocusable(target);
+          display.setKeyboardSelectedRow(relRow, !isFocusable);
+
+          // Do not cancel the event as the click may have occurred on a Cell.
+        }
+      } else if ("focus".equals(eventType)) {
+        // Move keyboard focus to match the currently focused element.
+        int relRow = event.getIndex() - display.getPageStart();
+        if (display.getKeyboardSelectedRow() != relRow) {
+          // Do not steal focus as this was a focus event.
+          display.setKeyboardSelectedRow(event.getIndex(), false);
+
+          // Do not cancel the event as the click may have occurred on a Cell.
+          return;
+        }
+      }
+    }
+
+    // Visible for testing.
+    void end() {
+      setKeyboardSelectedRow(display.getRowCount() - 1);
+    }
+
+    void handledEvent(CellPreviewEvent<?> event) {
+      event.setCanceled(true);
+      event.getNativeEvent().preventDefault();
+    }
+
+    // Visible for testing.
+    void home() {
+      setKeyboardSelectedRow(-display.getPageStart());
+    }
+
+    // Visible for testing.
+    void nextPage() {
+      KeyboardPagingPolicy keyboardPagingPolicy = display.getKeyboardPagingPolicy();
+      if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) {
+        // 0th index of next page.
+        setKeyboardSelectedRow(display.getPageSize());
+      } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) {
+        setKeyboardSelectedRow(display.getKeyboardSelectedRow() + PAGE_INCREMENT);
+      }
+    }
+
+    // Visible for testing.
+    void nextRow() {
+      setKeyboardSelectedRow(display.getKeyboardSelectedRow() + 1);
+    }
+
+    // Visible for testing.
+    void prevPage() {
+      KeyboardPagingPolicy keyboardPagingPolicy = display.getKeyboardPagingPolicy();
+      if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) {
+        // 0th index of previous page.
+        setKeyboardSelectedRow(-display.getPageSize());
+      } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) {
+        setKeyboardSelectedRow(display.getKeyboardSelectedRow() - PAGE_INCREMENT);
+      }
+    }
+
+    // Visible for testing.
+    void prevRow() {
+      setKeyboardSelectedRow(display.getKeyboardSelectedRow() - 1);
+    }
+
+    // Visible for testing.
+    void setKeyboardSelectedRow(int row) {
+      display.setKeyboardSelectedRow(row, true);
+    }
+  }
+
+  /**
    * Implementation of {@link HasDataPresenter.View} used by this widget.
-   *
+   * 
    * @param <T> the data type of the view
    */
   private static class View<T> implements HasDataPresenter.View<T> {
@@ -73,24 +229,14 @@
     }
 
     @Override
-    public <H extends EventHandler> HandlerRegistration addHandler(H handler,
-        Type<H> type) {
+    public <H extends EventHandler> HandlerRegistration addHandler(H handler, Type<H> type) {
       return hasData.addHandler(handler, type);
     }
 
     @Override
-    public void render(SafeHtmlBuilder sb, List<T> values, int start,
-        SelectionModel<? super T> selectionModel) {
-      hasData.renderRowValues(sb, values, start, selectionModel);
-    }
-
-    @Override
-    public void replaceAllChildren(List<T> values, SafeHtml html,
-        boolean stealFocus, boolean contentChanged) {
-      if (!contentChanged) {
-        // Early exit if the content is unchanged.
-        return;
-      }
+    public void replaceAllChildren(List<T> values, SelectionModel<? super T> selectionModel,
+        boolean stealFocus) {
+      SafeHtml html = renderRowValues(values, hasData.getPageStart(), selectionModel);
 
       // Removing elements can fire a blur event, which we ignore.
       hasData.isFocused = hasData.isFocused || stealFocus;
@@ -98,18 +244,40 @@
       hasData.isRefreshing = true;
       hasData.replaceAllChildren(values, html);
       hasData.isRefreshing = false;
+
+      // Ensure that the keyboard selected element is focusable.
+      Element elem = hasData.getKeyboardSelectedElement();
+      if (elem != null) {
+        hasData.setFocusable(elem, true);
+        if (hasData.isFocused) {
+          hasData.onFocus();
+        }
+      }
+
       fireValueChangeEvent();
     }
 
     @Override
-    public void replaceChildren(List<T> values, int start, SafeHtml html,
-        boolean stealFocus) {
+    public void replaceChildren(List<T> values, int start,
+        SelectionModel<? super T> selectionModel, boolean stealFocus) {
+      SafeHtml html = renderRowValues(values, hasData.getPageStart() + start, selectionModel);
+
       // Removing elements can fire a blur event, which we ignore.
       hasData.isFocused = hasData.isFocused || stealFocus;
       wasFocused = hasData.isFocused;
       hasData.isRefreshing = true;
       hasData.replaceChildren(values, start, html);
       hasData.isRefreshing = false;
+
+      // Ensure that the keyboard selected element is focusable.
+      Element elem = hasData.getKeyboardSelectedElement();
+      if (elem != null) {
+        hasData.setFocusable(elem, true);
+        if (hasData.isFocused) {
+          hasData.onFocus();
+        }
+      }
+
       fireValueChangeEvent();
     }
 
@@ -131,8 +299,7 @@
     }
 
     @Override
-    public void setKeyboardSelected(int index, boolean seleted,
-        boolean stealFocus) {
+    public void setKeyboardSelected(int index, boolean seleted, boolean stealFocus) {
       hasData.isFocused = hasData.isFocused || stealFocus;
       hasData.setKeyboardSelected(index, seleted, stealFocus);
     }
@@ -154,6 +321,27 @@
       hasData.fireEvent(new ValueChangeEvent<List<T>>(hasData.getVisibleItems()) {
       });
     }
+
+    /**
+     * Render a list of row values.
+     * 
+     * @param values the row values
+     * @param start the absolute start index of the values
+     * @param selectionModel the {@link SelectionModel}
+     * @return null, unless the implementation renders using SafeHtml
+     */
+    private SafeHtml renderRowValues(List<T> values, int start,
+        SelectionModel<? super T> selectionModel) {
+      try {
+        SafeHtmlBuilder sb = new SafeHtmlBuilder();
+        hasData.renderRowValues(sb, values, start, selectionModel);
+        return sb.toSafeHtml();
+      } catch (UnsupportedOperationException e) {
+        // If renderRowValues throws, the implementation will render directly in
+        // the replaceChildren method.
+        return null;
+      }
+    }
   }
 
   /**
@@ -164,13 +352,13 @@
   /**
    * Convenience method to convert the specified HTML into DOM elements and
    * return the parent of the DOM elements.
-   *
+   * 
    * @param html the HTML to convert
    * @param tmpElem a temporary element
    * @return the parent element
    */
-  static Element convertToElements(Widget widget,
-      com.google.gwt.user.client.Element tmpElem, SafeHtml html) {
+  static Element convertToElements(Widget widget, com.google.gwt.user.client.Element tmpElem,
+      SafeHtml html) {
     // Attach an event listener so we can catch synchronous load events from
     // cached images.
     DOM.setEventListener(tmpElem, widget);
@@ -185,13 +373,12 @@
 
   /**
    * Convenience method to replace all children of a Widget.
-   *
+   * 
    * @param widget the widget who's contents will be replaced
    * @param childContainer the container that holds the contents
    * @param html the html to set
    */
-  static void replaceAllChildren(Widget widget, Element childContainer,
-      SafeHtml html) {
+  static void replaceAllChildren(Widget widget, Element childContainer, SafeHtml html) {
     // If the widget is not attached, attach an event listener so we can catch
     // synchronous load events from cached images.
     if (!widget.isAttached()) {
@@ -212,15 +399,15 @@
    * 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 widget the widget who's contents will be replaced
    * @param childContainer the container that holds the contents
    * @param newChildren an element containing the new children
    * @param start the start index to replace
    * @param html the HTML to convert
    */
-  static void replaceChildren(Widget widget, Element childContainer,
-      Element newChildren, int start, SafeHtml html) {
+  static void replaceChildren(Widget widget, Element childContainer, Element newChildren,
+      int start, SafeHtml html) {
     // Get the first element to be replaced.
     int childCount = childContainer.getChildCount();
     Element toReplace = null;
@@ -251,7 +438,7 @@
     }
     return tmpElem;
   }
-  
+
   /**
    * A boolean indicating that the widget has focus.
    */
@@ -266,7 +453,8 @@
   private boolean isRefreshing;
 
   private final HasDataPresenter<T> presenter;
-  private HandlerRegistration selectionManagerReg; 
+  private HandlerRegistration keyboardSelectionReg;
+  private HandlerRegistration selectionManagerReg;
   private int tabIndex;
 
   /**
@@ -276,8 +464,7 @@
    * @param pageSize the page size
    * @param keyProvider the key provider, or null
    */
-  public AbstractHasData(final Element elem, final int pageSize,
-      final ProvidesKey<T> keyProvider) {
+  public AbstractHasData(final Element elem, final int pageSize, final ProvidesKey<T> keyProvider) {
     this(new Widget() {
       {
         setElement(elem);
@@ -292,11 +479,9 @@
    * @param pageSize the page size
    * @param keyProvider the key provider, or null
    */
-  public AbstractHasData(Widget widget, final int pageSize,
-      final ProvidesKey<T> keyProvider) {
+  public AbstractHasData(Widget widget, final int pageSize, final ProvidesKey<T> keyProvider) {
     initWidget(widget);
-    this.presenter = new HasDataPresenter<T>(this, new View<T>(this), pageSize,
-        keyProvider);
+    this.presenter = new HasDataPresenter<T>(this, new View<T>(this), pageSize, keyProvider);
 
     // Sink events.
     Set<String> eventTypes = new HashSet<String>();
@@ -309,12 +494,15 @@
     CellBasedWidgetImpl.get().sinkEvents(this, eventTypes);
 
     // Add a default selection event manager.
-    selectionManagerReg = addCellPreviewHandler(DefaultSelectionEventManager.<T> createDefaultManager());
+    selectionManagerReg =
+        addCellPreviewHandler(DefaultSelectionEventManager.<T> createDefaultManager());
+
+    // Add a default keyboard selection handler.
+    setKeyboardSelectionHandler(new DefaultKeyboardSelectionHandler<T>(this));
   }
 
   @Override
-  public HandlerRegistration addCellPreviewHandler(
-      CellPreviewEvent.Handler<T> handler) {
+  public HandlerRegistration addCellPreviewHandler(CellPreviewEvent.Handler<T> handler) {
     return presenter.addCellPreviewHandler(handler);
   }
 
@@ -325,26 +513,23 @@
    * @param handler the handle
    * @return the registration for the handler
    */
-  public HandlerRegistration addLoadingStateChangeHandler(
-      LoadingStateChangeEvent.Handler handler) {
+  public HandlerRegistration addLoadingStateChangeHandler(LoadingStateChangeEvent.Handler handler) {
     return presenter.addLoadingStateChangeHandler(handler);
   }
 
   @Override
-  public HandlerRegistration addRangeChangeHandler(
-      RangeChangeEvent.Handler handler) {
+  public HandlerRegistration addRangeChangeHandler(RangeChangeEvent.Handler handler) {
     return presenter.addRangeChangeHandler(handler);
   }
 
   @Override
-  public HandlerRegistration addRowCountChangeHandler(
-      RowCountChangeEvent.Handler handler) {
+  public HandlerRegistration addRowCountChangeHandler(RowCountChangeEvent.Handler handler) {
     return presenter.addRowCountChangeHandler(handler);
   }
 
   /**
    * Get the access key.
-   *
+   * 
    * @return the access key, or -1 if not set
    * @see #setAccessKey(char)
    */
@@ -355,7 +540,7 @@
   /**
    * Get the row value at the specified visible index. Index 0 corresponds to
    * the first item on the page.
-   *
+   * 
    * @param indexOnPage the index on the page
    * @return the row value
    * @deprecated use {@link #getVisibleItem(int)} instead
@@ -382,6 +567,22 @@
     return presenter.getKeyboardPagingPolicy();
   }
 
+  /**
+   * Get the index of the row that is currently selected via the keyboard,
+   * relative to the page start index.
+   * 
+   * <p>
+   * This is not same as the selected row in the {@link SelectionModel}. The
+   * keyboard selected row refers to the row that the user navigated to via the
+   * keyboard or mouse.
+   * </p>
+   * 
+   * @return the currently selected row, or -1 if none selected
+   */
+  public int getKeyboardSelectedRow() {
+    return presenter.getKeyboardSelectedRow();
+  }
+
   @Override
   public KeyboardSelectionPolicy getKeyboardSelectionPolicy() {
     return presenter.getKeyboardSelectionPolicy();
@@ -396,7 +597,7 @@
    * Return the range size.
    * 
    * @return the size of the range as an int
-   *
+   * 
    * @see #getVisibleRange()
    * @see #setPageSize(int)
    */
@@ -406,9 +607,9 @@
 
   /**
    * Return the range start.
-   *
+   * 
    * @return the start of the range as an int
-   *
+   * 
    * @see #getVisibleRange()
    * @see #setPageStart(int)
    */
@@ -478,7 +679,7 @@
    * Handle browser events. Subclasses should override
    * {@link #onBrowserEvent2(Event)} if they want to extend browser event
    * handling.
-   *
+   * 
    * @see #onBrowserEvent2(Event)
    */
   @Override
@@ -493,8 +694,11 @@
     // Verify that the target is still a child of this widget. IE fires focus
     // events even after the element has been removed from the DOM.
     EventTarget eventTarget = event.getEventTarget();
-    if (!Element.is(eventTarget)
-        || !getElement().isOrHasChild(Element.as(eventTarget))) {
+    if (!Element.is(eventTarget)) {
+      return;
+    }
+    Element target = Element.as(eventTarget);
+    if (!getElement().isOrHasChild(Element.as(eventTarget))) {
       return;
     }
     super.onBrowserEvent(event);
@@ -508,43 +712,14 @@
       // Remember the blur state.
       isFocused = false;
       onBlur();
-    } else if ("keydown".equals(eventType) && !isKeyboardNavigationSuppressed()) {
-      // A key event indicates that we have focus.
+    } else if ("keydown".equals(eventType)) {
+      // A key event indicates that we already have focus.
       isFocused = true;
-
-      // Handle keyboard navigation. Prevent default on navigation events to
-      // prevent default scrollbar behavior.
-      int keyCode = event.getKeyCode();
-      switch (keyCode) {
-        case KeyCodes.KEY_DOWN:
-          presenter.keyboardNext();
-          event.preventDefault();
-          return;
-        case KeyCodes.KEY_UP:
-          presenter.keyboardPrev();
-          event.preventDefault();
-          return;
-        case KeyCodes.KEY_PAGEDOWN:
-          presenter.keyboardNextPage();
-          event.preventDefault();
-          return;
-        case KeyCodes.KEY_PAGEUP:
-          presenter.keyboardPrevPage();
-          event.preventDefault();
-          return;
-        case KeyCodes.KEY_HOME:
-          presenter.keyboardHome();
-          event.preventDefault();
-          return;
-        case KeyCodes.KEY_END:
-          presenter.keyboardEnd();
-          event.preventDefault();
-          return;
-        case 32:
-          // Prevent the list box from scrolling.
-          event.preventDefault();
-          return;
-      }
+    } else if ("mousedown".equals(eventType)
+        && CellBasedWidgetImpl.get().isFocusable(Element.as(target))) {
+      // If a natively focusable element was just clicked, then we must have
+      // focus.
+      isFocused = true;
     }
 
     // Let subclasses handle the event now.
@@ -559,8 +734,19 @@
   }
 
   /**
+   * Redraw a single row using the existing data.
+   * 
+   * @param absRowIndex the absolute row index to redraw
+   */
+  public void redrawRow(int absRowIndex) {
+    int relRowIndex = absRowIndex - getPageStart();
+    checkRowBounds(relRowIndex);
+    setRowData(absRowIndex, Collections.singletonList(getVisibleItem(relRowIndex)));
+  }
+
+  /**
    * {@inheritDoc}
-   *
+   * 
    * @see #getAccessKey()
    */
   @Override
@@ -586,6 +772,55 @@
     presenter.setKeyboardPagingPolicy(policy);
   }
 
+  /**
+   * Set the keyboard selected row. The row index is the index relative to the
+   * current page start index.
+   * 
+   * <p>
+   * If keyboard selection is disabled, this method does nothing.
+   * </p>
+   * 
+   * <p>
+   * If the keyboard selected row is outside of the range of the current page
+   * (that is, less than 0 or greater than or equal to the page size), the page
+   * or range will be adjusted depending on the keyboard paging policy. If the
+   * keyboard paging policy is limited to the current range, the row index will
+   * be clipped to the current page.
+   * </p>
+   * 
+   * @param row the row index relative to the page start
+   */
+  public final void setKeyboardSelectedRow(int row) {
+    setKeyboardSelectedRow(row, true);
+  }
+
+  /**
+   * Set the keyboard selected row and optionally focus on the new row.
+   * 
+   * @param row the row index relative to the page start
+   * @param stealFocus true to focus on the new row
+   * @see #setKeyboardSelectedRow(int)
+   */
+  public void setKeyboardSelectedRow(int row, boolean stealFocus) {
+    presenter.setKeyboardSelectedRow(row, stealFocus, true);
+  }
+
+  /**
+   * Set the handler that handles keyboard selection/navigation.
+   */
+  public void setKeyboardSelectionHandler(CellPreviewEvent.Handler<T> keyboardSelectionReg) {
+    // Remove the old manager.
+    if (this.keyboardSelectionReg != null) {
+      this.keyboardSelectionReg.removeHandler();
+      this.keyboardSelectionReg = null;
+    }
+
+    // Add the new manager.
+    if (keyboardSelectionReg != null) {
+      this.keyboardSelectionReg = addCellPreviewHandler(keyboardSelectionReg);
+    }
+  }
+
   @Override
   public void setKeyboardSelectionPolicy(KeyboardSelectionPolicy policy) {
     presenter.setKeyboardSelectionPolicy(policy);
@@ -593,7 +828,7 @@
 
   /**
    * Set the number of rows per page and refresh the view.
-   *
+   * 
    * @param pageSize the page size
    * @see #setVisibleRange(Range)
    * @see #getPageSize()
@@ -605,7 +840,7 @@
   /**
    * Set the starting index of the current visible page. The actual page start
    * will be clamped in the range [0, getSize() - 1].
-   *
+   * 
    * @param pageStart the index of the row that should appear at the start of
    *          the page
    * @see #setVisibleRange(Range)
@@ -655,9 +890,8 @@
    * <p>
    * By default, selection occurs when the user clicks on a Cell or presses the
    * spacebar. If you need finer control over selection, you can specify a
-   * {@link DefaultSelectionEventManager} using 
-   * {@link #setSelectionModel(SelectionModel, com.google.gwt.view.client.CellPreviewEvent.Handler)}.
-   * {@link DefaultSelectionEventManager} provides some default
+   * {@link DefaultSelectionEventManager} using
+   * {@link #setSelectionModel(SelectionModel, com.google.gwt.view.client.CellPreviewEvent.Handler)}. {@link DefaultSelectionEventManager} provides some default
    * implementations to handle checkbox based selection, as well as a blacklist
    * or whitelist of columns to prevent or allow selection.
    * </p>
@@ -714,14 +948,13 @@
   }
 
   @Override
-  public void setVisibleRangeAndClearData(Range range,
-      boolean forceRangeChangeEvent) {
+  public void setVisibleRangeAndClearData(Range range, boolean forceRangeChangeEvent) {
     presenter.setVisibleRangeAndClearData(range, forceRangeChangeEvent);
   }
 
   /**
    * Check if a cell consumes the specified event type.
-   *
+   * 
    * @param cell the cell
    * @param eventType the event type to check
    * @return true if consumed, false if not
@@ -733,21 +966,20 @@
 
   /**
    * Check that the row is within the correct bounds.
-   *
+   * 
    * @param row row index to check
    * @throws IndexOutOfBoundsException
    */
   protected void checkRowBounds(int row) {
     if (!isRowWithinBounds(row)) {
-      throw new IndexOutOfBoundsException("Row index: " + row + ", Row size: "
-          + getRowCount());
+      throw new IndexOutOfBoundsException("Row index: " + row + ", Row size: " + getRowCount());
     }
   }
 
   /**
    * 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
    */
@@ -757,7 +989,7 @@
 
   /**
    * Check whether or not the cells in the view depend on the selection state.
-   *
+   * 
    * @return true if cells depend on selection, false if not
    */
   protected abstract boolean dependsOnSelection();
@@ -770,44 +1002,46 @@
   protected abstract Element getChildContainer();
 
   /**
+   * Get the element that represents the specified index.
+   * 
+   * @param index the index of the row value
+   * @return the the child element, or null if it does not exist
+   */
+  protected Element getChildElement(int index) {
+    Element childContainer = getChildContainer();
+    int childCount = childContainer.getChildCount();
+    return (index < childCount) ? childContainer.getChild(index).<Element> cast() : null;
+  }
+
+  /**
    * Get the element that has keyboard selection.
-   *
+   * 
    * @return the keyboard selected element
    */
   protected abstract Element getKeyboardSelectedElement();
 
   /**
-   * Get the row index of the keyboard selected row.
-   *
-   * @return the row index
-   */
-  protected int getKeyboardSelectedRow() {
-    return presenter.getKeyboardSelectedRow();
-  }
-
-  /**
    * Get the key for the specified value.
-   *
+   * 
    * @param value the value
    * @return the key
    */
   protected Object getValueKey(T value) {
     ProvidesKey<T> keyProvider = getKeyProvider();
-    return (keyProvider == null || value == null) ? value
-        : keyProvider.getKey(value);
+    return (keyProvider == null || value == null) ? value : keyProvider.getKey(value);
   }
 
   /**
    * Check if keyboard navigation is being suppressed, such as when the user is
    * editing a cell.
-   *
+   * 
    * @return true if suppressed, false if not
    */
   protected abstract boolean isKeyboardNavigationSuppressed();
 
   /**
    * Checks that the row is within bounds of the view.
-   *
+   * 
    * @param row row index to check
    * @return true if within bounds, false if not
    */
@@ -823,7 +1057,7 @@
 
   /**
    * Called after {@link #onBrowserEvent(Event)} completes.
-   *
+   * 
    * @param event the event that was fired
    */
   protected void onBrowserEvent2(Event event) {
@@ -852,32 +1086,36 @@
   }
 
   /**
-   * Called when selection changes.
-   * 
-   * @deprecated this method is never called by AbstractHasData, render the
-   *             selected styles in
-   *             {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
-   */
-  @Deprecated
-  protected void onUpdateSelection() {
-  }
-
-  /**
    * Render all row values into the specified {@link SafeHtmlBuilder}.
-   *
+   * 
+   * <p>
+   * Subclasses can optionally throw an {@link UnsupportedOperationException} if
+   * they prefer to render the rows in
+   * {@link #replaceAllChildren(List, SafeHtml)} and
+   * {@link #replaceChildren(List, int, SafeHtml)}. In this case, the
+   * {@link SafeHtml} argument will be null. Though a bit hacky, this is
+   * designed to supported legacy widgets that use {@link SafeHtmlBuilder}, and
+   * newer widgets that use other builders, such as the ElementBuilder API.
+   * </p>
+   * 
    * @param sb the {@link SafeHtmlBuilder} to render into
    * @param values the row values
    * @param start the absolute start index of the values
    * @param selectionModel the {@link SelectionModel}
+   * @throws UnsupportedOperationException if the values will be rendered in
+   *           {@link #replaceAllChildren(List, SafeHtml)} and
+   *           {@link #replaceChildren(List, int, SafeHtml)}
    */
-  protected abstract void renderRowValues(SafeHtmlBuilder sb, List<T> values,
-      int start, SelectionModel<? super T> selectionModel);
+  protected abstract void renderRowValues(SafeHtmlBuilder sb, List<T> values, int start,
+      SelectionModel<? super T> selectionModel) throws UnsupportedOperationException;
 
   /**
    * Replace all children with the specified html.
-   *
+   * 
    * @param values the values of the new children
-   * @param html the html to render in the child
+   * @param html the html to render, or null if
+   *          {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
+   *          throws an {@link UnsupportedOperationException}
    */
   protected void replaceAllChildren(List<T> values, SafeHtml html) {
     replaceAllChildren(this, getChildContainer(), html);
@@ -888,10 +1126,12 @@
    * 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, relative to the page start
-   * @param html the HTML to convert
+   * @param html the html to render, or null if
+   *          {@link #renderRowValues(SafeHtmlBuilder, List, int, SelectionModel)}
+   *          throws an {@link UnsupportedOperationException}
    */
   protected void replaceChildren(List<T> values, int start, SafeHtml html) {
     Element newChildren = convertToElements(html);
@@ -900,14 +1140,14 @@
 
   /**
    * Reset focus on the currently focused cell.
-   *
+   * 
    * @return true if focus is taken, false if not
    */
   protected abstract boolean resetFocusOnCell();
 
   /**
    * Make an element focusable or not.
-   *
+   * 
    * @param elem the element
    * @param focusable true to make focusable, false to make unfocusable
    */
@@ -930,17 +1170,16 @@
 
   /**
    * Update an element to reflect its keyboard selected state.
-   *
+   * 
    * @param index the index of the element
    * @param selected true if selected, false if not
    * @param stealFocus true if the row should steal focus, false if not
    */
-  protected abstract void setKeyboardSelected(int index, boolean selected,
-      boolean stealFocus);
+  protected abstract void setKeyboardSelected(int index, boolean selected, boolean stealFocus);
 
   /**
    * Update an element to reflect its selected state.
-   *
+   * 
    * @param elem the element to update
    * @param selected true if selected, false if not
    * @deprecated this method is never called by AbstractHasData, render the
@@ -956,12 +1195,11 @@
    * Add a {@link ValueChangeHandler} that is called when the display values
    * change. Used by {@link CellBrowser} to detect when the displayed data
    * changes.
-   *
+   * 
    * @param handler the handler
    * @return a {@link HandlerRegistration} to remove the handler
    */
-  final HandlerRegistration addValueChangeHandler(
-      ValueChangeHandler<List<T>> handler) {
+  final HandlerRegistration addValueChangeHandler(ValueChangeHandler<List<T>> handler) {
     return addHandler(handler, ValueChangeEvent.getType());
   }
 
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 a73b63e..ca06a36 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellList.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellList.java
@@ -130,14 +130,6 @@
   interface Template extends SafeHtmlTemplates {
     @Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\" style=\"outline:none;\" >{2}</div>")
     SafeHtml div(int idx, String classes, SafeHtml cellContents);
-
-    @Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\" style=\"outline:none;\" tabindex=\"{2}\">{3}</div>")
-    SafeHtml divFocusable(int idx, String classes, int tabIndex,
-        SafeHtml cellContents);
-
-    @Template("<div onclick=\"\" __idx=\"{0}\" class=\"{1}\" style=\"outline:none;\" tabindex=\"{2}\" accesskey=\"{3}\">{4}</div>")
-    SafeHtml divFocusableWithKey(int idx, String classes, int tabIndex,
-        char accessKey, SafeHtml cellContents);
   }
 
   /**
@@ -212,8 +204,7 @@
    * @param keyProvider an instance of ProvidesKey<T>, or null if the record
    *          object should act as its own key
    */
-  public CellList(final Cell<T> cell, Resources resources,
-      ProvidesKey<T> keyProvider) {
+  public CellList(final Cell<T> cell, Resources resources, ProvidesKey<T> keyProvider) {
     super(Document.get().createDivElement(), DEFAULT_PAGE_SIZE, keyProvider);
     this.cell = cell;
     this.style = resources.cellListStyle();
@@ -354,20 +345,6 @@
   }
 
   /**
-   * Called when a user action triggers selection.
-   * 
-   * @param event the event that triggered selection
-   * @param value the value that was selected
-   * @param indexOnPage the index of the value on the page
-   * @deprecated use
-   *             {@link #addCellPreviewHandler(com.google.gwt.view.client.CellPreviewEvent.Handler)}
-   *             instead
-   */
-  @Deprecated
-  protected void doSelection(Event event, T value, int indexOnPage) {
-  }
-
-  /**
    * Fire an event to the cell.
    * 
    * @param context the {@link Context} of the cell
@@ -375,8 +352,7 @@
    * @param parent the parent of the cell
    * @param value the value of the cell
    */
-  protected void fireEventToCell(Context context, Event event, Element parent,
-      T value) {
+  protected void fireEventToCell(Context context, Event event, Element parent, T value) {
     Set<String> consumedEvents = cell.getConsumedEvents();
     if (consumedEvents != null && consumedEvents.contains(event.getType())) {
       boolean cellWasEditing = cell.isEditing(context, parent, value);
@@ -384,6 +360,7 @@
       cellIsEditing = cell.isEditing(context, parent, value);
       if (cellWasEditing && !cellIsEditing) {
         CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
+          @Override
           public void execute() {
             setFocus(true);
           }
@@ -452,8 +429,7 @@
     // Forward the event to the cell.
     String idxString = "";
     Element cellTarget = target;
-    while ((cellTarget != null)
-        && ((idxString = cellTarget.getAttribute("__idx")).length() == 0)) {
+    while ((cellTarget != null) && ((idxString = cellTarget.getAttribute("__idx")).length() == 0)) {
       cellTarget = cellTarget.getParentElement();
     }
     if (idxString.length() > 0) {
@@ -470,27 +446,15 @@
       }
 
       // Get the cell parent before doing selection in case the list is redrawn.
-      boolean isSelectionHandled = cell.handlesSelection()
-          || KeyboardSelectionPolicy.BOUND_TO_SELECTION == getKeyboardSelectionPolicy();
+      boolean isSelectionHandled =
+          cell.handlesSelection()
+              || KeyboardSelectionPolicy.BOUND_TO_SELECTION == getKeyboardSelectionPolicy();
       Element cellParent = getCellParent(cellTarget);
       T value = getVisibleItem(indexOnPage);
       Context context = new Context(idx, 0, getValueKey(value));
-      CellPreviewEvent<T> previewEvent = CellPreviewEvent.fire(this, event,
-          this, context, value, cellIsEditing, isSelectionHandled);
-      if (isClick && !cellIsEditing && !isSelectionHandled) {
-        doSelection(event, value, indexOnPage);
-      }
-
-      // Focus on the cell.
-      if (isClick) {
-        /*
-         * If the selected element is natively focusable, then we do not want to
-         * steal focus away from it.
-         */
-        boolean isFocusable = CellBasedWidgetImpl.get().isFocusable(target);
-        isFocused = isFocused || isFocusable;
-        getPresenter().setKeyboardSelectedRow(indexOnPage, !isFocusable, false);
-      }
+      CellPreviewEvent<T> previewEvent =
+          CellPreviewEvent.fire(this, event, this, context, value, cellIsEditing,
+              isSelectionHandled);
 
       // Fire the event to the cell if the list has not been refreshed.
       if (!previewEvent.isCanceled()) {
@@ -549,8 +513,7 @@
     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);
+      boolean isSelected = selectionModel == null ? false : selectionModel.isSelected(value);
 
       StringBuilder classesBuilder = new StringBuilder();
       classesBuilder.append(i % 2 == 0 ? evenItem : oddItem);
@@ -561,24 +524,7 @@
       SafeHtmlBuilder cellBuilder = new SafeHtmlBuilder();
       Context context = new Context(i, 0, getValueKey(value));
       cell.render(context, value, cellBuilder);
-
-      if (i == keyboardSelectedRow) {
-        // This is the focused item.
-        if (isFocused) {
-          classesBuilder.append(keyboardSelectedItem);
-        }
-        char accessKey = getAccessKey();
-        if (accessKey != 0) {
-          sb.append(TEMPLATE.divFocusableWithKey(i, classesBuilder.toString(),
-              getTabIndex(), accessKey, cellBuilder.toSafeHtml()));
-        } else {
-          sb.append(TEMPLATE.divFocusable(i, classesBuilder.toString(),
-              getTabIndex(), cellBuilder.toSafeHtml()));
-        }
-      } else {
-        sb.append(TEMPLATE.div(i, classesBuilder.toString(),
-            cellBuilder.toSafeHtml()));
-      }
+      sb.append(TEMPLATE.div(i, classesBuilder.toString(), cellBuilder.toSafeHtml()));
     }
   }
 
@@ -596,8 +542,7 @@
   }
 
   @Override
-  protected void setKeyboardSelected(int index, boolean selected,
-      boolean stealFocus) {
+  protected void setKeyboardSelected(int index, boolean selected, boolean stealFocus) {
     if (!isRowWithinBounds(index)) {
       return;
     }
diff --git a/user/src/com/google/gwt/user/cellview/client/CellTableBuilder.java b/user/src/com/google/gwt/user/cellview/client/CellTableBuilder.java
new file mode 100644
index 0000000..4ba4b1d
--- /dev/null
+++ b/user/src/com/google/gwt/user/cellview/client/CellTableBuilder.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2011 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.user.cellview.client;
+
+import com.google.gwt.cell.client.Cell.Context;
+import com.google.gwt.cell.client.HasCell;
+import com.google.gwt.dom.builder.shared.ElementBuilderBase;
+import com.google.gwt.dom.builder.shared.TableRowBuilder;
+
+/**
+ * Builder used to construct a CellTable.
+ * 
+ * @param <T> the row data type
+ */
+public interface CellTableBuilder<T> {
+
+  /**
+   * Utility to help build a table.
+   * 
+   * @param <T> the row data type
+   */
+  abstract static class Utility<T> {
+
+    /**
+     * Only instantiable by CellTable implementation.
+     */
+    Utility() {
+    }
+
+    /**
+     * Create a {@link Context} object for the specific column index that can be
+     * passed to a Cell.
+     * 
+     * @param column the column index of the context
+     * @return a {@link Context} object
+     */
+    public abstract Context createContext(int column);
+
+    /**
+     * Render a Cell into the specified {@link ElementBuilderBase}. Use this
+     * method to ensure that the Cell Widget properly handles events originating
+     * in the Cell.
+     * 
+     * <p>
+     * The {@link ElementBuilderBase} must be in a state where attributes and
+     * html can be appended. If the builder already contains a child element,
+     * this method will fail.
+     * </p>
+     * 
+     * @param <C> the data type of the cell
+     * @param builder the {@link ElementBuilderBase} to render into
+     * @param context the {@link Context} of the cell
+     * @param column the column or {@link HasCell} to render
+     * @param rowValue the row value to render
+     * @see #createContext(int)
+     */
+    public abstract <C> void renderCell(ElementBuilderBase<?> builder, Context context,
+        HasCell<T, C> column, T rowValue);
+
+    /**
+     * Add a row to the table.
+     * 
+     * @return the row to add
+     */
+    public abstract TableRowBuilder startRow();
+  }
+
+  /**
+   * Build zero or more table rows for the specified row value using the
+   * {@link Utility}.
+   * 
+   * @param rowValue the value for the row to render
+   * @param absRowIndex the absolute row index
+   * @param utility the utility used to build the table
+   */
+  void buildRow(T rowValue, int absRowIndex, Utility<T> utility);
+}
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 b7befb3..9c7cd7f 100644
--- a/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.java
+++ b/user/src/com/google/gwt/user/cellview/client/CellTreeNodeView.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
@@ -63,7 +63,7 @@
 
 /**
  * A view of a tree node.
- *
+ * 
  * @param <T> the type that this view contains
  */
 // TODO(jlabanca): Convert this to be the type of the child and create lazily.
@@ -72,8 +72,8 @@
   interface Template extends SafeHtmlTemplates {
     @Template("<div onclick=\"\" style=\"{0}position:relative;\""
         + " class=\"{1}\">{2}<div class=\"{3}\">{4}</div></div>")
-    SafeHtml innerDiv(SafeStyles cssString, String classes, SafeHtml image,
-        String itemValueStyle, SafeHtml cellContents);
+    SafeHtml innerDiv(SafeStyles cssString, String classes, SafeHtml image, String itemValueStyle,
+        SafeHtml cellContents);
 
     @Template("<div><div style=\"{0}\" class=\"{1}\">{2}</div></div>")
     SafeHtml outerDiv(SafeStyles cssString, String classes, SafeHtml content);
@@ -84,7 +84,7 @@
    * class is intentionally static because we might move it to a new
    * {@link CellTreeNodeView}, and we don't want non-static references to the
    * old {@link CellTreeNodeView}.
-   *
+   * 
    * @param <C> the child item type
    */
   static class NodeCellList<C> implements HasData<C> {
@@ -101,12 +101,10 @@
       }
 
       @Override
-      public <H extends EventHandler> HandlerRegistration addHandler(H handler,
-          Type<H> type) {
+      public <H extends EventHandler> HandlerRegistration addHandler(H handler, Type<H> type) {
         return handlerManger.addHandler(type, handler);
       }
 
-      @Override
       public void render(SafeHtmlBuilder sb, List<C> values, int start,
           SelectionModel<? super C> selectionModel) {
         // Cache the style names that will be used for each child.
@@ -184,15 +182,19 @@
                   cellBuilder.toSafeHtml());
 
           SafeStyles outerPadding =
-              SafeStylesUtils.fromTrustedString("padding-" + paddingDirection + ": " + paddingAmount
-                  + "px;");
+              SafeStylesUtils.fromTrustedString("padding-" + paddingDirection + ": "
+                  + paddingAmount + "px;");
           sb.append(template.outerDiv(outerPadding, outerClasses.toString(), innerDiv));
         }
       }
 
       @Override
-      public void replaceAllChildren(List<C> values, SafeHtml html,
-          boolean stealFocus, boolean contentChanged) {
+      public void replaceAllChildren(List<C> values, SelectionModel<? super C> selectionModel,
+          boolean stealFocus) {
+        // Render the children.
+        SafeHtmlBuilder sb = new SafeHtmlBuilder();
+        render(sb, values, 0, selectionModel);
+
         // Hide the child container so we can animate it.
         if (nodeView.tree.isAnimationEnabled()) {
           nodeView.ensureAnimationFrame().getStyle().setDisplay(Display.NONE);
@@ -201,9 +203,7 @@
         // Replace the child nodes.
         nodeView.tree.isRefreshing = true;
         Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, 0);
-        if (contentChanged) {
-          AbstractHasData.replaceAllChildren(nodeView.tree, childContainer, html);
-        }
+        AbstractHasData.replaceAllChildren(nodeView.tree, childContainer, sb.toSafeHtml());
         nodeView.tree.isRefreshing = false;
 
         // Trim the list of children.
@@ -218,8 +218,7 @@
         loadChildState(values, 0, savedViews);
 
         // If this is the root node, move keyboard focus to the first child.
-        if (nodeView.isRootNode()
-            && nodeView.tree.getKeyboardSelectedNode() == nodeView
+        if (nodeView.isRootNode() && nodeView.tree.getKeyboardSelectedNode() == nodeView
             && values.size() > 0) {
           nodeView.tree.keyboardSelect(nodeView.children.get(0), false);
         }
@@ -231,16 +230,19 @@
       }
 
       @Override
-      public void replaceChildren(List<C> values, int start, SafeHtml html,
-          boolean stealFocus) {
-        Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values,
-            start);
+      public void replaceChildren(List<C> values, int start,
+          SelectionModel<? super C> selectionModel, boolean stealFocus) {
+        // Render the children.
+        SafeHtmlBuilder sb = new SafeHtmlBuilder();
+        render(sb, values, 0, selectionModel);
+
+        Map<Object, CellTreeNodeView<?>> savedViews = saveChildState(values, start);
 
         nodeView.tree.isRefreshing = true;
-        Element newChildren = AbstractHasData.convertToElements(nodeView.tree,
-            getTmpElem(), html);
-        AbstractHasData.replaceChildren(nodeView.tree, childContainer,
-            newChildren, start, html);
+        SafeHtml html = sb.toSafeHtml();
+        Element newChildren = AbstractHasData.convertToElements(nodeView.tree, getTmpElem(), html);
+        AbstractHasData
+            .replaceChildren(nodeView.tree, childContainer, newChildren, start, html);
         nodeView.tree.isRefreshing = false;
 
         loadChildState(values, start, savedViews);
@@ -252,19 +254,17 @@
       }
 
       @Override
-      public void setKeyboardSelected(int index, boolean selected,
-          boolean stealFocus) {
+      public void setKeyboardSelected(int index, boolean selected, boolean stealFocus) {
         // Keyboard selection is handled by CellTree.
         Element elem = childContainer.getChild(index).cast();
-        setStyleName(getSelectionElement(elem),
-            nodeView.tree.getStyle().cellTreeKeyboardSelectedItem(), selected);
+        setStyleName(getSelectionElement(elem), nodeView.tree.getStyle()
+            .cellTreeKeyboardSelectedItem(), selected);
       }
 
       @Override
       public void setLoadingState(LoadingState state) {
         nodeView.updateImage(state == LoadingState.LOADING);
-        showOrHide(nodeView.emptyMessageElem, state == LoadingState.LOADED
-            && presenter.isEmpty());
+        showOrHide(nodeView.emptyMessageElem, state == LoadingState.LOADED && presenter.isEmpty());
       }
 
       /**
@@ -360,13 +360,12 @@
        * 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) {
+      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<?>>();
@@ -419,16 +418,16 @@
     private final NodeInfo<C> nodeInfo;
     private CellTreeNodeView<?> nodeView;
 
-    public NodeCellList(final NodeInfo<C> nodeInfo,
-        final CellTreeNodeView<?> nodeView, int pageSize) {
+    public NodeCellList(final NodeInfo<C> nodeInfo, final CellTreeNodeView<?> nodeView, int pageSize) {
       this.defaultPageSize = pageSize;
       this.nodeInfo = nodeInfo;
       this.nodeView = nodeView;
       cell = nodeInfo.getCell();
 
       // Create a presenter.
-      presenter = new HasDataPresenter<C>(this, new View(
-          nodeView.ensureChildContainer()), pageSize, nodeInfo.getProvidesKey());
+      presenter =
+          new HasDataPresenter<C>(this, new View(nodeView.ensureChildContainer()), pageSize,
+              nodeInfo.getProvidesKey());
 
       // Disable keyboard selection because it is handled by CellTree.
       presenter.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.DISABLED);
@@ -451,14 +450,12 @@
     }
 
     @Override
-    public HandlerRegistration addRangeChangeHandler(
-        RangeChangeEvent.Handler handler) {
+    public HandlerRegistration addRangeChangeHandler(RangeChangeEvent.Handler handler) {
       return presenter.addRangeChangeHandler(handler);
     }
 
     @Override
-    public HandlerRegistration addRowCountChangeHandler(
-        RowCountChangeEvent.Handler handler) {
+    public HandlerRegistration addRowCountChangeHandler(RowCountChangeEvent.Handler handler) {
       return presenter.addRowCountChangeHandler(handler);
     }
 
@@ -544,8 +541,7 @@
     }
 
     @Override
-    public void setVisibleRangeAndClearData(Range range,
-        boolean forceRangeChangeEvent) {
+    public void setVisibleRangeAndClearData(Range range, boolean forceRangeChangeEvent) {
       presenter.setVisibleRangeAndClearData(range, forceRangeChangeEvent);
     }
   }
@@ -582,8 +578,7 @@
     @Override
     public int getIndex() {
       assertNotDestroyed();
-      return (nodeView.parentNode == null) ? 0
-          : nodeView.parentNode.children.indexOf(nodeView);
+      return (nodeView.parentNode == null) ? 0 : nodeView.parentNode.children.indexOf(nodeView);
     }
 
     @Override
@@ -652,7 +647,7 @@
 
     /**
      * Check the child bounds.
-     *
+     * 
      * @param index the index of the child
      * @throws IndexOutOfBoundsException if the child is not in range
      */
@@ -684,7 +679,8 @@
   /**
    * The element used in place of an image when a node has no children.
    */
-  private static final SafeHtml LEAF_IMAGE = SafeHtmlUtils.fromSafeConstant("<div style='position:absolute;display:none;'></div>");
+  private static final SafeHtml LEAF_IMAGE = SafeHtmlUtils
+      .fromSafeConstant("<div style='position:absolute;display:none;'></div>");
 
   private static final Template template = GWT.create(Template.class);
 
@@ -695,7 +691,7 @@
 
   /**
    * Returns the element that parents the cell contents of the node.
-   *
+   * 
    * @param nodeElem the element that represents the node
    * @return the cell parent within the node
    */
@@ -705,7 +701,7 @@
 
   /**
    * Returns the element that selection is applied to.
-   *
+   * 
    * @param nodeElem the element that represents the node
    * @return the cell parent within the node
    */
@@ -715,7 +711,7 @@
 
   /**
    * Returns the element that selection is applied to.
-   *
+   * 
    * @param nodeElem the element that represents the node
    * @return the cell parent within the node
    */
@@ -735,7 +731,7 @@
 
   /**
    * Show or hide an element.
-   *
+   * 
    * @param element the element to show or hide
    * @param show true to show, false to hide
    */
@@ -844,7 +840,7 @@
 
   /**
    * Construct a {@link CellTreeNodeView}.
-   *
+   * 
    * @param tree the parent {@link CellTreeNodeView}
    * @param parent the parent {@link CellTreeNodeView}
    * @param parentNodeInfo the {@link NodeInfo} of the parent
@@ -875,7 +871,7 @@
 
   /**
    * Check whether or not this node is open.
-   *
+   * 
    * @return true if open, false if closed
    */
   public boolean isOpen() {
@@ -884,7 +880,7 @@
 
   /**
    * Sets whether this item's children are displayed.
-   *
+   * 
    * @param open whether the item is open
    * @param fireEvents true to fire events if the state changes
    * @return true if successfully opened, false otherwise.
@@ -934,8 +930,7 @@
         showOrHide(showMoreElem, false);
         showOrHide(emptyMessageElem, false);
         if (!isRootNode()) {
-          setStyleName(getCellParent(), tree.getStyle().cellTreeOpenItem(),
-              true);
+          setStyleName(getCellParent(), tree.getStyle().cellTreeOpenItem(), true);
         }
         ensureAnimationFrame().getStyle().setProperty("display", "");
         onOpen(nodeInfo);
@@ -976,7 +971,7 @@
 
   /**
    * Unregister the list handler and destroy all child nodes.
-   *
+   * 
    * @param destroy true to destroy this node
    */
   protected void cleanup(boolean destroy) {
@@ -1017,7 +1012,7 @@
   /**
    * Returns an instance of TreeNodeView of the same subclass as the calling
    * object.
-   *
+   * 
    * @param <C> the data type of the node's children
    * @param nodeInfo a NodeInfo object describing the child nodes
    * @param childElem the DOM element used to parent the new TreeNodeView
@@ -1025,8 +1020,8 @@
    * @param viewData view data associated with the node
    * @return a TreeNodeView of suitable type
    */
-  protected <C> CellTreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo,
-      Element childElem, C childValue, Object viewData) {
+  protected <C> CellTreeNodeView<C> createTreeNodeView(NodeInfo<C> nodeInfo, Element childElem,
+      C childValue, Object viewData) {
     return new CellTreeNodeView<C>(tree, this, nodeInfo, childElem, childValue);
   }
 
@@ -1049,21 +1044,22 @@
     boolean cellWasEditing = parentCell.isEditing(context, cellParent, value);
 
     // Update selection.
-    boolean isSelectionHandled = parentCell.handlesSelection()
-        || KeyboardSelectionPolicy.BOUND_TO_SELECTION == tree.getKeyboardSelectionPolicy();
+    boolean isSelectionHandled =
+        parentCell.handlesSelection()
+            || KeyboardSelectionPolicy.BOUND_TO_SELECTION == tree.getKeyboardSelectionPolicy();
     HasData<T> display = (HasData<T>) parentNode.listView;
-    CellPreviewEvent<T> previewEvent = CellPreviewEvent.fire(display, event,
-        display, context, value, cellWasEditing, isSelectionHandled);
+    CellPreviewEvent<T> previewEvent =
+        CellPreviewEvent.fire(display, event, display, context, value, cellWasEditing,
+            isSelectionHandled);
 
     // Forward the event to the cell.
-    if (previewEvent.isCanceled()
-        || !cellParent.isOrHasChild(Element.as(event.getEventTarget()))) {
+    if (previewEvent.isCanceled() || !cellParent.isOrHasChild(Element.as(event.getEventTarget()))) {
       return;
     }
     Set<String> consumedEvents = parentCell.getConsumedEvents();
     if (consumedEvents != null && consumedEvents.contains(eventType)) {
-      parentCell.onBrowserEvent(context, cellParent, value, event,
-          parentNodeInfo.getValueUpdater());
+      parentCell
+          .onBrowserEvent(context, cellParent, value, event, parentNodeInfo.getValueUpdater());
       tree.cellIsEditing = parentCell.isEditing(context, cellParent, value);
       if (cellWasEditing && !tree.cellIsEditing) {
         CellBasedWidgetImpl.get().resetFocus(new Scheduler.ScheduledCommand() {
@@ -1085,7 +1081,7 @@
 
   /**
    * Returns the element corresponding to the open/close image.
-   *
+   * 
    * @return the open/close image element
    */
   protected Element getImageElement() {
@@ -1113,14 +1109,13 @@
 
   /**
    * Set up the node when it is opened.
-   *
+   * 
    * @param nodeInfo the {@link NodeInfo} that provides information about the
    *          child values
    * @param <C> the child data type of the node
    */
   protected <C> void onOpen(final NodeInfo<C> nodeInfo) {
-    NodeCellList<C> view = new NodeCellList<C>(nodeInfo, this,
-        tree.getDefaultNodeSize());
+    NodeCellList<C> view = new NodeCellList<C>(nodeInfo, this, tree.getDefaultNodeSize());
     listView = view;
     view.setSelectionModel(nodeInfo.getSelectionModel());
     nodeInfo.setDataDisplay(view);
@@ -1128,7 +1123,7 @@
 
   /**
    * Ensure that the animation frame exists and return it.
-   *
+   * 
    * @return the animation frame
    */
   Element ensureAnimationFrame() {
@@ -1143,7 +1138,7 @@
 
   /**
    * Ensure that the child container exists and return it.
-   *
+   * 
    * @return the child container
    */
   Element ensureChildContainer() {
@@ -1156,7 +1151,7 @@
 
   /**
    * Ensure that the content container exists and return it.
-   *
+   * 
    * @return the content container
    */
   Element ensureContentContainer() {
@@ -1167,8 +1162,7 @@
       // TODO(jlabanca): I18N no data string.
       emptyMessageElem = Document.get().createDivElement();
       emptyMessageElem.setInnerHTML("no data");
-      setStyleName(emptyMessageElem, tree.getStyle().cellTreeEmptyMessage(),
-          true);
+      setStyleName(emptyMessageElem, tree.getStyle().cellTreeEmptyMessage(), true);
       showOrHide(emptyMessageElem, false);
       contentContainer.appendChild(emptyMessageElem);
 
@@ -1202,7 +1196,7 @@
 
   /**
    * Get a {@link TreeNode} with a public API for this node view.
-   *
+   * 
    * @return the {@link TreeNode}
    */
   TreeNode getTreeNode() {
@@ -1222,7 +1216,7 @@
 
   /**
    * Check if this node is a root node.
-   *
+   * 
    * @return true if a root node
    */
   boolean isRootNode() {
@@ -1231,7 +1225,7 @@
 
   /**
    * Check if the value of this node is selected.
-   *
+   * 
    * @return true if selected, false if not
    */
   boolean isSelected() {
@@ -1260,7 +1254,7 @@
 
   /**
    * Select or deselect this node with the keyboard.
-   *
+   * 
    * @param selected true if selected, false if not
    * @param stealFocus true to steal focus
    */
@@ -1303,22 +1297,21 @@
 
   /**
    * Add or remove the keyboard selected style.
-   *
+   * 
    * @param selected true if selected, false if not
    */
   void setKeyboardSelectedStyle(boolean selected) {
     if (!isRootNode()) {
       Element selectionElem = getSelectionElement(getElement());
       if (selectionElem != null) {
-        setStyleName(selectionElem,
-            tree.getStyle().cellTreeKeyboardSelectedItem(), selected);
+        setStyleName(selectionElem, tree.getStyle().cellTreeKeyboardSelectedItem(), selected);
       }
     }
   }
 
   /**
    * Select or deselect this node.
-   *
+   * 
    * @param selected true to select, false to deselect
    */
   void setSelected(boolean selected) {
@@ -1345,7 +1338,7 @@
 
   /**
    * Update the image based on the current state.
-   *
+   * 
    * @param isLoading true if still loading data
    */
   private void updateImage(boolean isLoading) {
@@ -1358,8 +1351,7 @@
     boolean isTopLevel = parentNode.isRootNode();
     SafeHtml html = tree.getClosedImageHtml(isTopLevel);
     if (open) {
-      html = isLoading ? tree.getLoadingImageHtml()
-          : tree.getOpenImageHtml(isTopLevel);
+      html = isLoading ? tree.getLoadingImageHtml() : tree.getOpenImageHtml(isTopLevel);
     }
     if (nodeInfoLoaded && nodeInfo == null) {
       html = LEAF_IMAGE;
diff --git a/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java b/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
index c3d219a..84ad29e 100644
--- a/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
+++ b/user/src/com/google/gwt/user/cellview/client/HasDataPresenter.java
@@ -15,13 +15,14 @@
  */
 package com.google.gwt.user.cellview.client;
 
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayInteger;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.shared.EventHandler;
 import com.google.gwt.event.shared.GwtEvent;
 import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
 import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
 import com.google.gwt.view.client.CellPreviewEvent;
@@ -40,7 +41,6 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
-import java.util.TreeSet;
 
 /**
  * <p>
@@ -98,42 +98,27 @@
     <H extends EventHandler> HandlerRegistration addHandler(final H handler, GwtEvent.Type<H> type);
 
     /**
-     * Construct the HTML that represents the list of values, taking the
-     * selection state into account.
-     * 
-     * @param sb the {@link SafeHtmlBuilder} to build into
-     * @param values the values to render
-     * @param start the absolute start index that is being rendered
-     * @param selectionModel the {@link SelectionModel}
-     */
-    void render(SafeHtmlBuilder sb, List<T> values, int start,
-        SelectionModel<? super T> selectionModel);
-
-    /**
-     * Replace all children with the specified html.
+     * Replace all children with the specified values.
      * 
      * @param values the values of the new children
-     * @param html the html to render in the child
+     * @param selectionModel the {@link SelectionModel}
      * @param stealFocus true if the row should steal focus, false if not
-     * @param contentChanged indicates whether or not the content has changed
-     *          since the last call. If the content has not changed, widgets can
-     *          choose not to rerender themselves
      */
-    void replaceAllChildren(List<T> values, SafeHtml html, boolean stealFocus,
-        boolean contentChanged);
+    void replaceAllChildren(List<T> values, SelectionModel<? super T> selectionModel,
+        boolean stealFocus);
 
     /**
-     * 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.
+     * Replace 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, relative to the pageStart
-     * @param html the HTML to convert
+     * @param selectionModel the {@link SelectionModel}
      * @param stealFocus true if the row should steal focus, false if not
      */
-    void replaceChildren(List<T> values, int start, SafeHtml html, boolean stealFocus);
+    void replaceChildren(List<T> values, int start, SelectionModel<? super T> selectionModel,
+        boolean stealFocus);
 
     /**
      * Re-establish focus on an element within the view if the view already had
@@ -409,6 +394,18 @@
    */
   private static final double REDRAW_THRESHOLD = 0.30;
 
+  /**
+   * Sort a native integer array numerically.
+   * 
+   * @param array the array to sort
+   */
+  private static native void sortJsArrayInteger(JsArrayInteger array) /*-{
+    // sort() sorts lexicographically by default.
+    array.sort(function(x, y) {
+      return x - y;
+    });
+  }-*/;
+
   private final HasData<T> display;
 
   /**
@@ -422,13 +419,6 @@
   private final ProvidesKey<T> keyProvider;
 
   /**
-   * 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. This is useful for apps that continuously refresh the view.
-   */
-  private SafeHtml lastContents = null;
-
-  /**
    * The pending state of the presenter to be pushed to the view.
    */
   private PendingState<T> pendingState;
@@ -630,39 +620,6 @@
   }
 
   /**
-   * Check if the next call to {@link #keyboardNext()} would succeed.
-   * 
-   * @return true if there is another row accessible by the keyboard
-   */
-  public boolean hasKeyboardNext() {
-    if (KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy) {
-      return false;
-    } else if (getKeyboardSelectedRow() < getVisibleItemCount() - 1) {
-      return true;
-    } else if (!keyboardPagingPolicy.isLimitedToRange()
-        && (getKeyboardSelectedRow() + getPageStart() < getRowCount() - 1 || !isRowCountExact())) {
-      return true;
-    }
-    return false;
-  }
-
-  /**
-   * Check if the next call to {@link #keyboardPrev()} would succeed.
-   * 
-   * @return true if there is a previous row accessible by the keyboard
-   */
-  public boolean hasKeyboardPrev() {
-    if (KeyboardSelectionPolicy.DISABLED == keyboardSelectionPolicy) {
-      return false;
-    } else if (getKeyboardSelectedRow() > 0) {
-      return true;
-    } else if (!keyboardPagingPolicy.isLimitedToRange() && getPageStart() > 0) {
-      return true;
-    }
-    return false;
-  }
-
-  /**
    * Check whether or not there is a pending state. If there is a pending state,
    * views might skip DOM updates and wait for the new data to be rendered when
    * the pending state is resolved.
@@ -689,70 +646,9 @@
   }
 
   /**
-   * Move keyboard selection to the last row.
-   */
-  public void keyboardEnd() {
-    if (!keyboardPagingPolicy.isLimitedToRange()) {
-      setKeyboardSelectedRow(getRowCount() - 1, true, false);
-    }
-  }
-
-  /**
-   * Move keyboard selection to the absolute 0th row.
-   */
-  public void keyboardHome() {
-    if (!keyboardPagingPolicy.isLimitedToRange()) {
-      setKeyboardSelectedRow(-getPageStart(), true, false);
-    }
-  }
-
-  /**
-   * Move keyboard selection to the next row.
-   */
-  public void keyboardNext() {
-    if (hasKeyboardNext()) {
-      setKeyboardSelectedRow(getKeyboardSelectedRow() + 1, true, false);
-    }
-  }
-
-  /**
-   * Move keyboard selection to the next page.
-   */
-  public void keyboardNextPage() {
-    if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) {
-      // 0th index of next page.
-      setKeyboardSelectedRow(getPageSize(), true, false);
-    } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) {
-      setKeyboardSelectedRow(getKeyboardSelectedRow() + PAGE_INCREMENT, true, false);
-    }
-  }
-
-  /**
-   * Move keyboard selection to the previous row.
-   */
-  public void keyboardPrev() {
-    if (hasKeyboardPrev()) {
-      setKeyboardSelectedRow(getKeyboardSelectedRow() - 1, true, false);
-    }
-  }
-
-  /**
-   * Move keyboard selection to the previous page.
-   */
-  public void keyboardPrevPage() {
-    if (KeyboardPagingPolicy.CHANGE_PAGE == keyboardPagingPolicy) {
-      // 0th index of previous page.
-      setKeyboardSelectedRow(-getPageSize(), true, false);
-    } else if (KeyboardPagingPolicy.INCREASE_RANGE == keyboardPagingPolicy) {
-      setKeyboardSelectedRow(getKeyboardSelectedRow() - PAGE_INCREMENT, true, false);
-    }
-  }
-
-  /**
    * Redraw the list with the current data.
    */
   public void redraw() {
-    lastContents = null;
     ensurePendingState().redrawRequired = true;
   }
 
@@ -777,6 +673,12 @@
       return;
     }
 
+    // Clip the row index if the paging policy is limited.
+    if (keyboardPagingPolicy.isLimitedToRange()) {
+      // index will be 0 if visible item count is 0.
+      index = Math.max(0, Math.min(index, getVisibleItemCount() - 1));
+    }
+
     // The user touched the view.
     ensurePendingState().viewTouched = true;
 
@@ -995,18 +897,27 @@
    * range, moving selection from one row to another, or moving keyboard
    * selection.
    * 
+   * <p>
    * Visible for testing.
+   * </p>
    * 
-   * @param modifiedRows the indexes of modified rows
+   * <p>
+   * This method has the side effect of sorting the modified rows.
+   * </p>
+   * 
+   * @param modifiedRows the unordered indexes of modified rows
    * @return up to two ranges that encompass the modified rows
    */
-  List<Range> calculateModifiedRanges(TreeSet<Integer> modifiedRows, int pageStart, int pageEnd) {
+  List<Range> calculateModifiedRanges(JsArrayInteger modifiedRows, int pageStart, int pageEnd) {
+    sortJsArrayInteger(modifiedRows);
+
     int rangeStart0 = -1;
     int rangeEnd0 = -1;
     int rangeStart1 = -1;
     int rangeEnd1 = -1;
     int maxDiff = 0;
-    for (int index : modifiedRows) {
+    for (int i = 0; i < modifiedRows.length(); i++) {
+      int index = modifiedRows.get(i);
       if (index < pageStart || index >= pageEnd) {
         // The index is out of range of the current page.
         continue;
@@ -1188,8 +1099,13 @@
     }
     isResolvingState = true;
 
-    // Keep track of the absolute indexes of modified rows.
-    TreeSet<Integer> modifiedRows = new TreeSet<Integer>();
+    /*
+     * Keep track of the absolute indexes of modified rows.
+     * 
+     * Use a native array to avoid dynamic casts associated with emulated Java
+     * Collections.
+     */
+    JsArrayInteger modifiedRows = JavaScriptObject.createArray().cast();
 
     // Get the values used for calculations.
     State<T> oldState = state;
@@ -1299,10 +1215,10 @@
       if (isSelected) {
         pending.selectedRows.add(i);
         if (!wasSelected) {
-          modifiedRows.add(i);
+          modifiedRows.push(i);
         }
       } else if (wasSelected) {
-        modifiedRows.add(i);
+        modifiedRows.push(i);
       }
     }
 
@@ -1330,14 +1246,14 @@
         replacedEmptyRange = true;
       }
       for (int i = start; i < start + length; i++) {
-        modifiedRows.add(i);
+        modifiedRows.push(i);
       }
     }
 
     // Add keyboard rows to modified rows if we are going to render anyway.
-    if (modifiedRows.size() > 0 && keyboardRowChanged) {
-      modifiedRows.add(oldState.getKeyboardSelectedRow());
-      modifiedRows.add(pending.keyboardSelectedRow);
+    if (modifiedRows.length() > 0 && keyboardRowChanged) {
+      modifiedRows.push(oldState.getKeyboardSelectedRow());
+      modifiedRows.push(pending.keyboardSelectedRow);
     }
 
     // Calculate the modified ranges.
@@ -1391,16 +1307,10 @@
       if (redrawRequired) {
         // Redraw the entire content.
         SafeHtmlBuilder sb = new SafeHtmlBuilder();
-        view.render(sb, pending.rowData, pending.pageStart, selectionModel);
-        SafeHtml newContents = sb.toSafeHtml();
-        boolean contentChanged = !newContents.equals(lastContents);
-        lastContents = newContents;
-        view.replaceAllChildren(pending.rowData, newContents, pending.keyboardStealFocus,
-            contentChanged);
+        view.replaceAllChildren(pending.rowData, selectionModel, pending.keyboardStealFocus);
         view.resetFocus();
       } else if (range0 != null) {
-        // Replace specific rows.
-        lastContents = null;
+        // Surgically replace specific rows.
 
         // Replace range0.
         {
@@ -1408,8 +1318,7 @@
           int relStart = absStart - pageStart;
           SafeHtmlBuilder sb = new SafeHtmlBuilder();
           List<T> replaceValues = pending.rowData.subList(relStart, relStart + range0.getLength());
-          view.render(sb, replaceValues, absStart, selectionModel);
-          view.replaceChildren(replaceValues, relStart, sb.toSafeHtml(), pending.keyboardStealFocus);
+          view.replaceChildren(replaceValues, relStart, selectionModel, pending.keyboardStealFocus);
         }
 
         // Replace range1 if it exists.
@@ -1418,8 +1327,7 @@
           int relStart = absStart - pageStart;
           SafeHtmlBuilder sb = new SafeHtmlBuilder();
           List<T> replaceValues = pending.rowData.subList(relStart, relStart + range1.getLength());
-          view.render(sb, replaceValues, absStart, selectionModel);
-          view.replaceChildren(replaceValues, relStart, sb.toSafeHtml(), pending.keyboardStealFocus);
+          view.replaceChildren(replaceValues, relStart, selectionModel, pending.keyboardStealFocus);
         }
 
         view.resetFocus();
diff --git a/user/src/com/google/gwt/user/client/ui/CustomScrollPanel.java b/user/src/com/google/gwt/user/client/ui/CustomScrollPanel.java
index 3ef57f2..dcb07dc 100644
--- a/user/src/com/google/gwt/user/client/ui/CustomScrollPanel.java
+++ b/user/src/com/google/gwt/user/client/ui/CustomScrollPanel.java
@@ -410,6 +410,11 @@
 
   @Override
   public void setWidget(Widget w) {
+    // Early exit if the widget is unchanged. Avoids updating the scrollbars.
+    if (w == getWidget()) {
+      return;
+    }
+
     super.setWidget(w);
     maybeUpdateScrollbars();
   }
diff --git a/user/test/com/google/gwt/dom/builder/shared/GwtElementBuilderImplTestBase.java b/user/test/com/google/gwt/dom/builder/shared/GwtElementBuilderImplTestBase.java
index 8747617..9b23c61 100644
--- a/user/test/com/google/gwt/dom/builder/shared/GwtElementBuilderImplTestBase.java
+++ b/user/test/com/google/gwt/dom/builder/shared/GwtElementBuilderImplTestBase.java
@@ -50,18 +50,6 @@
     assertEquals("myTitle", div.getTitle());
   }
 
-  public void testFinishTwice() {
-    DivBuilder builder = factory.createDivBuilder();
-    assertNotNull(builder.finish());
-
-    try {
-      builder.finish();
-      fail("Expected IllegalStateException: cannot call finish() twice");
-    } catch (IllegalStateException e) {
-      // Expected.
-    }
-  }
-
   public void testBuildTable() {
     // Build a table.
     TableBuilder tableBuilder = factory.createTableBuilder().id("mytable");
@@ -89,6 +77,42 @@
     }
   }
 
+  public void testFinishTwice() {
+    DivBuilder builder = factory.createDivBuilder();
+    assertNotNull(builder.finish());
+
+    try {
+      builder.finish();
+      fail("Expected IllegalStateException: cannot call finish() twice");
+    } catch (IllegalStateException e) {
+      // Expected.
+    }
+  }
+
+  public void testGetDepth() {
+    DivBuilder parent = factory.createDivBuilder();
+    assertEquals(1, parent.getDepth());
+
+    DivBuilder child = parent.startDiv();
+    assertEquals(2, child.getDepth());
+
+    DivBuilder grandchild0 = child.startDiv();
+    assertEquals(3, grandchild0.getDepth());
+    grandchild0.endDiv();
+    assertEquals(2, child.getDepth());
+
+    DivBuilder grandchild1 = child.startDiv();
+    assertEquals(3, grandchild1.getDepth());
+    grandchild1.endDiv();
+    assertEquals(2, child.getDepth());
+
+    child.endDiv();
+    assertEquals(1, parent.getDepth());
+
+    parent.endDiv();
+    assertEquals(0, parent.getDepth());
+  }
+
   /**
    * Children nested at multiple levels.
    * 
diff --git a/user/test/com/google/gwt/user/cellview/client/AbstractCellTableTestBase.java b/user/test/com/google/gwt/user/cellview/client/AbstractCellTableTestBase.java
index 713c664..9fe43da 100644
--- a/user/test/com/google/gwt/user/cellview/client/AbstractCellTableTestBase.java
+++ b/user/test/com/google/gwt/user/cellview/client/AbstractCellTableTestBase.java
@@ -17,18 +17,23 @@
 
 import com.google.gwt.cell.client.AbstractCell;
 import com.google.gwt.cell.client.Cell;
-import com.google.gwt.cell.client.TextCell;
 import com.google.gwt.cell.client.Cell.Context;
+import com.google.gwt.cell.client.TextCell;
+import com.google.gwt.dom.builder.shared.TableRowBuilder;
 import com.google.gwt.dom.client.Document;
 import com.google.gwt.dom.client.NativeEvent;
-import com.google.gwt.dom.client.TableCellElement;
 import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.dom.client.TableCellElement;
+import com.google.gwt.dom.client.TableSectionElement;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
+import com.google.gwt.user.cellview.client.AbstractHasData.DefaultKeyboardSelectionHandler;
+import com.google.gwt.user.cellview.client.HasKeyboardPagingPolicy.KeyboardPagingPolicy;
 import com.google.gwt.user.cellview.client.LoadingStateChangeEvent.LoadingState;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
 import com.google.gwt.user.client.ui.HasVerticalAlignment;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.view.client.Range;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -95,6 +100,121 @@
     assertEquals(1, loadingStates.size());
   }
 
+  /**
+   * Test that an error is thrown if the builder ends the table body element
+   * instead of the table row element.
+   */
+  public void testBuildTooManyEnds() {
+    final List<Integer> builtRows = new ArrayList<Integer>();
+    T table = createAbstractHasData(new TextCell());
+    CellTableBuilder<String> builder =
+        new AbstractCellTable.DefaultCellTableBuilder<String>(table) {
+          @Override
+          public void buildRow(String rowValue, int absRowIndex,
+              CellTableBuilder.Utility<String> utility) {
+            builtRows.add(absRowIndex);
+            TableRowBuilder tr = utility.startRow();
+            tr.endTR(); // End the tr.
+            tr.end(); // Accidentally end the table section.
+
+            // Try to start another row.
+            try {
+              utility.startRow();
+              fail("Expected IllegalStateException: tbody is already ended");
+            } catch (IllegalStateException e) {
+              // Expected.
+            }
+          }
+        };
+    table.setTableBuilder(builder);
+    table.setVisibleRange(0, 1);
+    populateData(table);
+    table.getPresenter().flush();
+
+    assertEquals(1, builtRows.size());
+    assertEquals(0, builtRows.get(0).intValue());
+  }
+
+  /**
+   * Test that the table works if a row value is rendered into multiple rows.
+   */
+  public void testBuildMultipleRows() {
+    T table = createAbstractHasData(new TextCell());
+    CellTableBuilder<String> builder =
+        new AbstractCellTable.DefaultCellTableBuilder<String>(table) {
+          @Override
+          public void buildRow(String rowValue, int absRowIndex,
+              CellTableBuilder.Utility<String> utility) {
+            super.buildRow(rowValue, absRowIndex, utility);
+
+            // Add child rows to row five.
+            if (absRowIndex == 5) {
+              for (int i = 0; i < 4; i++) {
+                TableRowBuilder tr = utility.startRow();
+                tr.startTD().colSpan(2).text("child " + i).endTD();
+                tr.endTR();
+              }
+            }
+          }
+        };
+    table.setTableBuilder(builder);
+    table.setVisibleRange(0, 10);
+    populateData(table);
+    table.getPresenter().flush();
+
+    // Verify the structure.
+    TableSectionElement tbody = table.getTableBodyElement();
+    assertEquals(14, tbody.getChildCount());
+    assertEquals("child 0", getBodyElement(table, 6, 0).getInnerText());
+    assertEquals("child 1", getBodyElement(table, 7, 0).getInnerText());
+    assertEquals("child 2", getBodyElement(table, 8, 0).getInnerText());
+    assertEquals("child 3", getBodyElement(table, 9, 0).getInnerText());
+
+    // Verify the row values are accessible.
+    assertEquals("test 5", table.getVisibleItem(5));
+    assertEquals("test 9", table.getVisibleItem(9));
+
+    // Verify the child elements map correctly.
+    assertEquals(4, table.getChildElement(4).getSectionRowIndex());
+    assertEquals(5, table.getChildElement(5).getSectionRowIndex());
+    assertEquals(10, table.getChildElement(6).getSectionRowIndex());
+  }
+
+  /**
+   * Test that the table works if a row value is rendered into zero rows.
+   */
+  public void testBuildZeroRows() {
+    T table = createAbstractHasData(new TextCell());
+    CellTableBuilder<String> builder =
+        new AbstractCellTable.DefaultCellTableBuilder<String>(table) {
+          @Override
+          public void buildRow(String rowValue, int absRowIndex,
+              CellTableBuilder.Utility<String> utility) {
+            // Skip row index 5.
+            if (absRowIndex != 5) {
+              super.buildRow(rowValue, absRowIndex, utility);
+            }
+          }
+        };
+    table.setTableBuilder(builder);
+    table.setVisibleRange(0, 10);
+    populateData(table);
+    table.getPresenter().flush();
+
+    // Verify the structure.
+    TableSectionElement tbody = table.getTableBodyElement();
+    assertEquals(9, tbody.getChildCount());
+
+    // Verify the row values are accessible.
+    assertEquals("test 5", table.getVisibleItem(5));
+    assertEquals("test 9", table.getVisibleItem(9));
+
+    // Verify the child elements map correctly.
+    assertEquals(4, table.getChildElement(4).getSectionRowIndex());
+    assertNull(table.getChildElement(5));
+    assertEquals(5, table.getChildElement(6).getSectionRowIndex());
+  }
+
   public void testCellAlignment() {
     T table = createAbstractHasData(new TextCell());
     Column<String, String> column = new Column<String, String>(new TextCell()) {
@@ -151,14 +271,14 @@
 
   public void testCellEvent() {
     IndexCell<String> cell = new IndexCell<String>("click");
-    AbstractCellTable<String> table = createAbstractHasData(cell);
+    T table = createAbstractHasData(cell);
     RootPanel.get().add(table);
     table.setRowData(createData(0, 10));
     table.getPresenter().flush();
 
     // Trigger an event at index 5.
     NativeEvent event = Document.get().createClickEvent(0, 0, 0, 0, 0, false, false, false, false);
-    table.getRowElement(5).getCells().getItem(0).dispatchEvent(event);
+    getBodyElement(table, 5, 0).getFirstChildElement().dispatchEvent(event);
     cell.assertLastBrowserEventIndex(5);
     cell.assertLastEditingIndex(5);
 
@@ -202,6 +322,274 @@
     assertTrue(getBodyElement(table, 1, 1).getClassName().contains(" test_1"));
   }
 
+  public void testDefaultKeyboardSelectionHandlerChangePage() {
+    T table = createAbstractHasData();
+    DefaultKeyboardSelectionHandler<String> keyHandler =
+        new DefaultKeyboardSelectionHandler<String>(table);
+    table.setKeyboardSelectionHandler(keyHandler);
+    HasDataPresenter<String> presenter = table.getPresenter();
+
+    table.setRowCount(100, true);
+    table.setVisibleRange(new Range(50, 10));
+    populateData(table);
+    presenter.flush();
+    table.setKeyboardPagingPolicy(KeyboardPagingPolicy.CHANGE_PAGE);
+
+    // keyboardPrev in middle.
+    table.setKeyboardSelectedRow(1);
+    presenter.flush();
+    assertEquals(1, table.getKeyboardSelectedRow());
+    keyHandler.prevRow();
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+
+    // keyboardPrev at beginning goes to previous page.
+    keyHandler.prevRow();
+    populateData(table);
+    presenter.flush();
+    assertEquals(9, table.getKeyboardSelectedRow());
+    assertEquals(new Range(40, 10), table.getVisibleRange());
+
+    // keyboardNext in middle.
+    table.setKeyboardSelectedRow(8);
+    presenter.flush();
+    assertEquals(8, table.getKeyboardSelectedRow());
+    keyHandler.nextRow();
+    presenter.flush();
+    assertEquals(9, table.getKeyboardSelectedRow());
+
+    // keyboardNext at end.
+    keyHandler.nextRow();
+    populateData(table);
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+    assertEquals(new Range(50, 10), table.getVisibleRange());
+
+    // keyboardPrevPage.
+    table.setKeyboardSelectedRow(5);
+    presenter.flush();
+    assertEquals(5, table.getKeyboardSelectedRow());
+    keyHandler.prevPage();
+    populateData(table);
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+    assertEquals(new Range(40, 10), table.getVisibleRange());
+
+    // keyboardNextPage.
+    table.setKeyboardSelectedRow(5);
+    presenter.flush();
+    assertEquals(5, table.getKeyboardSelectedRow());
+    keyHandler.nextPage();
+    populateData(table);
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+    assertEquals(new Range(50, 10), table.getVisibleRange());
+
+    // keyboardHome.
+    keyHandler.home();
+    populateData(table);
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+    assertEquals(new Range(0, 10), table.getVisibleRange());
+
+    // keyboardPrev at first row.
+    keyHandler.prevRow();
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+
+    // keyboardEnd.
+    keyHandler.end();
+    populateData(table);
+    presenter.flush();
+    assertEquals(9, table.getKeyboardSelectedRow());
+    assertEquals(new Range(90, 10), table.getVisibleRange());
+
+    // keyboardNext at last row.
+    keyHandler.nextRow();
+    presenter.flush();
+  }
+
+  public void testDefaultKeyboardSelectionHandlerCurrentPage() {
+    T table = createAbstractHasData();
+    DefaultKeyboardSelectionHandler<String> keyHandler =
+        new DefaultKeyboardSelectionHandler<String>(table);
+    table.setKeyboardSelectionHandler(keyHandler);
+    HasDataPresenter<String> presenter = table.getPresenter();
+
+    table.setRowCount(100, true);
+    table.setVisibleRange(new Range(50, 10));
+    populateData(table);
+    presenter.flush();
+    table.setKeyboardPagingPolicy(KeyboardPagingPolicy.CURRENT_PAGE);
+
+    // keyboardPrev in middle.
+    table.setKeyboardSelectedRow(1);
+    presenter.flush();
+    assertEquals(1, table.getKeyboardSelectedRow());
+    keyHandler.prevRow();
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+
+    // keyboardPrev at beginning.
+    keyHandler.prevRow();
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+    assertEquals(new Range(50, 10), table.getVisibleRange());
+
+    // keyboardNext in middle.
+    table.setKeyboardSelectedRow(8);
+    presenter.flush();
+    assertEquals(8, table.getKeyboardSelectedRow());
+    keyHandler.nextRow();
+    presenter.flush();
+    assertEquals(9, table.getKeyboardSelectedRow());
+
+    // keyboardNext at end.
+    keyHandler.nextRow();
+    presenter.flush();
+    assertEquals(9, table.getKeyboardSelectedRow());
+    assertEquals(new Range(50, 10), table.getVisibleRange());
+
+    // keyboardPrevPage.
+    keyHandler.prevPage(); // ignored.
+    presenter.flush();
+    assertEquals(9, table.getKeyboardSelectedRow());
+    assertEquals(new Range(50, 10), table.getVisibleRange());
+
+    // keyboardNextPage.
+    keyHandler.nextPage(); // ignored.
+    presenter.flush();
+    assertEquals(9, table.getKeyboardSelectedRow());
+    assertEquals(new Range(50, 10), table.getVisibleRange());
+
+    // keyboardHome.
+    keyHandler.home();
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+    assertEquals(new Range(50, 10), table.getVisibleRange());
+
+    // keyboardEnd.
+    keyHandler.end();
+    presenter.flush();
+    assertEquals(9, table.getKeyboardSelectedRow());
+    assertEquals(new Range(50, 10), table.getVisibleRange());
+  }
+
+  public void testDefaultKeyboardSelectionHandlerIncreaseRange() {
+    int pageStart = 150;
+    int pageSize = 10;
+    int increment = HasDataPresenter.PAGE_INCREMENT;
+
+    T table = createAbstractHasData();
+    DefaultKeyboardSelectionHandler<String> keyHandler =
+        new DefaultKeyboardSelectionHandler<String>(table);
+    table.setKeyboardSelectionHandler(keyHandler);
+    HasDataPresenter<String> presenter = table.getPresenter();
+
+    table.setRowCount(300, true);
+    table.setVisibleRange(new Range(pageStart, pageSize));
+    populateData(table);
+    presenter.flush();
+    table.setKeyboardPagingPolicy(KeyboardPagingPolicy.INCREASE_RANGE);
+
+    // keyboardPrev in middle.
+    table.setKeyboardSelectedRow(1);
+    presenter.flush();
+    assertEquals(1, table.getKeyboardSelectedRow());
+    keyHandler.prevRow();
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+
+    // keyboardPrev at beginning.
+    keyHandler.prevRow();
+    populateData(table);
+    presenter.flush();
+    pageStart -= increment;
+    pageSize += increment;
+    assertEquals(increment - 1, table.getKeyboardSelectedRow());
+    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
+
+    // keyboardNext in middle.
+    table.setKeyboardSelectedRow(pageSize - 2);
+    presenter.flush();
+    assertEquals(pageSize - 2, table.getKeyboardSelectedRow());
+    keyHandler.nextRow();
+    presenter.flush();
+    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());
+
+    // keyboardNext at end.
+    keyHandler.nextRow();
+    populateData(table);
+    presenter.flush();
+    pageSize += increment;
+    assertEquals(pageSize - increment, table.getKeyboardSelectedRow());
+    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
+
+    // keyboardPrevPage within range.
+    table.setKeyboardSelectedRow(increment);
+    presenter.flush();
+    assertEquals(increment, table.getKeyboardSelectedRow());
+    keyHandler.prevPage();
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
+
+    // keyboardPrevPage outside range.
+    keyHandler.prevPage();
+    populateData(table);
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+    pageStart -= increment;
+    pageSize += increment;
+    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
+
+    // keyboardNextPage inside range.
+    keyHandler.nextPage();
+    presenter.flush();
+    assertEquals(increment, table.getKeyboardSelectedRow());
+    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
+
+    // keyboardNextPage outside range.
+    table.setKeyboardSelectedRow(pageSize - 1);
+    presenter.flush();
+    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());
+    keyHandler.nextPage();
+    populateData(table);
+    presenter.flush();
+    pageSize += increment;
+    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());
+    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
+
+    // keyboardHome.
+    keyHandler.home();
+    populateData(table);
+    presenter.flush();
+    pageSize += pageStart;
+    pageStart = 0;
+    assertEquals(0, table.getKeyboardSelectedRow());
+    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
+
+    // keyboardPrev at first row.
+    keyHandler.prevRow();
+    presenter.flush();
+    assertEquals(0, table.getKeyboardSelectedRow());
+    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
+
+    // keyboardEnd.
+    keyHandler.end();
+    pageSize = 300;
+    populateData(table);
+    presenter.flush();
+    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());
+    assertEquals(new Range(0, pageSize), table.getVisibleRange());
+
+    // keyboardNext at last row.
+    keyHandler.nextRow();
+    presenter.flush();
+    assertEquals(pageSize - 1, table.getKeyboardSelectedRow());
+    assertEquals(new Range(pageStart, pageSize), table.getVisibleRange());
+  }
+
   public void testGetColumnIndex() {
     T table = createAbstractHasData();
     Column<String, String> col0 = new IdentityColumn<String>(new TextCell());
@@ -262,6 +650,54 @@
     assertNotNull(table.getRowElement(9));
   }
 
+  public void testGetSubRowElement() {
+    T table = createAbstractHasData(new TextCell());
+    CellTableBuilder<String> builder =
+        new AbstractCellTable.DefaultCellTableBuilder<String>(table) {
+          @Override
+          public void buildRow(String rowValue, int absRowIndex,
+              CellTableBuilder.Utility<String> utility) {
+            super.buildRow(rowValue, absRowIndex, utility);
+
+            // Add some children.
+            for (int i = 0; i < 4; i++) {
+              TableRowBuilder tr = utility.startRow();
+              tr.startTD().colSpan(2).text("child " + absRowIndex + ":" + i).endTD();
+              tr.endTR();
+            }
+          }
+        };
+    table.setTableBuilder(builder);
+    table.setVisibleRange(0, 5);
+    populateData(table);
+    table.getPresenter().flush();
+
+    // Verify the structure.
+    TableSectionElement tbody = table.getTableBodyElement();
+    assertEquals(25, tbody.getChildCount());
+
+    // Test sub rows within range.
+    assertEquals(0, table.getSubRowElement(0, 0).getSectionRowIndex());
+    assertEquals(1, table.getSubRowElement(0, 1).getSectionRowIndex());
+    assertEquals(4, table.getSubRowElement(0, 4).getSectionRowIndex());
+    assertEquals(5, table.getSubRowElement(1, 0).getSectionRowIndex());
+    assertEquals(8, table.getSubRowElement(1, 3).getSectionRowIndex());
+    assertEquals(20, table.getSubRowElement(4, 0).getSectionRowIndex());
+    assertEquals(24, table.getSubRowElement(4, 4).getSectionRowIndex());
+
+    // Sub row does not exist within the row.
+    assertNull(table.getSubRowElement(0, 5));
+    assertNull(table.getSubRowElement(4, 5));
+
+    // Row index out of bounds.
+    try {
+      assertNull(table.getSubRowElement(5, 0));
+      fail("Expected IndexOutOfBoundsException: row index is out of bounds");
+    } catch (IndexOutOfBoundsException e) {
+      // Expected.
+    }
+  }
+
   public void testInsertColumn() {
     T table = createAbstractHasData();
     assertEquals(0, table.getColumnCount());
@@ -346,6 +782,31 @@
     assertNull(table.getEmptyTableWidget());
   }
 
+  public void testSetKeyboardSelectedRow() {
+    AbstractCellTable<String> table = createAbstractHasData(new TextCell());
+    table.setVisibleRange(0, 10);
+
+    // Without a subrow.
+    table.setKeyboardSelectedRow(5);
+    assertEquals(5, table.getKeyboardSelectedRow());
+    assertEquals(0, table.getKeyboardSelectedSubRow());
+
+    // Specify a subrow.
+    table.setKeyboardSelectedRow(6, 2, false);
+    assertEquals(6, table.getKeyboardSelectedRow());
+    assertEquals(2, table.getKeyboardSelectedSubRow());
+
+    // Change the subrow.
+    table.setKeyboardSelectedRow(6, 5, false);
+    assertEquals(6, table.getKeyboardSelectedRow());
+    assertEquals(5, table.getKeyboardSelectedSubRow());
+
+    // Change the row.
+    table.setKeyboardSelectedRow(7);
+    assertEquals(7, table.getKeyboardSelectedRow());
+    assertEquals(0, table.getKeyboardSelectedSubRow());
+  }
+
   public void testSetLoadingIndicator() {
     AbstractCellTable<String> table = createAbstractHasData(new TextCell());
 
diff --git a/user/test/com/google/gwt/user/cellview/client/AbstractHasDataTestBase.java b/user/test/com/google/gwt/user/cellview/client/AbstractHasDataTestBase.java
index 5f4bc4f..9e8cc4d 100644
--- a/user/test/com/google/gwt/user/cellview/client/AbstractHasDataTestBase.java
+++ b/user/test/com/google/gwt/user/cellview/client/AbstractHasDataTestBase.java
@@ -267,4 +267,14 @@
     }
     return toRet;
   }
+
+  /**
+   * Populate the entire range of a view.
+   */
+  protected void populateData(AbstractHasData<String> view) {
+    Range range = view.getVisibleRange();
+    int start = range.getStart();
+    int length = range.getLength();
+    view.setRowData(start, createData(start, length));
+  }
 }
diff --git a/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java b/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java
index e287e41..9fa0422 100644
--- a/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java
+++ b/user/test/com/google/gwt/user/cellview/client/HasDataPresenterTest.java
@@ -15,14 +15,14 @@
  */
 package com.google.gwt.user.cellview.client;
 
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayInteger;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.event.shared.EventHandler;
 import com.google.gwt.event.shared.GwtEvent;
 import com.google.gwt.event.shared.GwtEvent.Type;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.junit.client.GWTTestCase;
-import com.google.gwt.safehtml.shared.SafeHtml;
-import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
 import com.google.gwt.user.cellview.client.HasDataPresenter.View;
 import com.google.gwt.user.cellview.client.HasKeyboardPagingPolicy.KeyboardPagingPolicy;
 import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
@@ -42,7 +42,6 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.TreeSet;
 
 /**
  * Tests for {@link HasDataPresenter}.
@@ -52,8 +51,7 @@
   /**
    * A mock {@link SelectionChangeEvent.Handler} used for testing.
    */
-  private static class MockSelectionChangeHandler implements
-      SelectionChangeEvent.Handler {
+  private static class MockSelectionChangeHandler implements SelectionChangeEvent.Handler {
 
     private boolean eventFired;
 
@@ -100,18 +98,29 @@
    */
   private static class MockView<T> implements View<T> {
 
+    /**
+     * A call to replacement.
+     */
+    private static class Replacement {
+      private final boolean isReplaceAll;
+      private final int size;
+      private final int start;
+
+      public Replacement(boolean isReplaceAll, int start, int size) {
+        this.isReplaceAll = isReplaceAll;
+        this.start = start;
+        this.size = size;
+      }
+    }
+
     private int childCount;
     private List<Integer> keyboardSelectedRow = new ArrayList<Integer>();
     private List<Boolean> keyboardSelectedRowState = new ArrayList<Boolean>();
-    private final List<SafeHtml> lastHtml = new ArrayList<SafeHtml>();
+    private List<Replacement> lastReplacement = new ArrayList<Replacement>();
     private LoadingState loadingState;
-    private boolean replaceAllChildrenCalled;
-    private boolean replaceAllChildrenCalledWithSameContent;
-    private boolean replaceChildrenCalled;
 
     @Override
-    public <H extends EventHandler> HandlerRegistration addHandler(H handler,
-        Type<H> type) {
+    public <H extends EventHandler> HandlerRegistration addHandler(H handler, Type<H> type) {
       throw new UnsupportedOperationException();
     }
 
@@ -135,32 +144,27 @@
       assertEquals(0, keyboardSelectedRow.size());
     }
 
-    public void assertLastHtml(String html) {
-      if (html == null) {
-        assertTrue(lastHtml.isEmpty());
-      } else {
-        assertEquals(html, lastHtml.remove(0).asString());
-      }
-    }
-
     public void assertLoadingState(LoadingState expected) {
       assertEquals(expected, loadingState);
     }
 
-    public void assertReplaceAllChildrenCalled(boolean expected) {
-      assertEquals(expected, replaceAllChildrenCalled);
-      replaceAllChildrenCalled = false;
+    public void assertReplaceAllChildrenCalled(int size) {
+      assertFalse("replaceAllChildren was not called", lastReplacement.isEmpty());
+      Replacement call = lastReplacement.remove(0);
+      assertTrue("replaceChildren called instead of replaceAllChidren", call.isReplaceAll);
+      assertEquals(size, call.size);
     }
 
-    public void assertReplaceAllChildrenCalled(boolean expected, boolean sameContent) {
-      assertReplaceAllChildrenCalled(expected);
-      assertEquals(sameContent, replaceAllChildrenCalledWithSameContent);
-      replaceAllChildrenCalledWithSameContent = false;
+    public void assertReplaceChildrenCalled(int start, int size) {
+      assertFalse("replaceChildren was not called", lastReplacement.isEmpty());
+      Replacement call = lastReplacement.remove(0);
+      assertFalse("replaceAllChildren called instead of replaceChidren", call.isReplaceAll);
+      assertEquals(start, call.start);
+      assertEquals(size, call.size);
     }
 
-    public void assertReplaceChildrenCalled(boolean expected) {
-      assertEquals(expected, replaceChildrenCalled);
-      replaceChildrenCalled = false;
+    public void assertReplaceChildrenNotCalled() {
+      assertTrue(lastReplacement.isEmpty());
     }
 
     public int getChildCount() {
@@ -168,27 +172,17 @@
     }
 
     @Override
-    public void render(SafeHtmlBuilder sb, List<T> values, int start,
-        SelectionModel<? super T> selectionModel) {
-      sb.appendHtmlConstant("start=").append(start);
-      sb.appendHtmlConstant(",size=").append(values.size());
-    }
-
-    @Override
-    public void replaceAllChildren(List<T> values, SafeHtml html,
-        boolean stealFocus, boolean contentChanged) {
-      replaceAllChildrenCalledWithSameContent = !contentChanged;
-      childCount = values.size();
-      replaceAllChildrenCalled = true;
-      lastHtml.add(html);
-    }
-
-    @Override
-    public void replaceChildren(List<T> values, int start, SafeHtml html,
+    public void replaceAllChildren(List<T> values, SelectionModel<? super T> selectionModel,
         boolean stealFocus) {
+      childCount = values.size();
+      lastReplacement.add(new Replacement(true, -1, values.size()));
+    }
+
+    @Override
+    public void replaceChildren(List<T> values, int start,
+        SelectionModel<? super T> selectionModel, boolean stealFocus) {
       childCount = Math.max(childCount, start + values.size());
-      replaceChildrenCalled = true;
-      lastHtml.add(html);
+      lastReplacement.add(new Replacement(false, start, values.size()));
     }
 
     @Override
@@ -196,8 +190,7 @@
     }
 
     @Override
-    public void setKeyboardSelected(int index, boolean selected,
-        boolean stealFocus) {
+    public void setKeyboardSelected(int index, boolean selected, boolean stealFocus) {
       keyboardSelectedRow.add(index);
       keyboardSelectedRowState.add(selected);
     }
@@ -216,8 +209,7 @@
   public void testAddRowCountChangeHandler() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     MockRowCountChangeHandler handler = new MockRowCountChangeHandler();
 
     // Adding a handler should not invoke the handler.
@@ -258,8 +250,7 @@
   public void testAddRangeChangeHandler() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     MockRangeChangeHandler handler = new MockRangeChangeHandler();
 
     // Adding a handler should not invoke the handler.
@@ -324,8 +315,7 @@
     // Use the bad view in a presenter.
     MockView<String> view = new MockView<String>();
     HasData<String> listView = new MockHasData<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setSelectionModel(badModel);
     presenter.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.BOUND_TO_SELECTION);
     testPresenterWithBadUserCode(presenter);
@@ -333,125 +323,93 @@
 
   /**
    * Test that the presenter can gracefully handle a view that throws exceptions
-   * when rendering the content.
-   */
-  public void testBadViewRender() {
-    MockView<String> badView = new MockView<String>() {
-      @Override
-      public void render(SafeHtmlBuilder sb, List<String> values, int start,
-          SelectionModel<? super String> selectionModel) {
-        throw new NullPointerException();
-      }
-    };
-
-    // Use the bad view in a presenter.
-    HasData<String> listView = new MockHasData<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        badView, 10, null);
-    testPresenterWithBadUserCode(presenter);
-  }
-
-  /**
-   * Test that the presenter can gracefully handle a view that throws exceptions
    * when rendering the children.
    */
   public void testBadViewReplaceChildren() {
     MockView<String> badView = new MockView<String>() {
       @Override
-      public void replaceAllChildren(List<String> values, SafeHtml html,
-          boolean stealFocus, boolean contentChanged) {
+      public void replaceAllChildren(List<String> values,
+          SelectionModel<? super String> selectionModel, boolean stealFocus) {
         throw new NullPointerException();
       }
 
       @Override
       public void replaceChildren(List<String> values, int start,
-          SafeHtml html, boolean stealFocus) {
+          SelectionModel<? super String> selectionModel, boolean stealFocus) {
         throw new NullPointerException();
       }
     };
 
     // Use the bad view in a presenter.
     HasData<String> listView = new MockHasData<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        badView, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, badView, 10, null);
     testPresenterWithBadUserCode(presenter);
   }
 
   public void testCalculateModifiedRanges() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
-    TreeSet<Integer> rows = new TreeSet<Integer>();
+    JsArrayInteger rows = JavaScriptObject.createArray().cast();
 
     // Empty set of rows.
     assertListContains(presenter.calculateModifiedRanges(rows, 0, 10));
 
     // One row in range.
-    rows.add(5);
-    assertListContains(presenter.calculateModifiedRanges(rows, 0, 10),
-        new Range(5, 1));
+    rows.push(5);
+    assertListContains(presenter.calculateModifiedRanges(rows, 0, 10), new Range(5, 1));
 
     // One row not in range.
     assertListContains(presenter.calculateModifiedRanges(rows, 6, 10));
 
     // Consecutive rows (should return only one range).
-    rows.add(6);
-    rows.add(7);
-    rows.add(8);
-    assertListContains(presenter.calculateModifiedRanges(rows, 0, 10),
-        new Range(5, 4));
+    rows.push(6);
+    rows.push(7);
+    rows.push(8);
+    assertListContains(presenter.calculateModifiedRanges(rows, 0, 10), new Range(5, 4));
 
     // Disjoint rows. Should return two ranges.
-    rows.add(10);
-    rows.add(11);
-    assertListContains(presenter.calculateModifiedRanges(rows, 0, 20),
-        new Range(5, 4), new Range(10, 2));
+    rows.push(10);
+    rows.push(11);
+    assertListContains(presenter.calculateModifiedRanges(rows, 0, 20), new Range(5, 4), new Range(
+        10, 2));
 
     // Multiple gaps. The largest gap should be between the two ranges.
-    rows.add(15);
-    rows.add(17);
-    assertListContains(presenter.calculateModifiedRanges(rows, 0, 20),
-        new Range(5, 7), new Range(15, 3));
+    rows.push(15);
+    rows.push(17);
+    assertListContains(presenter.calculateModifiedRanges(rows, 0, 20), new Range(5, 7), new Range(
+        15, 3));
   }
 
   public void testClearSelectionModel() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     assertNull(presenter.getSelectionModel());
 
     // Initialize some data.
     populatePresenter(presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
 
     // Set the selection model.
     SelectionModel<String> model = new MockSelectionModel<String>(null);
     model.setSelected("test 0", true);
     presenter.setSelectionModel(model);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
-    view.assertLastHtml("start=0,size=1");
+    view.assertReplaceChildrenCalled(0, 1);
 
     // Clear the selection model without updating the view.
     presenter.clearSelectionModel();
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml(null);
+    view.assertReplaceChildrenNotCalled();
   }
 
   public void testDefaults() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     assertEquals(0, presenter.getRowCount());
     assertFalse(presenter.isRowCountExact());
@@ -465,8 +423,7 @@
   public void testFindIndexOfBestMatch() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     populatePresenter(presenter);
 
     // Select the second element.
@@ -491,23 +448,21 @@
   public void testFlush() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Data should not be pushed to the view until flushed.
     populatePresenter(presenter);
-    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenNotCalled();
 
     // Now the data is pushed.
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceAllChildrenCalled(10);
   }
 
   public void testGetCurrentPageSize() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setRowCount(35, true);
 
     // First page.
@@ -521,8 +476,7 @@
   public void testIsEmpty() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Non-zero row count.
     presenter.setRowCount(1, true);
@@ -542,328 +496,6 @@
     presenter.flush();
     assertFalse(presenter.isEmpty());
   }
-  
-  public void testKeyboardNavigationChangePage() {
-    HasData<String> listView = new MockHasData<String>();
-    MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
-    presenter.setRowCount(100, true);
-    presenter.setVisibleRange(new Range(50, 10));
-    populatePresenter(presenter);
-    presenter.flush();
-    presenter.setKeyboardPagingPolicy(KeyboardPagingPolicy.CHANGE_PAGE);
-
-    // keyboardPrev in middle.
-    presenter.setKeyboardSelectedRow(1, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(0, false);
-    view.assertKeyboardSelectedRow(1, true);
-    assertTrue(presenter.hasKeyboardPrev());
-    presenter.keyboardPrev();
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRow(1, false);
-    view.assertKeyboardSelectedRow(0, true);
-
-    // keyboardPrev at beginning goes to previous page.
-    assertTrue(presenter.hasKeyboardPrev());
-    presenter.keyboardPrev();
-    populatePresenter(presenter);
-    presenter.flush();
-    assertEquals(9, presenter.getKeyboardSelectedRow());
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    assertEquals(new Range(40, 10), presenter.getVisibleRange());
-
-    // keyboardNext in middle.
-    presenter.setKeyboardSelectedRow(8, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(9, false);
-    view.assertKeyboardSelectedRow(8, true);
-    assertTrue(presenter.hasKeyboardNext());
-    presenter.keyboardNext();
-    presenter.flush();
-    assertEquals(9, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRow(8, false);
-    view.assertKeyboardSelectedRow(9, true);
-
-    // keyboardNext at end.
-    assertTrue(presenter.hasKeyboardNext());
-    presenter.keyboardNext();
-    populatePresenter(presenter);
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    assertEquals(new Range(50, 10), presenter.getVisibleRange());
-
-    // keyboardPrevPage.
-    presenter.setKeyboardSelectedRow(5, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(0, false);
-    view.assertKeyboardSelectedRow(5, true);
-    presenter.keyboardPrevPage();
-    populatePresenter(presenter);
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    assertEquals(new Range(40, 10), presenter.getVisibleRange());
-
-    // keyboardNextPage.
-    presenter.setKeyboardSelectedRow(5, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(0, false);
-    view.assertKeyboardSelectedRow(5, true);
-    presenter.keyboardNextPage();
-    populatePresenter(presenter);
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    assertEquals(new Range(50, 10), presenter.getVisibleRange());
-
-    // keyboardHome.
-    presenter.keyboardHome();
-    populatePresenter(presenter);
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    assertEquals(new Range(0, 10), presenter.getVisibleRange());
-
-    // keyboardPrev at first row.
-    assertFalse(presenter.hasKeyboardPrev());
-    presenter.keyboardPrev();
-    presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertKeyboardSelectedRowEmpty();
-
-    // keyboardEnd.
-    presenter.keyboardEnd();
-    populatePresenter(presenter);
-    presenter.flush();
-    assertEquals(9, presenter.getKeyboardSelectedRow());
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    assertEquals(new Range(90, 10), presenter.getVisibleRange());
-
-    // keyboardNext at last row.
-    assertFalse(presenter.hasKeyboardNext());
-    presenter.keyboardNext();
-    presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertKeyboardSelectedRowEmpty();
-  }
-
-  public void testKeyboardNavigationCurrentPage() {
-    HasData<String> listView = new MockHasData<String>();
-    MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
-    presenter.setVisibleRange(new Range(50, 10));
-    populatePresenter(presenter);
-    presenter.flush();
-    presenter.setKeyboardPagingPolicy(KeyboardPagingPolicy.CURRENT_PAGE);
-
-    // keyboardPrev in middle.
-    presenter.setKeyboardSelectedRow(1, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(0, false);
-    view.assertKeyboardSelectedRow(1, true);
-    assertTrue(presenter.hasKeyboardPrev());
-    presenter.keyboardPrev();
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRow(1, false);
-    view.assertKeyboardSelectedRow(0, true);
-
-    // keyboardPrev at beginning.
-    assertFalse(presenter.hasKeyboardPrev());
-    presenter.keyboardPrev();
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRowEmpty();
-
-    // keyboardNext in middle.
-    presenter.setKeyboardSelectedRow(8, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(0, false);
-    view.assertKeyboardSelectedRow(8, true);
-    assertTrue(presenter.hasKeyboardNext());
-    presenter.keyboardNext();
-    presenter.flush();
-    assertEquals(9, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRow(8, false);
-    view.assertKeyboardSelectedRow(9, true);
-
-    // keyboardNext at end.
-    assertFalse(presenter.hasKeyboardNext());
-    presenter.keyboardNext();
-    presenter.flush();
-    assertEquals(9, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRowEmpty();
-
-    // keyboardPrevPage.
-    presenter.keyboardPrevPage();
-    presenter.flush();
-    view.assertKeyboardSelectedRowEmpty();
-
-    // keyboardNextPage.
-    presenter.keyboardNextPage();
-    presenter.flush();
-    view.assertKeyboardSelectedRowEmpty();
-
-    // keyboardHome.
-    presenter.keyboardHome();
-    presenter.flush();
-    view.assertKeyboardSelectedRowEmpty();
-
-    // keyboardEnd.
-    presenter.keyboardEnd();
-    presenter.flush();
-    view.assertKeyboardSelectedRowEmpty();
-  }
-
-  public void testKeyboardNavigationIncreaseRange() {
-    int pageStart = 150;
-    int pageSize = 10;
-    int increment = HasDataPresenter.PAGE_INCREMENT;
-    HasData<String> listView = new MockHasData<String>();
-    MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
-    presenter.setRowCount(300, true);
-    presenter.setVisibleRange(new Range(pageStart, pageSize));
-    populatePresenter(presenter);
-    presenter.flush();
-    presenter.setKeyboardPagingPolicy(KeyboardPagingPolicy.INCREASE_RANGE);
-
-    // keyboardPrev in middle.
-    presenter.setKeyboardSelectedRow(1, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(0, false);
-    view.assertKeyboardSelectedRow(1, true);
-    assertTrue(presenter.hasKeyboardPrev());
-    presenter.keyboardPrev();
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRow(1, false);
-    view.assertKeyboardSelectedRow(0, true);
-
-    // keyboardPrev at beginning.
-    assertTrue(presenter.hasKeyboardPrev());
-    presenter.keyboardPrev();
-    populatePresenter(presenter);
-    presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    pageStart -= increment;
-    pageSize += increment;
-    assertEquals(increment - 1, presenter.getKeyboardSelectedRow());
-    assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
-
-    // keyboardNext in middle.
-    presenter.setKeyboardSelectedRow(pageSize - 2, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(increment - 1, false);
-    view.assertKeyboardSelectedRow(pageSize - 2, true);
-    assertTrue(presenter.hasKeyboardNext());
-    presenter.keyboardNext();
-    presenter.flush();
-    assertEquals(pageSize - 1, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRow(pageSize - 2, false);
-    view.assertKeyboardSelectedRow(pageSize - 1, true);
-
-    // keyboardNext at end.
-    assertTrue(presenter.hasKeyboardNext());
-    presenter.keyboardNext();
-    populatePresenter(presenter);
-    presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    pageSize += increment;
-    assertEquals(pageSize - increment, presenter.getKeyboardSelectedRow());
-    assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
-
-    // keyboardPrevPage within range.
-    presenter.setKeyboardSelectedRow(increment, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(pageSize - increment, false);
-    view.assertKeyboardSelectedRow(increment, true);
-    presenter.keyboardPrevPage();
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRow(increment, false);
-    view.assertKeyboardSelectedRow(0, true);
-    assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
-
-    // keyboardPrevPage outside range.
-    presenter.keyboardPrevPage();
-    populatePresenter(presenter);
-    presenter.flush();
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    pageStart -= increment;
-    pageSize += increment;
-    assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
-
-    // keyboardNextPage inside range.
-    presenter.keyboardNextPage();
-    presenter.flush();
-    assertEquals(increment, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRow(0, false);
-    view.assertKeyboardSelectedRow(increment, true);
-    assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
-
-    // keyboardNextPage outside range.
-    presenter.setKeyboardSelectedRow(pageSize - 1, false, false);
-    presenter.flush();
-    view.assertKeyboardSelectedRow(increment, false);
-    view.assertKeyboardSelectedRow(pageSize - 1, true);
-    presenter.keyboardNextPage();
-    populatePresenter(presenter);
-    presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    pageSize += increment;
-    assertEquals(pageSize - 1, presenter.getKeyboardSelectedRow());
-    assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
-
-    // keyboardHome.
-    presenter.keyboardHome();
-    populatePresenter(presenter);
-    presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    pageSize += pageStart;
-    pageStart = 0;
-    assertEquals(0, presenter.getKeyboardSelectedRow());
-    assertEquals(new Range(pageStart, pageSize), presenter.getVisibleRange());
-
-    // keyboardPrev at first row.
-    assertFalse(presenter.hasKeyboardPrev());
-    presenter.keyboardPrev();
-    presenter.flush();
-    view.assertKeyboardSelectedRowEmpty();
-
-    // keyboardEnd.
-    presenter.keyboardEnd();
-    populatePresenter(presenter);
-    presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertKeyboardSelectedRowEmpty();
-    assertEquals(299, presenter.getKeyboardSelectedRow());
-    assertEquals(new Range(0, 300), presenter.getVisibleRange());
-
-    // keyboardNext at last row.
-    assertFalse(presenter.hasKeyboardNext());
-    presenter.keyboardNext();
-    presenter.flush();
-    view.assertKeyboardSelectedRowEmpty();
-  }
 
   /**
    * Test that we can detect an infinite loop caused by user code updating the
@@ -872,8 +504,8 @@
   public void testLoopDetection() {
     HasData<String> listView = new MockHasData<String>();
     final MockView<String> view = new MockView<String>();
-    final HasDataPresenter<String> presenter = new HasDataPresenter<String>(
-        listView, view, 10, null);
+    final HasDataPresenter<String> presenter =
+        new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setSelectionModel(new SingleSelectionModel<String>() {
       @Override
       public boolean isSelected(String object) {
@@ -899,13 +531,13 @@
   public void testPendingCommand() {
     HasData<String> listView = new MockHasData<String>();
     final MockView<String> view = new MockView<String>();
-    final HasDataPresenter<String> presenter = new HasDataPresenter<String>(
-        listView, view, 10, null);
+    final HasDataPresenter<String> presenter =
+        new HasDataPresenter<String>(listView, view, 10, null);
 
     // Data should not be pushed to the view until the pending command executes.
     populatePresenter(presenter);
     assertTrue(presenter.hasPendingState());
-    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenNotCalled();
 
     // The pending command is scheduled. Wait for it to execute.
     delayTestFinish(5000);
@@ -913,7 +545,7 @@
       @Override
       public void execute() {
         assertFalse(presenter.hasPendingState());
-        view.assertReplaceAllChildrenCalled(true);
+        view.assertReplaceAllChildrenCalled(10);
         finishTest();
       }
     });
@@ -922,8 +554,7 @@
   public void testRedraw() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Initialize some data.
     presenter.setRowCount(10, true);
@@ -931,33 +562,27 @@
     presenter.flush();
     assertEquals(10, presenter.getVisibleItemCount());
     assertEquals("test 0", presenter.getVisibleItem(0));
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
 
     // Redraw.
     presenter.redraw();
-    view.assertReplaceAllChildrenCalled(false);
+    view.assertReplaceChildrenNotCalled();
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
   }
 
   public void testSetKeyboardSelectedRowBound() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setVisibleRange(new Range(0, 10));
     populatePresenter(presenter);
     presenter.flush();
 
     // The default is ENABLED.
-    assertEquals(KeyboardSelectionPolicy.ENABLED,
-        presenter.getKeyboardSelectionPolicy());
+    assertEquals(KeyboardSelectionPolicy.ENABLED, presenter.getKeyboardSelectionPolicy());
 
     // Change to bound with paging.
     presenter.setKeyboardSelectionPolicy(KeyboardSelectionPolicy.BOUND_TO_SELECTION);
@@ -1010,8 +635,7 @@
   public void testSetKeyboardSelectedRowFiresOneSelectionEvent() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setVisibleRange(new Range(0, 10));
     populatePresenter(presenter);
     presenter.flush();
@@ -1021,8 +645,7 @@
     presenter.setKeyboardPagingPolicy(KeyboardPagingPolicy.CHANGE_PAGE);
 
     // Add a selection model.
-    MockSingleSelectionModel<String> model = new MockSingleSelectionModel<String>(
-        null);
+    MockSingleSelectionModel<String> model = new MockSingleSelectionModel<String>(null);
     presenter.setSelectionModel(model);
     presenter.flush();
     assertNull(model.getSelectedObject());
@@ -1063,15 +686,13 @@
   public void testSetKeyboardSelectedRowChangePage() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setVisibleRange(new Range(10, 10));
     populatePresenter(presenter);
     presenter.flush();
 
     // Default policy is CHANGE_PAGE.
-    assertEquals(KeyboardPagingPolicy.CHANGE_PAGE,
-        presenter.getKeyboardPagingPolicy());
+    assertEquals(KeyboardPagingPolicy.CHANGE_PAGE, presenter.getKeyboardPagingPolicy());
 
     // Default to row 0.
     assertEquals(0, presenter.getKeyboardSelectedRow());
@@ -1113,7 +734,7 @@
     presenter.flush();
     assertEquals(0, presenter.getKeyboardSelectedRow());
     assertEquals("test 20", presenter.getKeyboardSelectedRowValue());
-    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceAllChildrenCalled(10);
     view.assertKeyboardSelectedRowEmpty();
     assertEquals(20, presenter.getVisibleRange().getStart());
     assertEquals(10, presenter.getVisibleRange().getLength());
@@ -1125,7 +746,7 @@
     presenter.flush();
     assertEquals(9, presenter.getKeyboardSelectedRow());
     assertEquals("test 19", presenter.getKeyboardSelectedRowValue());
-    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceAllChildrenCalled(10);
     view.assertKeyboardSelectedRowEmpty();
     assertEquals(10, presenter.getVisibleRange().getStart());
     assertEquals(10, presenter.getVisibleRange().getLength());
@@ -1134,8 +755,7 @@
   public void testSetKeyboardSelectedRowCurrentPage() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setVisibleRange(new Range(10, 10));
     populatePresenter(presenter);
     presenter.flush();
@@ -1177,8 +797,6 @@
     presenter.setKeyboardSelectedRow(10, false, false);
     presenter.flush();
     assertEquals(9, presenter.getKeyboardSelectedRow());
-    view.assertKeyboardSelectedRow(9, false);
-    view.assertKeyboardSelectedRow(9, true);
     view.assertKeyboardSelectedRowEmpty();
     assertEquals(10, presenter.getVisibleRange().getStart());
     assertEquals(10, presenter.getVisibleRange().getLength());
@@ -1187,8 +805,7 @@
   public void testSetKeyboardSelectedRowDisabled() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setVisibleRange(new Range(10, 10));
     populatePresenter(presenter);
     presenter.flush();
@@ -1208,8 +825,7 @@
   public void testSetKeyboardSelectedRowIncreaseRange() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setVisibleRange(new Range(10, 10));
     populatePresenter(presenter);
     presenter.flush();
@@ -1248,7 +864,7 @@
     populatePresenter(presenter);
     presenter.flush();
     assertEquals(10, presenter.getKeyboardSelectedRow());
-    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceAllChildrenCalled(pageSize);
     view.assertKeyboardSelectedRowEmpty();
     assertEquals(10, presenter.getVisibleRange().getStart());
     pageSize += HasDataPresenter.PAGE_INCREMENT;
@@ -1259,7 +875,7 @@
     populatePresenter(presenter);
     presenter.flush();
     assertEquals(9, presenter.getKeyboardSelectedRow());
-    view.assertReplaceAllChildrenCalled(true);
+    view.assertReplaceAllChildrenCalled(pageSize);
     view.assertKeyboardSelectedRowEmpty();
     assertEquals(0, presenter.getVisibleRange().getStart());
     pageSize += 10;
@@ -1269,8 +885,7 @@
   public void testSetRowCount() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     view.assertLoadingState(null);
 
     // Set size to 100.
@@ -1299,8 +914,7 @@
   public void testSetRowCountNoBoolean() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     try {
       presenter.setRowCount(100);
@@ -1313,8 +927,7 @@
   public void testSetRowCountTrimsCurrentPage() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     view.assertLoadingState(null);
 
     // Initialize some data.
@@ -1326,9 +939,7 @@
     assertEquals(10, presenter.getVisibleItemCount());
     assertEquals("test 0", presenter.getVisibleItem(0));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
 
     // Trim the size.
@@ -1338,20 +949,17 @@
     assertEquals(new Range(0, 10), presenter.getVisibleRange());
     assertEquals(8, presenter.getVisibleItemCount());
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=8");
+    view.assertReplaceAllChildrenCalled(8);
     view.assertLoadingState(LoadingState.LOADED);
   }
 
   public void testSetRowData() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setVisibleRange(new Range(5, 10));
     presenter.flush();
-    view.assertLastHtml("start=5,size=0");
+    view.assertReplaceAllChildrenCalled(0);
     view.assertLoadingState(LoadingState.LOADING);
 
     // Page range same as data range.
@@ -1359,9 +967,7 @@
     presenter.setRowData(5, createData(5, 10));
     assertPresenterRowData(expectedData, presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=5,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     assertEquals(10, view.getChildCount());
     view.assertLoadingState(LoadingState.LOADED);
 
@@ -1371,9 +977,7 @@
     presenter.setRowData(7, createData(100, 2));
     assertPresenterRowData(expectedData, presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
-    view.assertLastHtml("start=7,size=2");
+    view.assertReplaceChildrenCalled(2, 2);
     assertEquals(10, view.getChildCount());
     view.assertLoadingState(LoadingState.LOADED);
 
@@ -1383,9 +987,7 @@
     presenter.setRowData(3, createData(200, 4));
     assertPresenterRowData(expectedData, presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
-    view.assertLastHtml("start=5,size=2");
+    view.assertReplaceChildrenCalled(0, 2);
     assertEquals(10, view.getChildCount());
     view.assertLoadingState(LoadingState.LOADED);
 
@@ -1395,9 +997,7 @@
     presenter.setRowData(13, createData(300, 4));
     assertPresenterRowData(expectedData, presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
-    view.assertLastHtml("start=13,size=2");
+    view.assertReplaceChildrenCalled(8, 2);
     assertEquals(10, view.getChildCount());
     view.assertLoadingState(LoadingState.LOADED);
 
@@ -1406,9 +1006,7 @@
     presenter.setRowData(3, createData(400, 20));
     assertPresenterRowData(expectedData, presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=5,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     assertEquals(10, view.getChildCount());
     view.assertLoadingState(LoadingState.LOADED);
   }
@@ -1419,8 +1017,7 @@
   public void testSetRowValuesChangesDataSize() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Set the initial data size.
     presenter.setRowCount(10, true);
@@ -1446,8 +1043,7 @@
   public void testSetRowValuesEmptySet() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Set the initial data size.
     presenter.setRowCount(10, true);
@@ -1458,18 +1054,16 @@
     presenter.setRowData(0, createData(0, 0));
     presenter.flush();
     view.assertLoadingState(LoadingState.LOADING);
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
+    view.assertReplaceAllChildrenCalled(0);
   }
 
   public void testSetRowValuesOutsideRange() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     presenter.setVisibleRange(new Range(5, 10));
     presenter.flush();
-    view.assertLastHtml("start=5,size=0");
+    view.assertReplaceAllChildrenCalled(0);
     view.assertLoadingState(LoadingState.LOADING);
 
     // Page range same as data range.
@@ -1477,27 +1071,21 @@
     presenter.setRowData(5, createData(5, 10));
     assertPresenterRowData(expectedData, presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=5,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
 
     // Data range past page end.
     presenter.setRowData(15, createData(15, 5));
     assertPresenterRowData(expectedData, presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml(null);
+    view.assertReplaceChildrenNotCalled();
     view.assertLoadingState(LoadingState.LOADED);
 
     // Data range before page start.
     presenter.setRowData(0, createData(0, 5));
     assertPresenterRowData(expectedData, presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml(null);
+    view.assertReplaceChildrenNotCalled();
     view.assertLoadingState(LoadingState.LOADED);
   }
 
@@ -1507,26 +1095,22 @@
   public void testSetRowValuesRequiresRedraw() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 100, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 100, null);
 
     // Initialize 100% of the rows.
     populatePresenter(presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
+    view.assertReplaceAllChildrenCalled(100);
 
     // Modify 30% of the rows.
     presenter.setRowData(0, createData(0, 30));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
+    view.assertReplaceChildrenCalled(0, 30);
 
     // Modify 31% of the rows.
     presenter.setRowData(0, createData(0, 31));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
+    view.assertReplaceAllChildrenCalled(100);
 
     /*
      * Modify 4 rows in a 5 row table. This should NOT require a redraw because
@@ -1534,12 +1118,10 @@
      */
     presenter.setRowCount(5, true);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
+    view.assertReplaceAllChildrenCalled(5);
     presenter.setRowData(0, createData(0, 4));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
+    view.assertReplaceChildrenCalled(0, 4);
   }
 
   /**
@@ -1550,25 +1132,20 @@
   public void testSetRowValuesSameContents() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     view.assertLoadingState(null);
 
     // Initialize some data.
     presenter.setVisibleRange(new Range(0, 10));
     presenter.setRowData(0, createData(0, 10));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true, false);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
 
     // Set the same data over the entire range.
     presenter.setRowData(0, createData(0, 10));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true, true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
   }
 
@@ -1578,8 +1155,7 @@
   public void testSetRowValuesSparse() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     view.assertLoadingState(null);
 
     List<String> expectedData = createData(5, 3);
@@ -1592,67 +1168,53 @@
     presenter.setRowData(5, createData(5, 3));
     assertPresenterRowData(expectedData, presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=8");
+    view.assertReplaceAllChildrenCalled(8);
     view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
   }
 
   public void testSetSelectionModel() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
     assertNull(presenter.getSelectionModel());
 
     // Initialize some data.
     presenter.setVisibleRange(new Range(0, 10));
     populatePresenter(presenter);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
 
     // Set the selection model.
     SelectionModel<String> model = new MockSelectionModel<String>(null);
     model.setSelected("test 0", true);
     presenter.setSelectionModel(model);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
-    view.assertLastHtml("start=0,size=1");
+    view.assertReplaceChildrenCalled(0, 1);
 
     // Select something.
     model.setSelected("test 2", true);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
-    view.assertLastHtml("start=2,size=1");
+    view.assertReplaceChildrenCalled(2, 1);
 
     // Set selection model to null.
     presenter.setSelectionModel(null);
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
-    view.assertLastHtml("start=0,size=1");
-    view.assertLastHtml("start=2,size=1");
-    view.assertLastHtml(null);
+    view.assertReplaceChildrenCalled(0, 1);
+    view.assertReplaceChildrenCalled(2, 1);
+    view.assertReplaceChildrenNotCalled();
   }
 
   public void testSetVisibleRange() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Set the range the first time.
     presenter.setVisibleRange(new Range(0, 100));
     assertEquals(new Range(0, 100), presenter.getVisibleRange());
     assertEquals(0, presenter.getVisibleItemCount());
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml(null);
+    view.assertReplaceChildrenNotCalled();
     view.assertLoadingState(LoadingState.LOADING);
 
     // Set the range to the same value.
@@ -1660,9 +1222,7 @@
     assertEquals(new Range(0, 100), presenter.getVisibleRange());
     assertEquals(0, presenter.getVisibleItemCount());
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml(null);
+    view.assertReplaceChildrenNotCalled();
     view.assertLoadingState(LoadingState.LOADING);
 
     // Set the start to a negative value.
@@ -1685,8 +1245,7 @@
   public void testSetVisibleRangeAndClearDataDifferentRange() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Add a range change handler.
     final List<Range> events = new ArrayList<Range>();
@@ -1703,9 +1262,7 @@
     assertEquals(new Range(5, 10), presenter.getVisibleRange());
     assertEquals(10, presenter.getVisibleItemCount());
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=5,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
     assertEquals(1, events.size());
 
@@ -1714,9 +1271,7 @@
     assertEquals(new Range(0, 10), presenter.getVisibleRange());
     assertEquals(0, presenter.getVisibleItemCount());
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=0");
+    view.assertReplaceAllChildrenCalled(0);
     view.assertLoadingState(LoadingState.LOADING);
     assertEquals(2, events.size());
   }
@@ -1724,8 +1279,7 @@
   public void testSetVisibleRangeAndClearDataSameRange() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Add a range change handler.
     final List<Range> events = new ArrayList<Range>();
@@ -1741,9 +1295,7 @@
     assertEquals(new Range(0, 10), presenter.getVisibleRange());
     assertEquals(10, presenter.getVisibleItemCount());
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
     assertEquals(0, events.size());
 
@@ -1752,9 +1304,7 @@
     assertEquals(new Range(0, 10), presenter.getVisibleRange());
     assertEquals(0, presenter.getVisibleItemCount());
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=0");
+    view.assertReplaceAllChildrenCalled(0);
     view.assertLoadingState(LoadingState.LOADING);
     assertEquals(0, events.size());
   }
@@ -1762,8 +1312,7 @@
   public void testSetVisibleRangeAndClearDataSameRangeForceEvent() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Add a range change handler.
     final List<Range> events = new ArrayList<Range>();
@@ -1779,9 +1328,7 @@
     assertEquals(new Range(0, 10), presenter.getVisibleRange());
     assertEquals(10, presenter.getVisibleItemCount());
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
     assertEquals(0, events.size());
 
@@ -1790,9 +1337,7 @@
     assertEquals(new Range(0, 10), presenter.getVisibleRange());
     assertEquals(0, presenter.getVisibleItemCount());
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=0");
+    view.assertReplaceAllChildrenCalled(0);
     view.assertLoadingState(LoadingState.LOADING);
     assertEquals(1, events.size());
   }
@@ -1800,8 +1345,7 @@
   public void testSetVisibleRangeDecreasePageSize() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Initialize some data.
     presenter.setVisibleRange(new Range(0, 10));
@@ -1810,9 +1354,7 @@
     assertEquals(10, presenter.getVisibleItemCount());
     assertEquals("test 0", presenter.getVisibleItem(0));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
 
     // Decrease the page size.
@@ -1821,17 +1363,14 @@
     assertEquals(8, presenter.getVisibleItemCount());
     assertEquals("test 0", presenter.getVisibleItem(0));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=8");
+    view.assertReplaceAllChildrenCalled(8);
     view.assertLoadingState(LoadingState.LOADED);
   }
 
   public void testSetVisibleRangeDecreasePageStart() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Initialize some data.
     presenter.setVisibleRange(new Range(10, 30));
@@ -1840,9 +1379,7 @@
     assertEquals(10, presenter.getVisibleItemCount());
     assertEquals("test 0", presenter.getVisibleItem(0));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=10,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
 
     // Decrease the start index.
@@ -1853,17 +1390,14 @@
     assertEquals(null, presenter.getVisibleItem(1));
     assertEquals("test 0", presenter.getVisibleItem(2));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=8,size=12");
+    view.assertReplaceAllChildrenCalled(12);
     view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
   }
 
   public void testSetVisibleRangeIncreasePageSize() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Initialize some data.
     presenter.setVisibleRange(new Range(0, 10));
@@ -1872,9 +1406,7 @@
     assertEquals(10, presenter.getVisibleItemCount());
     assertEquals("test 0", presenter.getVisibleItem(0));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.LOADED);
 
     // Increase the page size.
@@ -1883,17 +1415,14 @@
     assertEquals(10, presenter.getVisibleItemCount());
     assertEquals("test 0", presenter.getVisibleItem(0));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml(null);
+    view.assertReplaceChildrenNotCalled();
     view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
   }
 
   public void testSetVisibleRangeIncreasePageStart() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Initialize some data.
     presenter.setVisibleRange(new Range(0, 20));
@@ -1902,9 +1431,7 @@
     assertEquals(10, presenter.getVisibleItemCount());
     assertEquals("test 0", presenter.getVisibleItem(0));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=0,size=10");
+    view.assertReplaceAllChildrenCalled(10);
     view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
 
     // Increase the start index.
@@ -1913,17 +1440,14 @@
     assertEquals(8, presenter.getVisibleItemCount());
     assertEquals("test 2", presenter.getVisibleItem(0));
     presenter.flush();
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
-    view.assertLastHtml("start=2,size=8");
+    view.assertReplaceAllChildrenCalled(8);
     view.assertLoadingState(LoadingState.PARTIALLY_LOADED);
   }
 
   public void testSetVisibleRangeInts() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     try {
       presenter.setVisibleRange(0, 100);
@@ -1940,23 +1464,18 @@
   public void testSetVisibleRangeResetPageStart() {
     HasData<String> listView = new MockHasData<String>();
     MockView<String> view = new MockView<String>();
-    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView,
-        view, 10, null);
+    HasDataPresenter<String> presenter = new HasDataPresenter<String>(listView, view, 10, null);
 
     // Initialize the view.
     populatePresenter(presenter);
     presenter.flush();
-    view.assertLastHtml("start=0,size=10");
-    view.assertReplaceAllChildrenCalled(true);
-    view.assertReplaceChildrenCalled(false);
+    view.assertReplaceAllChildrenCalled(10);
 
     // Move pageStart to 2, then back to 0.
     presenter.setVisibleRange(new Range(2, 8));
     presenter.setVisibleRange(new Range(0, 10));
     presenter.flush();
-    view.assertLastHtml("start=0,size=2");
-    view.assertReplaceAllChildrenCalled(false);
-    view.assertReplaceChildrenCalled(true);
+    view.assertReplaceChildrenCalled(0, 2);
   }
 
   /**
@@ -1967,8 +1486,7 @@
    * @param expected the expected values
    * @param presenter the presenter
    */
-  private <T> void assertPresenterRowData(List<T> expected,
-      HasDataPresenter<T> presenter) {
+  private <T> void assertPresenterRowData(List<T> expected, HasDataPresenter<T> presenter) {
     assertEquals(expected.size(), presenter.getVisibleItemCount());
     for (int i = 0; i < expected.size(); i++) {
       assertEquals(expected.get(i), presenter.getVisibleItem(i));