Brings history support to samples/expenses. Doesn't handle paging --
sorry Chris, out of time -- and doesn't separate widgets from their
presenters. But still not a bad clean up, and hey! Book marks!

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


git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@9108 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/DenialPopup.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/DenialPopup.java
new file mode 100644
index 0000000..0ec6adf
--- /dev/null
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/DenialPopup.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.expenses.client;
+
+import com.google.gwt.dom.client.Style.Unit;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.sample.expenses.client.style.Styles;
+import com.google.gwt.sample.expenses.shared.ExpenseProxy;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.TextBox;
+
+/**
+ * The popup used to enter the rejection reason.
+ */
+  class DenialPopup extends PopupPanel {
+  private final Button cancelButton = new Button("Cancel",
+      new ClickHandler() {
+        public void onClick(ClickEvent event) {
+          reasonDenied = "";
+          hide();
+        }
+      });
+  private final Button confirmButton = new Button("Confirm",
+      new ClickHandler() {
+        public void onClick(ClickEvent event) {
+          reasonDenied = reasonBox.getText();
+          hide();
+        }
+      });
+
+  private ExpenseProxy expenseRecord;
+  private final TextBox reasonBox = new TextBox();
+  private String reasonDenied;
+
+  public DenialPopup() {
+    super(false, true);
+    setStyleName(Styles.common().popupPanel());
+    setGlassEnabled(true);
+    confirmButton.setWidth("11ex");
+    cancelButton.setWidth("11ex");
+    reasonBox.getElement().getStyle().setMarginLeft(10.0, Unit.PX);
+    reasonBox.getElement().getStyle().setMarginRight(10.0, Unit.PX);
+
+    HorizontalPanel hPanel = new HorizontalPanel();
+    hPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
+    hPanel.add(new HTML("<b>Reason:</b>"));
+    hPanel.add(reasonBox);
+    hPanel.add(confirmButton);
+    hPanel.add(cancelButton);
+    setWidget(hPanel);
+    cancelButton.getElement().getParentElement().getStyle().setPaddingLeft(
+        5.0, Unit.PX);
+  }
+
+  public ExpenseProxy getExpenseRecord() {
+    return expenseRecord;
+  }
+
+  public String getReasonDenied() {
+    return reasonDenied;
+  }
+
+  public void popup() {
+    center();
+    reasonBox.setFocus(true);
+  }
+
+  public void setExpenseRecord(ExpenseProxy expenseRecord) {
+    this.expenseRecord = expenseRecord;
+  }
+
+  public void setReasonDenied(String reasonDenied) {
+    this.reasonDenied = reasonDenied;
+    reasonBox.setText(reasonDenied);
+  }
+}
\ No newline at end of file
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseDetails.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportDetails.java
similarity index 89%
rename from samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseDetails.java
rename to samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportDetails.java
index eea8b71..455e0cf 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseDetails.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportDetails.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.sample.expenses.client;
 
+import com.google.gwt.activity.shared.Activity;
 import com.google.gwt.cell.client.AbstractInputCell;
 import com.google.gwt.cell.client.Cell;
 import com.google.gwt.cell.client.DateCell;
@@ -27,7 +28,6 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.dom.client.NativeEvent;
 import com.google.gwt.dom.client.SelectElement;
-import com.google.gwt.dom.client.Style.Unit;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -50,6 +50,8 @@
 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.expenses.client.place.ReportListPlace;
+import com.google.gwt.sample.expenses.client.place.ReportPlace;
 import com.google.gwt.sample.expenses.client.style.Styles;
 import com.google.gwt.sample.expenses.shared.EmployeeProxy;
 import com.google.gwt.sample.expenses.shared.ExpenseProxy;
@@ -63,13 +65,11 @@
 import com.google.gwt.user.cellview.client.Column;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.ui.AbstractImagePrototype;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
-import com.google.gwt.user.client.ui.HasVerticalAlignment;
-import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.TextBox;
@@ -89,9 +89,43 @@
  * Details about the current expense report on the right side of the app,
  * including the list of expenses.
  */
