Création d’une application de type CRUD avec Wicket

Cet article va présenter le framework Apache Wicket et ce en s’appuyant sur la création d’une application de suivi de bugs (Bug tracker) qu’on nommera “Min.Bug.Tra”. Une telle application est un cas particulier de ce qu’on appèle CRUD (Create, Read, Update et Delete) mais qui a l’avantage de traiter une problématique réelle.

1. Installation et Configuration

On commence par créer une application web Java. Maven, ou encore les IDEs tel que Eclipse et Netbeans génèrent automatiquement la structure de telles applications. Dans tous les cas, vous pouvez partir sur le projet maven fourni avec cet article sous forme de fichier zip ou via la page du projet sur GitHub.

Il faut ensuite configurer cette application pour pouvoir utiliser Wicket. Pour ce faire, il suffit d’ajouter la déclaration d’un filtre dans le descripteur de l’application web web.xml:

<filter>
	<filter-name>min-bug-tra</filter-name>
	<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
	<init-param>
		<param-name>applicationClassName</param-name>
		<param-value>min.bug.tra.web.MinBugTraApp</param-value>
	</init-param>
	<init-param>
		<param-name>configuration</param-name>
		<param-value>development</param-value>
	</init-param>
</filter>
<filter-mapping>
	<filter-name>min-bug-tra</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

Ce filtre s'attend à ce qu'on lui indique le nom qualifiée d'une classe Java qui représente l'application Wicket dont voici le code:

public class MinBugTraApp extends WebApplication {

	@Override
	public Class<? extends WebPage> getHomePage() {
		return null;
	}

}

Cette classe, qui doit étendre WebApplication, permet de configurer plusieurs aspects de l'application qu'elle représente, comme la page d'accueil par exemple.
Ceci se fait en implémentant la méthode getHomePage et en retournant la classe Java de la page d'accueil de l'application (on retourne null ici car on n'a pas encore crée aucune page Wicket).

Notez aussi qu'il vous est possible de générer directement une application Wicket fonctionelle via l'archetype quickstart de Wicket.

2. La couche métier (sort of)

2.1. L'objet persistant

Dans le cadre de cet article, on se contentera d'une seule entité représentant un bug, ou plus généralement une tache, qui peut être soit un bug (une anomalie de fonctionnement), une amélioration d'une fonctionnalité existante ou enfin une nouvelle fonctionnalité. Cette entité est nommée Task et est composée des champs suivants:

id
représente l'identifiant de la tache dans la base de données
title
le titre de la tache
description
un texte qui donne plus de détails sur une tache
type
le type de la tache. C'est une énumération qui représente les 3 types des taches : bug, amélioration ou nouvelle fonctionnalité
status
l'état d'une tache. C'est une énumération qui représente les 2 états des taches: ouverte ou fermée.

Voici le code source Java de cette entité:

public class Task {
	public static enum TaskType {
		BUG, ENHANCEMENT, NEW_FEATURE;
	}

	public static enum TaskStatus {
		OPEN, CLOSED;
	}

	private Long id;
	private String title;
	private String description;
	private TaskType type;
	private TaskStatus status;

	//getters et setters
}

2.2. La couche d'accès aux données

Dans le cadre de cet article, on ne va pas s'intéresser à l'implémentation concrète de la couche d'accès aux données (basée sur JDBC dans ce cas ci), car le point sera plutôt mis sur Wicket, on se contentera donc de lister les déclarations des fonctionnalités qu'elle expose :

public class TaskRepository {
	public List<Task> selectAll() {...}

	public Task selectById(Long id) {...}

	public Task insert(Task task) {...}

	public boolean update(Task task) {...}

	public boolean deleteById(Long id) {...}
}

  • selectAll qui retourne la liste de toutes les taches de la base de données
  • selectById qui retourne la tache avec l'identifiant spécifié
  • insert qui insère une nouvelle tache dans la base de données
  • upate qui met à jour une tache existante
  • deleteById qui supprime la tache avec l'identifiant spécifié

3. Philosophie de Wicket

Une application web développée avec le framework Wicket est constituée d'un ensemble de pages. Une page est définie par une classe Java et une page HTML portqnt le même nom. Une page est elle même construite avec un ensemble de composants organisées de façon hiérarchique (un composant peut contenir d'autres composants, et ainsi de suite). Chacun de ces composants est nécessairement associé à une balise (<div>, <input>, etc.) dans la page HTML et ce via un identifiant (qui devrait être le même dans la balise HTML et dans le composant Java).

