»
February 01, 2010
»

GWT History Management With Places

This is a second post in two post series about History Management in GWT. In first part I introduced the definition of Place and few necessary “design patterns” which makes it easy to implement history aware rich applications in GWT. The only missing piece of puzzle I left till the next time was cleaning up the implementation of SimpleHistoryManager.

In first part the SimpleHistoryManager is the central compnent in whole application History architecture. It:

  • calls History.addItem(String)
  • handles history token value changes
  • handles application Events which should be turned in history items
  • fires application Events for each History Place

While for small applications this central history manager class may suffice, it is not flexible enough and it only grows in size as application functionalty grows. This “doesn’t scale”. We need to split HistoryManager in logical parts that can be easily managed independently. For history management, the smallest part is Place class and for each application place, Place implementation should:

  • fire application Events on place change
  • handle application Events which should be turned in History items

Also there are few things what Place implementations should not do:

  • parse, compare string tokens
  • create new string tokens
  • call History methods directly

Those tasks can be perfectly be abstracted away in Place library that can be reused (and improved separately) between projects.

It happens I’ve written Places library and the following will explain how to setup and use its public API. If you’re currious about implementation details, feel free to dig in to the sources.

Downloading

Currently Places is at 1.0-SNAPSHOT state. I’ll be releasing 1.0 soon.

Using Maven

If you’re using Maven, add the following repository to your pom (or better in your settings.xml / Nexus):

<repositories>
  <repository>
    <id>amateurinmotion</id>
    <name>amateurinmotion m2 repository</name>
    <url>http://www.amateurinmotion.com/repository</url>
  </repository>
</repositories>

And add dependencies:

<dependency>
  <groupId>com.ampatspell.places</groupId>
  <artifactId>places-client</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
  <groupId>com.ampatspell.places</groupId>
  <artifactId>places-client</artifactId>
  <version>1.0-SNAPSHOT</version>
  <classifier>sources</classifier>
  <scope>compile</scope>
</dependency>

Warning: Places library declares few dependencies which are not in central Maven repository like GIN, GWT, Guice version which works in AppEngine. They reside in my Maven repository and will be included in compile classpath. If it is not desired, maven dependency exclusions are your friends.

Manually

Places and its dependencies:

Now that you have all necessary jars we can move on GWT and GIN configuration.

Setup

Add the inherits to gwt.xml:

<module>
  <inherits name="com.ampatspell.bones.core.Core"/>
  <inherits name="com.ampatspell.places.Places"/>
</module>

Install PlacesModule in your GIN Module:

public class ExampleGinModule extends AbstractGinModule {

  @Override
  protected void configure() {
    // Bones (optional)
    install(new BonesCoreGinModule());
    bind(DispatchServiceAsync.class).to(DispatchServiceAsyncImpl.class);
    
    // Places Service
    install(new PlaceServiceModule());
    bind(PlacesLogger.class).to(GWTPlacesLogger.class);
    bind(EventBus.class).to(BaseEventBus.class); // or your EventBus implementation
  }
  
  @Provides
  @Singleton
  public List<Place> providePlaces() {
    List<Place> places = new ArrayList<Place>();
    return places;
  }  

}

If you don’t want to bring all Bones functionality (post about Bones library comming soon), you may install only PlacesModule and bind EventBus.class to your HandlerManager implementation that poses as EventBus in your application. No other dependencies exist.

Also, as we can see, there’s binding to empty List of Place instances bound. There we should list all application places.

Place

Lets start with Place interface and talk about what is Place scope is and what implementations should do:

public interface Place {

  /**
   * History token url sub-pattern for this {@link Place}.
   *
   * @return string pattern for {@link Place}. May be blank or null
   */
  String getPattern();

  /**
   * Returns if {@link Place#getPattern()} overrides it's parent pattern.
   *
   * @return <code>true</code> if pattern overrides parent, <code>false</code> otherwise
   */
  boolean overridesParent();

  /**
   * @return parent {@link Place} or null
   */
  Place getParent();

  /**
   * Parameters which will be added to token
   *
   * @return additional parameter names or null
   */
  String[] getAdditionalParameters();

