History in GWT

From Zanecorpwiki

Jump to: navigation, search

WIP

If you Google "gwt histroy" (as of 2010-04-07), you'll find plenty of articles that walk you through the "basics" on how to build support the forward and back buttons as well as page reloads and bookmarks in a GWT application. However, what these articles cover is really just the very basic technical building blocks. They uniformly fail to address even the basic design and conceptual questions inherent in the task.

Contents

Conceptual Break Down

Interface Taxonomy and History

As far as history goes, we can divide interface into broad groups. I'll call the first "wizards". These are characterized by sequential page-like views. Wizards are heavily associated with information gathering.

The second group is "palettes". Palettes are characterized by the presence a set of tools which may in general be accessed at any time. Think PhotoShop, OpenOffice, etc.

History shows up in wizards in the same way it does for your web browser. User's expect to step back and forth between views, reload the view from a certain point, etc. In the palettes, history provides the undo/redo functionality.

These are fundamentally different activities, however. In the case of wizards, history means a remembering the sequence of user views. In a palette, history is a history of the state of the data.

The two ideas can be combined in which case history tracks the state of both the application and the design. E.g.: "open expense report" - "open ledger" - "change transaction" - "add transaction". The combined approach is typical for a complex palette interface with many sub-palette and view tools. Palette interfaces can embed wizards, and vice versa.

The app you're dealing with and the kind of history you want need to be carefully considered. For instance, a loan application is probably a pure wizard and history should deal with views views. One wouldn't expect to undo the second page of entries when I browse back to the first. On the other hand, stepping back through undos in a multi-palette, multi-view application can be very confusing if application state (recording the view utilized at the time a change was made) is not included in the undo history.

Tracking History

This is probably the most challenging part of enabling history. You need a way to reconstruct the relevant state at any (recorded) point in time. For views/application state, this generally involves recalling the view in effect at a given point in time.

"A view" means both a data set and a rendering protocol. In some applications, it will only be necessary to track one or the other other. E.g., a simple address book may have only one rendering protocol: the address pages. History could be a simple list recording sequence of the selected data sets. In this case, Tom, Bob, Sue, Mary, Tom, etc. Some applications deal with a single data set, but provide multiple renderings--like our imagined loan application which spreads a single form across multiple pages.

When talking about undo/redo history stacks, what we track is essentially the state of the data set itself through time. This is usually done by tracking a diff history, but the effect is the same.

The history itself is always a sequence of some sort. The question is "what are the contents of the history?"

Side Effects

You can't talk about generic history without considering side effects. If I'm browsing Amazon, using the back button to pull up that thing I was looking at three page views ago is perfectly legitimate. Amazon, as an application, should respond by recreating the view I had of that page, essentially behaving the same as if I'd pulled up the page directly. However, if I back into a completed transaction, the Amazon app should respond by telling me the transaction has already been completed and letting me know I can't go back to it.

The difference is that the transaction pages have a side effect. They cause money to be transferred and books to be ordered. Browsing is side effect free. In general, actions with side effects cannot be recreated and require special handling. Side effect free actions generally can and should be recreated just the same every time.

Implementation

Basics

GWT 2.0 provides the History class aid in implementing history. The three key methods in the class are:

History.addValueChangeHandler(ValueChangeHandler handler); // registers a history event listener
History.newItem(Strink token); // adds a new item and fires a history change event
History.newItem(String token, boolean fire); // adds a new item and optionally fires a change event

Your general setup can look something like this:

public class MyEntryPoint implements EntryPoint, ValueChangeHandler<String> {
  public void onModuleLoad() {
    History.registerValueChangeHandler(this);
    // app may enter at any point the history stack (reload, bookmark) so we use the
    // history framework to determine our initial state
    History.fireCurrentHistoryState();
  }

  public void handleHistorEvent(String token) {
    ... decipher token to determine requested history state and dispatch ...
  }

  // the methods that actually do stuff are private, forcing users of this class to
  // always go through the history mechanisms
  private void handleAction1() {...}

  private void handleView1() {...}

  /**
   * See #handleHistoryEvent. A necesasry alias for the more descriptively named method.
   */
  @Override
  public void onValueChange(final ValueChangeEvent<String> event) {
    handleHistoryState(event.getValue());
  }
}

There are a few other details to enabling history. Google "gwt history" to find a quick example of a history enabled toy app for the basic setup.

History is Not Just the Past

"History is now" may sound trite, but it's true. Let's say you have your entry point class that sets up the application and basic interface. For any non-trivial application, there are going to be sub-interfaces, commands, etc. that are implemented in other classes.

In the above example, the entry point class is acting as a controller (a la, model-view-controller)--a history controller. History events all get routed through the handleHistoryEvent method which then analyzes the history token (and perhaps other state) and dispatches the request to a private handler (which will often call an associated class to draw a specific interface or take a specific action). Both the user interface and our own logic (e.g. to open a second view from a first view) are all routed through the history controller.

This has the advantage of automatically capturing all events in history. It might look something like this:

public class Menu ... {
  ...

  @UIHandler("view1")
  public void openView1(ClickEvent event) {
    ... do UI stuff ...
    History.newItem("view1:" + sequence);
  }

  @UIHandler("command1")
  public void doCommand1(CilckEvent event) {
    ... do UI stuff ...
    History.newItem("command1:" + sequence);
  }
  ...
}

Hopefully this is pretty straightforward. The potentially strange bit is that you're using history to capture current actions, not just recall actions. Think of "newItem" as both registration and initiation for actions.

The alternative would be just call the actions directly and make the actions responsible for updating the history sequence (with Historty.newItem(token, false)). This might seem more straightforward at first but is relatively fragile and error prone.

