Beaucoup d’applications web sont désormais basées sur AJAX, et offrent ainsi une interface plus réactive et plus dynamique, en minimisant (voire supprimant) les rechargements de page : les données à affichées sont chargées en arrière-plan puis insérées dans la page web, à coup de JavaScript (manipulation du DOM).
Contrairement à une page web entièrement générée par le serveur, cette page doit alors faire preuve d’une intelligence supplémentaire afin de gérer l’état de l’application à chaque instant de son utilisation. Peu importe donc la technologie employée sur le serveur, on s’attarde donc à trouver une technologie évoluée coté client, forcément basée sur JavaScript, seul langage à être embarqué dans tous les navigateurs modernes. Ont alors vu le jour de multiples framework JavaScript, tels jQuery, MooTools, Prototype ou encore Scriptaculous.
Google a tenté une approche différente en proposant un framework Java, couplé à un traducteur Java vers JavaScript. Une réelle prouesse technique, qui promet une abstraction quasi-totale du navigateur. L’avantage est que le codage se fait en Java, langage plus rigoureux que JavaScript, et ainsi plus simple à débugguer.
Avec GWT, l’application web, c’est une page web. S’il y a différentes vues (un formulaire de création, une liste, …), c’est via le DOM qu’on affiche les widgets qu’il faut. Mais pour le navigateur, l’URL reste la même, et si on recharge la page, la vue peut alors être perdue. De la même manière, l’utilisation du bouton Précédent dans le navigateur ne permet pas de naviguer, comme sur un site web classique. Sauf si… Si on réussit à faire croire au navigateur que l’URL a changé sans recharger la page, ça peut marcher.
Les ancres dans les URL
Dans une URL, on peut faire référence à un élément de la page (identifié de manière unique par un attribut id) en suffisant l’URL par #<id de l’élément>. La navigateur va alors défiler la page jusqu’à ce que l’élément concerné soit visible (s’il l’est). Cela permet de naviguer au sein d’une même page, et c’est très souvent utilisé pour une table des matières en début de page. L’avantage est que le navigateur considère tout changement derrière le caractère # d’une URL (l’ancre) comme étant une URL différente, ayant son entrée propre dans l’historique des pages visitées. En même temps, un changement d’ancre ne recharge pas la page.
La classe History de GWT
En GWT, on peut être notifié par un système d’événement d’un changement d’ancre (et donc sans que la page ne soit rechargée) et connaître le nom de l’ancre. Ainsi, on peut exécuter du code et afficher telle ou telle vue en fonction de l’ancre actuelle. La classe History dans GWT permet cela. Et GWT propose une technique très efficace pour gérer ces changements dans l’interface en fonction de l’URL
Activity et Place
GWT part du principe suivant : à une URL correspond un affichage de l’application. Cet état (visuel) de l’application qui dépend de l’URL s’appelle une Place.
A cette Place, on associe généralement une Activity, c’est à dire une fonctionnalité de l’application.
Cette Activity va alors exécuter le code qu’il faut pour placer l’interface dans l’état voulue par l’URL.
Pour résumer :
- de l’URL (ancre, encore appelé history token), on déduit une Place via un PlaceHistoryMapper,
- de cette Place, on déduit une Activity via un ActivityMapper.

Dans la pratique, cela se traduit se cette manière-là.