  /**
   * Called before first place is invoked
   *
   * @param binding binding context for moving to next {@link Place} and 
   *                for registering event handlers
   */
  void bind(PlaceBindingContext binding);

  void unbind();

  /**
   * Called when History token matches given {@link Place} ir one of the 
   * child {@link Place}s.
   * <p/>
   * Note: {@code invocation.invoke()} <b>must</b> be called in each invoke call
   *
   * @param invocation context for getting current parameters (from pattern or from 
   *                   additional parameters) and continuing invocation chain
   */
  void invoke(PlaceInvocationContext invocation);

}

Place implementation does three things (three scopes):

  • Matching — provides information about History token pattern parts which this place manages
  • Binding — handles application Events which should add next History item
  • Invocation — fires application Events to restore place
Scope Method group Description
Matching getPattern() Relative History token pattern string
(may be blank or null)
Matching overridesParent() Whether pattern returned by getPattern() overrides
its parent pattern
Matching getParent() Parent Place (may be null)
Binding bind(PlaceBindingContext) Callback method for adding application Event handlers that should be
turned into new History items
Binding unbind() Invoked on HistoryService unbind. Nearly always useless
Invocation invoke(PlaceInvocationContext) Called when new history token matches this Place pattern
Binding & Invocation getAdditionalParameters() Additional parameter names of which values will be stored
after main token as name=value pairs (a la url parameters)
and should be added for consequent invocations

And my apologies for over-complicated explanation. As we’ll see later, this thing is easy to use.

Matching

Token matching is done using pattern and parent. A pattern can be composed by constant string parts and parameters using {name} syntax. If Place declares:

Pattern Parent Overrides parent
/person/show/{key} null false

The place will match:

Token Matches Key value
/person/show/zeeba true “zeeba”
/person/show/larry true “larry”
/person/show/ false
/person/show/zeeba/ false1

1 — the default parameter matcher uses the /^([a-zA-Z0-9\-_]+)(.*)$/ regex to find parameter value range in string. Let me know if it is too limiting.

When Place implementation returns other place as its parent things start to get more interesting. Those are all Place patterns used in demo application:

Place Pattern Parent Overrides parent
DefaultRedirectPlace("/index") null null false
IndexPlace /index SectionPlace true
PeoplePlace /people SectionPlace true
ShowPersonPlace /show PersonKeyPlace false
EditPersonPlace /edit PersonKeyPlace false
NewPersonPlace /new PersonPlace false
Parents
BasePlace null null false
SectionPlace /{section} BasePlace false
PersonPlace /person BasePlace false
PersonKeyPlace /{key} PersonPlace false

The resuling “full” place patterns are:

places.png

By the way, this UI is included in library. See “Registered Places” section below.

Where for example /person/zeeba/show will match ShowPersonPlace that has the following full parent-child hierarchy:

  • ShowPersonPlace
  • PersonKeyPlace
  • PersonPlace
  • BasePlace

Each of parent places takes a part in all three Place scopes: matching, binding and invocation. It makes sense because this way place binding and invocation can be greatly simplified for end-level Place implementations. Multiple Places can share common parent that can also be bound as @Singleton.

Also while all shown patterns are “one-slash with string or parameter”, its perfectly fine to have pattern with more than one parameter with some delimiter (“/” or something else) between (for example /list/{all}/{page} with PeoplePlace as parent and few additional parameters will work just fine).

Invocation

When Place matches current History token, invoke(PlaceInvocationContext) callbacks are invoked for whole this Place parent-child hierarchy. Invocations are starting with root parent.

For ShowPersonPlace invocation chain contents are:

// BasePlace
String title = invocation.get("title", "Places Example");
invocation.fireEvent(new SetTitleEvent(title));
invocation.proceed();

// PersonPlace
invocation.fireEvent(new WorkspaceSetContentEvent(people, new Command() {
  public void execute() {
    invocation.fireEvent(new MenuSetActiveEvent("people"));
    invocation.proceed();
  }
}));

// PersonKeyPlace
String key = invocation.get("key");
invocation.fireEvent(new ListSetActiveEvent(key));
invocation.proceed();

// ShowPersonPlace
final String key = invocation.get("key");
invocation.fireEvent(new PeopleSetContentEvent(show, new Command() {
  public void execute() {
    invocation.fireEvent(new PersonShowEvent(key));
  }
}));

