Pet Theory header image 2

Building Pet 7: The Command Pattern for History

May 10th, 2007 by matt

When they implement history at all, all-Flash sites tend to offer history-like gestures like a Back/Home/Before arrow in lieu of the actual, click-by-click back- and forward-tracking of a browser.

This degradation of a useful and universally expected feature usually makes navigating these sites awkward, often painful.

So I decided (after preliminary reflections recorded in this post) that my playground site should be equipped with a full-fledged history.

This decision committed me to a systematic approach to navigation, because I suspected that the more systematic the navigation, the more manageable the history...and this suspicion proved justified. Once I had set up a navigation system in which every navigation was handled by the same piece of code and used the same syntax, adding history was trivial.

If you are diagramming an application that includes history, consider this: the more systematic the navigation logic, the simpler the history. And vice-versa: the more piecemeal the navigation logic, the more complex the history must be in order to fit various navigations into a series of history steps.

If you want history for your application, it's up to you to arrive at the sweet spot that allows the most functionality for least effort.

What Browser History Is

You already know what history is, and a hundred times a day you use that knowledge with blithe confidence.

But it might be useful to spell it out.

1) History keeps a record of new locations that the user visits.

freshseries.jpg

2) History keeps track of where the user is currently. Often that means the last new location.

historyindex.jpg

3) Historical navigations either go back to the last location:

backstep.jpg

Or forward to the next location:

forwardstep.jpg

4) If a new location is picked while the user is at any location but the last, all locations forward of the current location are removed, and the history continues from there:

lopped.jpg

If you are old enough to remember in detail the first couple of times you browsed, you might recall being confused by this: you can go back, but some history might have been lopped off.

And that's it, really. History operations are pretty basic. You can google-fetch, read and tinker with a History class, but it'd probably be quicker just to write your own.

Storing Navigation Commands for History

For the browser, a location is a url that points to an html page...but for an all-Flash site, what exactly is a "location"?

For my playground site, the immediate answer would be a module, because my modules are by definition the target of a navigation.

But remember that my modules are not associated with content in a predictable manner. BaseModule and BaseNavigable don't even keep a reference to content. And this isn't a peculiarity of my all-Flash architecture; if you have unique content associated predictably with a standarized container like a html page, you probably aren't--or shouldn't be--doing an all-Flash site.

Plan B for defining the units of history is to refer to the navigation rather than the content that the navigation delivers.

In my case, this was attractive because my navigational structure was a tree of modules linked by each module's internal navigables (strategies that manage navigations, kept references to children, currently selected, etc.). Not all modules had children of their own, but all modules were the children of parent modules, and all navigations had the syntax "parent module goes to child module."

Actionscript:
  1. //every navigation looks something like this
  2. aModule.navigable.navigate(aChildIndex);
  3. //modules implement the navigate method diversely

Having History store complete navigation statements rather than indices was especially attractive, because now, back and forth history navigations were simply a matter of replaying the code:

commandversusindex.jpg

In a navigation-based history, the units of history become "this module selecting this child module" rather than "this location."

So how to store and replay statement? In the course of sequencing animations or transitions, I had accomplished similar tasks before...but that code was hard to adapt and gratuitously involving. Only after I learned the Command pattern was I able to mass produce wooden crates for History storage.

A Command object is simple. It holds two data: a function, and that function's parameters; it implements a single method, execute(), which re-couples the data: myFunction(myParameters).

Let's follow the birth and life of a navigation command:

In the navigable strategy, after a fresh navigation, the navigate method terminates by calling this method:

Actionscript:
  1. public function dispatchNavigation(aIndex:uint, historical:Boolean=false, isDirect:Boolean=false, browserUpdate:Boolean=true):void
  2. {
  3.     //selection info advertised
  4.     dispatchEvent(new SelectModuleEvent(children[aIndex], historical, isDirect, browserUpdate));
  5.            
  6.     if (historical==false)
  7.     {   
  8.         //the command is created, and sent out in a command event
  9.         //the command will be stored in History
  10.         dispatchEvent (new CommandEvent (new NavCommand(mod, index)));
  11.     }
  12. }

NavCommand follows the classic pattern of a command, with a few twists:

Actionscript:
  1. public class NavCommand implements ICommand
  2. {
  3.     public var mod:IModule;
  4.     public var index:uint;
  5.     public function NavCommand(aModule:IModule, aIndex:uint)
  6.     {
  7.         mod=aModule;
  8.         index=aIndex;
  9.     }
  10.     public function execute(isDirect:Boolean):void
  11.     {
  12.         mod.navigable.navigateHistory(index, isDirect);
  13.     }   
  14. }

The Mediator object listens for both events dispatched in dispatchNavigation. When it hears SelectModuleEvent, it does whatever is appropriate for that selection (loads, wakes, gets controls, etc). When it hears the CommandEvent, it relays the event to History for storage:

Actionscript:
  1. private function onFreshNav(e:CommandEvent):void
  2. {
  3.     history.addToHistory(e.command);
  4. }

