Afficher des tableaux sexy avec Wicket


Les tableaux HTML sont plus lisibles lorsque les lignes paires sont facilement différenciables des lignes impaires, par exemple grâce à un fond de couleur différente.
Voyons comment Wicket permet d'industrialiser ce traitement, afin qu'il soit facilement applicable à n'importe quel tableau.

Pour l'exemple, nous afficherons la liste des Locales suportées par la JVM (langage et pays).

Afficher un tableau avec Wicket

Tout d'abord, rappelons comment on affiche un tableau en Wicket.

Du point de vue HTML, il suffit d'utiliser les balises standard : <table>, <tr>, <td>.
Comme c'est l'élément <tr> (et tout son contenu) qui sera répété pour chaque Locale à afficher, c'est lui qui porte l'attribut "wicket:id".

  1. <table>
  2. <tr>
  3. <th>Country</th>
  4. <th>Language</th>
  5. </tr>
  6. <tr wicket:id="localesList">
  7. <td wicket:id="localeCountry">Kronos</td>
  8. <td wicket:id="localeLanguage">Klingon</td>
  9. </tr>
  10. </table>

Du point de vue Java, ce n'est guère plus compliqué grâce au composant ListView.
Comme tout composant Wicket, il respecte l'architecture MVC :

  • Un Modèle (IModel) doit lui être fourni - ici, la liste des Locales disponibles ;
  • Le ListView lui-même encapsule la logique du Contrôleur, en bouclant sur les éléments du Modèle ;
  • La Vue est construite dynamiquement par la méthode populateItem(), sur laquelle nous nous concentrerons plus loin.

Voici donc le squelette de notre Page :

  1. public class StripedTablePage extends WebPage {
  2.  
  3. /** A list of Locales */
  4. List<Locale> locales = Arrays.asList(Locale.getAvailableLocales());
  5.  
  6. /** Constructor */
  7. public StripedTablePage() {
  8.  
  9. ListView<Locale> localesList = new ListView<Locale>("localesList", locales) {
  10. @Override
  11. protected void populateItem(final ListItem<Locale> item) {
  12. // TODO : build the View
  13. }
  14. };
  15. add(localesList);
  16. }
  17.  
  18. }

Le paramètre "item" de la méthode "populateItem()" a deux utilités :

  • Il donne accès à l'élément courant : Object element = item.getModelObject()) ;
  • Il représente la racine des composants de la Vue, qui doivent donc y être attachés : item.add(new Label(...)).

Dans notre cas, deux Labels servent à afficher la langue et le pays de chaque Locale. Voici donc la méthode complétée :

  1. @Override
  2. protected void populateItem(final ListItem<Locale> item) {
  3. // Retrieve the current Locale
  4. final Locale loc = item.getModelObject();
  5.  
  6. // Add a Label for the Country
  7. item.add(new Label("localeCountry", new AbstractReadOnlyModel<String>() {
  8. @Override
  9. public String getObject() {
  10. return loc.getDisplayCountry(loc);
  11. }
  12. }));
  13.  
  14. // Add a Label for the Language
  15. item.add(new Label("localeLanguage", new AbstractReadOnlyModel<String>() {
  16. @Override
  17. public String getObject() {
  18. return loc.getDisplayLanguage(loc);
  19. }
  20. }));
  21. }

Amélioration du rendu avec les styles CSS