Certains composants manipulent directement leur balise associée dans le fichier HTML de leur parent (qui peut être une page ou un autre composant), comme par exemple le composant Label qui permet de modifier le contenu textuel de sa balise.
D'autres composants ont leur propre HTML (dans un fichier à part) qu'ils injectent (après transformation) dans leur balise associée dans le HTML de leur parent, comme par exemple le compoant TabbedPanel qui insère son markup (les onglets) dans sa balise.

Avec wicket, la partie présentation est du HTML pur (ou presque): il n'y a aucune logique ni de langage de templating à la JSTL ou Freemarker par exemple (affichage conditionnel, itération, valeurs dynamiques, etc.). Toute la logique d'affichage et de contrôle se trouve dans le code Java des pages et des composants.

Dans la suite de cet article, les moyens concrets de construction de pages web avec Wicket seront montrées au fur et à mesure de l'implémentation des différentes fonctionnalités de l'application "Min.Bug.Tra".

4. La page Listing

On va commencer par la page de listing qui permet, comme son nom l'indique, de lister l'ensemble des taches stockées dans la base de données.

Pour ce faire, on commence par créer une classe Java ListingPage qui étend WebPage ainsi qu'une page html portant le même nom (mais avec l'extension .html) dans le même package (Mais pas nécessairement dans le même dossier sur le disque, comme c'est le cas avec Maven, où les fichiers html, css, properties ou autres fichiers non java vont dans le dossier src/main/resources)

Voici le bout de HTML qu'on va utiliser pour représenter une tache:

<div class="task">
	<img class="type" src="type.png" />
	<h2>[titre]</h2>
	<p>[description]</p>
</div>

Dans ce cas ci, on désire:

  1. répéter le div pour chaque tache récupérée de la base.
  2. Changer l'image utilisée selon te type de la tache (un cercle rouge pour un bug, un cercle bleu pour une amélioration et un cercle vert pour une nouvelle fonctionnalité)
  3. Placer le titre de la tache dans la balise <h2>
  4. Placer la description de la tache dans la balise <p>
  5. Ce n'est pas visible dans l'extrait HTML, mais pour une tache à l'état fermée, on désire ajouter la classe closed au <div> parent pour que le rendu visuel de ces taches puisse être modifié via CSS.

Il faut donc, comme dit plus haut, associer les balises qu'on désire agir dessus à des composants Wicket qui eux appliqueront la logique de l'affichage.

Cette association se fait via l'attribut spécial wicket:id qu'il faut ajouter aux balises concernées, comme le montre cet extrait:

<div wicket:id="task" class="task">
	<img wicket:id="type" class="type" />
	<h2 wicket:id="title">[titre]</h2>
	<p wicket:id="description">[description]</p>
</div>

Dans la classe ListPage.java, et dans le constructeur de cette classe, on construit le graphe des composants correspondant aux balises du HTML.

Pour la balise <div> qu'on désire répéter, Wicket propose plusieurs composants répéteurs (qui permettent de répéter une balise HTML autant de fois qu'indiqué).

Un tel composant est ListView à qui on passe l'identifiant (spécifié avec l'attribut wicket:id de la balise sur la quelle il va agir ainsi qu'un modèle qui retourne une liste d'éléments qu'il va itérer dessus.

Dans ce cas ci, l'identifiant de la balise div est "task" et la liste d'éléments est la liste des taches qu'on retrouve de la base de données.

public class ListPage extends WebPage {
	public ListPage() {
		IModel<List<Task>> listModel = new LoadableDetachableModel<List<Task>>() {
			@Override
			protected List<Task> load() {
				return new TaskRepository().selectAll();
			}
		};
		ListView<Task> list = new ListView<Task>("task", listModel) {

			@Override
			protected void populateItem(final ListItem<Task> item) {
				Task t = item.getModelObject();

			}
		};
		add(list);
	}
}

Comme le montre l'extrait de code posté plus haut, on commence par créer un modèle qui retourne toutes les taches de la base de données. Les modèles sont un concept central à Wicket et nécessitent de ce fait un article dédié, chose que j'ai déjà faite dans mon article Exploration des modèles de Wicket.

