Cet article dédié aux Widget GWT propose quelques bases d’architecture visant à obtenir un composant performant, maintenable et évolutif.
Le développement d’une Widget GWT comporte trois phases principales :
- définition des propriétés
- mise en place du gestionnaire d’évènements
- Implémentation du comportement
Pour illustrer la mise en oeuvre de ces phases, crééons tout d’abord à l’aide d’UiBinder :
Ce qui nous génère le code suivant :
[sourcecode language=”java”]
public class TestWidget extends Composite implements HasText {
private static TestWidgetUiBinder uiBinder = GWT
.create(TestWidgetUiBinder.class);
interface TestWidgetUiBinder extends UiBinder {
}
public TestWidget() {
initWidget(uiBinder.createAndBindUi(this));
}
@UiField
Button button;
public TestWidget(String firstName) {
initWidget(uiBinder.createAndBindUi(this));
button.setText(firstName);
}
@UiHandler("button")
void onClick(ClickEvent e) {
Window.alert("Hello!");
}
public void setText(String text) {
button.setText(text);
}
public String getText() {
return button.getText();
}
}
[/sourcecode]
Définition des propriétés
C’est la partie la plus simple à mettre en oeuvre, il s’agit de simples propriétés java avec leurs getter and setter
[sourcecode language=”java”]
public TestWidget extends Composite implements HasText {
…
private String prop1;
public void setProp1(String prop1){
this.prop1 = prop1;
}
public String getProp1(){
return prop1;
}
}
[/sourcecode]
En rajoutant une propriété à un classe qui hérite de Composite ou Panel, on peut ainsi l’éditer dans GwtDesigner :
A noter qu’on peut également utiliser des enums
[sourcecode language=”java”]
public TestWidget extends Composite implements HasText {
…
public enum EnumTest{Choix1, Choix2};
private EnumTest prop2;
public void setProp2(EnumTest prop2){
this.prop2 = prop2;
}
public EnumTest getProp2(){
return prop2;
}
}
[/sourcecode]
qui seront proposées sous forme de liste déroulante :
Pour l’instant nos nouvelles propriétés ne sont pas utilisées par le composant.
Imaginons une quelconque action qui pourrait leur être associée, par exemple modifier le texte du bouton :
[sourcecode language=”java”]
public void setProp1(String prop1){
this.prop1 = prop1;
button.setText(prop1);
}
[/sourcecode]
Cette solution, suffisante dans le cas de notre exemple, s’avère rapidement contre productive car très souvent le comportement d’un composant se base sur une combinaison de propriétés
Ex : nous voudrions que le texte du bouton=prop1 si prop2=choix1, ou ” dans les autres cas
dans la logique précédente cela reviendrait à :
[sourcecode language=”java”]
public void setProp1(String prop1) {
this.prop1 = prop1;
if(EnumTest.Choix1.equals(getProp2())){
button.setText(prop1);
} else {
button.setText("");
}
}
public void setProp2(EnumTest prop2){
this.prop2 = prop2;
if(EnumTest.Choix1.equals(getProp2())){
button.setText(prop1);
} else {
button.setText("");
}
}
[/sourcecode]
On voit qu’il y a un doublon du code, qui peut vite devenir exponentiel au fur et à mesure que le composant se complexifie.
Il est donc préférable d’utiliser une méthode centrale de mise à jour, par exemple ‘update’ :
[sourcecode language=”java”]
public void update(){
if(EnumTest.Choix1.equals(getProp2())){
button.setText(getProp1());
} else {
button.setText("");
}
}
public void setProp1(String prop1) {
this.prop1 = prop1;
update();
}
public void setProp2(EnumTest prop2){
this.prop2 = prop2;
update();
}
[/sourcecode]
La méthode update sera ensuite déléguée au gestionnaire de comportement (cf chapitre Comportement).
A noter que dans l’exemple précédent, la méthode update est systématiquement appellée lors d’un set de propriété.
Pour optimiser le composant et pouvoir setter toutes les propriétés avant de le rafraichir, il est préférable d’appeler manuellement cette méthode tout en gardant le bénéfice de la mise à jour immédiate pour le mode développement (GwtDesigner ne peut pas appeler de lui même la méthode update). GWT fournit une classe utilitaire (com.google.gwt.core.client.GWT) qui permet de
détecter via la fonction ‘isProdMode’ si le composant est exécuté en environnement de développement ou en mode de production.
[sourcecode language=”java”]
public void autoUpdate(){
if(!GWT.isProdMode()){
update();
}
}
public void setProp1(String prop1) {
this.prop1 = prop1;
autoUpdate();
}
public void setProp2(EnumTest prop2){
this.prop2 = prop2;
autoUpdate();
}
[/sourcecode]
Avec le code ci dessus le composant sera bien rafraichit en mode design :
GWT fournit des interfaces pour notifier qu’une widget possède un certain type de propriétés.
Par exemple notre widget de test implémente l’interface ‘HasText’ qui indique que la Widget possède un champ texte.
Il en existe de nombreuses autres telles que ‘HasValue’, ‘HasCaption’, ‘HasHTML’, etc… qui sont particulièrement utiles pour les test de typage.
Il est pertinent de les utiliser et si besoin d’en déclarer de nouvelles.
Gestion des évènements
La première chose à faire est de déclarer les évènements que nous souhaitons exposer. Dans le méchanisme GWT, cette déclaration se fait à l’aide d’interfaces :
pour un évènement donné ‘[EventType]Event’ l’interface associée sera ‘Has[EventType]Handlers’
Ex : pour l’évènement MouseOverEvent l’interface associé sera : HasMouseOverHandlers, pour l’évènement MouseOutEvent : HasMouseOutHandlers, etc…
La méthode déclarée par cette interface permet d’associer une fonction callback ([EventType]Handler) à l’évènement en question.
NB : il est important de continuer à respecter cette nomenclature lorsque l’on déclare des évènements personnalisés.
Associons notre widget à l’évènement on click (ClickEvent)
[sourcecode language=”java”]
public class TestWidget extends Composite implements HasText, HasClickHandlers {
…
@Override
public HandlerRegistration addClickHandler(ClickHandler handler) {
// TODO Auto-generated method stub
return null;
}
}
[/sourcecode]
L’évènement est désormais disponible sous GWT Designer :
Dans un premier temps, redirigeons simplement le clic du bouton vers notre gestionnaire :
[sourcecode language=”java”]
@Override
public HandlerRegistration addClickHandler(ClickHandler handler) {
return button.addClickHandler(handler);
}
[/sourcecode]
Avant d’aller plus loin, attardons nous un instant sur quelques fonctions importantes de gestion des évènements :
fireEvent :déclenche la propogation d’un évènement. Tous ceux qui se seront abonnés à cet évènement (via addHandler notamment) seront notifiés.
addHandler : permet de s’abonner à un évènement. Cette fonction associe un évènement à une fonction callback
sinkEvents : fonction de bas niveau de gestion des évènements. Sert à s’inscrire à des évènements
Cette fonction est importante car elle permet de traiter tous les évènements qui ne sont pas gérés par addHandler
onBrowserEvent : méthode qui reçoit les évènements enregistrés par sinkEvents
Ce méchanisme appliqué à notre architecture fera également intervenir deux méthodes importantes :
onAttach : cet méthode est appelée lorsque la Widget est rattachée au DOM.
Tous les évènements gérés par notre Widget y seront inscrits au gestionnaire d’évènements
onDetach : cet méthode est appelée lorsque la Widget est déttachée du DOM.
Tous les évènements gérés par notre Widget y seront désinscrits du gestionnaire d’évènements
A présent poursuivons, nous souhaitons maintenant que l’évènement clic soit déclenché lorsque l’utilisateur clic sur n’importe quelle partie du composant et non pas simplement le bouton.
Cela s’avère déjà plus complexe à mettre en oeuvre. En effet, si le bouton peut recevoir les clics, le composant lui même (étendu de la classe Composite) ou le HTMLPanel qui contient le bouton ne le peuvent pas via la méthode ‘addHandler’. Il devient alors nécessaire d’utiliser les fonctions de bas niveau ‘sinkEvents’ et ‘onBrowserEvent’.
Tout d’abord, indiquons que la gestion du clic n’est plus assurée par le bouton mais par le composant lui même.
[sourcecode language=”java”]
@Override
public HandlerRegistration addClickHandler(ClickHandler handler) {
return addHandler(handler, ClickEvent.getType());
}
[/sourcecode]
Puis dans les fonctions onAttach et onDetach, inscrivons et désincrivons l’évènement onClick
[sourcecode language=”java”]
@Override
public void onAttach(){
super.onAttach();
sinkEvents(Event.ONCLICK);
}
@Override
public void onDetach(){
super.onDetach();
unsinkEvents(Event.ONCLICK);
}
[/sourcecode]
Finalement, récupérons l’évènement dans la méthode ‘onBrowserEvent’ et redirigeons le vers notre gestionnaire via ‘fireEvent’ :
[sourcecode language=”java”]
@Override
public void onBrowserEvent(Event event){
switch (DOM.eventGetType(event)) {
case Event.ONCLICK: ClickEvent.fireNativeEvent(event, this);
break;
}
}
[/sourcecode]
NB : il n’est pas nécessaire de faire appel à super.onOnBrowserEvent vu que seuls les évènements enregistrés par sinkEvents seront envoyés à cette fonction.
Comportement
Le code qui déterminera le comportement du composant doit être séparé du composant lui même.
Pour cela définissons un contrat d’interface Presenter intégré dans notre widget :
[sourcecode language=”java”]
public TestWidget extends Composite implements HasText, HasClickHandlers {
…
public interface Presenter{
void update();
}
private Presenter presenter;
public Presenter getPresenter(){
return presenter;
}
public void setPresenter(Presenter presenter){
this.presenter = presenter;
}
…
}
[/sourcecode]
Le ou les implémentations de cette interface devront se faire dans des classes java séparéee pour pouvoir être utilisées par d’autres composants
[sourcecode language=”java”]
public class DefaultPresenter implements TestWidget.Presenter{
private TestWidget testWidget;
public DefaultPresenter(TestWidget testWidget){
this.testWidget = testWidget;
}
@Override
public void update(){
}
public TestWidget getTestWidget(){
return testWidget;
}
}
[/sourcecode]
NB : l’instance du composant est passée au constructeur
Spécifion maintenant une implementation par défaut à notre widget pour qu’elle soit directement exploitable (GWT Designer utilise le constructeur vide) et rajoutons un constructeur permetttant de spécifier une autre implémentation de l’interface :
[sourcecode language=”java”]
public TestWidget extends Composite implements HasText {
…
public TestWidget(){
this.presenter = getDefaultPresenter();
}
public TestWidget(Presenter presenter){
this.presenter = presenter;
}
protected Presenter getDefaultPresenter(){
return new DefaultPresenter(this);
}
…
}
[/sourcecode]
Passons maintenant à la phase d’architecture logicielle proprement dite en utilisant les classe abstraites et les génériques pour rendre le composant le plus évolutif posssible :
[sourcecode language=”java”]
/* Comportement commun toutes les widget (composite ou panel) */
public interface WidgetPresenter {
void update();
void onWidgetAttach();
void onWidgetDetach();
void onBrowserEvent(Event event);
}
[/sourcecode]
[sourcecode language=”java”]
/* Classe abstraite pour les composants de type composite */
public abstract class AbstractComposite<P extends AbstractComposite.Presenter> extends Composite {
public interface Presenter extends WidgetPresenter {
}
private P presenter;
public P getPresenter(){
return presenter;
}
public void setPresenter(P presenter){
this.presenter = presenter;
initWidget();
}
public AbstractComposite(){
this.presenter = getDefaultPresenter();
initWidget();
}
public AbstractComposite(P presenter){
this.presenter = presenter;
initWidget();
}
protected abstract void initWidget();
protected abstract P getDefaultPresenter();
protected void update(){
getPresenter().update();
}
protected void autoUpdate(){
if(!GWT.isProdMode()){
update();
}
}
@Override
public void onAttach(){
super.onAttach();
getPresenter().onWidgetAttach();
}
@Override
public void onDetach(){
super.onDetach();
getPresenter().onWidgetDetach();
}
@Override
public void onBrowserEvent(Event event){
getPresenter().onBrowserEvent(event);
}
}
[/sourcecode]
NB : les méthodes liées au comportement sont maintenant déléguées au Presenter et non plus implémentées dans le composant.
Notre widget est ainsi simplifiée :
[sourcecode language=”java”]
public class TestWidget extends AbstractComposite implements HasText, HasClickHandlers{
private static TestWidgetUiBinder uiBinder = GWT
.create(TestWidgetUiBinder.class);
interface TestWidgetUiBinder extends UiBinder {
}
public interface Presenter extends AbstractComposite.Presenter{
}
@Override
protected void initWidget() {
initWidget(uiBinder.createAndBindUi(this));
}
@UiField
Button button;
private String prop1;
public enum EnumTest{Choix1, Choix2};
private EnumTest prop2;
public void setText(String text) {
button.setText(text);
}
public String getText() {
return button.getText();
}
public void setProp1(String prop1) {
this.prop1 = prop1;
autoUpdate();
}
public String getProp1(){
return prop1;
}
public void setProp2(EnumTest prop2){
this.prop2 = prop2;
autoUpdate();
}
public EnumTest getProp2(){
return this.prop2;
}
public TestWidget(){
super();
}
public TestWidget(Presenter presenter){
super(presenter);
}
@Override
protected Presenter getDefaultPresenter(){
return new DefaultPresenter(this);
}
@Override
public HandlerRegistration addClickHandler(ClickHandler handler) {
return addHandler(handler, ClickEvent.getType());
}
}
[/sourcecode]
et pour finir notre classe de gestion du comportement :
[sourcecode language=”java”]
public class DefaultPresenter implements TestWidget.Presenter{
private TestWidget testWidget;
public DefaultPresenter(TestWidget testWidget){
this.testWidget = testWidget;
}
public TestWidget getTestWidget(){
return testWidget;
}
@Override
public void update() {
if(EnumTest.Choix1.equals(getTestWidget().getProp2())){
getTestWidget().setText(getTestWidget().getProp1());
} else {
getTestWidget().setText("");
}
}
@Override
public void onWidgetAttach() {
getTestWidget().sinkEvents(Event.ONCLICK);
}
@Override
public void onWidgetDetach() {
getTestWidget().unsinkEvents(Event.ONCLICK);
}
@Override
public void onBrowserEvent(Event event) {
switch (DOM.eventGetType(event)){
case Event.ONCLICK: ClickEvent.fireNativeEvent(event, getTestWidget());
break;
}
}
}
[/sourcecode]
Voila notre widget est opérationnelle et peut être utilisée comme n’importe quelle widget GWT standard !
Les sources sont disponibles sous notre googlecode (http://ineat-conseil.googlecode.com/svn/blog/gwt/Widget)
J’en conviens, pour un résultat aussi simple le contenu de cet article peut paraître une usine à gaz :), mais sous une forme ou l’autre la mise en place d’une architecture solide pour la gestion de vos composants s’avèra nécessaire.