In either case, some part of the code has to determine whether the action is an actual historical item or a new action. In some cases, as when pulling up a particular page, it won't matter and that question can be skipped. In other cases, as when issuing an action with a side-effect, the question is vital.

It may be useful to mix the styles in specific cases, but in general, the history dispatcher method should be used whenever possible.

Sequence Markers

In the above example, the history dispatcher is called with two bits of information: the requested action, and the sequence key. The idea behind the action is clear enough: it names the thing to be done.

The sequence key tells us where in the history stream we are. The tokens passed into the newItem method are the keys into the stream. Without the sequence, we might end up with something like: "command1" - "view1" - "command2" - "view1". If The user backs up and we're presented with a "view1" token, we don't know which one it is. That's why need something like "command1-1" - "view1-2" - "command2-3" - "view1-4".

Tokens and Tracking State

This is where most of the legwork and knowledge of your specific data and application get used. The goal is straightforward: associate history tokens with a point in history. For view applications, this is conceptually straightforward, and in the simple case--where there's no expectation or desire to track data changes--you don't even need a sequence. As far as the history is concerned, each view is the same and your history is nothing more than a list of last commands called, so a one-to-one mapping between the token strings and commands is sufficient.

There's often more to it. For instance, if the application involves filling out the same form multiple times--like an address book--you need to remember not only the specific view, but the key data associated with the last look at the view. Again, you're tokens may be sufficient, we just need to add sequence data which, in this example, is the key field reference like, "view-dave" - "view-mary" - "search-xyz".

In the above example, 'dave' and and 'mary' tell us what data to load for the view. This would be the key field to the address entries and, in practice we'd expect to see ID numbers, but it's the same idea.

The "xyz" key in the search reference could either tell us the entire contents of the search (in which case it would probably be something like "search-name=foo&group=bar") or it could be a key into a data structure that tracks past searches. The hash key of the search string for instance.

For full state change tracking, the token tells us where to scroll to in a diff history. E.g.: "view-dave" - "view-mary" - "search-xyz" - "view-bob" - "edit-xzy". Stepping back one step would undo our edits. Stepping back another two would return us to the search results.

Again, the "xyz" in edit could either be an encoding of the actual work done, so the token itself tells us how to undo/redo the work, or it can be a reference into a side data set that tracks the changes. In some cases, it may make sense to maintain running snapshots.

Sessions and Reloading

There is one final issue to deal with, and we will have covered more or less all the key topics. The first step to enable reload support is to enable persistent history. This means routing the history stream through or communicating it to the from the client to server. This can be done as it happens, or from time time.

Second, we'll need a session key. In order to support multiple history streams, occurring simultaneously or at different points in time, you need the idea of a session. If your application is entirely client side and the idea of a "session" short lived (i.e., can never survive closing the browser), sessions aren't necessary.

However, if you want to support long lived or multiple sessions, you'll need to associate each history stream with a session key. The session key allows the server to differentiate multiple history streams for the same user (or in the absence of users). For instance, it keeps a separate undo stack for each document, allowing the user to edit multiple documents at the same time.

Theoretically there are two ways to handle the session key: by encoding it with the history token or as a query parameter. I haven't tried it myself, but there's chatter on the net regarding problems with encoding the session key as a query parameter, so we'll take the former route.

The token might look something like sessionKey + ":" + action + ":" + sequenceKey. Note that the URLs (sans-anchor) still have one-to-one mappings.

The challenge here is that if we generate the session key server side as part of a JSP page that loads the module, then we can't access the anchor because it's not sent to the server. So, we have to inject a little JavaScript into the page itself:

<script type="text/javascript">
var sessionKey = unescape(location.hash.substring(1));
if (sessionKey != null && sessionKey.length > 0) {
	sessionKey = sessionKey.replace(/:.+/, '');
	document.getElementById('sessionKey').value = sessionKey;
	document.getElementById('reload').value = 'true';
}
</script>

The full sequence of events goes like this:

  • server presents page with embedded GWT based purely on URL
  • in-page script updates the session key as necessary
  • GWT/JavaScript client loads
  • GWT client pulls session key from page, which either creates a new session or ties to previous session
  • GWT client examines anchor to determine history position
  • session key is passed asynchronously to server to with position data to retrieve the persisted information from the stream

Similar techniques allow for bookmarking. It's just a question of storing the stream long enough.

Adding History

If you think you might use history, be sure and at least consider history when building the application. Rehabbing an existing application can is often much trickier than just building the app with history in the first place.

A well designed app with history support is necessarily more complex than a similar, but strictly linear app because the history version is more flexible. That flexibility comes at a cost and means that if you don't build in history at the outset, you'll likely and very reasonably make assumptions to simplify the application. Why support jumping around in the flow if you don't need to?

If you do find yourself rehabbing an existing wizard-style application, consider just rewriting the flow control code (if it's well isolated) rather than trying to add history support into the existing flow. The flows will be fundamentally different and it's very likely easier just to start over. On the tactical level, this means redoing the initialization code and all the code associated to controls which move the user from one view to another.

Hopefully you can start at the beginning and add support for back/forward to each page in the sequence individually, testing them as you go. Forms that were always empty before need to be updated to load existing data and modify previously entered data.

Adding undo/redo to an existing application works in the same way. The key here is to eventually drive all changes through the dispatch method. In most well-built, modern apps, this should be a case of finding all the call sites for a particular action, replacing them with a call to the dispatcher with an appropriately constructed token and is something that could be done incrementally, adding undo/redo support to one action at a time.

In either case, the most frustrating and time consuming part will probably be locating and weeding out assumptions regarding the original linearity. These are often explicit and not necessarily obvious, even when the code is well understood.

Personal tools