Dans ce cas particulier, on a choisi l'implémentation LoadableDetachableModel qui permet de :

  • Retrouver une copie fraîche de la liste des taches à chaque rafraîchissement de la page
  • Ne pas stocker cette liste dans la session

Par la suite, on instancie un composant de type ListView, en passant à son constructeur l'identifiant "task" ainsi que le modèle des taches.

La classe ListView est une classe abstraite, et pour l'instancier on doit implémenter la méthode populateItem.

Pour chaque élément de la liste qu'on lui a passé en paramètre, ListView duplique le contenu de la balise HTML qui lui est associée puis appelle la méthode populateItem en lui passant un objet de type ListItem qui permet de manipuler son contenu.

Dans ce cas ci, on désire renseigner le contenu des balise <h2> avec le titre de la tache et <p> avec sa description. Pour ce faire, on peut utiliser le composant Label de Wicket qui permet justement de renseigner le contenu textuel d'une balise:

item.add(new Label("title", t.getTitle()));
item.add(new Label("description", t.getDescription().replace(
		"\n", "<br />")).setEscapeModelStrings(false));
item.add(new Image("type", t.getType() == TaskType.BUG ? 
				BUG_IMAGE
				: t.getType() == TaskType.ENHANCEMENT ?
					ENHANCEMENT_IMAGE
					: NEW_FEATURE_IMAGE));
if (t.getStatus() == TaskStatus.CLOSED) {
	item.add(new SimpleAttributeModifier("class", "task closed"));
}

Pour la description, on applique un traitement supplémentaire qui consiste à remplacer les retours à la ligne \n par des balises <br> pour les garder dans le rendu HTML. Par défaut, le composant Label échappe les caractères spéciaux (comme <, > , etc.). Ceci peut être désactivé avec la méthode setEscapeModelStrings.

En ce qui regarde l'image qui représente le type de la tache, on peut utiliser le composant Image de Wicket qui permet de manipuler les balises image. Pour ce faire, on commence par créer des références (pour des raisons de performance, on ne les crée qu'une seule fois comme des champs statiques et finaux dans la page qui les utilise, ListPage ici) vers les 3 images qui représentent les types d'une tache :

private static final ResourceReference BUG_IMAGE = new ResourceReference(
		TaskTypeImage.class, "pics/circle_red.png");
private static final ResourceReference ENHANCEMENT_IMAGE = new ResourceReference(
		TaskTypeImage.class, "pics/circle_blue.png");
private static final ResourceReference NEW_FEATURE_IMAGE = new ResourceReference(
		TaskTypeImage.class, "pics/circle_green.png");

Ensuite, dans populateItem, on crée le composant Image en lui passant l'identifiant wicket de la balise <img> ainsi que la référence de l'image, qui est déterminée selon le type de la tache.

Reste maintenant à ajouter la classe "closed" au <div> pour les taches à l'état fermée. Ceci peut être accompli avec un autre concept de Wicket qui est la notion des Behaviors. Ces derniers s'attachent à un composant et en changent le fonctionnement.
Par exemple, le behavior SimpleAttributeModifier permet de changer la valeur d'un attribut de la balise HTML associée au composant auquel il appartient.
Dans ce cas ci, on l'ajoute au <div> quand la tache est à l'état fermée et on passe à son constructeur le nom de l'attribut "class" ainsi que la valeur "task closed".

Maintenant que la page est crée, il ne faut pas oublier de modifier la méthode getHomePage de l'application Wicket pour faire en sorte qu'elle retourne ListPage.class au lieu de null.

Après avoir inséré quelques données de tests, voici de quoi le résultat de cette page a l'air:

Pour lancer l'application avec maven, il faut exécuter la commande suivante dans le dossier contenant le fichier pom.xml:

mvn jetty:run

Puis ouvrir cette adresse dans un navigateur : http://localhost:8080/

5. La page de création

Cette page va permettre d'ajouter une nouvelle tache via un formulaire que l'utilisateur remplira.

On commence donc par créer une page HTML NewPage.html dont voici le code:

