Browser History Management in GWT using Activity and Place

Browser history management is an important aspect of a web application. An optimized history management will allow faster navigation and better responsiveness. The reason why GWT constitues a special case is its use of a single page (called RootPanel) approach ( SPA). The page can be seen as board on which components are added or deleted depending on the current context. Without history management in GWT, the application user will face the following issues:
          –  If the user hits the back button, the browser will navigate to the previous page used by the user before entering the application and not to the previous state of the application.
           – If the user wants to bookmark the current state of the application, the bookmark will point to the first page of the application.

To tackle these issues, GWT introduced history management mechanisms starting from its 2.1 release. Activity and Place are helper classes that allow developpers to manage history by assigning names to application states using # ( Fragment Identifier). They work best in concordance with an MVP architecture. In this tutorial, we will go throught how to use Activities and Places to effectively manage history in a GWT application.

Requirements:
Eclipse
GWT plugin
the application we built last tutorial: 

Our application has two pages or states: a login and main page.

First of all, we are going to change our MVP setup. We are going to move our Presenter interface inside our view, and have it implemented by the activity which will play the role of the Presenter as well.

LoginView.java


public class LoginView extends Composite implements IsWidget {
 HorizontalPanel container;
 Label loginLabel;
 Label passwordLabel;
 TextBox loginField;
 PasswordTextBox passwordField;
 Button loginButton;
 private Presenter presenter;

 
 public HasClickHandlers getLoginButton() {
  return loginButton;
 }
 
 public LoginView(){
  container = new HorizontalPanel();
  loginField = new TextBox();
  loginButton = new Button("Login");
  passwordField = new PasswordTextBox();
  loginLabel = new Label("Login");
  passwordLabel = new Label("Password");
  
  container.add(loginLabel);
  container.add(loginField);
  container.add(passwordLabel);
  container.add(passwordField);
  container.add(loginButton);
  
 }

 @Override
 public Widget asWidget() {
  return container;
 }
 
 public Presenter getPresenter() {
  return presenter;
 }

 public void setPresenter(Presenter presenter) {
  this.presenter = presenter;
 }

 public interface Presenter{
        public void goTo(Place place);
        public void loginButtonEvent();
 }

}

Next we are going to create the LoginPlace and LoginActivity, which are required for managing history. A Place object holds the application state at a particular point.

 LoginPlace.java


public class LoginPlace extends Place {
 String name;
 public LoginPlace(String placeName){ 
  this.name = placeName;
 }
 public String getPlaceName(){ 
  return name;
 }
  public static class Tokenizer implements PlaceTokenizer<LoginPlace> {
         @Override
         public String getToken(LoginPlace place) {
             return place.getPlaceName();
         }
         @Override
         public LoginPlace getPlace(String token) {
             return new LoginPlace(token);
         }
     }
}

As you may notice, the LoginPlace class has an inner static class called Tokenizer which implements the PlaceTokenizer. This last is used for serializing the place name into a token that will be used by the browser to refer to the place. The next step is to define LoginActivity.java. Before doing so, we need to define our client factory which will be used for dependency injection.

ClientFactory.java


public interface ClientFactory {
     LoginView getLoginView();
     MainPageView getMainPageView();
     AppController getAppController();
     EventBus getEventBus();
     PlaceController getPlaceController();
}

ClientFactoryImpl.java


public class ClientFactoryImpl implements ClientFactory {
 LoginView login = new LoginView();
 MainPageView mainPage = new MainPageView();
 HandlerManager controllerBus = new HandlerManager(null);
 EventBus eventBus = new SimpleEventBus();
 PlaceController controller = new PlaceController(eventBus);

 @Override
 public LoginView getLoginView() {
  return login;
 }

 @Override
 public MainPageView getMainPageView() {
  return mainPage;
 }


 @Override
 public EventBus getEventBus() {
  return eventBus;
 }

 @Override
 public PlaceController getPlaceController() {
  return controller;
 }

}


We need add this piece of code in our module definition xml file.

 

<replace-with class="com.opencode.client.ClientFactoryImpl">
    <when-type-is class="com.opencode.client.ClientFactory">
   </when-type-is></replace-with>

Now, we are ready to define our activity :

LoginActivity.java


public class LoginActivity extends AbstractActivity implements LoginView.Presenter {
 
 ClientFactory factory;
 
 String name;
 
 public LoginActivity(LoginPlace loginPlace, ClientFactory clientFactory){
  this.factory = clientFactory;
  this.name = loginPlace.getPlaceName();
 }

 @Override
 public void start(AcceptsOneWidget panel, EventBus eventBus) {
  LoginView view = factory.getLoginView();
        view.setPresenter(this);
  panel.setWidget(view.asWidget());
  bindEvents();
  
 }
 
 public void bindEvents(){
  loginButtonEvent();
 }
 
 

 @Override
 public void loginButtonEvent() {
  factory.getLoginView().getLoginButton().addClickHandler(new ClickHandler(){
   @Override
   public void onClick(ClickEvent event) {
    System.out.println("inside event");
    goTo(new MainPagePlace("MainPage"));
               
   }
  });
 }
 
 @Override
 public void goTo(Place place) {
  factory.getPlaceController().goTo(place);
 }
}

The next step is to define an ActivityMapper which maps each Place to an Activity. In addition, we are going to define a HistoryMapper that handles all the place tokens of our application.

MyActivityMapper.java


public class MyActivityMapper implements ActivityMapper {
 private ClientFactory clientFactory;  
 
 public MyActivityMapper(ClientFactory factory){
  this.clientFactory = factory;
 }
 @Override
 public Activity getActivity(Place place) {
  if(place instanceof LoginPlace){
   return new LoginActivity((LoginPlace) place, clientFactory);
  }else if(place instanceof MainPagePlace){
   return new MainPageActivity((MainPagePlace)place, clientFactory);
  }else if(place instanceof UserSelectPlace){
   return new MainPageActivity((MainPagePlace)place, clientFactory);
  }
  return null;
 }
}

MyHistoryMapper.java


@WithTokenizers({LoginPlace.Tokenizer.class, MainPagePlace.Tokenizer.class})
public interface MyHistoryMapper extends PlaceHistoryMapper  {
}

Finally, we need to update our entry point with all the new components:

MVPexample.java


public class MVPexample implements EntryPoint {
 private LoginPlace welcomePlace = new LoginPlace("login");
 private SimplePanel appWidget = new SimplePanel();
 
 public void onModuleLoad() {
  ClientFactory clientFactory = GWT.create(ClientFactory.class);
  PlaceController controller = clientFactory.getPlaceController();
  
  EventBus bus = clientFactory.getEventBus();
        ActivityMapper activityMapper = new MyActivityMapper(clientFactory);
        ActivityManager activityManager = new ActivityManager(activityMapper, bus);
        activityManager.setDisplay(appWidget);

        MyHistoryMapper historyMapper= GWT.create(MyHistoryMapper.class);
        final PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper);
        historyHandler.register(controller, bus, welcomePlace);
        
        RootPanel.get().add(appWidget);
        historyHandler.handleCurrentHistory();
 }
}

Result:

Same thing goes for MainPageView.java.

Full example code available at: https://github.com/gwidgets/mvpexample

Archive

Subscribe

Atom