Gestion des évènements (Wicket 1.5+)


Wicket supporte les événements !

Depuis la version 1.5, il est désormais possible d'utiliser un mécanisme d’événements pour faire communiquer différents composants de l'application. Grâce à une API simple, il est désormais possible à n'importe quel composant Wicket d’émettre ou de recevoir un événement, quelle que soit sa position dans l'arbre des composants.

Quel besoin avons nous en Wicket d'un tel mécanisme d’événements ?

Suppression du couplage Parent / Enfant

Quand nous développons des composants, nous souhaitons en général qu'ils soient réutilisables, et complètement indépendants. En effet, si un composant a besoin d'appeler une méthode de son parent, il est préférable d'éviter d'utiliser une référence ce parent. Si cette règle n'est pas respectée, on parle alors d'un couplage fort entre les deux composants. Pour éviter cela, la solution la plus simple est de passer un objet Callback à notre composant. Le contrat du Callback, ou méthode de rappel, instanciée par le composant Parent (en implémentant de façon anonyme une interface) , et est appelé par le composant Enfant.

Voici un exemple sur l'application ZenContact :

Cette application met en œuvre un ListContactsPanel qui affiche une liste de Contacts, et un SearchPanel, qui permet à l'utilisateur de rechercher un contact.

SearchPanel

Le formulaire du SearchPanel permet de soumettre une chaîne de caractère qui va être utile pour trier la liste des contacts. Lorsque que le formulaire est soumis, il est donc nécessaire de passer cette chaîne en paramètre à la page ListContactPage. Pour cela, nous allons créer une interface SearchCallback :

public interface SearchCallback {
	public void onSearch(String searchString);
}

La méthode onSearch doit être appelée à la soumission du formulaire c'est pourquoi on passe un SearchCallback à notre SearchPanel :

public SearchPanel(String id, SearchCallback searchCallback) {
	super(id);
	this.searchCallback = searchCallback;
	add(new SearchForm("searchForm"));
}
 
private class SearchForm extends Form<String> {
 
	private static final long serialVersionUID = 1L;
	private TextField<String> searchString;
 
	public SearchForm(String id) {
		super(id);
		searchString = new TextField<String>("searchString", new Model<String>(""));
		add(searchString);
	}
 
	@Override
	public void onSubmit() {
		searchCallback.onSearch(searchString.getValue());
	}
}

Enfin, il ne reste plus qu'à récupérer le paramètre dans le composant Parent, ici la ListContactsPage :

public class ListContactsPage extends ZenContactBasePage {
 
	public ListContactsPage(PageParameters params) {
		final String searchString = params.get("searchString").toString();
 
		add(new ListContactsPanel("contactsPanel", searchString));
		add(new SearchPanel("searchPanel", new SearchCallback() {
			@Override
			public void onSearch(String searchString) {
				PageParameters params = new PageParameters();
				params.add("searchString", searchString);
				setResponsePage(ListContactsPage.class, params);
			}
		}));
	}
}

Cependant, cette solution ne supprime qu'un seul couplage entre le parent et l'enfant. Et si jamais le SearchPanel était inclus dans un Panel intermédiaire, il faudrait alors passer cet objet Callback dans toute la hiérarchie des composants.

Utilisation du bus d'évènements

La version 1.5 de Wicket répond à une grande demande de la communauté : la gestion native des événements dans le Framework. L'API est assez simple à mettre en place. D'un côté, n'importe quel Composant est capable d'envoyer un event avec la méthode send, de l'autre, n'importe quel composant est capable d'écouter cet événement en overridant la méthode onEvent.

Petit exemple avec notre application ZenContact et notre SearchPanel:

Tout d'abord, créons l'objet qui sera émis par l'évènement. Cela nous permet de passer un contexte à l'événement; dans notre cas, la searchString :

public class SearchEvent {
	private final String value;
 
	public SearchEvent(String value) {
		this.value = value;
	}
 
	public String getValue() {
		return value;
	}
}

Modifions la méthode onSubmit de notre formulaire pour qu'elle envoie un l'évènement à toute l'application. Les paramètres de la méthode seront expliqués plus tard.

public class SearchPanel extends Panel {
 
	public SearchPanel(String id) {
		...
	}
 
	private class SearchForm extends Form<String> {
 
		public SearchForm(String id) {
			...
		}
 
		@Override
		public void onSubmit() {
			send(getPage(), Broadcast.BREADTH, new SearchEvent(searchField.getValue()));
		}
	}

Enfin, modifions directement notre composant ListContactPanel pour qu'il modifie la liste de contacts. Il suffit d'Overrider la méthode onEvent, ce qui nous permet de récupérer notre SearchEvent.

public class ListContactsPanel extends Panel {
 
	private String searchString;
 
	@Override
	public void onEvent(IEvent<?> event) {
		super.onEvent(event);
		if (event.getPayload() instanceof SearchEvent) {
			SearchEvent searchEvent = (SearchEvent) event.getPayload();
			searchString = searchEvent.getValue();
		}
	}
 
	public ListContactsPanel(String id, String searchStringParam) {
		super(id);
 
		//Ici, nous passons un LoadableDetachableModel à notre ListView pour que notre service 
		//de recherche des contacts soit appelé à chaque fois que le composant est rendu
		IModel<List<Contact>> contactsModel = new LoadableDetachableModel<List<Contact>>() {
 
			@Override
			protected List<Contact> load() {
				List<Contact> contacts = MockContactsFinder.find(searchString);
				return contacts;
			}
		};
		ListView<Contact> contactsView = new ListView<Contact>("contactsList", contactsModel ) {
			@Override
			protected void populateItem(ListItem<Contact> item) {
				...
			}
		};
		this.add(contactsView);
	}
 
}

Vous remarquerez que ce n'est plus la page qui récupère l'événement, mais le Composant lui même.

Qui peut recevoir l'événement ?

Tout cela a l'air bien simple, mais qu'en est-il des autres paramètres disponibles lors de l'envoi de l'événement ?

En effet, l'API nous permet de préciser quels composants vont pouvoir recevoir l'event, et dans quel ordre.

Send Event

Le premier argument nous permet de spécifier quel est l'élément de plus haut niveau qui sera capable de recevoir l'événement. Si jamais nous nous situons au niveau de l'Application, alors tous les composants pourront récupérer l'événement. Tout d'abord, essayons d'écouter l'événement au niveau de l'Application, de la Session, de la Page, du SearchPanel, et du ListContactsPanel en ajoutant onEvent dans toutes ces classes :

@Override
public void onEvent(IEvent<?> event) {
	if (event.getPayload() instanceof SearchEvent) {
		System.out.println("Event received in " + getClass());
	}
}

Si nous l'envoyons au niveau de l'application, tous les composants le recevront :

send(getApplication(), Broadcast.BREADTH, new SearchEvent(searchField.getValue()));
Event received in class com.zenika.training.zencontact.wicket.application.ZenContactApplication
Event received in class com.zenika.training.zencontact.wicket.application.ZenContactSession
Event received in class com.zenika.training.zencontact.wicket.pages.ListContactsPage
Event received in class com.zenika.training.zencontact.wicket.panels.ListContactsPanel
Event received in class com.zenika.training.zencontact.wicket.panels.SearchPanel

Si nous limitons l'envoi à la page, alors la Session et l'Application ne recevront pas l'événement :

send(getPage(), Broadcast.BREADTH, new SearchEvent(searchField.getValue()));
Event received in class com.zenika.training.zencontact.wicket.pages.ListContactsPage
Event received in class com.zenika.training.zencontact.wicket.panels.ListContactsPanel
Event received in class com.zenika.training.zencontact.wicket.panels.SearchPanel

Ordre de traversée

Le deuxième argument de la méthode send est l'ordre de traversée de l'event. En effet, il est possible de préciser quatre différents ordres de traversée. Selon ce que vous choisissez, les méthodes onEvent de chaque classe ne seront alors pas appelées dans le même ordre, ou même pas appelées du tout.

Broadcast.BREADTH va traverser toute l'application du niveau le plus haut, jusqu'aux composants
Event received in class com.zenika.training.zencontact.wicket.application.ZenContactApplication
Event received in class com.zenika.training.zencontact.wicket.application.ZenContactSession
Event received in class com.zenika.training.zencontact.wicket.pages.ListContactsPage
Event received in class com.zenika.training.zencontact.wicket.panels.ListContactsPanel
Event received in class com.zenika.training.zencontact.wicket.panels.SearchPanel
Broadcast.DEPTH effectue l'ordre de traversée inverse : des composants vers l'application
Event received in class com.zenika.training.zencontact.wicket.panels.ListContactsPanel
Event received in class com.zenika.training.zencontact.wicket.panels.SearchPanel
Event received in class com.zenika.training.zencontact.wicket.pages.ListContactsPage
Event received in class com.zenika.training.zencontact.wicket.application.ZenContactSession
Event received in class com.zenika.training.zencontact.wicket.application.ZenContactApplication
Broadcast.BUBBLE va traverser les composants dans le même ordre que Broadcast.DEPTH, mais seulement la cible et ses parents seront notifiés
send(SearchPanel.this, Broadcast.BUBBLE, new SearchEvent(searchField.getValue()));
Event received in class com.zenika.training.zencontact.wicket.panels.SearchPanel
Event received in class com.zenika.training.zencontact.wicket.pages.ListContactsPage
Event received in class com.zenika.training.zencontact.wicket.application.ZenContactSession
Event received in class com.zenika.training.zencontact.wicket.application.ZenContactApplication

On remarque ici que le ListContactsPanel n'a pas été appelé. En effet, ce n'est pas un parent du SearchPanel

Broadcast.EXACT : comme son nom l'indique, ceci ne notifie que le composant passé dans le premier paramètre (La page, l'application, la session, soi-même, ou encore un autre composant).
send(SearchPanel.this, Broadcast.EXACT, new SearchEvent(searchField.getValue()));
Event received in class com.zenika.training.zencontact.wicket.panels.SearchPanel

À suivre : un exemple d'utilisation des Evénts avec Ajax...


Commentaires

1. Le Mardi 13 décembre 2011, 09:55 par dutrieux

Super, très intéressante.

Fil des commentaires de ce billet