<form wicket:id="form">
	<ul>
		<li>Type</li>
		<li>
			<select wicket:id="type" type="text" name="type"></select>
		</li>
		
		<li>Titre</li>
		<li class="full"><input wicket:id="title" type="text" ></li>
	
		<li>Description</li>
		<li class="full">
			<textarea wicket:id="description" cols="76" rows="5" name="description"></textarea>
		</li>
		
		<li><input wicket:id="submit" value="ajouter()" type="submit"></li>
	</ul>
</form>

Le formulaire HTML montré plus haut est composé de :

  • Une liste déroulante pour spécifier le type de la tache : bug, amélioration ou nouvelle fonctionnalité
  • Un champ de saisi pour renseigner le titre de la tache
  • Une zone de saisi pour renseigner la description de la tache
  • Un bouton de soumission

On n'a pas prévu, délibérément, un moyen pour spécifier l'état d'un tache (ouvert ou fermé) car on a estimé qu'une nouvelle tache est obligatoirement à l'état ouvert à sa création.

Notez qu'on associe au formulaire ainsi qu'aux différents champs des identifiants wicket pour pouvoir les associer à des composants Wicket dans la partie Java.

Notez aussi qu'on ne renseigne pas la méthode ni l'action du formulaire, ni encore les différentes options de la liste déroulante: ces détails sont gérés par la partie Java.

Pour implémenter cette dernière, le point délicat est (encore) les modèles.
En effet, les différents champs doivent être attachés à un modèle pour qu'on puisse récupérer les valeurs saisies par l'utilisateur, ou encore le sens inverse, c'est à dire fournir les valeurs par défaut de ces champs (utile pour l'édition d'une tache par exemple).

La partie Java va consister en une classe NewPage donc voici le code:

public class NewPage extends WebPage {

	public NewPage() {
		IModel<Task> model = new CompoundPropertyModel<Task>(
				new LoadableDetachableModel<Task>() {

					@Override
					protected Task load() {
						return new Task(null, "", "", TaskType.BUG,
								TaskStatus.OPEN);
					}
				});

		Form<Task> form = new Form<Task>("form", model) {
			@Override
			protected void onSubmit() {
				Task p = getModelObject();
				TaskRepository dao = new TaskRepository();
				dao.insert(p);
				setResponsePage(ListPage.class);
			}

		};

		DropDownChoice<TaskType> taskType = new DropDownChoice<TaskType>(
				"type", Arrays.asList(TaskType.values()), TASK_TYPE_RENDERER);
		form.add(taskType);

		TextField<String> titleField = new TextField<String>("title");
		titleField.setRequired(true);

		form.add(titleField);

		TextArea<String> descriptionField = new TextArea<String>("description");
		form.add(descriptionField);

		Button submit = new Button("submit");
		form.add(submit);

		add(form);
	}
}

On commence par créer un modèle encapsulant une tache. L'implémentation choisie est CompoundPropertyModel, qui une fois associé au formulaire, va fournir automatiquement un modèle pour chacun de ses champs, ce qui est bien moins verbeux et simple à coder que de créer et fournir à chacun des champs sont propre modèle encapsulant le champ de la classe tache auquel il correspond. De nouveau, veuillez consulter cet article qui présente en détail la notion de modèles de Wicket.

Pour que ce modèle fonctionne, il faut nécessairement que les identifiants Wicket des composants (ou encore des balises à correspondent aux champs de la classe Task auxquels ils correspondent).

Normalement, CompoundPropertyModel s'attend à ce qu'on lui passe l'objet dont on désire remplir les champs avec le formulaire dans son constructeur. Mais dans ce cas ci, on lui passe plutôt un autre modèle, un LoadableDetachableModel qui crée à la demande une nouvelle instance fraîche d'un tache.

Par la suite, on crée un formulaire (org.apache.wicket.markup.html.form.Form) en lui passant comme paramètre l'identifiant de la balise <form> associée ainsi que le modèle crée plus tôt. Il faut ensuite redéfinir la méthode onSubmit du formulaire pour gérer sa soumission.

Dans ce cas ci, c'est aussi simple que de récupérer l'instance d'un Task encapsulée par le modèle du formulaire dont Wicket a bien gentiment rempli les champs à partir de la saisie de l'utilisateur dans le formulaire et d'utiliser la classe TaskRepository pour l'insérer dans la base de données. On redirige enfin l'utilisateur vers la page de listing des taches via la méthode setResponsePage.

