by ampatspell
in Code
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:
History.addItem(String)Events which should be turned in history itemsEvents for each History PlaceWhile 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:
Events on place changeEvents which should be turned in History itemsAlso there are few things what Place implementations should not do:
History methods directlyThose 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.
Currently Places is at 1.0-SNAPSHOT state. I’ll be releasing 1.0 soon.
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.
Places and its dependencies:
Now that you have all necessary jars we can move on GWT and GIN configuration.
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.
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):
History token pattern parts which this place managesEvents which should add next History itemEvents 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() overridesits 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.
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:
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:
ShowPersonPlacePersonKeyPlacePersonPlaceBasePlaceEach 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).
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).
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.
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.
Library also contains “Registered Places” UI for visualizing currently registered Place — pattern 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(); } }
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.