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 :

  1. définition des propriétés
  2. mise en place du gestionnaire d’évènements
  3. Implémentation du comportement

Pour illustrer la mise en oeuvre de ces phases, crééons tout d’abord à l’aide d’UiBinder :

UiBinder

Ce qui nous génère le code suivant :

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();
	}

}

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

public TestWidget extends Composite implements HasText {

	...

	private String prop1;

	public void setProp1(String prop1){
		this.prop1 = prop1;
	}

	public String getProp1(){
		return prop1;
	}
}

En rajoutant une propriété à un classe qui hérite de Composite ou Panel, on peut ainsi l’éditer dans GwtDesigner :

Propriété simple

A noter qu’on peut également utiliser des enums

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;
	}
}

qui seront proposées sous forme de liste déroulante :

Propriété enum

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 :

public void setProp1(String prop1){
	this.prop1 = prop1;
	button.setText(prop1);
}

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 à :

	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("");
		}
	}

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’ :

	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();
	}

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.

	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();
	}

Avec le code ci dessus le composant sera bien rafraichit en mode design :

test widget

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)

public class TestWidget extends Composite implements HasText, HasClickHandlers {

...

	@Override
	public HandlerRegistration addClickHandler(ClickHandler handler) {
		// TODO Auto-generated method stub
		return null;
	}
}

L’évènement est désormais disponible sous GWT Designer :

click event

Dans un premier temps, redirigeons simplement le clic du bouton vers notre gestionnaire :


	@Override
	public HandlerRegistration addClickHandler(ClickHandler handler) {
		return button.addClickHandler(handler);
	}

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.

	@Override
	public HandlerRegistration addClickHandler(ClickHandler handler) {
		return addHandler(handler, ClickEvent.getType());
	}

Puis dans les fonctions onAttach et onDetach, inscrivons et désincrivons l’évènement onClick

	@Override
	public void onAttach(){
		super.onAttach();

		sinkEvents(Event.ONCLICK);
	}

	@Override
	public void onDetach(){
		super.onDetach();

		unsinkEvents(Event.ONCLICK);
	}

Finalement, récupérons l’évènement dans la méthode ‘onBrowserEvent’ et redirigeons le vers notre gestionnaire via ‘fireEvent’ :

	@Override
	public void onBrowserEvent(Event event){

		switch (DOM.eventGetType(event)) {
	        case Event.ONCLICK:	ClickEvent.fireNativeEvent(event, this);
	        			     	break;
	    }
	}

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 :

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;
	}

	...
}

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

	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;
		}

	}

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 :

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);
	}

	...
}

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 :

/* Comportement commun toutes les widget (composite ou panel) */

public interface WidgetPresenter {

	void update();

	void onWidgetAttach();

	void onWidgetDetach();

	void onBrowserEvent(Event event);
}
/* 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);
	}
}

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 :

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());
	}
}

et pour finir notre classe de gestion du comportement :

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;
		 }
	}
}

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.