Dans la suite du code du constructeur de la page, on crée et on ajoute les différents composants du formulaire en commençant par la liste déroulante.

Pour ce faire, on a utilisé le composant DropDownChoice dont le constructeur prend comme arguments l'identifiant de la balise <select> associée, la liste des valeurs possibles qui dans ce cas sont les valeurs de l'énumération TaskType ainsi qu'un troisième argument qui permet de personnaliser l'affichage des options de la liste dont voici le code:

private static final IChoiceRenderer<TaskType> TASK_TYPE_RENDERER = new IChoiceRenderer<TaskType>() {

	public Object getDisplayValue(TaskType type) {
		switch (type) {
		case BUG:
			return "Bug";
		case NEW_FEATURE:
			return "Nouvelle fonctionnalité";
		case ENHANCEMENT:
			return "Amélioration";
		}
		return null;
	}

	public String getIdValue(TaskType type, int arg1) {
		return type.name();
	}
};

Ce que fait le code montré plus haut est d'associer à une valeur de TaskType l'intitulé de l'option à afficher ainsi que son identifiant qui sera utilisé pour renseigner l'attribut value des balises option générées.

On ajoute ensuite le composant liste déroulante au formulaire, et non pas à la page: il faut nécessairement garder la même hiérarchie dans le HTML et dans le code Java.

On crée ensuite un composant de type TextField qui représente un champ de saisie (<input type="text" />) et on indique que sa valeur est obligatoire avec la méthode setRequired. et on l'ajoute au formulaire.

Reste plus qu'à créer le composant correspondant à la zone de saisie de la description d'un tache, TextArea dans ce cas ci et on l'ajoute lui aussi au formulaire.

Enfin, on ajoute le formulaire lui même à la page.

Pour pouvoir accéder à cette page, on va ajouter un lien qui pointe dessus depuis la page de listing.

On commence par modifier le HTML de la page listing pour y ajouter une balise <a>:

<a wicket:id="link" href="#" class="nav">{ new(BUG|FEAT|ENH) }</a>

Notez qu'on a associé un identifiant wicket au lien et qu'on a pas renseigné la destination réelle de ce lien car ce sera géré plutôt dans la partie Java (dans le constructeur de la classe ListPage):

add(new BookmarkablePageLink<Void>("link", NewPage.class));

On instancie un composant de type BookmarkablePageLink qui correspond à un lien et on l'ajoute à la page.

Le constructeur de ce composant prend, en plus de l'usuel identifiant wicket de la balise associée, la classe de la page destination, NewPage dans ce cas ci. Lors de la génération de la page, Wicket s'occupe de renseigner l'attribut href du lien pour qu'il pointe vers la page de création d'une nouvelle tache.

Voici un aperçu de la page de création:

6. L'édition

Il s'agit ici de créer une page permettant d'éditer une tache existante, pour changer son intitulé par exemple, ou n'importe quel autre champ.

La page d'édition est, à quelques différences près, la même que la page de création d'une nouvelle tache à ces différences près:

  • Les différents champs devraient être renseignés par les valeurs des attributs correspondants de la tache associé au lieu d'être vides
  • L'état d'une tache devrait être présent et éditable
  • Le bouton de soumission devrait afficher "Mettre à jour" au lieu de "Ajouter"
  • La soumission du formulaire devrai mettre à jour la tache en cours d'édition au lieu d'en créer une nouvelle.

Pour réutiliser ce qu'on avait déjà codé et éviter de se répéter, on va adapter la page de création pour qu'elle puisse aussi gérer l'édition.

6.1. Passage d'information entre page

La mise en place de la page d'édition implique le passage d'une information de la page listing vers la page d'édition qui est la tache qu'on désire modifier.
De façon générale, on dispose de plusieurs moyens pour faire passer une information d'une page à une autre:

  • Encoder l'information à passer dans l'URL d'un lien: <a href="edit?task=5" par exemple. En accédant à la page d'édition avec une telle url, on peut récupérer l'identifiant de la tache à modifier. C'est cette méthode qui sera utilisé par la suite.
  • Utiliser un formulaire pour passer l'information qui sera généralement renseigné dans un champ de type hidden

