GWT Data Binding (3/3) : présentation de la problématique
Dans ce dernier article, nous verrons comment générer dynamiquemt une classe, et comment l’utiliser dans notre projet GWT.
Utiliser un Generator maison
Quand on utilise GWT.create(Classe.class)
, GWT utilise le générateur par défaut qui renvoie le nom de la classe. Pour spécifier à GWT d’utiliser un autre générateur, il faut ajouter ces lignes dans le fichier de configuration XML du module.
<generate-with class=”com.zenika.tutorial.rebind.WrapperGenerator”> <when-type-assignable class=”com.zenika.tutorial.gwt.client.BusinessObject”/> </generate-with>
Cette partie de code indique à GWT d’utiliser WrapperGenerator
pour tout appel d’objet implémentant l’interface BusinessObject
. L’interface BusinessObject
est vide et ne sert qu’à identifier les classes pour l’appel de WrapperGenerator
. En ce sens, la technique que nous présentons est légèrement instrusive.
La classe WrapperGenerator
étend la classe abstraite Generator
de GWT (contenu dans gwt-dev-windows.jar) et se trouve par convention dans un package .rebind
en dehors du module GWT. Ceci est normal étant donné que cette classe n’est pas amenée à être compilée ou à fournir un service RPC.
/** * Méthode appelé lors de la séquence de Deferred Binding initié par l’appel GWT.create(). Renvoie le nom de la classe à instancier. */ public class WrapperGenerator extends Generator { public String generate(TreeLogger logger, GeneratorContext context, String typeClass) throws UnableToCompleteException { WrapperCreator binder = new WrapperCreator(logger, context, typeClass); String className = binder.createWrapper(); return className; } }
La méthode generate prend en paramètre :
TreeLogger
qui sert à loguer les messages
GeneratorContext
qui gère les méta-données
typeClass
qui est le nom de la classe passée en paramètre de GWT.create
Et elle renvoie le nom de la classe à instancier. Ici, nous voulons qu’elle renvoie PersonneWrapper
au lieu de Personne
. Cette classe PersonneWrapper
n’existe pas encore et sera créée dynamiquement à partir de la classe Personne par la classe WrapperCreator
.
Générer une classe dynamiquement
La dernière étape est la création du wrapper avec la classe WrapperCreator
. Nous allons décomposer son fonctionnement.
Les paramètres de la méthode generate sont récupérés dans le constructeur. L’objet TypeOracle
extrait du GeneratorContext
est une Classe de Gwt qui permet d’accéder aux informations (nom de la classe, méthodes, paramètres de méthodes, etc.) de la classe passée en paramètre de la méthode GWT.create()
(ici la classe Personne
) de manière similaire à la réflexion java.
public WrapperCreator(TreeLogger logger, GeneratorContext context, String typeName) { this.logger = logger; this.context = context; this.typeName = typeName; this.typeOracle = context.getTypeOracle(); }
Ensuite la méthode createWrapper()
est appelée par WrapperGenerator
. Celle-ci récupère dans l’oracle le nom de la classe demandée et appelle la méthode getSourceWriter()
.
public String createWrapper() { try { JClassType classType = typeOracle.getType(typeName); SourceWriter source = getSourceWriter(classType); } }
public SourceWriter getSourceWriter(JClassType classType) { String packageName = classType.getPackage().getName(); String simpleName = classType.getSimpleSourceName() + "Wrapper"; ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(packageName, simpleName); composer.addImplementedInterface("com.zenika.tutorial.gwt.client.Wrapper"); PrintWriter printWriter = context.tryCreate(logger, packageName, simpleName); if (printWriter == null) { return null; } else { SourceWriter sw = composer.createSourceWriter(context, printWriter); return sw; } }
Le SourceWriter
gère le flux textuel associé à la définition de la classe à générer. Il fonctionne comme un FileWriter
sauf qu’il gère en plus l’indentation. Le SourceWriter
renvoyé contient la déclaration du package, du nom de la classe et de l’interface à implémenter.
Il ne reste plus qu’à écrire le code de la classe. Pour cela, on se sert de l’oracle pour générer les méthodes getAttribute()
et setAttribute()
en fonction des propriétés de l’objet Personne
. Voici la partie de code qui crée la méthode getAttribute()
(le code complet de la classe est disponible en téléchargement ).
public void getSourceWriter(JClassType classType) { JMethod[] methods = classType.getMethods(); for (int i = 0; i < methods.length; i++) { String methodName = methods[i].getName(); JParameter[] methodParameters = methods[i].getParameters(); JType returnType = methods[i].getReturnType(); if (methodName.startsWith("get") & methodParameters.length == 0) { source.println("if (attr.equals(\"" + methodName.substring(3).toLowerCase() + "\")) {"); source.indent(); source.println("return content." + methodName + "();"); source.outdent(); source.print("} else "); } } source.println("{"); source.indent(); source.println("return null;"); source.outdent(); source.println("}"); }
Utiliser le wrapper
Maintenant pour utiliser le wrapper il vous suffit de taper ceci :
Wrapper wrapper = (Wrapper) GWT.create(Personne.class); wrapper.setContent(personne);
Et vous pouvez accéder aux données contenues dans personne avec la fonction getAttribute de wrapper (par exemple, getAttribute(”nom”)).
Voici un exemple d’utilistaion concrète du Wrapper :
// Création du modèle, un pojo simple normalement récupéré par un service RPC GWT final Personne personne = new Personne("Robert", "Charlebois"); // Création du wrapper du modèle pour pouvoir binder (c’est à dire le lier à l’interface) Wrapper wrapper = (Wrapper) GWT.create(Personne.class); // wrappe le modèle wrapper.setContent(personne); // on bind le nom et le prenom avec deux champs de saisis RootPanel.get().add(Binder.bind(new TextBox(), wrapper, "prenom")); RootPanel.get().add(Binder.bind(new TextBox(), wrapper, "nom")); Button button = new Button("Inspecter les nouvelles valeurs du modele"); button.addClickListener(new ClickListener() { public void onClick(Widget sender) { // on teste pour voir si l’IHM a bien répercuté les nouvelles valeurs sur le modèle. Window.alert(personne.getPrenom() + " " + personne.getNom()); } }); RootPanel.get().add(button);
Pour la suite
Nous avons mis à votre disposition en annexe un fichier zip contenant les codes sources du générateur de wrapper et un example de binding de données.
Je tiens particulièrement à remercier Ray Cromwell qui sur son blog Timepedia a été une des premières personnes à documenter le Generator
.
Ce tutorial est une mise en bouche vers un tutorial plus complet axé lui-aussi sur le data binding. Nous verrons notamment comment créer une classe Bind
qui prendra en charge la création du wrapper de manière complètement générique et la création dynamique de validator.
Pour toutes questions, n’hésitez pas à me contacter à cette adresse : pierre.queinnec@zenika.com