PlaceInvocationContext (source) has the proceed() method which needs more clarification.

As we’ve seen in post’s first part, Place setup Events must not be fired one after another in one “batch”, instead next Event should only be fired after previous event is handled and presenter hierarchy is able to handle Events (see On-Ready Event Callback).

For this to work in Place parent-child hierarchy PlaceService needs to be notified when next Place.invoke(PlaceInvocationContext) can be safely called. This is done using proceed() method.

So while BasePlace invokes proceed() immediately, PersonPlace invokes it only after PeoplePresenter is bound and ready to receive events because otherwise ListSetActiveEvent won’t be handled.

Other PlaceInvocationMethods should be obvious (see javadocs for interface for more info).

Binding

Place.bind(PlaceBindingContext) callback is used to register application specific Event handlers for events which should be turned into the places. This is done using provided PlaceBindingContext which allows to register to EventBus and takes care of removing handlers on PlacesService unbind. Also PlaceBindingContext (sources) declares StateBuilder state() method which allows in declared event handlers perform transition to next History place (add new History item). StateBuilder (sources) allows to set values for declared pattern parameters and also parameters that are added after “?” as key=value pairs).

One of in ShowPersonPlace.onBind declared event handlers is:

binding.addHandler(PersonShowPlaceEvent.getType(), new PersonShowPlaceHandler() {
  public void onPersonShowPlace(PersonShowPlaceEvent event) {
    binding.state().set("key", event.getKey()).invoke();
  }
});

So when ShowPersonPlaceEvent is fired, this is handled here and History.newItem is called with ShowPersonPlace full history token and event provided person key.

/person/{key}/show
/person/zeeba/show

At some later time StateBuilder will have invokeCurrent() method which will allow to stay in “current” place and only change some additional parameters.

Additional Parameters

Sometimes it can be useful to declare some parameters as “persistent” between place invocations. grid=true comes in mind as a decent example. If all Place implementations share directly or indirectly single BasePlace parent with grid as a additional parameter, the grid value will be added for each and every place invocation and the same BasePlace can fire SetGridVisible event in invoke to show the grid (useful for laying out UI components).

Demo application has title declared as additional parameter in BasePlace and in invoke it fires SetTitleEvent:

@Singleton
public class BasePlace extends AbstractPlace {

  @Inject
  protected BasePlace() {
    addAdditionalParameter("title");
  }

  @Override
  public void onBind(final PlaceBindingContext binding) {
    binding.addHandler(SetTitlePlaceEvent.getType(), new SetTitlePlaceHandler() {
      public void onSetTitlePlace(SetTitlePlaceEvent event) {
        binding.state().set("title", event.getTitle()).invoke();
      }
    });
  }

  @Override
  public void onInvoke(PlaceInvocationContext invocation) {
    String title = invocation.get("title", "Places Example");
    invocation.fireEvent(new SetTitleEvent(title));
    invocation.proceed();
  }

}

To see the functionality in action, open demo application with ‘Additional Parameters’ as title and click on few links. The result is that the string “Additional Parameters” follows the place changes without explicit set in each binding().state() call.

Registered Places

Library also contains “Registered Places” UI for visualizing currently registered Placepattern bindings in application. Basic usage:

import com.ampatspell.places.client.api.PlacesService;
import com.ampatspell.places.client.ui.places.PlacesPresenter;

public class Example implements EntryPoint {

  public void onModuleLoad() {
    ExampleGinjector injector = GWT.create(ExampleGinjector.class);

    PlacesService placeService = injector.getPlacesService();
    placeService.bind();

    PlacesPresenter placesPresenter = injector.getPlacesPresenter();
    placesPresenter.bind();
    RootPanel.get().add(placesPresenter.getView().getViewWidget());
    placesPresenter.show();
  }

}

EOF

I hope this will help someone at least a bit to understand this approach of GWT History management in general and use of PlacesService. I’ve spent a lot of time trying to come up with some simple and useful history management guidelines for my projects and this is best approach I’m using with good results.

One more time few links:

Questions? Feel free to mail me directly to email address which can be found in about section.

 
Internet Explorer 6
Are you serious?