martedì 30 agosto 2011

Wicket: Dati in Tabella e Pagine che si Estendono

Come secondo esempio andiamo a vedere qualcosa di più spinto.

Non importa cosa faccia la vostra applicazione perché prima o poi dovrete mostrare qualche dato in una tabella. Che siano dati presi da un DB, da filesystem, inseriti in una FORM, alla fine sempre una tabella dovrete mostrare.

Wicket permette di stampare dati in una moltitudine di maniere. Qui elenco le classi del framework che potreste usare:
Ognuna di esse permette di gestire varie tipologie di dati e di manipolazione delle stesse. Molti usano la semplice DataTable che risulta forse la più intuitiva.

Parte 1 - La cosa della riusabilità

Alla fine del post su Hello World vi ho detto che una delle caratteristiche belle di Wicket è il riutilizzo del codice. L'esempio che segue non è esattamente adiacente a questa filosofia per i motivi che vi andrò a spiegare però è bene che ci spenda due parole sopra a riguardo.

In Wicket le componenti possono vivere da sole e richiamate all'occorrenza. In parte è normale se pensate che parliamo di un linguaggio ad oggetti. In qualsiasi linguaggio ad oggetti, se create una classe bicicletta stand alone, questa potrà essere usata ed istanziata ovunque da chi la vuole usare.

In Wicket la cosa funzione ad un livello ancora superiore. Come vedremo in un prossimo esempio quando parleremo di PANEL, un oggetto può vivere da solo (usando altre classi ovviamente e perché no, altri PANEL) ed essere chiamata dove serve.

Per esempio un PANEL che gestisce il classico TOTEM LOGIN può essere costruito a parte gestendo internamente l'interrogazione a database per poi essere usato sia da varie pagine della nostra applicazione che da altre applicazioni in altri progetti.

Nell'esempio che segue andremo a creare una tabella dinamica con dentro dei dati fittizi. La faremo creare come codice di una vera pagina e non di un pannello. Questo ovviamente "congela" la tabella all'interno della pagina.
A differenza di PANEL che possono essere sparse ovunque (anche la stessa più volte dentro la stessa pagina), una PAGE può solo sottostare ad una gerarchia di pagina. Quindi la nostra pagina table (che chiameremo tablePage) dovrà necessariamente estendere la WelcomePage di prima se vogliamo che anche il codice di quest'ultima venga stampanto.
NON possiamo infatti chiedere alla WelcomePage di stampare dentro di sé il contenuto di un'altra pagina perché la gerarchia funziona all'inverso.

Quello che avremmo potuto fare è invece riempire la WelcomePage con il codiche restituito da vari Panel chiamati all'occorrenza.

Parte 2 - Piccoli Cambiamenti all'esempio Hello World.

Andiamo a vedere velocemente i piccoli cambiamenti che accadranno per far funzionare il tutto.

Abbiamo detto sopra che la pagina TablePage dovrà estendere la WelcomePage. Questo significa che l'applicazione non istanzierà più la WelcomePage ma direttamente la TablePage che nel suo costruttore chiamerà ovviamente quello della sua superclasse ovvero WelcomePage.

public Class<? extends Page> getHomePage() {
        return TablePage.class;
    }

Ecco come cambia il metodo getHomePage. TablePage estenderà la WelcomePage e ne chiamerà il costruttore (come vedremo dopo).

Il prossimo cambiamento è più interessante. Noi stiamo estendo la WelcomePage ma se non specifichiamo qualcosa, come fa il Wicket a sapere dove andrà messo il codice HTML della TablePage?

Per fare questo inseriamo nel codice HTML WelcomePage.html il seguente tag:

<wicket:child></wicket:child>

Questo tag dice a Wicket che la dentro ci andrà qualcosa messo da qualcuno che mi estenderà. Nel nostro caso TablePage. Questo tag potete metterlo in un punto qualsiasi, legale ovviamente secondo lo standard W3C, del vostro HTML.

Parte 3 - Dati Fittizi Cercasi

Ora decidiamo cosa stampare esattamente in questa tabella che ancora non sappiamo com'è fatta. Per iniziare direi di fare una semplice tabella con 2 colonne con Nome e Cognome.

Per fare le cose per bene vogliamo mappare queste cose in una classe apposita. Potremmo ovviamente stampare direttamente le stringe nel punto in cui creiamo la tabella ma noi vogliamo mostrare una manipolazione di dati effettiva che potreste dover usare.

Poniamo che un tizio compili una form e voi dobbiate fare una query al DB restituendo i contatti di una agenda. Ogni contatto è plausibile che sia mappato su un oggetto che ne condivide i campi.

In questo caso quindi creeremo una semplice classe ContactExample con dentro due variabili di tipo stringa Name e Surname.

package it.trewps.heavyexamples.web;

import java.io.Serializable;

public class ContactExample implements Serializable{

    private static final long serialVersionUID = 1L;
    
    private String contactName         = "";
    private String contactSurname     = "";
    
    public ContactExample(String name, String surname){
        setContactName (name);
        setContactSurname (surname);
    }

