Articles avec le tag ‘GWT’

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 :

  1. de l’URL (ancre, encore appelé history token), on déduit une Place via un PlaceHistoryMapper,
  2. 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 | &lt;a href='#edit:1'&gt;Editer id=1&lt;/a&gt; | &lt;a href='#edit:3'&gt;Editer id=3&lt;/a&gt;"
		));
	}
 
}

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 :

  1. 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.
  2. 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…

J’ai mis à jour Safari 5 (car quelques nouveautés intéressantes) et je n’ai remarqué aucun souci avec le plugin GWT, ça passe !

J’suis pas non plus complètement fou, je m’étais renseigné, et ce qu’il ressortait c’est que ça passe quand on est sous Mac OS X 10.6 et que Safari tourne en 64 bits. Si vous n’êtes pas dans cette configuration-là, je ne vous conseille pas la mise à jour de Safari.

GXT (grâce à GWT) permet de créer des applications web riches, dont l’ergonomie est assez proche d’une application lourde. Nous avons déjà eu l’occasion de publier des articles à ce sujet sur ce blog. Il est assez simple de faire des tests unitaires sur des services dans les couches basses d’une application. Et encore, dès qu’il y a une base de données en jeu, ça se complique (et c’est d’ailleurs très souvent le cas). Mais qu’en est-il des interfaces graphiques ? Evidemment, certains éléments graphiques peuvent être testés via des tests unitaires, mais comment s’assurer que l’interface, dans son ensemble, répond aux besoins ?

C’est là qu’intervient Selenium. Selenium est un jeu d’outils qui permettent d’automatiser les tests d’interface graphique, en pilotant un navigateur via du code (ce code, c’est le testcase). Par exemple, on va pouvoir demander le chargement d’une page, effectuer un clic sur un lien bien précis, vérifier que la page contient un texte, … Une session complète sera dédiée à Selenium au DeveloperForum 5, le 8 octobre 2009 à Strasbourg.

Mais avant de pouvoir tester nos interfaces, il faut mettre en place Selenium. Voici comment faire fonctionner Selenium dans un projet GWT dans Eclipse sur Mac OS X Snow Leopard (10.6).

Comment ça marche ?

Il faut deux éléments pour faire fonctionner le tout :

  • Selenium IDE est une extension Firefox qui permet d’enregistrer les actions réalisées dans la fenêtre de Firefox et d’ainsi produire le code du testcase (dans le langage de votre choix – dans notre cas, ce sera Java).
  • Selenium RC (Remote Control) est un serveur capable de piloter un navigateur et pouvant recevoir des commandes via le réseau, déclenchées par le testcase.

Mise en place

Utilisez Firefox pour télécharger et installer Selenium IDE (extension Firefox) : http://seleniumhq.org/download/
Téléchargez ensuite Selenium RC et décompresser l’archive dans le répertoire de votre choix. Depuis ce répertoire, lancez Selenium RC via la ligne de commande :

java -jar selenium-server-1.0.1/selenium-server.jar

Le serveur se lance et devrait vous afficher les gentillesses suivantes :

10:53:44.369 INFO - Java: Apple Inc. 14.1-b02-90
10:53:44.370 INFO - OS: Mac OS X 10.6.1 i386
10:53:44.379 INFO - v1.0.1 [2696], with Core v@VERSION@ [@REVISION@]
10:53:44.461 INFO - Version Jetty/5.1.x
10:53:44.462 INFO - Started HttpContext[/selenium-server/driver,/selenium-server/driver]
10:53:44.463 INFO - Started HttpContext[/selenium-server,/selenium-server]
10:53:44.463 INFO - Started HttpContext[/,/]
10:53:44.475 INFO - Started SocketListener on 0.0.0.0:4444
10:53:44.475 INFO - Started org.mortbay.jetty.Server@1ff7a1e

Premier test

Dans Firefox, allez sur une page web de votre choix puis, dans le menu Outils, choisissez Selenium IDE. Commencez à surfer sur la page web et regardez Selenium « Big Brother » IDE enregistrer toutes vos actions dans le navigateur… Exactement comme lorsqu’on enregistre un macro dans certains logiciels. Après avoir navigué dans quelques pages, sélectionnez du texte dans la page et faites un clic droit puis sélectionnez l’option verifyTextPresent <le texte que vous avez sélectionné>.

Dans le menu de la fenêtre de Selenium, choisissez Exporter le test sous… puis choisissez Java (JUnit). Enregistrez le fichier avec le nom de votre choix, sans oublier l’extension .java.

Dans Eclipse, dans votre projet GWT/GXT, créez un nouveau dossier de sources nommé test (si ce n’est pas déjà fait). Ajoutez-y le fichier généré par Selenium IDE puis corrigez le nom du package ainsi que le nom de la classe. Corrigez le build path de votre projet en ajoutant le JAR selenium-java-client-driver.jar (que vous trouverez dans l’archive de Selenium que vous avez téléchargée). Clic droit sur la classe dans Eclipse puis Run As > JUnit Test.

Et là, c’est le drame : une erreur se produit lors du lancement de Firefox. Après le passage à Snow Leopard, une incompatibilité de librairie est arrivée. Pour la contourner, il existe une solution qui vaut ce qu’elle vaut (j’adore cette expression qui ne veut rien dire dans l’absolu et qui pourtant veut bien dire… ce qu’elle veut dire !) :

  • Quittez Firefox
  • Lancer le Terminal puis rendez-vous dans /Applications/Firefox.app/Contents/MacOS.
  • Renommez libsqlite3.dylibmv libsqlite3.dylib _libsqlite3.dylib

Relancez votre testcase et tout devrait rentrer dans l’ordre !

Quelle histoire, ce sabotage de Google ! Un peu de sérieux, en attendant des nouvelles de ce cher Serge-Jean (plus facile à écrire qu’à dire !).

Je me suis fait avoir par quelque chose d’assez subtil avec GWT. En parsant du XML avec le XMLParser fourni par GWT, j’ai eu besoin de récupérer la valeur d’attributs optionnels. J’avais donc écrit des blocs de code qui ressemblent à peu près à ceci :

NamedNodeMap paramAttrs = paramNode.getAttributes();
String view = null;
try {
   view = paramAttrs.getNamedItem("view").getNodeValue();
} catch (NullPointerException e) { }

En me disant que si un élément est null dans la chaîne d’exécution, alors view vaudrait tout simplement null. Ceci est valable en Java, mais une fois traduit en JavaScript par GWT, ça ne marche pas du tout. La solution est donc de tester « à la main » les valeurs nulles :

NamedNodeMap paramAttrs = paramNode.getAttributes();
Node item = paramAttrs.getNamedItem("view");
String view = item != null ? item.getNodeValue() : null;

Dans cet exemple, je vous l’accorde, ce n’est pas plus long et c’est même plus propre, mais j’avais un peu plus d’éléments dans ma ligne, et donc plusieurs valeurs (potentiellement nulles) à tester.

L’utilisation d’un RowLayout dans un conteneur supprime la possibilité d’avoir une barre de défilement pour ce conteneur. En fait, la raison est assez simple : RowLayout calcule la position et la taille précises de chaque enfant, et c’est à chacun de ses enfants de gérer sa barre de défilement, au besoin.

Donc, si vous avez besoin d’une barre de défilement dans un panneau, n’utilisez pas RowLayout : dans la plupart des cas, un FlowLayout peut être préférable. Utiliser un FlowLayout revient à utiliser un RowLayout(Orientation.VERTICAL) avec des contraintes RowData(1, -1) pour chaque enfant (c’est-à-dire : utiliser toute la largeur disponible et laisser chacun des enfants calculer sa propre hauteur).

Il peut parfois être utile de savoir si l’on est en mode hosted ou en mode web dans une application GWT. En fait, cela m’est surtout utile pour faire des tests : en mode hosted (en cours de développement), j’attaque ma servlet de test et en mode web (déployé, donc), j’attaque la servlet réelle de l’application.

Pour cela, il faut utiliser la méthode statique isScript() de la classe GWT :

if ( GWT.isScript() ) {
   // Fonctionnement en mode web (déployé)
}