Mettre en place un arbre paresseux
avec GWT
Le but de ce billet est de présenter une technique simple pour mettre en place un arbre paresseux (lazy tree) avec GWT.
GWT offre des primitives basiques pour la création d’un arbre, qui sont :
- Le composant TreeItem : représente un nœud dans l’arbre et peut contenir de sous nœuds du même type.
- Le composant Tree : représente la racine de l’arbre et peut contenir des TreeItems
Construire un arbre revient donc à créer autant d’instances de TreeItem que nécessaire et des les assembler pour obtenir l’hiérarchie souhaitée.
Cependant, cette méthode n’est pas applicable dans le cas où :
- On dispose d’un grand nombre des nœuds (des centaines voire des milliers)
- La récupération des nœuds est coûteuse : accès à la base de données et/ou au réseau
Dans des cas pareils, mieux vaut être paresseux et ne charger des nœuds que quand nécessaire (ne charger les fils d’un nœud que quand l’utilisateur l’ouvre).
Dans le cadre de ce billet, et comme source de données pour l’arbre, on suppose disposer d’un service exposé en RPC dont voici l’interface :
package com.iptech.client;
import java.util.ArrayList;
import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;
@RemoteServiceRelativePath("treeService")
public interface TreeService extends RemoteService {
ArrayList<String> getChildren(String parent)
throws IllegalArgumentException;
}
Dans l’implémentation de ce service, j’ai introduit (via un sleep) un délais de 2 secondes, pour simuler l’obtention d’une ressource coûteuse et pour que le comportement paresseux de l’arbre soit plus visible.
public class Application implements EntryPoint {
private static final String CHARGEMENT_EN_COURS = "Chargement en cours ...";
private final TreeServiceAsync treeService = GWT.create(TreeService.class);
public void onModuleLoad() {
TreeItem root = new TreeItem(CHARGEMENT_EN_COURS);
Tree browseTree = new Tree();
browseTree.addItem(root);
treeService.getChildren(null, new TreeRootCallback(browseTree));
browseTree.addOpenHandler(new OpenHandler<TreeItem>() {
public void onOpen(OpenEvent<TreeItem> event) {
if (needsLoading(event.getTarget())) {
treeService.getChildren(event.getTarget().getText(),
new TreeItemCallback(event.getTarget()));
}
}
});
RootPanel.get().add(browseTree);
}
A noter que l’interface TreeServiceAsync (qui est la version asynchrone de TreeService) n’est pas présente dans le code source de cet exemple car elle sera généré automatiquement par GWT Maven plugin lors de la construction du projet.
Je commence donc par créer l’instance du composant du composant Tree et d’y ajouter un élément avec le message “Chargement en cours …”
J’invoque ensuite le service présenté plus haut pour récupérer la liste des nœuds racines de l’arbre. Cet appel se fait de façon asynchrone, d’où l’utilisation d’un callback dont voici le code :
public static final class TreeRootCallback implements
AsyncCallback<ArrayList<String>> {
private Tree browseTree;
public TreeRootCallback(Tree browseTree) {
super();
this.browseTree = browseTree;
}
public void onFailure(Throwable caught) {
caught.printStackTrace();
}
public void onSuccess(ArrayList<String> names) {
browseTree.removeItems();
for (String name : names) {
TreeItem ti = new TreeItem(name);
ti.addItem(CHARGEMENT_EN_COURS);
browseTree.addItem(ti);
}
}
}
A la réception de la réponse, le callback supprime les éléments de l’arbre (le message “Chargement en cours …” plus précisément) et pour chaque chaîne retournée :
- Crée un item avec la chaîne comme texte et l’ajoute à l’arbre
- A chaque item crée ajoute un fils avec le message “Chargement en cours …“
Retournons à la méthode onModuleLoad.
L’étape suivante consiste à attacher à l’arbre un listener qui intercepte les évènements d’ouverture d’un nœud, et si ce dernier nécessite le chargement de ses fils (dans le cas où il a un seul fils et que ce dernier a le texte “Chargement en cours …“), fait appel à treeService en lui passant un autre callback dont voici le code :
public static final class TreeItemCallback implements
AsyncCallback<ArrayList<String>> {
private TreeItem treeItem;
public TreeItemCallback(TreeItem treeItem) {
super();
this.treeItem = treeItem;
}
public void onFailure(Throwable caught) {
caught.printStackTrace();
}
public void onSuccess(ArrayList<String> names) {
treeItem.removeItems();
for (String name : names) {
TreeItem ti = new TreeItem(name);
ti.addItem(CHARGEMENT_EN_COURS);
treeItem.addItem(ti);
}
}
}
Ce second callback ressemble beaucoup au premier, excepté qu’il opère sur un TreeItem au lieu d’un Tree.
A ce propos, j’étais étonné du fait que Tree et TreeItem n’ont pas un ancêtre commun, bien qu’ils sont très similaires conceptuellement (tous les deux sont des conteneurs d’autres TreeItems, ce qui se fait trahir par le fait qu’ils partagent plusieurs méthodes avec le même nom et signatures).
Code source
Un projet maven avec le code source complet de ce qui a été présenté dans ce billet est disponible sous la licence MIT sur GitHub à l’adresse suivante : http://github.com/jawher/gwt-lazy-tree/
Une fois ce projet récupéré, il suffit d’exécuter la commande “mvn gwt:run” dans son dossier pour exécuter et tester cette application.

12 avril 2010 à 16:00
Un sujet intéressant puisque -pour moi- il touche à l’optimisation.
Le plus souvent on ne se rend compte de tels pbms que post livraison, cad lorsque le client challenge l’interface
avec un “data store” réel.
J’ai eu un cas similaire, mais il m’etais possible -heureusement- de le resoudre sans recourir à ce concept de lazy tree.
Car en fait, on peut s’ apercevoir (par profiling) que la cause majeur consiste au faite que l’ajout
d’un item déclenche un code de rendering. Ainsi, désactiver le rendering, puis charger et inserer totalement
la chose, avant de réactiver le rendering, peut etre suffisant.
Puisque je développe en C++ j’ai pas compris grande chose de votre code abstrait, mais je pense que si l’on voulais porter
l’abstraction à son maximum, l’on devrait peut etre penser à généraliser la chose à plusieurs controles :
penser à la notion de lazy control.
Dans le cas d’un Tree c’est l’expansion d’un noeud qui pilote le lazy loading : dans le cas d’une liste ce serait le defilement de la scrollbar à son minimum, ainsi de suite.
Finalement merci pour votre volonté à partager de l’information.
12 avril 2010 à 16:16
Merci Ahmed pour le commentaire.
J’avais moi aussi, y’a des sciècles, à résoudre le problème que tu décris, où il faut désactiver le rendu d’un composant (tree ou liste) avant d’y ajouter un grand nombre de composants pour améliorer le temps de réponse et éviter le ‘flicker’ de l’écran.
Par contre, ce que dont je parle dans ce billet traite de la latence de la récupération des données et non pas de leur rendu.
Ton idée sur le “lazy control” en général est intéressante. Elle est implémenté dans JFace (une surcouche de SWT) qui non seulement te permet de coder en MVC/MVP et de séparer la création des composants graphiques de leur population (et récupération) par les données, mais aussi gère l’aspect lazy de façon transparente.