    public void setContactName(String contactName) {
        this.contactName = contactName;
    }

    public String getContactName() {
        return contactName;
    }

    public void setContactSurname(String contactSurname) {
        this.contactSurname = contactSurname;
    }

    public String getContactSurname() {
        return contactSurname;
    }
    
}

Il codice è molto semplice. Nel costruttore passo Nome e Cognome e inizializzo le mie due variabili interne che possiedono ognuna i classici metodi GET e SET.
Mi soffermo solo su quel implements Serializable. Dato che parliamo di web application, occorre dire a Java e Wicket che questi dati sono serializzabili.

Parte 4 - TablePage.html

Prima di vedere il codice java della classe, più complesso, vediamo come si comporta l'HTML della nostra TablePage.

Ovviamente non ci aspettiamo una pagina web completa con HTML, HEAD e tutto il resto dato che la stiamo ereditando dalla WelcomePage. Noi dobbiamo in qualche modo attaccarci a quel tag child visto sopra.

<wicket:extend>

<table cellspacing="0" class="dataview">
    <tbody>
       <tr wicket:id="trContacts">
         <td><span wicket:id="tdName"></span></td>
         <td><span wicket:id="tdSurname"></span></td>
       </tr>
    </tbody>
</table>

<div wicket:id="navigator"></div>

</wicket:extend>

Il codice include solo la parte strettamente necessaria a stampare la tabella. Ripeto. Un codice di questo tipo ha poco senso perché una cosa del genere avrebbe avuto più senso in un Panel.
Ad ogni modo la cosa importante è il tag wicket:extend che serve a Wicket per dire che quel pezzo di codice sarà appeso a qualcosa che avrà un tag child.

La seconda cosa importante sono i tre ID di wicket nella quale appenderemo i dati derivati dal nostro Data Provider ovvero il nome e cognome. E' importante notare che abbiamo messo una sola riga con due colonne ma in realtà la tabella sarà popolata di più righe pari al numero totale di elementi nella lista restituitami dal data provider.

Parte 5 - TablePage.java

Andiamo ora ad analizzare il codice più importante e quello che farà tutto. L'ho pesantemente commentato così da scrivere meno roba qui e lasciare che ogni istruzione abbia la sua spiegazione.

package it.trewps.heavyexamples.web;

import java.util.ArrayList;
import org.apache.wicket.Page;
import org.apache.wicket.markup.html.basic.Label;
import org.apache.wicket.markup.html.navigation.paging.PagingNavigator;
import org.apache.wicket.markup.repeater.Item;
import org.apache.wicket.markup.repeater.data.DataView;
import org.apache.wicket.markup.repeater.data.ListDataProvider;

public class TablePage extends WelcomePage {

    //creo una variabile myContact di tipo ContactExample
    //Non serve specificare se privata o pubblica perché è a uso strettamente interno
    //Se mettessi privati la IDE mi romperebbe le balle per crearci i metodi GET e SET
    //In realtà usandola solo nel metodo someDataRetriever, potrei anche dichiarla direttamente
    //dentro al metodo
    ContactExample myContact = null; 
    //creo una variabile di tipo ArrayList che conterrà ContactExample
    //In Java è buona norma specificare anche il tipo di dato che l'array dovrà contenere
    ArrayList<ContactExample> myContactList          = new ArrayList<ContactExample>();
    
    public TablePage(){
        super();
        buildPage();
    }
    
    public void buildPage(){
        //Chiamo il metodo che riempie i dati
        //In realtà esso agisce già su myContactList essendo globale alla classe
        //ma avendo specifica che deve ritornarla, la rimetto nuovamente nella
        //variabile globale della classe. Avrei potuto mettere void come tipo di ritorno
        //alla classe e lanciare il metodo senza settarlo a niente (es. someDataRetriever();)
        myContactList = someDataRetriever();
        
        //Creo un Data view che deve avere come primo parametro l'ID wicket su cui mappare (nel
        //nostro caso un TR dato che avremo più righe. Poi un ListDataPrivider ovvero qualcosa che
        //ritorna in dati veri e propri. 
        //La IDE vi chiederà di estendere il metodo populateItem che serve a dire alla classe come
        //e dove deve prendere i dati da mettere dentro. Nel nostro caso prendiamo oggetti di tipo
        //ContactExample a sua volta presi da myContactList che è passata come vedete come variabile
        //al costruttore. A questo punto il sistema, differentemente a come potete pensare, non crea
        //dei TD riempiendoli poi con nome e cognome ma fa il contrario. Associat alle due variabile 
        //una LABEL ovvero un'area sensibile sulla pagina legata a Nome e Cognome. Sarà poi Wicket
        //che prendendo dataView, si occuperà di stampare il tutto
        final DataView dataView = new DataView("trContacts", new ListDataProvider(myContactList)) {
                private static final long serialVersionUID = 1L;
                public void populateItem(final Item contactItem) {
                    final ContactExample singleContact = (ContactExample) contactItem.getModelObject();
                    contactItem.add(new Label("tdName"        , singleContact.getContactName()));
                    contactItem.add(new Label("tdSurname"    , singleContact.getContactSurname()));
                }
         };
         
         //Qui imposto il numero di elementi per pagina
         dataView.setItemsPerPage(10);
         //Qui aggiungo la variabile al flusso Wicket della pagina per riempire
         //i tag wicket con ID = qualcosa (nel nostro caso trContract e le colonne con tdName e tdSurname)
         add(dataView);
         //Qui aggiungo un navigator ovvero il classico page scroller per fare avanti e indietro
         add(new PagingNavigator("navigator", dataView));
    }
    
    
    //Questo sarebbe un fittizzio metodo che prende dati da qualche parte
    public ArrayList<ContactExample> someDataRetriever(){
        //Dato che non ho un DB o qualcosa da cui prendere i dati
        //Mi creo un nome base Mario Rossi a cui attacco un numero progressivo
        //creato dentro un FOR
        String name     = "Mario ";
        String surname    = "Rossi ";
        
        //Alla fine di questo ciclo avrò un array di oggetti di tipo ContactExample
        //le cue due variabili interne saranno inizializzate a roba tipo:
        // Mario 1 Rossi 2, Mario 2 Rossi 4 ecc...
        for (int i = 1; i < 50; i++) {            
            //Contateno le due stringe base con la conversione da char a String di dynamicChar
            //che era a sua volta un cast da intero a char (riga sopra)
            this.myContact = new ContactExample(name+Integer.toString(i),
                                                surname+Integer.toString((i*2)));
            this.myContactList.add(this.myContact);
        }
        return myContactList;
    }
    
}

Le cose interessanti di questa classe sono molteplici. Vediamo le più importanti.

Il costruttore ovviamente chiama il costruttore del padre ovvero WelcomePage e poi la sua funzione che popola il codice del proprio HTML andando ovviamente a prendere i dati dal data provider che qui è simulato da un metodo chiamato someDataRetriever.

Abbiamo creato due variabili globali alla classe per gestire un oggetto ContactExample ed un array di oggetti dello stesso tipo.

Questi viene popolato dentro al for che riempie l'array di oggetti di tipo ContactExample semplicemente impostare nome e cognome Mario Rossi con dei numeri attaccati per far capire che sono diversi.

La parte fondamentale è però il codice DataView di cui riprendiamo un estratto:

myContactList = someDataRetriever();
        
        final DataView dataView = new DataView("trContacts", new ListDataProvider(myContactList)) {
                private static final long serialVersionUID = 1L;
                public void populateItem(final Item contactItem) {
                    final ContactExample singleContact = (ContactExample) contactItem.getModelObject();
                    contactItem.add(new Label("tdName"        , singleContact.getContactName()));
                    contactItem.add(new Label("tdSurname"    , singleContact.getContactSurname()));
                }
         };

Per prima cosa otteniamo il nostro elenco di contatti chiamando someDataRetriever().
Creiamo quindi una variabile di tipo DataView chiamata banalmente dataView. Gli passiamo come primo parametro il famoso label della TR che lui dovrà iterare e come secondo parametro una istanza diretta di ListDataProvider.

Tutto il gioco è infatti gestito da quest'ultima classe che in ingresso prende ovviamente l'ArrayList con i contatti. Ovviamente questa è una classe generica e non sa che tipo di oggetti saranno eventualmente contenuti nella lista (nel costro caso un ExampleContact). Per questo motivo viene richiamato il metodo getModelObject che è come dire: "Non so bene cosa avrò ma lo mappo comunque nudo e crudo sull'item che sto iterando". Se guardate infatti dopo posso chiamare i due metodi interni all'oggetto ContactExample per ottenere nome e cognome. In pratica il sistema prende una lista di oggetti, li mappa su un ITEM nudo e crudo tramite la getModelObject e poi usa l'item come se stesse usando l'oggetto originale anche se questo è automaticamente mappato sugli oggetti della lista. In questo modo può settare l'iterator con i Label dei TD al cui interno va a mettere i dati dell'oggetto ContactExample.

Infine nel codice aggiungiamo anche un Navigator ovvero la classica barra per scorrere le pagine dato che gli abbiamo detto che ne vogliamo 10 per pagina.

A differenza di altri linguaggi, non ci interesseremo di parametri passati via get, di fare paginazioni a mano ecc...

Il bello di Wicket è che a noi tocca solo pensare all'HTML e alla logica dei dati gestiti in Java. Al resto ci pensa lui.

Qua sotto un esempio del risultato.
Abbiamo nuovamente il nostro Hello World grazie alla chiamata al costruttore della WelcomePage.
Al posto del tag child abbiamo la nostra bella tabella (con un css per rendere gialle le celle) ed in basso la nostra navigation bar. A questo punto possiamo abbellire tramite CSS.

Come detto sopra tra l'altro, la navigation bar funziona automaticamente. I link vengono creati da wicket e sarà il server e wicket a gestirli.

Nessun commento:

Posta un commento