Now suppose that, after another fresh navigation, there is a backward history step:

Actionscript:
  1. //in the Mediator
  2. history.historyStep(false, true);
  3. //in History
  4. public function historyStep(isForward:Boolean=true, isDirect:Boolean=false):void
  5. {
  6.     var command:ICommand=getNext(isForward);
  7.     (isForward)?historyIndex++:historyIndex--;
  8.     lastStepForward=isForward;
  9.     sendHistoryUpdate();
  10.     command.execute(isDirect);
  11. }

Notice that the command takes an isDirect parameter. That's because all my navigations can be non-animated, depending on context. (For instance, is the navigation part of a deep link?)

I resisted parameterizing the execute call at first. When I started using the History class, I was dead-set on keeping both History and its stored commands generic, so that any method calls, not just navigations, could be frozen and unfrozen. I had the wild notion that whatever "undo" functionality there was on the site could be subsumed by the back button. (Photoshop with a back button!)

With work, I could have preserved the decoupling of completely encapsulated commands and the history that stores them in a queue and invokes execute() on them. But given both the design complications (will users easily accept the continuum of "back" and "undo")? and the code complications (how would the NavCommands acquired contextual info for executions?), I abandoned this scheme.

Quirks of Navigation-Based History

Even if your navigation logic is distinct from mine, if your history is command- and navigation-based, you'll confront these quirks:

Home needs to be selected first. An HTML site's first location is the home page. But if your command-history home page comes up, nothing gets added to history because there have been no navigations. Once the first navigation occurs, it will be added...and the user will always be able to get back to THAT page, but not to the all-important home page. Exiled to the peripheries!

My solution was to make my application class extend the BaseModule class, and make the home module its only child. When the application initializes, the application navigates to home, and this navigation is the first pushed into the history. Your details will be different...but no doubt your solution will include the same virtual parent and dummy start-up navigation.

virtualparent.jpg

Navigating to a module must produce the same result regardless of the previous navigation. When a selected module replaces a sibling model, it usually won't matter if that selection is being done by a fresh selection, or a history back or forward navigation. In a picture series, for instance, pictures will simply replace each other.

This equivalence doesn't always hold, however. Take, for example, a picture-series module that opens on an intro paragraph and a next button that the user can press to cycle through a series of picture modules.

When the user hits the back button and tracks back through the picture modules, the paragraph will come up...but will current picture disappear? It should. Whatever code runs when the picture-series itself gets selected has take account of the fact that the last selected module might be a picture-module child, not its parent module. In this case, the code would check to see if any pictures are showing, and if so, get rid of them.

sameresult.jpg

In truth, this is the kind of laborious detail that would be lost among a welter of laborious details if you were dealing with navigations on an ad hoc basis. The problem only appears systematic once you've constructed a systematic navigation. For me, the next step was to add a method to the INavigable interface, setStart, which assigns a callback so the module can react to being selected:

Actionscript:
  1. //in BaseNavigable
  2. public function setStart(aFunction:Function):void
  3. {
  4.     assignedStart=aFunction;
  5. }
  6. //in Mediator, with a new selected module
  7. selectedModule.navigable.start(selectedEvent);

This function takes care of selection-specific logic (like zapping lingering pictures) rather than waking logic, which in any case does not always mirror the selection logic (the parent picture-series module stays "awake" even after its child picture modules steal selection).

Default selections must go under the radar. In practice, pictures series that commence with paragraphs will be rare. Nearly all will open on a first, default picture. So, for instance, the picture-series module might assign this callback for starting:

Actionscript:
  1. _navigable.setStart(gotoFirst);
  2.  
  3. protected function gotoFirst(e:SelecteModuleEvent):void
  4. {
  5.     _navigable.navigate(0);
  6. }

That works fine when the user is stepping forward. But what happens when the user hits the back button?

Two different navigations have been pushed into history:

Actionscript:
  1. //in the parent module of the picture series module
  2. _navigable.navigate(pictureSeriesIndex);
  3. //in the picture series module, in the start callback
  4. _navigable.navigate(0);

So while it took the user ONE click to reach both the picture series module and the default picture, it will take TWO clicks to get back to where they started...weird for the user.

One solution would be to treat the default picture as a picture apart...place it via a separate function...or embed it below the picture holder sprite?

But I knew that the "default selection" problem would require an ever-growing supply of hacks, so I girded up my loins and hyperspaced for a couple of seconds before the blatantly obvious solution materialized: Don't add the navigation to History. With my navigation logic, that meant hiding the navigation call inside a history navigation (history navigation aren't added to history, only fresh navigations are):

Actionscript:
  1. protected function gotoFirst(e:SelecteModuleEvent):void
  2. {
  3.     _navigable.navigateHistory(0)
  4. }

Notice that, according to History, picture module 0 never existed. And yet, every time the parent picture-series module is selected, it appears. This works whether the picture-series module has been selected for the first time, or is being revisited:

defaultselection.jpg

Tags: patterns · code1 Comment

Leave A Comment

1 response so far ↓