La mise en place de cette solution nécessite d'abord de gérer les urls des pages. En effet, et par défaut, Wicket a
associé aux pages des urls codifiés et pas (très) lisibles. C'est cependant facilement réglable et ce via la notion de montage de pages, qui permet d'associer une URL propre et personnalisable à une page (cette notion gère aussi l'encodage des paramètres dans l'URL).

Wicket propose plusieurs stratégies de montage des pages, mais on se contentera de celle qui est la plus simple à mettre en place (BookmarkablePageRequestTargetUrlCodingStrategy pour les plus curieux).

Cette stratégie, monte une page (celle d'édition par exemple) sur un identifiant qu'on spécifie, "edit" par exemple, de sorte que la page soit accessible par l'URL "http://serveur:port//edit".
Les paramètres de la pages seront eux encodés à la suite comme couples "clé/valeur". Ainsi, on peut faire en sorte que la page d'édition de la tache avec l'identifiant 5 soit accessible avec l'URL "http://serveur:port//edit/task/5"

Pour mettre en place cette stratégie de montage, il faut redéfinir la méthode init dans la classe représentant l'application Wicket et dans celle-ci, appeler la méthode mountBookmarkablePage en lui passant comme arguments l'identifiant de la page ainsi que sa classe, comme le montre le code suivant:

mountBookmarkablePage("list", ListPage.class);
mountBookmarkablePage("edit", EditPage.class);

6.2. Modifications de la page de listing

On va ici ajouter un lien "modifier" à chaque tache qui à son activation, amène l'utilisateur vers la page d'édition de la tache sélectionnée.

Pour ce faire, on commence par modifier le HTML de la page de listing pour y ajouter le lien (dans le <div>):

<a wicket:id="editLink" class="editLink" href="#">modifier()</a>

Notez qu'on a associé un identifiant wicket au lien et qu'on a pas renseigné la destination réelle de ce lien qui sera gérée plutôt par la partie Java (et Wicket) car elle esr dépendante de la tache et de la stratégie d'encodage choisie.

Dans la partie Java, et plus précisément dans la méthode populateItem, on ajoute ce bout de code :

item.add(new BookmarkablePageLink<Void>("editLink",
		EditPage.class, new PageParameters(Collections
				.singletonMap("task", t.getId()))));

On instancie donc un composant de type BookmarkablePageLink qui correspond à un lien, et bien, "bookmarkable".

Le constructeur de ce composant prend, en plus de l'usuel identifiant wicket de la balise associée, la classe de la page destination, EditPage dans ce cas ci ainsi que les paramètres à passer à cette page qui seront encodés dans l'url du lien.
Les paramètres peuvent être spécifiés comme un Map où les clés sont les noms des paramètres et les valeurs représentent les valeurs de ces paramètres.

Dans ce cas-ci, on désire passer l'identifiant de la tache à modifier comme paramètre à la page d'édition avec le nom "task".

Wicket s'occupe d'évaluer la classe de la page destination, les paramètres à lui passer ainsi que la stratégie de montage configurée pour la page destination pour générer la valeur de l'attribut href du lien.

6.3. Refactoring de la page de création

Il faut maintenant réfactorer la page de création pour qu'elle puisse supporter l'édition.

Avant de commencer, on va renommer la page NewPage.java ainsi que NewPage.html en EditPage.java et EditPage.html.

Par la suite, le premier point à gérer est de récupérer l'identifiant de la tache à modifier. Pour ce faire, il suffit de (re)définir le constructeur de la page qui prend un PageParameters en argument. Quand la page est invoqué par une url contenant des arguments, Wicket va automatiquement renseigner ces arguments dans une instance de PageParameter et le passer comme argument au constructeur de la page de destination.

On ajoute donc à la classe EditPage un second constructeur dont voici le code :

public EditPage(final PageParameters params) {
	final Long taskId = params.getLong("task");
}

Dans ce constructeur, on récupère l'identifiant de la tache à modifier et on le place dans une variable taskId.

Pour gérer le fait que les champs doivent être renseignés par les valeurs des attributs correspondants de la tache en cours d'édition, il suffit de modifier le modèle qu'on passe au formulaire pour qu'il retourne la tache à modifier au lieu d'une nouvelle instance.

On va donc extraire le code qui crée le formulaire du premier constructeur et le placer dans une méthode qui prend le modèle à utiliser comme argument. Le premier constructeur qui sera appelé lors de la création d'une nouvelle tache va créer le modèle adapté (qui retourne une nouvelle instance de Task) et le passer à la méthode qui construit le formulaire. Idem pour le second constructeur.

public EditPage() {
	IModel<Task> model = new CompoundPropertyModel<Task>(
			new LoadableDetachableModel<Task>() {

				@Override
				protected Task load() {
					return new Task(null, "", "", TaskType.BUG,
							TaskStatus.OPEN);
				}
			});

	build(model, false);
}

public EditPage(final PageParameters params) {
	final Long taskId = params.getLong("task");

	IModel<Task> model = new CompoundPropertyModel<Task>(
			new LoadableDetachableModel<Task>() {
				@Override
				protected Task load() {
					return new TaskRepository().selectById(taskId);
				}
			});
	build(model, true);
}

private void build(IModel<Task> model, final boolean edit) {
	Form<Task> form = new Form<Task>("form", model) {
		@Override
		protected void onSubmit() {
			Task p = getModelObject();
			TaskRepository dao = new TaskRepository();
			if (edit)
				dao.update(p);
			else
				dao.insert(p);
			setResponsePage(ListPage.class);
		}

	};

Le modèle que crée le second constructeur ressemble à celui crée par le premier, excepté le fait qu'il retrouve la tache via son identifiant de la base de données.

Notez aussi que la méthode qui construit le formulaire prend un second argument de type booléen qui indique si l'on est en mode édition ou création, et suivant sa valeur, la méthode onSubmit du formulaire utilise TaskRepository pour soit créer, soit mettre à jour une tache.

On devrait aussi ajouter un moyen pour contrôler l'état de la tache (ouvert ou fermé). On va utiliser des boutons radio pour ce faire.
On commence par modifier le HTML de la page pour ajouter ceci:

<li>Etat</li>
<li wicket:id="status"></li>

L'idée serait de répéter le second li pour chaque état possible d'une tache, et de créer un bouton radio dedans.

Notez qu'on a pas crée d'input car comme pour le statut d'un tache, c'est la partie Java qui spécifie les options possibles.

Dans la méthode build, on ajoute ceci:

RadioChoice<TaskStatus> rc = new RadioChoice<TaskStatus>("status",
		Arrays.asList(TaskStatus.values()), TASK_STATUS_RENDERER);
rc.setEnabled(edit);
form.add(rc);

On a donc crée un composant de type RadioChoice dont le constructeur, tout comme DropDownChoice, prend en argument et en plus de l'identifiant wicket de la balise associée la liste des valeurs possibles (les valeurs de l'énumération TaskStatus) ainsi qu'un troisième argument qui contrôle l'affichage des options dont voici le code:

private static final IChoiceRenderer<TaskStatus> TASK_STATUS_RENDERER = new IChoiceRenderer<TaskStatus>() {

	public Object getDisplayValue(TaskStatus type) {
		switch (type) {
		case OPEN:
			return "Ouvert";
		case CLOSED:
			return "Fermé";
		}
		return null;
	}

	public String getIdValue(TaskStatus type, int arg1) {
		return type.name();
	}

};

Comme dit plus tôt, on veut empêcher l'utilisateur de contrôler l'état d'une tache lors de sa création. On désactive donc les boutons radios dans le cas où l'on est en mode création via la méthode setEnabled.

Reste plus qu'à contrôler l'intitulé du bouton de soumission pour qu'il affiche "ajouter" en mode création et "mettre à jour" en mode édition.

Pour ce faire, on utilise un autre constructeur du composant Button qui prend comme second argument un modèle qui retourne son intitulé :

Button submit = new Button("submit", new Model<String>(
		edit ? "MettreAJour()" : "Ajouter()"));

7. La suppression

Il s'agit ici de donner un moyen pour pouvoir sélectionner et supprimer une tache depuis la page de listing.
On va opter par la méthode qui consiste à ajouter un lien "supprimer" dans chaque ligne de la liste des taches.

On va donc modifier la page html ListingPage.html pour ajouter un lien "supprimer" dans le <div> représentant une tache, ce qui donne ceci (ListPage.html):

<a wicket:id="deleteLink" class="deleteLink" href="#">_supprimer()</a>

Dans la partie Java de la page, et plus précisément dans le code de la méthode populateItem du composant ListView, on ajoute le composant Wicket associé à ce lien.

Link<Long> deleteLink = new Link<Long>("deleteLink",
		new Model<Long>(t.getId())) {
	@Override
	public void onClick() {
		new TaskRepository().deleteById(getModelObject());
	}
};
deleteLink.add(new SimpleAttributeModifier("onclick",
		"return confirm('Supprimer ["
			+ t.getTitle().replace("\"", "\\\"")+ "] ?');"));
item.add(deleteLink);

Le composant utilisé ici est org.apache.wicket.markup.html.link.Link qui diffère de BookmarkablePageLink montré précedemment dans la mesure où il ne redirige pas vers une autre page mais invoque plutôt sa méthode onClick.

Pour le construire, on lui passe l'usuel identifiant wicket de la balise associée mais aussi un modèle qui contient l'identifiant de la tache à supprimer quand il est activé.

Cet identifiant est récupéré dans la méthode onClick et est passée à la méthode deleteById de TaskRepository.

On a aussi ajouté le Behavior SimpleAttributeModifier pour renseigner l'attribut onclick de la balise <a> de sorte qu'en cliquant sur le lien, une boite de confirmation est affichée à l'utilisateur lui demandant de confirmer s'il veut vraiment supprimer la tache.

A l'exécution, ceci donne:

8. Code source

Le code source complet de l'application décrite dans cet article est disponible sur Github sous la forme d'un projet maven et sous la licence MIT.
Il vous est aussi possible de récupérer directement les sources sous forme d'un fichier zip.

9. Conclusion

Bien que plusieurs aspects du framework web Apache Wicket ont été présentés dans cet article, ce n'est en aucun cas une référence complète, et plusieurs autres possibilités n'ont pas été traitées, telque la création de nouveaux composants, la gestion de l'Ajax (très facile à mettre en oeuvre et ne nécessitant pas d'écrire du Javascript) ou encore la multitude des autres composants standard.