Afin de différencier les lignes paires et impaires, nous allons leur associer des classes CSS différentes :

  1. .evenRow {background-color: #FFF;}
  2. .oddRow {background-color: #DDF;}

Mais comment les appliquer aux balises <tr> ?
On pourrait éventuellement sous-classer le composant ListView pour contrôler le code HTML généré, mais cette solution est techniquement complexe et peu souple.
Nous utiliserons plutôt le système de "Behaviors" de Wicket : implémentant le design pattern Decorator, un Behavior encapsule un traitement particulier et peut être facilement appliqué à tout composant compatible.

Wicket en fournit justement deux, spécialisés dans la manipulation des attributs des balises HTML : AttributeModifier et AttributeAppender. Nous nous appuierons sur ce dernier pour ajouter la bonne classe CSS nos balises <tr>.

Pour fonctionner, notre Behavior a besoin du numéro de la ligne courante et des noms des classes CSS à appliquer. Tout le traitement proprement dit est réalisé par l'AttributeAppender dont on hérite :

  1. public class AlternateRowCssClassAttributeAppender extends AttributeAppender {
  2.  
  3. public static final String ATTRIBUTE_NAME = "class";
  4. public static final String VALUE_SEPARATOR = " ";
  5.  
  6. public AlternateRowCssClassAttributeAppender(final int index, final String evenRowCssClass, final String oddRowCssClass) {
  7. super(
  8. ATTRIBUTE_NAME, true,
  9. new AbstractReadOnlyModel<String>() {
  10. @Override
  11. public String getObject() {
  12. // Our algorithm
  13. return (index % 2 == 0) ? evenRowCssClass : oddRowCssClass;
  14. }
  15. },
  16. VALUE_SEPARATOR
  17. );
  18. }
  19.  
  20. }

Pour finir, il reste à l'appliquer à chaque ligne du tableau, lors de la construction de la Vue :

  1. @Override
  2. protected void populateItem(final ListItem<Locale> item) {
  3. // Retrieve the current Locale and add the Labels
  4. (...)
  5. // Apply the Behavior to colorize each other line
  6. item.add(new AlternateRowCssClassAttributeAppender(item.getIndex(), "evenRow", "oddRow"));
  7. }

Le code complet de notre page d'exemple devient donc :

  1. public class StripedTablePage extends WebPage {
  2.  
  3. /** A list of Locales */
  4. List<Locale> locales = Arrays.asList(Locale.getAvailableLocales());
  5.  
  6. /** Constructor */
  7. public StripedTablePage() {
  8. ListView<Locale> localesList = new ListView<Locale>("localesList", locales) {
  9. @Override
  10. protected void populateItem(final ListItem<Locale> item) {
  11. // Apply the Behavior to colorize each other line
  12. item.add(new AlternateRowCssClassAttributeAppender(item.getIndex(), "evenRow", "oddRow"));
  13.  
  14. // Retrieve the current Locale
  15. final Locale loc = item.getModelObject();
  16.  
  17. // Add a Label for the Country
  18. item.add(new Label("localeCountry", new AbstractReadOnlyModel<String>() {
  19. @Override
  20. public String getObject() {
  21. return loc.getDisplayCountry(loc);
  22. }
  23. }));
  24.  
  25. // Add a Label for the Language
  26. item.add(new Label("localeLanguage", new AbstractReadOnlyModel<String>() {
  27. @Override
  28. public String getObject() {
  29. return loc.getDisplayLanguage(loc);
  30. }
  31. }));
  32. }
  33.  
  34. };
  35. add(localesList);
  36. }
  37.  
  38. }

Conclusion

Après avoir revu la façon dont Wicket gérait les tableaux, nous avons développé un Behavior personnalisé permettant d'appliquer des styles CSS différents aux lignes paires et impaires.
Vous pouvez désormais l'ajouter à votre librairie de composants réutilisables !

Le code source de l'article est disponible en pièce jointe de ce billet.
Le script Gradle fourni permet de le compiler et de lancer un serveur Jetty intégré :

  1. gradle jettyRun

L'application est alors accessible à l'adresse suivante : http://localhost:8080/Wicket-tips/


Commentaires

1. Le Mardi 6 octobre 2009, 15:00 par alex95

Bonjour,
Merci pour l'article mais une petite capture du résultat serait pratique.
Voir des tableaux sexy ;-)

2. Le Vendredi 30 octobre 2009, 08:53 par Victor

Hello, thanks for the article, reads very interesting. Please, could you attach a screenshot of the resulting table?
The URL you wrote at article end is on "localhost" so it's reachable only for you...

3. Le Lundi 2 novembre 2009, 10:33 par Olivier Croisier

Hello,
I'll attach a screenshot soon, but don't expect to be amazed : it's only a striped table (odd and even lines with different backgrounds).
As for the localhost URL, it's not an error : it's where you must point your browser to after you've followed the instructions to install and run the attached code sample.

4. Le Mercredi 17 février 2010, 15:19 par Nico

J'ai une petite question à des <th> du tableau qui est généré. Si ta liste est vide tu va constuire un tableau avec seulement des <th>. Admettons que je souhaite rendre le tableau invisible si la liste est vide (setVisible(false)), j'aurai toujours ses <th> qui vont trainer. Ne serait-ce pas possible de les définir dans le code Java de la ListView ?

Fil des commentaires de ce billet