- une classe par Place (ici : EditPlace et ListPlace),
- une classe par Activity (ici : EditActivity et ListActivity),
- un ActivityMapper (ici AppActivityMapper),
- un PlaceHistoryMapper (ici AppPlaceHistoryMapper),
- un point d’entrée (ici Index),
- une Factory (ici ClientFactory) pour quelques objets dont on a souvent besoin.
Dans notre point d’entrée (Index.java)
// Nous avons besoin d'un panneau dans lequel vont s'afficher les différentes
// vues de l'application.
// Ici, ce sera la page entière, mais on pourrait n'avoir qu'une partie
// centrale par exemple, avec des menus globaux autour qui seraient toujours
// là.
SimplePanel rootPanel = new SimplePanel();
RootLayoutPanel.get().add(rootPanel);
// Le bus où tous les événements "métiers" de l'application seront déclenchés.
EventBus eventBus = ClientFactory.INSTANCE.getEventBus();
// Un peu de plomberie...
PlaceController placeController = ClientFactory.INSTANCE.getPlaceController();
ActivityManager activityManager = new ActivityManager(
new AppActivityMapper(), eventBus
);
activityManager.setDisplay(rootPanel);
AppPlaceHistoryMapper historyMapper = GWT.create(AppPlaceHistoryMapper.class);
PlaceHistoryHandler historyHandler = new PlaceHistoryHandler(historyMapper);
// Place par défaut si rien n'est spécifié dans l'URL.
ListPlace defaultPlace = new ListPlace();
historyHandler.register(placeController, eventBus, defaultPlace);
// On déclenche tout ça !
historyHandler.handleCurrentHistory();
Ce qu’il est important de retenir ici :
- Il est obligatoire d’avoir un seul conteneur qui sera la destination des différentes vues de l’application.
- Ces initialisations sont certes un peu fastidieuses, mais elles peuvent être copiées/collées d’un projet à un autre (ou mutualisées, pourquoi pas, dans la ClientFactory).
- Le déclenchement réalisé sur la dernière ligne n’est pas automatique : ne l’oubliez pas !
- AppPlaceHistoryMapper est instancié via un defered binding (GWT.create()).
AppPlaceHistoryMapper :
package com.greenivory.gwt.tutorial.client;
import com.google.gwt.place.shared.PlaceHistoryMapper;
import com.google.gwt.place.shared.WithTokenizers;
@WithTokenizers( {
EditPlace.Tokenizer.class,
ListPlace.Tokenizer.class,
} )
public interface AppPlaceHistoryMapper extends PlaceHistoryMapper {
}
Ce qu’il est important de retenir ici :
- Il suffit de lister les différentes Place (en fait, leur Tokenizer) dans l’annotation @WithTokenizers.
AppActivityMapper :
package com.greenivory.gwt.tutorial.client;
import com.google.gwt.activity.shared.Activity;
import com.google.gwt.activity.shared.ActivityMapper;
import com.google.gwt.place.shared.Place;
public class AppActivityMapper implements ActivityMapper {
@Override
public Activity getActivity(Place place) {
if (place instanceof ListPlace) {
return new ListActivity((ListPlace) place);
}
if (place instanceof EditPlace) {
return new EditActivity((EditPlace) place);
}
return null;
}
}
Ce qu’il est important de retenir ici :
- A chaque Place, son Activity !
ClientFactory :
package com.greenivory.gwt.tutorial.client;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.event.shared.SimpleEventBus;
import com.google.gwt.place.shared.PlaceController;
public class ClientFactory {
private EventBus eventBus;
private PlaceController placeController;
public static final ClientFactory INSTANCE = new ClientFactory();
protected ClientFactory() {
eventBus = new SimpleEventBus();
placeController = new PlaceController(eventBus);
}
public EventBus getEventBus() {
return eventBus;
}
public PlaceController getPlaceController() {
return placeController;
}
}
Ce qu’il est important de retenir ici :
- Il est impératif d’utiliser toujours les mêmes instances du PlaceController et du EventBus dans toute l’application. La ClientFactory (qui est un singleton) est donc une bonne pratique pour les contenir.
ListPlace :
package com.greenivory.gwt.tutorial.client;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceTokenizer;
import com.google.gwt.place.shared.Prefix;
/**
* Les URLs sont du type : http://server/index.html#ancre:token.
* 'ancre' sert à déterminer la Place à utiliser.
* 'token' sert à transmettre des paramètres à la Place.
* Ce token peut être vide, mais le double-point est obligatoire.
*/
public class ListPlace extends Place {
public ListPlace() {
super();
}
@Prefix("list") // Ancre utilisée pour identifier cette Place.
public static class Tokenizer implements PlaceTokenizer {
@Override
public String getToken(ListPlace place) {
// Retourner le token en fonction des paramètres de la place.
// Ici, jamais de paramètre, donc toujours chaîne vide
// (mais jamais null !).
return "";
}
@Override
public ListPlace getPlace(String token) {
// En fonction du 'token' (derrière le double-point
// après l'ancre), instancier la Place.
// Ici, on ne gère pas de paramètres, donc on instancie
// toujours une ListPlace tout simple.
return new ListPlace();
}
}
}
ListActivity :
package com.greenivory.gwt.tutorial.client;
import com.google.gwt.activity.shared.Activity;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.place.shared.Place;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.user.client.ui.HTML;
public class ListActivity implements Activity {
/**
* @param place
*/
public ListActivity(Place place) {
}
@Override
public String mayStop() {
// Retourner null pour autoriser l'arrêt de l'Activity.
// Retourner une chaîne non-nulle pour demander à
// l'utilisateur si l'Activity doit être stoppée ou non.
return null;
}
/* (non-Javadoc)
* @see com.google.gwt.activity.shared.Activity#onCancel()
*/
@Override
public void onCancel() {
// Exécutée quand l'Activity n'est pas démarrée.
}
/* (non-Javadoc)
* @see com.google.gwt.activity.shared.Activity#onStop()
*/
@Override
public void onStop() {
// Exécutée quand l'Activity est arrêtée.
}
/* (non-Javadoc)
* @see com.google.gwt.activity.shared.Activity#start()
*/
@Override
public void start(AcceptsOneWidget panel, EventBus eventBus) {
// Code de l'Activity.
panel.setWidget(new HTML(
"Liste d'éléments | <a href='#edit:1'>Editer id=1</a> | <a href='#edit:3'>Editer id=3</a>"
));
}
}
Ce qu’il est important de retenir ici :
- La méthode start() est indéniablement la plus importante, mais les autres sont très pratiques pour contrôler la navigation : l’utilisateur peut-il arrêter cette Activity (méthode mayStop()) ?
EditPlace :
package com.greenivory.gwt.tutorial.client;
import com.google.gwt.place.shared.Place;
import com.google.gwt.place.shared.PlaceTokenizer;
import com.google.gwt.place.shared.Prefix;
/**
* Les URLs sont du type : http://server/index.html#ancre:token.
* 'ancre' sert à déterminer la Place à utiliser.
* 'token' sert à transmettre des paramètres à la Place. Peut être vide, mais le double-point est obligatoire.
*/
public class EditPlace extends Place {
private int id;
public EditPlace(int id) {
super();
this.id = id;
}
public final int getId() {
return id;
}
@Prefix("edit") // Ancre utilisée pour identifier cette Place.
public static class Tokenizer implements PlaceTokenizer {
@Override
public String getToken(EditPlace place) {
// Retourner le token en fonction des paramètres de la place.
// Ici, on s'attend à avoir l'ID en paramètre, donc on retourne une chaîne contenant l'ID de la Place.
return String.valueOf(place.getId());
}
@Override
public EditPlace getPlace(String token) {
// En fonction du 'token' (derrière le double-point après l'ancre), instancier la Place.
// Ici, le token contient l'ID de l'élément à éditer : nous n'avons qu'à le transformer en entier...
try {
return new EditPlace(Integer.parseInt(token));
} catch (NumberFormatException e) {
return new EditPlace(-1);
}
}
}
}
Ce qu’il est important de retenir ici :
- Il est possible de passer autant de paramètres qu’on le souhaite dans le token, pourvu qu’on le parse correctement.
- Vous êtes libres de créer tous les paramètres nécessaires dans la Place.
- Ne prenez pas à la légère la méthode getToken() : il est important qu’elle retourne le token qui permet de reconstruire une Place identique (avec les mêmes paramètres). getToken() doit être, en quelque sorte, le miroir de getPlace().
EditActivity :
package com.greenivory.gwt.tutorial.client;
import com.google.gwt.activity.shared.Activity;
import com.google.gwt.event.shared.EventBus;
import com.google.gwt.user.client.ui.AcceptsOneWidget;
import com.google.gwt.user.client.ui.HTML;
public class EditActivity implements Activity {
private EditPlace place;
/**
* @param place
*/
public EditActivity(EditPlace place) {
this.place = place;
}
@Override
public String mayStop() {
// Retourner null pour autoriser l'arrêt de l'Activity.
// Retourner une chaîne non-nulle pour demander à l'utilisateur si l'Activity doit être stoppée ou non.
return null;
}
/* (non-Javadoc)
* @see com.google.gwt.activity.shared.Activity#onCancel()
*/
@Override
public void onCancel() {
// Exécutée quand l'Activity n'est pas démarrée.
}
/* (non-Javadoc)
* @see com.google.gwt.activity.shared.Activity#onStop()
*/
@Override
public void onStop() {
// Exécutée quand l'Activity est arrêtée.
}
/* (non-Javadoc)
* @see com.google.gwt.activity.shared.Activity#start(com.google.gwt.user.client.ui.AcceptsOneWidget, com.google.gwt.event.shared.EventBus)
*/
@Override
public void start(AcceptsOneWidget panel, EventBus eventBus) {
// Code de l'Activity.
if (place.getId() == -1) {
panel.setWidget(new HTML("<strong>Paramètre incorrect dans l'URL.</strong>"));
} else {
panel.setWidget(new HTML("Edition de l'élement " + place.getId()));
}
}
}
Ce qu’il est important de retenir ici :
- Il est intéressant d’avoir accès à la Place pour en récupérer des informations (paramètres issus du token dans l’URL).
Une fois que tout est en place, changer de vue est un véritable jeu d’enfant ! Il y a deux possibilités :
- Dans un lien hypertexte, mettre l’URL de la Place vers laquelle on souhaite aller (attention au double-point derrière l’ancre, obligatoire, et souvent source de perte de temps).
Par exemple #list: pour se rendre sur la liste des éléments.
Ou #edit:3 pour se rendre sur la EditPlace pour éditer l’élément qui a l’ID 3.
- Dans du code GWT avec (par exemple) :
ClientFactory.INSTANCE.getPlaceController().goTo(new EditPlace(47));
Dans un prochain article, nous étudierons comment utiliser le pattern MVP pour construire des UI propres et éviter de faire des vues à rallonge…