In un articolo precedente, abbiamo realizzato il classico "HelloWorld" con MyFaces. In questo articolo possiamo dunque iniziare a mettere mano ai file di configurazione con i quali sviluppare le pagine JSF.
Cerchiamo quindi di conoscere meglio il framework JSF MyFaces, costruendo un esempio di controller.
Il progetto Esame
Questo progetto rappresenta una semplice tabella che suddivide automaticamente gli ipotetici studenti che hanno sostenuto un ipotetico esame, dividendoli tra chi è riuscito a passare e chi no. Per fare ciò non è necessario creare un progetto da zero, si può modificare quello già esistente.
I bean
La prima classe da creare è la classe Studente
, che rappresenterà, ovviamente, il singolo studente e andrà posizionato nel package it.html.bean
. Questo bean conterrà tre semplici attributi: Nome, Cognome e Voto (String
i primi due e int
il terzo), e soprattutto conterrà i metodi pubblici getter e setter con i quali il framework accederà al loro valore (Vedere parte 2 – Il primo bean per dettagli sui getter e setter dei bean).
La seconda è il repository (cioè il contenitore che mantiene i dati) che verrà chiamata ElencoStudenti
. Questa classe espone un unico ArrayList
di studenti che sarà la sua proprietà con i relativi getter
e setter
pubblici, oltre a due metodi per aggiungere e rimuovere uno studente dall'ArrayList
:
public void addStudente(final Studente s) {
studenti.add(s);
}
public void removeStudente(final Studente s) {
studenti.remove(s);
}
Link utili:
Il Controller
In un nuovo package, it.html.controller
, creare la classe EsameController.java
.
Un controller non è altro che un bean che interagisce con la pagina; il meccanismo dei getter
e dei setter
è lo stesso, e d'altronde esso deve essere registrato nel faces-config.xml proprio come gli altri bean. La differenza sta nella logica della struttura, per simulare il paradigma MVC il controller sarà il tramite tra la pagina e i bean.
Per cominciare fornire al controller due proprietà: un oggetto di tipo Studente e un oggetto di tipo ElencoStudenti
, con i relativi getter
e setter
. Aggiungere anche, come per la classe ElencoStudenti
, i due metodi addStudente
e removeStudente
, che chiameranno i corrispondenti metodi sulla classe del repository:
public String addStudente() {
elenco.addStudente(studente);
return "";
}
public String removeStudente() {
elenco.removeStudente(studente);
return "";
}
Sul motivo del return di tipo String
e vuoto torneremo più avanti, per ora basti sapere che concerne la navigazione tra le pagine (inesistente in questo progetto che consiste di una sola pagina).
Da notare che come argomento dei metodi addStudente
e removeStudente
è stata passata proprio la seconda proprietà del controller
private Studente studente;
La logica per suddividere e mostrare gli studenti promossi da quelli bocciati verrà inserita qui dentro, ma per non mettere troppa carne al fuoco verrà illustrata in seguito. Per ora ci si limiterà ad illustrare come aggiungere, rimuovere e mostrare l'elenco degli studenti.
Il faces-config.xml
Tutt'e tre le classi create fino ad ora andranno registrate come bean nel faces-config.xml come segue:
<managed-bean>
<managed-bean-name>studenteBean</managed-bean-name>
<managed-bean-class>it.html.bean.Studente</managed-bean-class>
<managed-bean-scope>none</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>elencoStudentiBean</managed-bean-name>
<managed-bean-class>it.html.bean.ElencoStudenti</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
<managed-bean>
<managed-bean-name>esameController</managed-bean-name>
<managed-bean-class>it.html.controller.EsameController</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
<managed-property>
<property-name>elenco</property-name>
<property-class>it.html.bean.ElencoStudenti</property-class>
<value>#{elencoStudentiBean}</value>
</managed-property>
<managed-property>
<property-name>studente</property-name>
<property-class>it.html.bean.Studente</property-class>
<value>#{studenteBean}</value>
</managed-property>
</managed-bean>
Rispetto al faces-config.xml del progetto Hello-World questo è, ovviamente, più complesso ed elaborato. Il primo bean si riferisce alla classe Studente
: dal momento che l'oggetto studente serve solo come contenitore di proprietà per gli altri (qualcosa di simile ad un value Bean, insomma), può utilizzare uno scope request
. Il bean riferito ad ElencoStudenti
invece giocherà il ruolo del repository: dunque, avendo la necessità di mantenere in memoria i dati che gli vengono passati (cioè l'elenco degli studenti), dovrà avere uno scope session
, altrimenti ad ogni richiesta del browser il suo contenuto andrebbe perso.
La dichiarazione più importante è quella del bean del controller: essa si avvantaggia di un meccanismo che è un po' il punto di forza del framework JSF. Le prime tre righe sono comuni anche agli altri due bean, la quarta riga invece mostra il tag <managed-property>. Questo tag permette di assegnare automaticamente dei valori alle proprietà del bean dichiarato (dunque non è una proprietà esclusiva dei controller), accedendo ai metodi getter
e setter
della proprietà (che devono dunque essere dichiarati correttamente).
I tag <property-name> e <property-class> sono abbastanza intuitivi, invece il tag <value> è più interessante, in quanto può prendere valori fissi (stringhe o numeri), ma la vera caratteristica interessante è la possibilità di inserire nel tag value un altro bean dichiarato nel faces-config.xml. In questo modo sarà l'applicazione che si occuperà di popolare le proprietà del controller, così, ad esempio, ad ogni richiesta il controller sarà sempre collegato al bean elencoStudentiBean
che è un bean di sessione, e dunque le operazioni che il controller esegue sulla sua proprietà elenco verranno in realtà eseguite sul bean elencoStudentiBean
.
Per esporre in modo più esauriente il meccanismo che governa la logica del progetto è meglio veder prima la pagina esame.jsp:
La pagina esame.jsp
Questa pagina è abbastanza ricca di contenuto: per cominciare si osservi il primo form, nel quale si possono inserire i dati relativi allo studente.
Il tag form è fondamentale, senza di esso non ci sarebbe la possibilità di inviare dei dati nella request. All'interno ci sono tre tag <h:inputText> che verrano renderizzate come tre Textbox, nelle quali inserire il nome, il cognome e il voto dello studente.
<f:view>
<h:form>
<h:inputText value="#{studenteBean.nome}" /> <br />
<h:inputText value="#{studenteBean.cognome}" /> <br />
<h:inputText value="#{studenteBean.voto}" /> <br />
<h:commandButton action="#{esameController.addStudente}" value="Inserisci" />
</h:form>
<h:form>
STUDENTI:
<h:dataTable value="#{esameController.elenco}" var="studente" border="1" >
<h:column>
<!-- DEFINISCO L'HEADER -->
<f:facet name="header">
<h:column>
<h:outputText value="Studenti"></h:outputText>
</h:column>
</f:facet>
<!-- DEFINISCO IL CONTENUTO -->
<h:outputText value="#{studente.nome} #{studente.cognome}"></h:outputText>
</h:column>
<h:column>
<!-- DEFINISCO L'HEADER -->
<f:facet name="header">
<h:column>
<h:outputText value="Voto"></h:outputText>
</h:column>
</f:facet>
<!-- DEFINISCO IL CONTENUTO -->
<h:outputText value="#{studente.voto}"></h:outputText>
</h:column>
<h:column>
<!-- DEFINISCO L'HEADER -->
<f:facet name="header">
<h:column>
<h:outputText value="Rimuovi"></h:outputText>
</h:column>
</f:facet>
<!-- DEFINISCO IL CONTENUTO -->
<h:commandButton action="#{esameController.removeStudente}" value="X">
<f:setPropertyActionListener target="#{esameController.studente}"
value="#{studente}" />
</h:commandButton>
</h:column>
</h:dataTable>
</f:view>
L'attributo value è il legame tra il tag e il contenuto del bean, che in questo caso è biunivoco, ovvero non si limita solo a mostrare sull'interfaccia il contenuto della proprietà, ma permetterà al framework anche di assegnarvi un valore inserito dall'utente al momento della sottomissione del form. Di quest'ultima si occupa il tag <h:commandButton> che renderizza un bottone al cui click è stato associato il metodo addStudente di esameController.
Cosa succede al click del bottone?
Senza andare troppo nello specifico: dalla pagina vengono assegnati i valori al bean studenteBean
, mentre il faces-config.xml assegna il bean studenteBean
alla proprietà studente
di esameController
, in modo che, al termine della fase di assegnazione, il controller abbia la sua proprietà studente
valorizzata con i dati della pagina, e in una delle successive fasi, durante la quale viene chiamato il metodo del controller, esso possa aggiungere quei dati al repository (anch'esso assegnato al controller dal faces-config.xml
).
A questo punto il framework renderizza la pagina andando a popolarla con i valori dei bean.
Dal momento che il bean studenteBean
esiste ancora, nel primo form i dati inseriti rimarranno visibili, si vedrà in seguito come svuotare il bean.
La maschera di inserimento per l'elenco degli studenti
Merita ora attenzione il secondo form della pagina: esso contiene un tag <h:datatable>, che disegna una tabella contenente l'elenco degli studenti con nome, cognome e voto.
Il primo attributo value
definisce un insieme di valori che il datatable dovrà renderizzare, in questo caso l'insieme è dato dalla variabile elenco di esameController
contenente gli studenti inseriti. Il resto del codice dentro il datatable definisce il modo in cui disegnare la tabella. Per fare ciò viene definita una variabile dall'attributo var
, che nel progetto è stata chiamata semplicemente studente
. In questo modo il datatable si comporta praticamente come un ciclo for, e compone ogni riga come viene definita al suo interno, valorizzando di volta in volta la variabile studente con ogni elemento della collezione ritornata da esameController.elenco
.
Le possibilità di personalizzazione sono vaste, nel caso esposto sopra, sono state definite, col tag <h:column>, tre colonne. All'interno di ogni colonna, come si evince dal commento, viene definito un header, cioè l'intestazione della colonna (che ovviamente non verrà ripetuta per ogni riga) e il contenuto della colonna. Avendo a disposizione una variabile contentente un oggetto, le proprietà dell'oggetto possono essere sistemate a piacimento, ad esempio nella prima colonna c'è il nome ed il cognome e nella seconda il voto, ma il codice più interessante sta nella terza colonna, contenente un comando per cancellare la riga. Il tag <h:commandButton> è lo stesso spiegato sopra per il bottone di sottomissione del form di inserimento dati, invece si può vedere che questo è legato all'azione di rimozione.
Se si ricontrolla la classe EsameController
si noterà che il metodo rimuoviStudente
richiede che gli si passi un oggetto di tipo studente, e che il faces-config.xml
associa automaticamente al bean studenteBean alla proprietà studente del controller. Attenzione però! Il fatto di aver chiamato la variabile del datatable studente di per sé non significa nulla per il faces-config.xml
, né per il framework. Serve dunque qualcosa per legare il controller alla variabile del datatable e quel qualcosa è il tag <f:setPropertyActionListener> i cui attributi sono piuttosto espliciti: value è il valore da assegnare alla proprietà indicata nel target. In questo modo al click del pulsante il controller avrà la sua proprietà studente valorizzata e potrà rimuoverla dal repository.
Da notare che, essendo la proprietà esameController.studente
legata tramite faces-config.xml
a studenteBean
, al click sul pulsante “X” il contenuto di esameController.studente
finirà dentro studenteBean
, che essendo esposto nel form di inserimento mostrerà i dati dello studente rimosso. Dunque il legame tra proprietà del bean e del controller è bidirezionale: valorizzando studenteBean
si può valorizzare la proprietà di esameController
, e viceversa!
Considerazioni sul modello MVC
Il progetto svolto è un buon esempio per illustrare e comprendere la logica MVC (che sta come sappiamo per Model-View-Controller).
La suddivisone dei capitoli riflette questa cosa: All'inizio sono stati creati i due bean che si possono considerare come il modello alla base. Essi sono ignoranti degli altri livelli. Poi è stato creato il controller, che esegue tutto il lavoro sul modello e si occupa di rendere disponibili i dati che verrano mostrati sul lato View: la pagina .jsp nella quale quindi ci si può concentrare solo sull'apparenza del sito.
Lo scopo di questo paradigma è permettere lo sviluppo indipendente del livello di presentazione e del modello, rendendo possibile, in teoria, applicare la stessa presentazione su un modello diverso, così come applicare allo stesso modello presentazioni diverse.
Migliorare il progetto
Il progetto ancora non è finito: bisogna suddividere gli studenti in due liste, i promossi e i bocciati, inoltre si possono apportare alcune migliorie grafiche.
Il principio per suddividere promossi e bocciati è elementare: voto > 18, il problema è decidere a chi delegare questa selezione. Ognuno dei tre livelli sarebbe in grado di implementare questa logica: nel modello si potrebbe, ad esempio, modificare la classe ElencoStudenti in modo da contenere due liste, bocciati e promossi, e un unico metodo addStudente che discrimina l'inserimento in una o nell'altra lista. Una soluzione del genere è accettabile su progetti il cui modello non ha elevate complessità, ma in genere è preferibile lasciare al modello la massima trasparenza e semplicità possibile (Più semplice a dirsi che a farsi). Nella presentazione invece sarebbe possibile, grazie alle potenzialità dei tag jsf, condizionare la visibilità di alcune colonne piuttosto che altre. Aggiungendo all'intestazione quesa riga:
<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core"%>
sarà possibile usare i tag jstl che permettono operazioni come questa:
<!-- PROMOSSI -->
<h:dataTable value="#{esameController.elenco.studenti}" var="studente" border="1" >
<c:if test="${studente.voto >18}">
<h:column>
<!-- DEFINISCO L'HEADER -->
<f:facet name="header">
...
permettendo di stampare solo le colonne degli studenti con voto maggiore di 18.
Le librerie jstl permettono di inserire operazioni logiche all'interno della pagina, ma non è obiettivo dell'articolo approfondirne le possibilità. Tuttavia è bene ricordare il rischio di farne un uso non appropriato: fondere la logica dell'applicazione nel livello presentazione è una pratica sconsigliata e poco coerente con il paradigma MVC.
Il codice scritto sopra infatti non è il modo migliore per gestire la visualizzazione di bocciati e promossi.
La scelta fatta per questo progetto identifica nel controller il candidato migliore per questo tipo di operazioni.
A questo punto nella classe EsameController sono stati aggiunti i seguenti metodi:
public ElencoStudenti getElencoPromossi() {
final ElencoStudenti promossi = new ElencoStudenti();
for (final Studente s : elenco.getStudenti()) {
if (s.getVoto() >= 18) {
promossi.addStudente(s);
}
}
return promossi;
}
public ElencoStudenti getElencoBocciati() {
final ElencoStudenti bocciati = new ElencoStudenti();
for (final Studente s : elenco.getStudenti()) {
if (s.getVoto() < 18) {
bocciati.addStudente(s);
}
}
return bocciati;
}
e la pagina di presentazione è stata modificata aggiungendo un dataTable e modificando il primo:
<h:form>
PROMOSSI
<h:dataTable value="#{esameController.elencoPromossi.studenti}" var="studente" border="1" >
...
</h:dataTable>
</h:form>
<h:form>
BOCCIATI
<h:dataTable value="#{esameController.elencoBocciati.studenti}" var="studente" border="1" >
...
</h:dataTable>
</h:form>
In questo modo sarà il controller ad occuparsi di suddividere gli studenti in base al voto, la presentazione dovrà solo visualizzarli.
Migliorare la presentazione
Ora che il progetto esegue ciò per cui è stato concepito, è possibile concentrarsi su come migliorare il lato presentazione: si vedrà dunque come "svuotare" dei valori appena inseriti il bean studente.
All'inserimento e rimozione di uno studente di solito è desiderabile che la maschera di inserimento rimanga vuota dopo l'operazione. Come già spiegato (per faces-config.xml
e la pagina esame.jsp
) questo è un effetto del legame che unisce pagina, controller e studenteBean. Senza inferire sul questo legame, è sufficiente:
in fase di inserimento: copiare i dati dello studente in un nuovo oggetto studente da inserire nel repository, per poi “pulire” la variabile studente del controller:
public String addStudente() {
final Studente toAdd = new Studente();
toAdd.setCognome(studente.getCognome());
toAdd.setNome(studente.getNome());
toAdd.setVoto(studente.getVoto());
elenco.addStudente(toAdd);
studente.setCognome("");
studente.setNome("");
studente.setVoto(0);
setStudente(null);
return "";
}
In questo caso è stato fatto tutto “a mano” nella addStudente, per mostrar bene i passaggi, ma è consigliabile dotare la classe Studente di un metodo Studente clone(Studente) e un metodo void pulisci().
Identica la fase di rimozione, con la sola differenza che non è necessaria nessuna copia dell'oggetto, è sufficiente rimuoverlo e poi svuotare il bean:
public String removeStudente() {
elenco.removeStudente(studente);
studente.setCognome("");
studente.setNome("");
studente.setVoto(0);
setStudente(null);
return "";
}