-public class ExpenseDetails extends Composite {
+public class ExpenseReportDetails extends Composite implements Activity {
 
-  interface ExpenseDetailsUiBinder extends UiBinder<Widget, ExpenseDetails> {
+  interface Binder extends UiBinder<Widget, ExpenseReportDetails> {
+  }
+
+  /**
+   * Fetches an employee and a report in parallel. A fine example of the kind of
+   * thing that will no longer be necessary when RequestFactory provides server
+   * side method chaining.
+   */
+  class EmployeeReportFetcher {
+    ReportProxy fetchedReport;
+    EmployeeProxy fetchedEmployee;
+
+    void Run(EntityProxyId<EmployeeProxy> employeeId,
+        EntityProxyId<ReportProxy> reportId,
+        final Receiver<EmployeeReportFetcher> callback) {
+      expensesRequestFactory.find(employeeId).fire(
+          new Receiver<EmployeeProxy>() {
+            @Override
+            public void onSuccess(EmployeeProxy response) {
+              fetchedEmployee = response;
+              if (fetchedReport != null) {
+                callback.onSuccess(EmployeeReportFetcher.this);
+              }
+            }
+          });
+      expensesRequestFactory.find(reportId).fire(new Receiver<ReportProxy>() {
+        @Override
+        public void onSuccess(ReportProxy response) {
+          fetchedReport = response;
+          if (fetchedEmployee != null) {
+            callback.onSuccess(EmployeeReportFetcher.this);
+          }
+        }
+      });
+    }
   }
 
   /**
@@ -287,72 +321,6 @@
     }
   }
 
-  /**
-   * The popup used to enter the rejection reason.
-   */
-  private class DenialPopup extends PopupPanel {
-    private final Button cancelButton = new Button("Cancel",
-        new ClickHandler() {
-          public void onClick(ClickEvent event) {
-            reasonDenied = "";
-            hide();
-          }
-        });
-    private final Button confirmButton = new Button("Confirm",
-        new ClickHandler() {
-          public void onClick(ClickEvent event) {
-            reasonDenied = reasonBox.getText();
-            hide();
-          }
-        });
-
-    private ExpenseProxy expenseRecord;
-    private final TextBox reasonBox = new TextBox();
-    private String reasonDenied;
-
-    public DenialPopup() {
-      super(false, true);
-      setStyleName(Styles.common().popupPanel());
-      setGlassEnabled(true);
-      confirmButton.setWidth("11ex");
-      cancelButton.setWidth("11ex");
-      reasonBox.getElement().getStyle().setMarginLeft(10.0, Unit.PX);
-      reasonBox.getElement().getStyle().setMarginRight(10.0, Unit.PX);
-
-      HorizontalPanel hPanel = new HorizontalPanel();
-      hPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);
-      hPanel.add(new HTML("<b>Reason:</b>"));
-      hPanel.add(reasonBox);
-      hPanel.add(confirmButton);
-      hPanel.add(cancelButton);
-      setWidget(hPanel);
-      cancelButton.getElement().getParentElement().getStyle().setPaddingLeft(
-          5.0, Unit.PX);
-    }
-
-    public ExpenseProxy getExpenseRecord() {
-      return expenseRecord;
-    }
-
-    public String getReasonDenied() {
-      return reasonDenied;
-    }
-
-    public void popup() {
-      center();
-      reasonBox.setFocus(true);
-    }
-
-    public void setExpenseRecord(ExpenseProxy expenseRecord) {
-      this.expenseRecord = expenseRecord;
-    }
-
-    public void setReasonDenied(String reasonDenied) {
-      this.reasonDenied = reasonDenied;
-      reasonBox.setText(reasonDenied);
-    }
-  }
-
   private static Template template;
 
   /**
@@ -365,34 +333,25 @@
    */
   private static final int REFRESH_INTERVAL = 5000;
 
-  private static ExpenseDetailsUiBinder uiBinder = GWT.create(ExpenseDetailsUiBinder.class);
+  private static Binder uiBinder = GWT.create(Binder.class);
 
-  @UiField
-  Element approvedLabel;
+  @UiField Element approvedLabel;
 
-  @UiField
-  Element costLabel;
+  @UiField Element costLabel;
 
-  @UiField
-  Element notes;
+  @UiField Element notes;
 
-  @UiField
-  TextBox notesBox;
+  @UiField TextBox notesBox;
 
-  @UiField
-  Anchor notesEditLink;
+  @UiField Anchor notesEditLink;
 
-  @UiField
-  Element notesEditLinkWrapper;
+  @UiField Element notesEditLinkWrapper;
 
-  @UiField
-  Element notesPending;
+  @UiField Element notesPending;
 
-  @UiField
-  Element reportName;
+  @UiField Element reportName;
 
-  @UiField
-  Anchor reportsLink;
+  @UiField Anchor reportsLink;
 
   @UiField(provided = true)
   CellTable<ExpenseProxy> table;
@@ -464,7 +423,9 @@
    */
   private double totalApproved;
 
-  public ExpenseDetails(ExpensesRequestFactory expensesRequestFactory) {
+  private ReportPlace place;
+
+  public ExpenseReportDetails(ExpensesRequestFactory expensesRequestFactory) {
     this.expensesRequestFactory = expensesRequestFactory;
     createErrorPopup();
     initTable();
@@ -506,23 +467,20 @@
     });
   }
 
+  public ReportListPlace getReportListPlace() {
+    ReportListPlace listPlace = place.getListPlace();
+    return listPlace == null ? ReportListPlace.ALL : listPlace;
+  }
+
   public Anchor getReportsLink() {
     return reportsLink;
   }
 
-  public void init(EventBus eventBus) {
-    EntityProxyChange.registerForProxyType(eventBus, ExpenseProxy.class,
-        new EntityProxyChange.Handler<ExpenseProxy>() {
-          public void onProxyChange(EntityProxyChange<ExpenseProxy> event) {
-            onExpenseRecordChanged(event);
-          }
-        });
-    EntityProxyChange.registerForProxyType(eventBus, ReportProxy.class,
-        new EntityProxyChange.Handler<ReportProxy>() {
-          public void onProxyChange(EntityProxyChange<ReportProxy> event) {
-            onReportChanged(event);
-          }
-        });
+  public String mayStop() {
+    return null;
+  }
+
+  public void onCancel() {
   }
 
   public void onExpenseRecordChanged(EntityProxyChange<ExpenseProxy> event) {
@@ -571,42 +529,57 @@
     }
   }
 
-  /**
-   * Set the {@link ReportProxy} to show.
-   * 
-   * @param report the {@link ReportProxy}
-   * @param department the selected department
-   * @param employee the selected employee
-   */
-  public void setReportRecord(ReportProxy report, String department,
-      EmployeeProxy employee) {
-    this.report = report;
-    knownExpenseKeys = null;
-    reportName.setInnerText(report.getPurpose());
-    costLabel.setInnerText("");
-    approvedLabel.setInnerText("");
-    unreconciledLabel.setInnerText("");
-    setNotesEditState(false, false, report.getNotes());
-    items.getList().clear();
-    totalApproved = 0;
+  public void onStop() {
+  }
 
-    // Update the breadcrumb.
-    reportsLink.setText(ExpenseList.getBreadcrumb(department, employee));
+  public void start(AcceptsOneWidget panel, EventBus eventBus) {
+    final ReportListPlace listPlace = place.getListPlace();
 
-    // Reset sorting state of table
-    lastComparator = defaultComparator;
-    if (allHeaders.size() > 0) {
-      for (SortableHeader header : allHeaders) {
-        header.setSorted(false);
-        header.setReverseSort(true);
-      }
-      allHeaders.get(0).setSorted(true);
-      allHeaders.get(0).setReverseSort(false);
-      table.redrawHeaders();
+    if (listPlace.getEmployeeId() == null) {
+      expensesRequestFactory.find(place.getReportId()).fire(
+          new Receiver<ReportProxy>() {
+            @Override
+            public void onSuccess(ReportProxy response) {
+              setReportRecord(response, listPlace.getDepartment(), null);
+            }
+          });
+    } else {
+      new EmployeeReportFetcher().Run(listPlace.getEmployeeId(),
+          place.getReportId(),
+          new Receiver<ExpenseReportDetails.EmployeeReportFetcher>() {
+            @Override
+            public void onSuccess(EmployeeReportFetcher response) {
+              setReportRecord(response.fetchedReport,
+                  listPlace.getDepartment(), response.fetchedEmployee);
+            }
+          });
     }
 
-    // Request the expenses.
-    requestExpenses();
+    EntityProxyChange.registerForProxyType(eventBus, ExpenseProxy.class,
+        new EntityProxyChange.Handler<ExpenseProxy>() {
+          public void onProxyChange(EntityProxyChange<ExpenseProxy> event) {
+            onExpenseRecordChanged(event);
+          }
+        });
+
+    EntityProxyChange.registerForProxyType(eventBus, ReportProxy.class,
+        new EntityProxyChange.Handler<ReportProxy>() {
+          public void onProxyChange(EntityProxyChange<ReportProxy> event) {
+            onReportChanged(event);
+          }
+        });
+
+    panel.setWidget(this);
+  }
+
+  /**
+   * In this application, called by {@link ExpensesActivityMapper} each time a
+   * ReportListPlace is posted. In a more typical set up, this would be a
+   * constructor argument to a one shot activity, perhaps managing a shared
+   * widget view instance.
+   */
+  public void updateForPlace(final ReportPlace place) {
+    this.place = place;
   }
 
   /**
@@ -996,6 +969,44 @@
   }
 
   /**
+   * Set the {@link ReportProxy} to show.
+   * 
+   * @param report the {@link ReportProxy}
+   * @param department the selected department, or ""
+   * @param employee the selected employee, or null
+   */
+  private void setReportRecord(ReportProxy report, String department,
+      EmployeeProxy employee) {
+    this.report = report;
+    knownExpenseKeys = null;
+    reportName.setInnerText(report.getPurpose());
+    costLabel.setInnerText("");
+    approvedLabel.setInnerText("");
+    unreconciledLabel.setInnerText("");
+    setNotesEditState(false, false, report.getNotes());
+    items.getList().clear();
+    totalApproved = 0;
+
+    // Update the breadcrumb.
+    reportsLink.setText(ExpenseReportList.getBreadcrumb(department, employee));
+
+    // Reset sorting state of table
+    lastComparator = defaultComparator;
+    if (allHeaders.size() > 0) {
+      for (SortableHeader header : allHeaders) {
+        header.setSorted(false);
+        header.setReverseSort(true);
+      }
+      allHeaders.get(0).setSorted(true);
+      allHeaders.get(0).setReverseSort(false);
+      table.redrawHeaders();
+    }
+
+    // Request the expenses.
+    requestExpenses();
+  }
+
+  /**
    * Show the error popup.
    * 
    * @param errorMessage the error message
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseDetails.ui.xml b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportDetails.ui.xml
similarity index 100%
rename from samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseDetails.ui.xml
rename to samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportDetails.ui.xml
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseList.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportList.java
similarity index 86%
rename from samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseList.java
rename to samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportList.java
index c0f0c40..bb40add 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseList.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportList.java
@@ -15,6 +15,7 @@
  */
 package com.google.gwt.sample.expenses.client;
 
+import com.google.gwt.activity.shared.Activity;
 import com.google.gwt.cell.client.AbstractCell;
 import com.google.gwt.cell.client.Cell;
 import com.google.gwt.cell.client.DateCell;
@@ -40,9 +41,9 @@
 import com.google.gwt.requestfactory.ui.client.EntityProxyKeyProvider;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
 import com.google.gwt.safehtml.shared.SafeHtmlUtils;
+import com.google.gwt.sample.expenses.client.place.ReportListPlace;
 import com.google.gwt.sample.expenses.client.style.Styles;
 import com.google.gwt.sample.expenses.shared.EmployeeProxy;
-import com.google.gwt.sample.expenses.shared.ExpenseProxy;
 import com.google.gwt.sample.expenses.shared.ExpensesRequestFactory;
 import com.google.gwt.sample.expenses.shared.ReportProxy;
 import com.google.gwt.uibinder.client.UiBinder;
@@ -50,19 +51,19 @@
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.cellview.client.CellTable;
 import com.google.gwt.user.cellview.client.Column;
-import com.google.gwt.user.cellview.client.SimplePager;
 import com.google.gwt.user.cellview.client.HasKeyboardSelectionPolicy.KeyboardSelectionPolicy;
+import com.google.gwt.user.cellview.client.SimplePager;
 import com.google.gwt.user.cellview.client.SimplePager.TextLocation;
 import com.google.gwt.user.client.Timer;
+import com.google.gwt.user.client.ui.AcceptsOneWidget;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.view.client.AsyncDataProvider;
-import com.google.gwt.view.client.HasData;
 import com.google.gwt.view.client.NoSelectionModel;
 import com.google.gwt.view.client.ProvidesKey;
 import com.google.gwt.view.client.Range;
+import com.google.gwt.view.client.RangeChangeEvent;
 import com.google.gwt.view.client.SelectionChangeEvent;
 
 import java.util.ArrayList;
@@ -74,10 +75,10 @@
 /**
  * The list of expense reports on the right side of the app.
  */
-public class ExpenseList extends Composite implements
-    EntityProxyChange.Handler<ReportProxy> {
+public class ExpenseReportList extends Composite implements
+    EntityProxyChange.Handler<ReportProxy>, Activity {
 
-  interface ExpenseListUiBinder extends UiBinder<Widget, ExpenseList> {
+  interface Binder extends UiBinder<Widget, ExpenseReportList> {
   }
 
   /**
@@ -175,27 +176,15 @@
       }
     }
   }
-  /**
-   * The data provider used to retrieve reports.
-   */
-  private class ReportDataProvider extends AsyncDataProvider<ReportProxy> {
 
-    ReportDataProvider(ProvidesKey<ReportProxy> keyProvider) {
-      super(keyProvider);
-    }
-
-    @Override
-    protected void onRangeChanged(HasData<ReportProxy> display) {
-      requestReports(false);
-    }
-  }
+  private static final ProvidesKey<ReportProxy> keyProvider = new EntityProxyKeyProvider<ReportProxy>();
 
   /**
    * The auto refresh interval in milliseconds.
    */
   private static final int REFRESH_INTERVAL = 5000;
 
-  private static ExpenseListUiBinder uiBinder = GWT.create(ExpenseListUiBinder.class);
+  private static Binder uiBinder = GWT.create(Binder.class);
 
   /**
    * Utility method to get the first part of the breadcrumb based on the
@@ -206,9 +195,10 @@
    * @return the breadcrumb
    */
   public static String getBreadcrumb(String department, EmployeeProxy employee) {
+    assert null != department;
     if (employee != null) {
       return "Reports for " + employee.getDisplayName();
-    } else if (department != null) {
+    } else if (!"".equals(department)) {
       return "Reports for " + department;
     } else {
       return "All Reports";
@@ -217,13 +207,14 @@
 
   @UiField
   Element breadcrumb;
-
   @UiField
   SimplePager pager;
-  @UiField(provided = true)
-  DefaultTextBox searchBox;
   @UiField
   Image searchButton;
+
+  @UiField(provided = true)
+  DefaultTextBox searchBox;
+
   /**
    * The main table. We provide this in the constructor before calling
    * {@link UiBinder#createAndBindUi(Object)} because the pager depends on it.
@@ -289,15 +280,9 @@
       "created", "purpose", "notes"};
 
   /**
-   * The data provider that provides reports.
-   */
-  private final ReportDataProvider reports = new ReportDataProvider(
-      new EntityProxyKeyProvider<ReportProxy>());
-
-  /**
    * The factory used to send requests.
    */
-  private ExpensesRequestFactory requestFactory;
+  private final ExpensesRequestFactory requestFactory;
 
   /**
    * The string that the user searched for.
@@ -309,15 +294,23 @@
    */
   private String startsWithSearch;
 
-  public ExpenseList() {
+  private ReportListPlace place;
+
+  private boolean running;
+
+  public ExpenseReportList(ExpensesRequestFactory requestFactory) {
+    this.requestFactory = requestFactory;
+
     // Initialize the widget.
     createTable();
+    table.addRangeChangeHandler(new RangeChangeEvent.Handler() {
+      public void onRangeChange(RangeChangeEvent event) {
+        requestReports(false);
+      }
+    });
     searchBox = new DefaultTextBox("search");
     initWidget(uiBinder.createAndBindUi(this));
 
-    // Add the view to the data provider.
-    reports.addDataDisplay(table);
-
     // Listen for key events from the text boxes.
     searchBox.addKeyUpHandler(new KeyUpHandler() {
       public void onKeyUp(KeyUpEvent event) {
@@ -344,10 +337,12 @@
     });
   }
 
-  public void init(ExpensesRequestFactory factory, EventBus eventBus) {
-    EntityProxyChange.registerForProxyType(eventBus, ReportProxy.class, this);
-    this.requestFactory = factory;
-    requestReports(false);
+  public String mayStop() {
+    return null;
+  }
+
+  public void onCancel() {
+    onStop();
   }
 
   public void onProxyChange(EntityProxyChange<ReportProxy> event) {
@@ -358,36 +353,43 @@
       if (record != null && changedId.equals(record.stableId())) {
         List<ReportProxy> changedList = new ArrayList<ReportProxy>();
         changedList.add(record);
-        reports.updateRowData(i + table.getPageStart(), changedList);
+        table.setRowData(i + table.getPageStart(), changedList);
       }
       i++;
     }
   }
 
-  /**
-   * Set the current department and employee to filter on.
-   * 
-   * @param department the department, or null if none selected
-   * @param employee the employee, or null if none selected
-   */
-  public void setEmployee(String department, EmployeeProxy employee) {
-    this.department = department;
-    this.employee = employee;
-    isCountStale = true;
-    searchBox.resetDefaultText();
-    startsWithSearch = null;
-    breadcrumb.setInnerText(getBreadcrumb(department, employee));
-    searchRegExp = null;
-
-    // Refresh the table.
-    pager.setPageStart(0);
-    requestReports(false);
+  public void onStop() {
+    running = false;
+    refreshTimer.cancel();
   }
 
   public void setListener(Listener listener) {
     this.listener = listener;
   }
 
+  public void start(AcceptsOneWidget panel, EventBus eventBus) {
+    running = true;
+    doUpdateForPlace();
+
+    EntityProxyChange.registerForProxyType(eventBus, ReportProxy.class, this);
+    requestReports(false);
+    panel.setWidget(this);
+  }
+
+  /**
+   * In this application, called by {@link ExpensesActivityMapper} each time a
+   * ReportListPlace is posted. In a more typical set up, this would be a
+   * constructor argument to a one shot activity, perhaps managing a shared
+   * widget view instance.
+   */
+  public void updateForPlace(final ReportListPlace place) {
+    this.place = place;
+    if (running) {
+      doUpdateForPlace();
+    }
+  }
+
   @UiFactory
   SimplePager createPager() {
     SimplePager p = new SimplePager(TextLocation.RIGHT);
@@ -520,6 +522,39 @@
     table.addColumn(new SpacerColumn<ReportProxy>());
   }
 
+  private void doUpdateForPlace() {
+    if (place.getEmployeeId() == null) {
+      findDepartmentOrEmployee(place.getDepartment(), null);
+    } else {
+      requestFactory.find(place.getEmployeeId()).fire(
+          new Receiver<EmployeeProxy>() {
+            @Override
+            public void onSuccess(EmployeeProxy response) {
+              findDepartmentOrEmployee("", response);
+            }
+          });
+    }
+  }
+
+  /**
+   * Set the current department and employee to filter on.
+   * 
+   * @param department the department, or null if none selected
+   * @param employee the employee, or null if none selected
+   */
+  private void findDepartmentOrEmployee(String department,
+      EmployeeProxy employee) {
+    this.department = department;
+    this.employee = employee;
+    isCountStale = true;
+    searchBox.resetDefaultText();
+    startsWithSearch = null;
+    breadcrumb.setInnerText(getBreadcrumb(department, employee));
+    searchRegExp = null;
+    pager.setPageStart(0);
+    requestReports(false);
+  }
+
   /**
    * Send a request for reports in the current range.
    * 
@@ -569,7 +604,7 @@
           if (this == lastDataSizeReceiver) {
             int count = response.intValue();
             // Treat count == 1000 as inexact due to AppEngine limitation
-            reports.updateRowCount(count, count != 1000);
+            table.setRowCount(count, count != 1000);
           }
         }
       };
@@ -585,10 +620,10 @@
           int size = newValues.size();
           if (size < table.getPageSize()) {
             // Now we know the exact data size
-            reports.updateRowCount(table.getPageStart() + size, true);
+            table.setRowCount(table.getPageStart() + size, true);
           }
           if (size > 0) {
-            reports.updateRowData(table.getPageStart(), newValues);
+            table.setRowData(table.getPageStart(), newValues);
           }
 
           // Add the new keys to the known keys.
@@ -597,10 +632,10 @@
             knownReportKeys = new HashSet<Object>();
           }
           for (ReportProxy value : newValues) {
-            Object key = reports.getKey(value);
+            Object key = keyProvider.getKey(value);
             if (!isInitialData && !knownReportKeys.contains(key)) {
-              (new PhaseAnimation.CellTablePhaseAnimation<ReportProxy>(table,
-                  value, reports)).run();
+              new PhaseAnimation.CellTablePhaseAnimation<ReportProxy>(table,
+                  value, keyProvider).run();
             }
             knownReportKeys.add(key);
           }
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseList.ui.xml b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportList.ui.xml
similarity index 100%
rename from samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseList.ui.xml
rename to samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseReportList.ui.xml
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseTree.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseTree.java
index 556a83b..49d29be 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseTree.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpenseTree.java
@@ -21,9 +21,11 @@
 import com.google.gwt.cell.client.TextCell;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.Style.Overflow;
+import com.google.gwt.requestfactory.shared.EntityProxyId;
 import com.google.gwt.requestfactory.shared.Receiver;
 import com.google.gwt.requestfactory.ui.client.EntityProxyKeyProvider;
 import com.google.gwt.safehtml.client.SafeHtmlTemplates;
+import com.google.gwt.safehtml.client.SafeHtmlTemplates.Template;
 import com.google.gwt.safehtml.shared.SafeHtml;
 import com.google.gwt.safehtml.shared.SafeHtmlBuilder;
 import com.google.gwt.sample.expenses.client.style.Styles;
@@ -58,7 +60,7 @@
      * @param department the selected department name
      * @param employee the selected employee
      */
-    void onSelection(String department, EmployeeProxy employee);
+    void onSelection(String department, EntityProxyId<EmployeeProxy> employeeId);
   }
 
   interface Template extends SafeHtmlTemplates {
@@ -251,7 +253,7 @@
 
   public ExpenseTree(ExpensesRequestFactory requestFactory) {
     this.requestFactory = requestFactory;
-    
+
     // Initialize the departments.
     List<String> departmentList = departments.getList();
     departmentList.add("All");
@@ -282,20 +284,21 @@
         Object selected = selectionModel.getSelectedObject();
         if (selected == null) {
           lastEmployee = null;
-          lastDepartment = null;
+          lastDepartment = "";
         } else if (selected instanceof EmployeeProxy) {
           lastEmployee = (EmployeeProxy) selected;
         } else if (selected instanceof String) {
           lastEmployee = null;
           if (model.isAllDepartment(selected)) {
-            lastDepartment = null;
+            lastDepartment = "";
           } else {
             lastDepartment = (String) selected;
           }
         }
 
         if (listener != null) {
-          listener.onSelection(lastDepartment, lastEmployee);
+          listener.onSelection(lastDepartment, lastEmployee == null ? null
+              : lastEmployee.stableId());
         }
       }
     });
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/Expenses.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/Expenses.java
index c4a66a7..c04cca8 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/Expenses.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/Expenses.java
@@ -16,7 +16,7 @@
 package com.google.gwt.sample.expenses.client;
 
 import com.google.gwt.core.client.EntryPoint;
-import com.google.gwt.sample.expenses.client.ioc.Factory;
+import com.google.gwt.sample.expenses.client.ioc.ExpensesFactory;
 import com.google.gwt.user.client.ui.RootLayoutPanel;
 
 /**
@@ -25,6 +25,6 @@
 public class Expenses implements EntryPoint {
 
   public void onModuleLoad() {
-    new Factory().getExpensesApp().run(RootLayoutPanel.get());
+    new ExpensesFactory().getExpensesApp().run(RootLayoutPanel.get());
   }
 }
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesActivityMapper.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesActivityMapper.java
new file mode 100644
index 0000000..ed58976
--- /dev/null
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesActivityMapper.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2010 Google Inc.
+ * 
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ * 
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.sample.expenses.client;
+
+import com.google.gwt.activity.shared.Activity;
+import com.google.gwt.activity.shared.ActivityMapper;
+import com.google.gwt.place.shared.Place;
+import com.google.gwt.sample.expenses.client.place.ReportListPlace;
+import com.google.gwt.sample.expenses.client.place.ReportPlace;
+
+/**
+ * ActivityMapper for the Expenses app.
+ */
+public class ExpensesActivityMapper implements ActivityMapper {
+
+  private final ExpenseReportDetails expenseDetails;
+  private final ExpenseReportList expenseList;
+
+  public ExpensesActivityMapper(ExpenseReportDetails expenseDetails,
+      ExpenseReportList expenseList) {
+    this.expenseDetails = expenseDetails;
+    this.expenseList = expenseList;
+  }
+
+  public Activity getActivity(Place place) {
+    if (place instanceof ReportListPlace) {
+      expenseList.updateForPlace((ReportListPlace) place);
+      return expenseList;
+    }
+
+    if (place instanceof ReportPlace) {
+      expenseDetails.updateForPlace((ReportPlace) place);
+      return expenseDetails;
+    }
+
+    return null;
+  }
+}
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesApp.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesApp.java
index 63625d0..0975f0a 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesApp.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesApp.java
@@ -15,16 +15,21 @@
  */
 package com.google.gwt.sample.expenses.client;
 
+import com.google.gwt.activity.shared.ActivityManager;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.place.shared.Place;
 import com.google.gwt.place.shared.PlaceController;
 import com.google.gwt.place.shared.PlaceHistoryHandler;
+import com.google.gwt.requestfactory.shared.EntityProxyId;
 import com.google.gwt.requestfactory.shared.Receiver;
 import com.google.gwt.requestfactory.shared.RequestEvent;
 import com.google.gwt.requestfactory.shared.UserInformationProxy;
 import com.google.gwt.requestfactory.ui.client.AuthenticationFailureHandler;
 import com.google.gwt.requestfactory.ui.client.LoginWidget;
+import com.google.gwt.sample.expenses.client.place.ReportListPlace;
+import com.google.gwt.sample.expenses.client.place.ReportPlace;
 import com.google.gwt.sample.expenses.shared.EmployeeProxy;
 import com.google.gwt.sample.expenses.shared.ExpensesRequestFactory;
 import com.google.gwt.sample.expenses.shared.ReportProxy;
@@ -46,23 +51,25 @@
 
   private static final Logger log = Logger.getLogger(ExpensesShell.class.getName());
 
+  private final ActivityManager activityManager;
   private final EventBus eventBus;
   private final PlaceController placeController;
   private final PlaceHistoryHandler placeHistoryHandler;
   private final ExpensesRequestFactory requestFactory;
   private final ExpensesShell shell;
 
-  private String lastDepartment;
-  private EmployeeProxy lastEmployee;
+  private EntityProxyId<EmployeeProxy> lastEmployee;
+  private String lastDepartment = "";
 
-  public ExpensesApp(ExpensesRequestFactory requestFactory, EventBus eventBus,
-      ExpensesShell shell, PlaceHistoryHandler placeHistoryHandler,
-      PlaceController placeController) {
-    this.requestFactory = requestFactory;
+  public ExpensesApp(ActivityManager activityManager, EventBus eventBus,
+      PlaceController placeController, PlaceHistoryHandler placeHistoryHandler,
+      ExpensesRequestFactory requestFactory, ExpensesShell shell) {
+    this.activityManager = activityManager;
     this.eventBus = eventBus;
-    this.shell = shell;
-    this.placeHistoryHandler = placeHistoryHandler;
     this.placeController = placeController;
+    this.placeHistoryHandler = placeHistoryHandler;
+    this.requestFactory = requestFactory;
+    this.shell = shell;
   }
 
   /**
@@ -76,10 +83,15 @@
     });
 
     final ExpenseTree expenseTree = shell.getExpenseTree();
-    final ExpenseList expenseList = shell.getExpenseList();
-    final ExpenseDetails expenseDetails = shell.getExpenseDetails();
+    final ExpenseReportList expenseList = shell.getExpenseList();
+    final ExpenseReportDetails expenseDetails = shell.getExpenseDetails();
 
-    root.add(shell);
+    // Handle breadcrumb events from Expense Details.
+    expenseDetails.getReportsLink().addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        placeController.goTo(expenseDetails.getReportListPlace());
+      }
+    });
 
     // Check for Authentication failures or mismatches
     RequestEvent.register(eventBus, new AuthenticationFailureHandler());
@@ -97,32 +109,31 @@
 
     // Listen for requests from ExpenseTree.
     expenseTree.setListener(new ExpenseTree.Listener() {
-      public void onSelection(String department, EmployeeProxy employee) {
-        lastDepartment = department;
+
+      public void onSelection(String department, EntityProxyId<EmployeeProxy> employee) {
         lastEmployee = employee;
-        expenseList.setEmployee(department, employee);
-        shell.showExpenseDetails(false);
+        lastDepartment = department;
+        placeController.goTo(new ReportListPlace(employee, department));
       }
     });
 
     // Listen for requests from the ExpenseList.
-    expenseList.setListener(new ExpenseList.Listener() {
+    expenseList.setListener(new ExpenseReportList.Listener() {
       public void onReportSelected(ReportProxy report) {
-        expenseDetails.setReportRecord(report, lastDepartment, lastEmployee);
-        shell.showExpenseDetails(true);
+        placeController.goTo(new ReportPlace( //
+            new ReportListPlace(lastEmployee, lastDepartment), //
+            report.stableId() //
+        ));
       }
     });
 
-    /*
-     * TODO these should be constructor arguments, and the inits should probably
-     * happen onLoad
-     */
-    expenseList.init(requestFactory, eventBus);
-    expenseDetails.init(eventBus);
+    // Give the ActivityManager a panel to run
+    activityManager.setDisplay(shell.getPanel());
 
     // Browser history integration
-    placeHistoryHandler.register(placeController, eventBus, new Place() {
-    });
+    placeHistoryHandler.register(placeController, eventBus, ReportListPlace.ALL);
     placeHistoryHandler.handleCurrentHistory();
+
+    root.add(shell);
   }
 }
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesShell.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesShell.java
index 0e79d0f..bee5035 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesShell.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesShell.java
@@ -16,13 +16,12 @@
 package com.google.gwt.sample.expenses.client;
 
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.requestfactory.ui.client.LoginWidget;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.DockLayoutPanel;
+import com.google.gwt.user.client.ui.HasOneWidget;
 import com.google.gwt.user.client.ui.Widget;
 
 /**
@@ -34,37 +33,31 @@
 
   private static ShellUiBinder uiBinder = GWT.create(ShellUiBinder.class);
 
-  @UiField
-  ExpenseList expenseList;
+  @UiField(provided = true)
+  final ExpenseReportList expenseList;
+  
+  @UiField(provided = true)
+  final ExpenseReportDetails expenseDetails;
+
   @UiField(provided = true)
   final ExpenseTree expenseTree;
-  @UiField
-  SlidingPanel slidingPanel;
-  @UiField
-  LoginWidget loginWidget;
-  @UiField
-  DockLayoutPanel dockLayout;
-  @UiField(provided = true)
-  final ExpenseDetails expenseDetails;
+  
+  @UiField SlidingPanel slidingPanel;
+  @UiField LoginWidget loginWidget;
+  @UiField DockLayoutPanel dockLayout;
 
-  public ExpensesShell(ExpenseTree expenseTree, ExpenseDetails expenseDetails) {
+  public ExpensesShell(ExpenseTree expenseTree, ExpenseReportList expenseList, ExpenseReportDetails expenseDetails) {
     this.expenseTree = expenseTree;
+    this.expenseList = expenseList;
     this.expenseDetails = expenseDetails;
     initWidget(uiBinder.createAndBindUi(this));
-
-    // Handle breadcrumb events from Expense Details.
-    expenseDetails.getReportsLink().addClickHandler(new ClickHandler() {
-      public void onClick(ClickEvent event) {
-        showExpenseDetails(false);
-      }
-    });
   }
 
-  public ExpenseDetails getExpenseDetails() {
+  public ExpenseReportDetails getExpenseDetails() {
     return expenseDetails;
   }
 
-  public ExpenseList getExpenseList() {
+  public ExpenseReportList getExpenseList() {
     return expenseList;
   }
 
@@ -72,19 +65,11 @@
     return expenseTree;
   }
 
-  /**
-   * @return the login widget
-   */
   public LoginWidget getLoginWidget() {
     return loginWidget;
   }
   
-  /**
-   * Show or hide the expense details. When showing, the expense list is hidden.
-   * 
-   * @param isShowing true to show details, false to show reports list
-   */
-  public void showExpenseDetails(boolean isShowing) {
-    slidingPanel.setWidget(isShowing ? expenseDetails : expenseList);
+  public HasOneWidget getPanel() {
+    return slidingPanel;
   }
 }
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesShell.ui.xml b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesShell.ui.xml
index bd54c12..a146dbd 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesShell.ui.xml
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ExpensesShell.ui.xml
@@ -68,8 +68,8 @@
       <g:center>
         <e:SlidingPanel ui:field='slidingPanel'>
           <!-- The order of the children determines which way they slide -->
-          <e:ExpenseList ui:field='expenseList' />
-          <e:ExpenseDetails ui:field='expenseDetails' />
+          <e:ExpenseReportList ui:field='expenseList' />
+          <e:ExpenseReportDetails ui:field='expenseDetails' />
         </e:SlidingPanel>
       </g:center>
     </g:DockLayoutPanel>
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/SlidingPanel.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/SlidingPanel.java
index 6b8d18b..95a422c 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/SlidingPanel.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/SlidingPanel.java
@@ -50,7 +50,7 @@
   }
 
   public void add(IsWidget w) {
-    add(w.asWidget());
+    add(asWidgetOrNull(w.asWidget()));
   }
 
   public void add(Widget w) {
@@ -82,12 +82,20 @@
   }
 
   public void setWidget(IsWidget w) {
-    setWidget(w.asWidget());
+    setWidget(asWidgetOrNull(w));
   }
 
+  /**
+   * Set the widget to show, adding it to the end of our sliding set if we
+   * haven't seen it before. Nulls are ignored.
+   */
   // Conflict btw deprecated Composite#setWidget and HasOneWidget#setWidget
   @SuppressWarnings("deprecation")
   public void setWidget(Widget widget) {
+    if (widget == null) {
+      return;
+    }
+    
     int newIndex = widgets.indexOf(widget);
 
     if (newIndex < 0) {
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ioc/Factory.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ioc/ExpensesFactory.java
similarity index 60%
rename from samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ioc/Factory.java
rename to samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ioc/ExpensesFactory.java
index f2dfc9e..aa5ec92 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ioc/Factory.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/ioc/ExpensesFactory.java
@@ -15,13 +15,17 @@
  */
 package com.google.gwt.sample.expenses.client.ioc;
 
+import com.google.gwt.activity.shared.ActivityManager;
+import com.google.gwt.activity.shared.ActivityMapper;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.shared.EventBus;
 import com.google.gwt.event.shared.SimpleEventBus;
 import com.google.gwt.place.shared.PlaceController;
 import com.google.gwt.place.shared.PlaceHistoryHandler;
-import com.google.gwt.sample.expenses.client.ExpenseDetails;
+import com.google.gwt.sample.expenses.client.ExpenseReportDetails;
+import com.google.gwt.sample.expenses.client.ExpenseReportList;
 import com.google.gwt.sample.expenses.client.ExpenseTree;
+import com.google.gwt.sample.expenses.client.ExpensesActivityMapper;
 import com.google.gwt.sample.expenses.client.ExpensesApp;
 import com.google.gwt.sample.expenses.client.ExpensesShell;
 import com.google.gwt.sample.expenses.client.place.ExpensesPlaceHistoryMapper;
@@ -34,30 +38,50 @@
  * <p>
  * TODO: Use {@link http ://code.google.com/p/google-gin/} to generate this
  */
-public class Factory {
+public class ExpensesFactory {
 
   private final EventBus eventBus = new SimpleEventBus();
   private final ExpensesRequestFactory requestFactory = GWT.create(ExpensesRequestFactory.class);
   private final ExpensesPlaceHistoryMapper historyMapper = GWT.create(ExpensesPlaceHistoryMapper.class);
   private final PlaceHistoryHandler placeHistoryHandler;
   private final PlaceController placeController = new PlaceController(eventBus);
+  private final ExpenseTree expenseTree = new ExpenseTree(requestFactory);
+  private final ExpenseReportList expenseList = new ExpenseReportList(requestFactory);
+  private final ExpenseReportDetails expenseDetails = new ExpenseReportDetails(
+      requestFactory);
+  private final ActivityMapper activityMapper = new ExpensesActivityMapper(
+      expenseDetails, expenseList);
+  private final ActivityManager activityManager = new ActivityManager(
+      activityMapper, eventBus);
 
-  public Factory() {
+  public ExpensesFactory() {
     requestFactory.initialize(eventBus);
     historyMapper.setFactory(this);
     placeHistoryHandler = new PlaceHistoryHandler(historyMapper);
   }
 
   public ExpensesApp getExpensesApp() {
-    return new ExpensesApp(requestFactory, eventBus, new ExpensesShell(
-        new ExpenseTree(requestFactory), new ExpenseDetails(requestFactory)),
-        placeHistoryHandler, placeController);
+    return new ExpensesApp(activityManager, eventBus, placeController,
+        placeHistoryHandler, requestFactory, new ExpensesShell(expenseTree,
+            expenseList, expenseDetails));
   }
 
+  /**
+   * Exposed for generated {@link ExpensesPlaceHistoryMapper}, which creates a
+   * bookmarkable place in the app for each type of
+   * {@link com.google.gwt.place.shared.PlaceTokenizer} it can find in the
+   * factory.
+   */
   public ReportListPlace.Tokenizer getListTokenizer() {
     return new ReportListPlace.Tokenizer(requestFactory);
   }
 
+  /**
+   * Exposed for generated {@link ExpensesPlaceHistoryMapper}, which creates a
+   * bookmarkable place in the app for each type of
+   * {@link com.google.gwt.place.shared.PlaceTokenizer} it can find in the
+   * factory.
+   */
   public ReportPlace.Tokenizer getReportTokenizer() {
     return new ReportPlace.Tokenizer(getListTokenizer(), requestFactory);
   }
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ExpensesPlaceHistoryMapper.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ExpensesPlaceHistoryMapper.java
index d5d16de..eae962a 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ExpensesPlaceHistoryMapper.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ExpensesPlaceHistoryMapper.java
@@ -16,7 +16,7 @@
 package com.google.gwt.sample.expenses.client.place;
 
 import com.google.gwt.place.shared.PlaceHistoryMapperWithFactory;
-import com.google.gwt.sample.expenses.client.ioc.Factory;
+import com.google.gwt.sample.expenses.client.ioc.ExpensesFactory;
 
 /**
  * This interface is the hub of your application's navigation system. It links
@@ -31,17 +31,17 @@
  * annotation below and list their corresponding
  * {@link com.google.gwt.place.shared.PlaceTokenizer PlaceTokenizer}s. Or if a
  * tokenizer needs more than a default constructor can provide, add a method to
- * the apps {@link Factory}.
+ * the apps {@link ExpensesFactory}.
  * 
  * <p>
  * This code generated object looks to both the {@literal @}WithTokenizers
  * annotation and the factory to infer the types of
  * {@link com.google.gwt.place.Place Place}s your app can navigate to. In this
- * case it will find the {@link Factory#getListTokenizer()} and
- * {@link Factory#getReportTokenizer()} methods, and so be able to handle
+ * case it will find the {@link ExpensesFactory#getListTokenizer()} and
+ * {@link ExpensesFactory#getReportTokenizer()} methods, and so be able to handle
  * {@link ReportListPlace}s and {@link ReportPlace}s.
  */
 // @WithTokenizers({MyNewPlace.Tokenizer, MyOtherNewPlace.Tokenizer})
 public interface ExpensesPlaceHistoryMapper extends
-    PlaceHistoryMapperWithFactory<Factory> {
+    PlaceHistoryMapperWithFactory<ExpensesFactory> {
 }
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ReportListPlace.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ReportListPlace.java
index 0f513c1..b260de2 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ReportListPlace.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ReportListPlace.java
@@ -29,11 +29,13 @@
 public class ReportListPlace extends Place {
 
   /**
-   * Tokenizer.
+   * Tokenizer, which by all rights should have been code generated. Stay tuned.
    */
   @Prefix("l")
   public static class Tokenizer implements PlaceTokenizer<ReportListPlace> {
     static final String SEPARATOR = "!";
+    private static final String NO_ID = "n";
+
     private final RequestFactory requests;
 
     public Tokenizer(RequestFactory requests) {
@@ -43,43 +45,47 @@
     public ReportListPlace getPlace(String token) {
       String bits[] = token.split(SEPARATOR);
 
-      if (bits.length != 3) {
+      if (bits.length != 2) {
         return null;
       }
 
-      String reporterIdToken = bits[0];
-      String search = bits[1];
-      int page = Integer.valueOf(bits[2]);
+      String department = URL.decodePathSegment(bits[0]);
+      String reporterIdToken = bits[1];
 
-      return new ReportListPlace(requests.<EmployeeProxy> getProxyId(reporterIdToken),
-          URL.decodePathSegment(search), page);
+      EntityProxyId<EmployeeProxy> proxyId = NO_ID.equals(reporterIdToken)
+          ? null : requests.<EmployeeProxy> getProxyId(reporterIdToken);
+      return new ReportListPlace(proxyId, department);
     }
 
     public String getToken(ReportListPlace place) {
-      return requests.getHistoryToken(place.getReporterId()) + SEPARATOR
-          + URL.encodePathSegment(place.getSearch());
+      EntityProxyId<EmployeeProxy> id = place.getEmployeeId();
+      String idToken = id == null ? NO_ID : requests.getHistoryToken(id);
+      return URL.encodePathSegment(place.getDepartment()) + SEPARATOR + idToken;
     }
   }
 
-  private final int page;
-  private final EntityProxyId<EmployeeProxy> reporterId;
-  private final String search;
+  public static final ReportListPlace ALL = new ReportListPlace(null, "");
 
-  public ReportListPlace(EntityProxyId<EmployeeProxy> reporter, String search, int page) {
-    this.reporterId = reporter;
-    this.search = search;
-    this.page = page;
+  private final EntityProxyId<EmployeeProxy> employeeId;
+  private final String department;
+
+  public ReportListPlace(EntityProxyId<EmployeeProxy> employeeId,
+      String department) {
+    this.employeeId = employeeId;
+    this.department = department;
   }
 
-  public int getPage() {
-    return page;
-  }
-  
-  public EntityProxyId<EmployeeProxy> getReporterId() {
-    return reporterId;
+  /**
+   * @return the department searched for, or null for none
+   */
+  public String getDepartment() {
+    return department;
   }
 
-  public String getSearch() {
-    return search;
+  /**
+   * @return the employee to focus on, or null for none
+   */
+  public EntityProxyId<EmployeeProxy> getEmployeeId() {
+    return employeeId;
   }
 }
diff --git a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ReportPlace.java b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ReportPlace.java
index a8c42f8..243d7f2 100644
--- a/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ReportPlace.java
+++ b/samples/expenses/src/main/java/com/google/gwt/sample/expenses/client/place/ReportPlace.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.sample.expenses.client.place;
 
+import static com.google.gwt.sample.expenses.client.place.ReportListPlace.Tokenizer.SEPARATOR;
+
 import com.google.gwt.place.shared.Place;
 import com.google.gwt.place.shared.PlaceTokenizer;
 import com.google.gwt.place.shared.Prefix;
@@ -41,30 +43,26 @@
       this.listTokenizer = listTokenizer;
     }
 
-    private static final String SEPARATOR = ReportListPlace.Tokenizer.SEPARATOR
-        + ReportListPlace.Tokenizer.SEPARATOR;
-
     public ReportPlace getPlace(String token) {
-      String[] bits = token.split(SEPARATOR);
-      if (bits.length != 2) {
+      int i = token.indexOf(SEPARATOR);
+      if (i < 0) {
         return null;
       }
 
-      String listPlaceToken = bits[0];
-      String reporterToken = bits[1];
-      
+      String reporterToken = token.substring(0, i);
+      String listPlaceToken = token.substring(i + SEPARATOR.length());
+
       return new ReportPlace(listTokenizer.getPlace(listPlaceToken),
           requests.<ReportProxy> getProxyId(reporterToken));
     }
 
     public String getToken(ReportPlace place) {
-      return listTokenizer.getToken(place.getListPlace()) + SEPARATOR
-          + requests.getHistoryToken(place.getReportId());
+      return requests.getHistoryToken(place.getReportId()) + SEPARATOR
+          + listTokenizer.getToken(place.getListPlace());
     }
   }
 
   private final ReportListPlace listPlace;
-
   private final EntityProxyId<ReportProxy> reportId;
 
   public ReportPlace(ReportListPlace listPlace,
@@ -80,4 +78,35 @@
   public EntityProxyId<ReportProxy> getReportId() {
     return reportId;
   }
+
+  @Override
+  public int hashCode() {
+    final int prime = 31;
+    int result = 1;
+    result = prime * result + ((listPlace == null) ? 0 : listPlace.hashCode());
+    result = prime * result + ((reportId == null) ? 0 : reportId.hashCode());
+    return result;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj)
+      return true;
+    if (obj == null)
+      return false;
+    if (getClass() != obj.getClass())
+      return false;
+    ReportPlace other = (ReportPlace) obj;
+    if (listPlace == null) {
+      if (other.listPlace != null)
+        return false;
+    } else if (!listPlace.equals(other.listPlace))
+      return false;
+    if (reportId == null) {
+      if (other.reportId != null)
+        return false;
+    } else if (!reportId.equals(other.reportId))
+      return false;
+    return true;
+  }
 }
diff --git a/user/src/com/google/gwt/place/shared/PlaceHistoryHandler.java b/user/src/com/google/gwt/place/shared/PlaceHistoryHandler.java
index 94fa17f..51aa52f 100644
--- a/user/src/com/google/gwt/place/shared/PlaceHistoryHandler.java
+++ b/user/src/com/google/gwt/place/shared/PlaceHistoryHandler.java
@@ -25,7 +25,8 @@
 import java.util.logging.Logger;
 
 /**
- * Monitors {@link PlaceChangeEvent}s and {@link com.google.gwt.user.client.History} events and keep them in sync.
+ * Monitors {@link PlaceChangeEvent}s and
+ * {@link com.google.gwt.user.client.History} events and keep them in sync.
  */
 public class PlaceHistoryHandler {
   private static final Logger log = Logger.getLogger(PlaceHistoryHandler.class.getName());
@@ -51,16 +52,29 @@
   /**
    * Optional delegate in charge of History related events. Provides nice
    * isolation for unit testing, and allows pre- or post-processing of tokens.
+   * Methods correspond to the like named methods on {@link History}.
    */
   public interface Historian {
-    // TODO - document
+    /**
+     * Adds a {@link com.google.gwt.event.logical.shared.ValueChangeEvent}
+     * handler to be informed of changes to the browser's history stack.
+     * 
+     * @param handler the handler
+     * @return the registration used to remove this value change handler
+     */
     HandlerRegistration addValueChangeHandler(
         ValueChangeHandler<String> valueChangeHandler);
 
-    // TODO - document
+    /**
+     * @return the current history token.
+     */
     String getToken();
 
-    // TODO - document
+    /**
+     * Adds a new browser history entry. Calling this method will cause
+     * {@link ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)}
+     * to be called as well.
+     */
     void newItem(String token, boolean issueEvent);
   }
 
@@ -73,11 +87,11 @@
   private Place defaultPlace = Place.NOWHERE;
 
   /**
-   * Create a new PlaceHistoryHandler with a {@link DefaultHistorian}.
-   * The DefaultHistorian is created via a call to GWT.create(), so an
-   * alternative default implementation can be provided through
-   * &lt;replace-with&gt; rules in a {@code gwt.xml} file.
-   *
+   * Create a new PlaceHistoryHandler with a {@link DefaultHistorian}. The
+   * DefaultHistorian is created via a call to GWT.create(), so an alternative
+   * default implementation can be provided through &lt;replace-with&gt; rules
+   * in a {@code gwt.xml} file.
+   * 
    * @param mapper a {@link PlaceHistoryMapper} instance
    */
   public PlaceHistoryHandler(PlaceHistoryMapper mapper) {
@@ -86,7 +100,7 @@
 
   /**
    * Create a new PlaceHistoryHandler.
-   *
+   * 
    * @param mapper a {@link PlaceHistoryMapper} instance
    * @param historian a {@link Historian} instance
    */
@@ -95,12 +109,19 @@
     this.historian = historian;
   }
 
-  // TODO - document
+  /**
+   * Handle the current history token. Typically called at application start, to
+   * ensure bookmark launches work.
+   */
   public void handleCurrentHistory() {
     handleHistoryToken(historian.getToken());
   }
 
-  // TODO - document
+  /**
+   * Initialize this place history handler.
+   * 
+   * @return a registration object to de-register the handler
+   */
   public HandlerRegistration register(PlaceController placeController,
       EventBus eventBus, Place defaultPlace) {
     this.placeController = placeController;