Cet article est aussi publié dans Yes Wicket!

10 Responses to Création d’une application de type CRUD avec Wicket

  1. Pingback: Création d’une application de type CRUD avec Wicket « Jawher's Blog

  2. Vraiment complet cet exemple, beau travail!

    • jawher says:

      Merci Loic !
      Le pire c’est que je suis convaincu que c’est encore incomplet et que j’ai passé sur plusieurs détails. Je me suis forcé à dire stop et le publier un moment donné, vu que j’avais commencé le truc y’a des mois déjà.

  3. Pingback: Tweets that mention Création d’une application de type CRUD avec Wicket « Jawher's Blog -- Topsy.com

  4. Pingback: Moulder in action « Jawher's Blog

  5. Foued SMETI says:

    Excellent travail !
    J’ai repris l’exemple et et j’ai modifié l’implémentation de:
    -La couche d’accès aux données en utilisant hibernate 3 avec les annotations.
    -L’injection de dépendance des DAO dans les pages Wicket en utilisant spring.

  6. jawher says:

    Salut Foued et merci !

    -La couche d’accès aux données en utilisant hibernate 3 avec les annotations.
    -L’injection de dépendance des DAO dans les pages Wicket en utilisant spring.

    Ah ! C’est pas mal l’intégration Spring dans Wicket hein ?

    Mais sinon, que penses tu de Wicket, par rapport à JSF par exemple ?

  7. Khaled says:

    Bravo pour ce post!

  8. Homer says:

    Copyright (c) 2011, Jawher Moussa

    Permission is hereby granted, free of charge, to any person obtaining a copy
    of this software and associated documentation files (the “Software”), to deal
    in the Software without restriction, including without limitation the rights
    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    copies of the Software, and to permit persons to whom the Software is
    furnished to do so, subject to the following conditions:

    The above copyright notice and this permission notice shall be included in
    all copies or substantial portions of the Software.

    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    THE SOFTWARE.

  9. hikaru95 says:

    Salut Jawher,

    Merci pour ces articles très complets, qui m’ont permis d’apprendre rapidement les fonctionnalités de base de wicket. :-)

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: