Sunday, April 24, 2011

History with Mvp4g

Even if you develop an ajax application with GWT (aka one page so one url application), you can still support browser history to improve the user's experience. GWT provides an easy mechanism that lets you store a token in url to simulate url changes. This way your user can navigate through your site thanks to the browser back and next arrows or bookmark different parts of your website. If you're not familiar with this mechanism, you can read the GWT documentation.

Setting up your history is probably the most important decision you will make for your application. You will need to decide when to save your application state and what to save to retrieve this state.

Mvp4g browser history support is based on a place service that can convert any event to a token and can then store it in the url (via the GWT History class). This place service will also listen to any url token changes and convert the token back to an event. Thus you can save your application state whenever you fire an event.

To convert your event to/from a token, the place service uses HistoryConverter. The history converter will be in charge of:
  • saving the necessary information to retrieve the application state when converting an event to a token,
  • retrieve this information and fire event(s) to set the application to the expected state.

In this tutorial, we'll see how to create and set history converter to manage browser history. I will also go over some advanced techniques to reduce the boilerplate code when writing history converters for multiple events.

What to bookmark?


We're going to reuse the example created for the first post. When looking at it, we can define 2 places where the user can be: displaying page1 screen or displaying page2 screen. We go to these places thanks to 2 events, goToPage1 and goToPage2. In the previous article I categorized these events as place events because their role is similar to the concept of Place introduced in GWT 2.1.

Thus we need to store these 2 events in our application. When storing these events, we also need to store the application state. In our case, the state is pretty simple, it's just the string fire with these events.

You can download the code of the example here (Project: Mvp4g_History).

Your first history converter


Creating your history converter

When creating a history converter, you need to implement the HistoryConverter interface and define the following methods:
  • convertFromToken: this method parses the parameters retrieved from the url and fires the appropriate event,
  • handling method of each event it has to convert. In this case, instead of returning void, it returns the event parameters string representation. This string will be added to the token,
  • isCrawable: if this method returns true, Mvp4g adds a “!” in front of the token so that it can be crawled by a search engine.

Now we'll create our history converter for page1.

The first step is to create our Page1HistoryConverter class.
@History
public class Page1HistoryConverter implements HistoryConverter<HistoryEventBus> {

    @Override
    public boolean isCrawlable() {
        return false;
    }

    ...
}
The class has to be annotated with @History. This annotation helps Mvp4g detect the history converter and allows you to set extra parameters (we'll go over them later in this post).
isCrawable method returns false as we don't need the generated token to be crawled by search engine.

The second step is to define the event handling method in the history converter to return the parameters string representation.

But first let's take a closer look at the token generated by Mvp4g. It can be divided in three:
  • event name: by default it's the event method name but you can override this value using the attribute 'name' of @Event.
  • separator: a character to separate the event name from the parameters string representation. By default, it's “?” but it can be overridden.
  • parameters string representation: this is the value returned by the event handling method of your history converter.

For our goToPage1 event, the string representation of the parameters is the parameter itself since we only have one parameter and it's already a string. Our handling method will be quite simple:
public String onGoToPage1(String name){
    return name;
}

The last step is to implement the convertFromToken method. This method has two goals:
  • retrieve information needed to set the application in the expected state. This information can be retrieved from the token, a cookie or the server.
  • fire event(s) to set the expected application state.
Why is the convertFromToken method in charge of firing the event instead of having Mvp4g do it automatically? There are two reasons for this. First, the process to retrieve application state information can be asynchronous if you have to call the server. Then, you may want to fire a different event than the one stored in the token. For example, the event stored in a token requires the user to be logged in. In your history converter, you could either fire one event if the user is logged in or another event to go to the login page if not.

If we take a look at the convertFromToken parameters, we can see that Mvp4g starts parsing the token for you by separating the event name from the parameters string representation. In our example, the converterFromToken method will be simple. The parameters string representation won't have to be parsed since we only have one string parameter for the event. So all we have to do is fire the goToPage1 event with the parameter stored in the token.
public void convertFromToken( String name, String param, HistoryEventBus eventBus ) {
    eventBus.goToPage1(param);
}

Associating your history converter

Now that we have created our history converter, we need to associate it with an event. This is done thanks to the attribute 'historyConverter' of the @Event that annotates your event method:
@Events(...)
public interface HistoryEventBus extends EventBus {
    ...
    @Event( handlers = Page1Presenter.class, historyConverter = PageHistoryConverter.class, name = "page1" )
    void goToPage1( String name );
    ...
}
We also set a name for our event using the attribute 'name'. Instead of having 'goToPage1' in the url, we'll display 'page1'.

Setting the init event

If you try to start your application now, you will get this error message:
You must define a History init event if you use history converters.
Whenever you set history in your application, you need to set up an init event. The init event is the event fired in case the token is empty or invalid.

Before we had the RootPresenter fire the goToPage1 event when the application started, now we want this action to happen only if the token is empty, otherwise the token stored in the history will determine the page to go to. So instead of having the RootPresenter handling the start event, it will handle a new event, the init event.
@Events(...)
public interface HistoryEventBus extends EventBusWithLookup{
    ...
    @InitHistory
    @Event( handlers = RootPresenter.class )
    void init();
    ...
}
@Presenter( view = RootView.class )
public class RootPresenter extends BasePresenter<IRootView, HistoryEventBus> implements IRootPresenter {
    ...
    public void onInit() {
        eventBus.goToPage1( "The application started." );
    }
    ...
}
We can notice that we annotated our init event with @InitHistory. This tells Mvp4g to use this event in case the token is empty or incorrect.

For more information on the init event, you can read this documentation.

History on start

If you start your application now, instead of having "The application started.", no message is displayed.

This occurs because when your application starts, it won't automatically handle the current token stored in the url. Even if you have no token, the init event won't be fired.

To tell Mvp4g to handle the current token stored in the url, you need to set the 'historyOnStart' attribute of @Events to true.
@Events( ..., historyOnStart = true )
public interface HistoryEventBus extends EventBus {
    ...
}

Setting history for goToPage2


Now that we set history for goToPage1, we need to set it for goToPage2. We could repeat the steps described in the previous section and create a history converter for goToPage2:
@History
public class Page2HistoryConverter implements HistoryConverter<HistoryEventBus> {

    public String onGoToPage2( String name ) {
        return name;
    }

    @Override
    public void convertFromToken( String historyName, String param, HistoryEventBus eventBus ) {
        eventBus.goToPage2( param );
    }

    @Override
    public boolean isCrawlable() {
        return false;
    }

}
In this example, it's ok since we only have two place events but in a real application with tens of place events, you will have to create a lot of boilerplate code.

Fortunately Mvp4g comes with features to let you create a history converter that can be reused for multiple place events.

Simplifying convertFromToken method

Our first step will be to create a convertFromToken method that can be used for both events.

The first implementation we could come up with could be this:
@Override
public void convertFromToken( String name, String param, HistoryEventBus eventBus ) {
    if(name.equals('page1')){
        eventBus.goToPage1(param);
    }
    else {
        eventBus.goToPage2(param);
    }
}
As you can imagine, it is not really convenient since you will have to add a new if statement for each event.

To simplify our method, we'll use another feature of Mvp4g, EventBusWithLookup. Thanks to this feature, you can fire an event using its name. All you have to do is have your event bus extend EventBusWithLookup:
@Events( ... )
public interface HistoryEventBus extends EventBusWithLookup {
...
}
This is really convenient for our convertFromToken method since the event name is one of the method parameters. We can then simplify our method to:
@Override
public void convertFromToken( String name, String param, HistoryEventBus eventBus ) {
    eventBus.dispatch( name );
}
The only thing missing is the string parameter that needs to be passed to our events. In both cases, the parameters string representation is passed directly to the event. With the dispatch method providing by the EventBusWithLookup method, you can pass as many parameters as you want. Our convertFromToken method will end up being:
@Override
public void convertFromToken( String name, String param, HistoryEventBus eventBus ) {
    eventBus.dispatch( name, param );
}
So we now have a simple convertFromToken method that can be used for both events.

Simplifying event handling methods

The first implementation we could come up with for this part could be:
public String goToPage1( String name ) {
    return name;
}
public String goToPage2( String name ) {
    return name;
}
These methods execute the same code so once again, we'd like to remove all boilerplate code.

This time we're going to use the attribute 'type' of the @History that annotates the history converter. This attribute defines which method the history converter has to implement to convert the event parameters to a string. Three options are available:
  • DEFAULT: the history converter has to implement the event handling method for each event,
  • NONE: no parameters string representation will be added to the token. The history converter doesn't have to define any method.
  • SIMPLE: this type has to implement one convertToToken method for each group of events that have the same parameters signature. This is the type we want to use here.

The convertToToken is a method that must return a string (the parameters string representation stored in the token). It has the same parameters as the event method plus a first string parameter that will contain the event name.

Both events have the same parameter signature, one string. We then need to define the following convertToToken method:
@History( type = HistoryConverterType.SIMPLE )
public class PageHistoryConverter implements HistoryConverter<HistoryEventBus> {

    public String convertToToken( String eventName, String name ) {
        return name;
    }
    ...
}
The convertToToken method just returns the string passed with the event. We also need to change the value of the attribute 'type' of @History to SIMPLE.

Associating your history converter to goToPage2

We have now created a history converter with a minimum of code that can be used for both events:
@History( type = HistoryConverterType.SIMPLE )
public class PageHistoryConverter implements HistoryConverter<HistoryEventBus> {

    public String convertToToken( String eventName, String name ) {
        return name;
    }

    @Override
    public void convertFromToken( String name, String param, HistoryEventBus eventBus ) {
        eventBus.dispatch( name, param );
    }

    @Override
    public boolean isCrawlable() {
        return false;
    }

}
The last step is to associate it with the events:
@Events( ... )
public interface HistoryEventBus extends EventBusWithLookup{
    ...
    @Event( ..., historyConverter = PageHistoryConverter.class, name = "page1" )
    void goToPage1( String name );

    @Event( ..., historyConverter = PageHistoryConverter.class, name = "page2" )
    void goToPage2( String name );

}
History converter is built as a singleton so even if it is associated with two events, only one converter will be instantiated.

What's next?


We now have setup history for our place events. HistoryConverter is a pretty simple yet powerful feature to set your history. It allows you to set urls without impacting your application by having a light coupling between the history converters and the rest of your application. You can also easily restore your application state by calling any service or firing any event you need in the convertFromToken method.

I hope with this tutorial, you now have a better understanding of how to manage your history. In the next post, I want to go over advanced history features: navigation control to prevent your user from leaving the page by accident, token generation for hyperlink and custom place service.

4 comments:

  1. Great article!

    I have a question: How would you create "hierarchical" tokens?

    Let me explain: consider in your example application the header had a listbox (values are: viewer,developer,guest,reader), whose selected value defines the first level of token "hierarchy". The tokens should be as such:
    #/viewer/page1
    #/viewer/page2
    #/developer/page1 etc
    where page1 and page2 content depends on the first level (viewer and developer in this case). In "plain" GWT MVP I'd have used

    History.addItem(getHeaderPresenter().getSelectedItemName() + "/"+getPagePresenter().getSelectedPageName());

    With Mvp4g, should I just inject the presenters into the HistoryConverter and construct the token I need or you think there could be a better way of doing this?

    ReplyDelete
  2. Thanks :)

    The case you described is a bit tricky but I think it can work with mvp4g.

    You will need the following events:

    @Event(..., name="page1")
    void goToPage1(String userType)

    @Event(..., name="page2")
    void goToPage2(String userType)

    The basic token generated by mvp4g would then be #page1/userType or #page2/userType.

    If you want to create tokens the way you described, you will have to create a custom place service to reverse name and parameters and add "/". You will then have something like this:

    public class MyPlaceService extends PlaceService {

    @Override
    public String tokenize( String eventName, String param) {

    return "/" + param + "/" + eventName;

    }

    @Override
    protected String[] parseToken( String token ) {
    String[] tab = param.parse("/");
    //first element is the event name
    //the second one is the parameters string representation
    return new String[]{tab[2],tab[1]};
    }

    }

    You can look at this page to see how to set your place service: http://code.google.com/p/mvp4g/wiki/PlaceService#Setting_a_Custom_Place_Service

    Hope this help,

    ReplyDelete
  3. will definitely try PlaceService... and thanks again for the great framework, keep going!

    ReplyDelete
  4. I updated the example with Mvp4g-1.4.0, the tutorial didn't change.

    ReplyDelete