JUG – Ancona Italy Il framework Wicket Andrea Del Bene Jug Marche [email protected] Quali sono i “guai” della programmazione web? ● ● Le tecnologie su cui si basa il web si basano su paradigmi molto diversi da quelli dei linguaggi di programmazione moderni (OOP in primis). Quando scegliamo le pagine web come GUI dobbiamo affrontare due grossi limiti di questa tecnologia: 1) Le pagine (le viste del programma) sono dei semplici file di testo da spedire al browser e non possono essere rappresentate ed usate con gli strumenti dell'OOP 2) L'HTTP è stateless e dobbiamo fare i salti mortali per associare uno stato alla navigazione di un utente sul nostro sito (di solito si ricorre all'oggetto Session) Wicket: pagine come oggetto ● ● ● ● Wicket è un framework a componenti per lo sviluppo di soluzioni web Java che propone una soluzione ad entrambi i problemi visti A differenza di altre soluzioni (JSP, Struts, Spring MVC, ecc...) le pagine web sono trattate come vere e proprie istanze di oggetto (classe WebPage). Non dobbiamo più usare direttamente Request e Response nel nostro codice e l'HTML è usato solo come template di visualizzazione, non contiene nè taglib nè codice java. Per fare ciò Wicket offre una gerarchia di classi molto simile a quella proposta da Swing per realizzare le GUI di applicazioni desktop Come vedremo trattare le pagine come oggetti offre una soluzione trasparente anche al problema della conservazione dello stato... Wicket VS Swing ● In Wicket ogni entità è un componente e la pagina (la classe WebPage) contiene a sua volta istanze di componenti. WebPage ha una funzione analoga alla classe JWindow (o JFrame) in Swing. Dov'è finito l'HTML? ● ● ● Se la pagina per lo sviluppatore è una classe Java come ottengo l'HTML finale da inviare al Browser? In Wicket la classe che rappresenta una pagina ha associata una pagina HTML standard che funziona da template e che per default deve trovarsi nella stessa cartella della classe ed avere lo stesso nome. La classe Wicket può mappare i suoi componenti interni con i tag presenti nella pagina HTML. Wicket offre un set di componenti con cui mappare i vari elementi di una pagina HTML ( paragrafi <p>, form, div, span, controlli di input, ecc...) Il file web.xml ● ● E' il momento di vedere un primo esempio di utilizzo di Wicket. L'applicazione è una classica web application Java. Il file web.xml deve dichiarare un elemento <filter> che istanzi una sottoclasse di org.apache.wicket.protocol.http.WebApplication (nel nostro caso la classe helloWorld.WicketApplication). <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <display-name>Wicket HelloWorld</display-name> <filter> <filter-name>WizardApplication</filter-name> <filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class> <init-param> <param-name>applicationClassName</param-name> <param-value>helloWorld.WicketApplication</param-value> </init-param> </filter> <filter-mapping> <filter-name>WizardApplication</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> La classe WicketApplication ● ● La classe WicketApplication oltre ad essere il cardine dell'applicazione Wicket, può sovrascrivere numerosi metodi della classe madre per customizzare il comportamento della nostra applicazione. Sovrascrivendo la funzione getHomePage si può specificare la pagina home page della nostra applicazione (HomePage nel nostro caso). package helloWorld; import org.apache.wicket.Page; import org.apache.wicket.protocol.http.WebApplication; public class WicketApplication extends WebApplication { @Override public Class<? extends Page> getHomePage() { return HomePage.class; } } Pagina HelloWorld ● La mappatura tra elementi HTML ed oggetti Java avviene aggiungendo al tag l'attributo wicket:id=”” <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <h1 wicket:id="label"></h1> </body> </html> HelloWorld.html ● Nella classe della pagina Wicket bisogna aggiungere un componente con id uguale al wicket:id dell'elemento che si vuole mappare. package helloWorld; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.basic.Label; public class HomePage extends WebPage { public HomePage(){ super(); add(new Label("label", "Hello World")); } } HelloWorld.java WicketHelloWorld ● Nel tag <h1> è stato inserito il testo fornito come secondo parametro nel costruttore del componente Label. ... <body> <h1 wicket:id="label"></h1> </body> ... ... public HomePage(){ super(); add(new Label("label", "Hello World")); } ... Wicket come template per layout ● ● ● L'esempio appena visto usa in maniera molto primitiva la capacità di Wicket di manipolare il contenuto di una pagina HTML usando codice Java. Questa tecnica può essere usata per creare il layout grafico delle nostre pagine usando un numero arbitrario di wicket:id nella nostra pagina ed “inniettando” il codice HTML dei vari elementi che la compongono (ad esempio: l'header, il menù di navigazione, il footer, il contenuto, ecc...) Per fare ciò useremo l'oggetto Panel di Wicket che permette di associare il contenuto di una pagina HTML ad un componente custom di Wicket e poterlo aggiungere in una pagina del framework. JUG – Ancona Italy Il framework Wicket Il layout con Wicket ● Layout di una pagina web ● Ora vogliamo espandere quanto visto nell'esempio precedente per creare un classico layout per le pagine del nostro sito: un header, un menù di sinistra, il contenuto centrale ed un footer conclusivo. Header Menù Contenuto Footer L'approccio “classico” ● Di solito per conferire alle pagine un layout stabilito ci si affida a tecnologie di templating server side che caricano i vari “pezzi” della pagina prima di spedirla al browser. <%@include file="../common/Jug4TendaHeader.jsp"%> <%@include file= "gestioneOspiteMenu.htm "%> <div id="Content"> <%@include file="../common/Jug4TendaFooter.jsp"%> Layout in Wicket ● Con Wicket possiamo creare una pagina di template principale (chiamiamola Jug4TendaTemplate) suddivisa nei componenti desiderati, ognuno rappresentato da un <div> e con il proprio wicket:id <div wicket:id=”headerPanel”></div> <div wicket:id=”menuPanel”> </div> <div wicket:id=”contentPanel”></div> <div wicket:id=”footerPanel”></div> I pannelli di Wicket ● I pannelli sono lo strumento con cui possiamo creare componenti personalizzati. Di fatto sono quasi come le pagine Wicket vere e proprio in quanto anche i pannelli: 1) Sono associati a pagine HTML (pagine HTML complete!) 2) Possono a loro volta contenere altri componenti ● ● A differenza delle pagine però i pannelli non possono essere rendereizzati stand alone, ma devono essere inseriti in una pagina web I pannelli sono gli strumenti ideali per suddividere la pagina nei sui componenti grafici e per poterli riutilizzare in più pagine. Il pannello header <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> </head> <body> <wicket:panel> <table width="100%" style="border: 0px none;"> <tbody><tr> <td><img alt="Jug4Tenda" src="wicketLayout_files/logo_jug4tenda.gif"></td> <td><h1>Gestione Anagrafica Accoglienze</h1></td> <td><img alt="Associazione di volontariato La Tenda d'Abramo" src="wicketLayout_files/logo_latendadabramo.gif"></td> </tr> </tbody> </table> </wicket:panel> </body> </html> ● HeaderPanel.html La pagina HTML di un pannello deve avere il tag <wicket:panel> all'interno del quale si trova l'HTML del pannello stesso. La classe del pannello header package helloWorld.layoutTenda; import org.apache.wicket.markup.html.panel.Panel; public class HeaderPanel extends Panel { } ● ● public HeaderPanel(String id) { super(id); } HeaderPanel.java La classe di un pannello Wicket deve avere almeno un costruttore che richiama il costruttore padre passandogli il wicket id come parametro stringa. Anche i pannelli così come le pagine devono avere la classe e il file HTML nella stessa cartella e devono avere lo stesso nome. Il pannello footer … <wicket:panel> <span class="piccolo">Powered by </span> <a target="_blank" title="vai al sito dello JUG Ancona,..” href="http://www.jugancona.it/"> <img title="vai al sito dello JUG Ancona, ..." alt="JUG Ancona, Java User Group di Ancona" src="wicketLayout_files/logo_jugancona.gif"></a> <img title="JUG, Java User Group" alt="JUG, Java User Group" src="wicketLayout_files/logo_jug.gif"> <a title="vai al sito del framework Spring " href="http://www.springframework.org/"> <img title="vai al sito del framework Spring" alt="Spring" src="wicketLayout_files/logo_spring.gif"></a> <a title="vai al sito di Mozilla Firefox..." href="http://www.mozilla.com/"> <img title="vai al sito di Mozilla Firefox..." alt="Firefox" src="wicketLayout_files/logo_firefox.gif"></a> </wicket:panel> FooterPanel.html … package helloWorld.layoutTenda; import org.apache.wicket.markup.html.panel.Panel; public class FooterPanel extends Panel { public FooterPanel(String id) { super(id); } } HeaderPanel.java Il pannello menu … <div class="menuTitle">Menu principale</div> <ul class="menuItems"> <li> <img src="wicketLayout_files/home.gif"> <a class="Offmouse" href="http://localhost:8080/jug4tenda/index.jsp" id="newOspite" onmouseout="this.className='Offmouse'" onmouseover="this.className='Onmouse'">Home</a></li> </ul> … MenuPanel.html package helloWorld.layoutTenda; import org.apache.wicket.markup.html.panel.Panel; public class MenuPanel extends Panel { public MenuPanel(String id) { super(id); } } MenuPanel.java La pagina template Jug4Wicket <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Jug4Tenda - Home page</title> ... </head> <body> <div id="intestazione" wicket:id="headerPanel">intestazione</div> <div id="pagina"> <div id="Menu" wicket:id="menuPanel">Menu</div> <div id="Content" wicket:id="content">Content</div> </div> <div id="piedipagina" wicket:id="footerPanel">piedipagina</div> </body> </html> package helloWorld.layoutTenda; import org.apache.wicket.markup.html.WebPage; public class JugTemplate extends WebPage { public JugTemplate(){ add(new HeaderPanel("headerPanel")); add(new MenuPanel("menuPanel")); add(new FooterPanel("footerPanel")); add(new Label("contentComponent", "content")); } } Demo JavaScript e CSS in Wicket ● Come abbiamo visto il codice HTML dei pannelli Wicket deve essere una pagina HTML a tutti gli effetti. Se nel file HTML di un pannello vogliamo scrivere (o importare) codice JavaScript o CSS questo va racchiuso nel tag <wicket:head> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>Insert title here</title> <wicket:head> <script>...</script> <wicket:head> ... </head> <body> ... <wicket:panel> … </wicket:panel> ... JUG – Ancona Italy Il framework Wicket ● Pagine come oggetti L'ereditarietà delle pagine Wicket ● ● ● Ora che abbiamo una pagina di template con il layout del nostro sito, possiamo sfruttare la normale ereditarietà degli oggetti per creare le altre pagine. In Wicket una sottoclasse di una pagina eredita dalla classe madre anche l'html (oltre al codice ovviamente). Questo vuol dire che la pagina figlia avrà di default lo stesso identico aspetto della pagina padre. package helloWorld.layoutTenda; import org.apache.wicket.markup.html.basic.Label; public class ChildPage extends JugTemplate { public ChildPage(){ super(); addOrReplace(new Label("contentComponent", "L'ereditariatà delle pagine Wicket e sia a livello di codice che di html")); } } Pagina “figlia” del template addOrReplace(new Label("contentComponent", "L'ereditariatà delle pagine Wicket e sia a livello di codice che di html") ● Per la pagina figlia non è stato necessario creare un HTML . Abbiamo solo “personalizzato” il contenuto. JUG – Ancona Italy Il framework Wicket I link e lo stato delle pagine ● I Wicket link ● ● ● In Wicket i link sono molto più complessi del loro corrispondente HTML e non hanno come scopo esclusivo quello di permettere ad un utente di cambiare pagina. Come gli altri componenti Wicket i link sono associabili ad un tag della pagina HTML tramite l'attributo wicket:id. Si noti che un link non deve per forza essere associato ad un tag anchor <a> ma può essere associato a qualsiasi tag che supporti l'evento JavaScript onClick (quasi tutti quindi). Quando si aggiunge un componente Link ad una pagina occorre sovrascrivere il metodo onClick() per specificare cosa deve essere fatto quando l'utente preme il link: add(new Link("link"){ @Override public void onClick() { } }); I Wicket link 2 ● Se si vuole cambiare pagina facendo click su un Link Wicket occorre esplicitarlo nel metodo onClick() usando la funzione setResponsePage(): add(new Link("link"){ @Override public void onClick() { setResponsePage(JugTemplate.class); } }); ● Se un link non mi fa cambiare pagina Wicket restituisce nuovamente la pagina corrente, senza però ricrearne un'istanza nuova, conservando quindi lo stato della mia pagina fino a quando non la abbandono. (Vi ricordate all'inizio il problema della conservazione dello stato in HTTP?) Il metodo onBeforeRender ● ● ● Una pagina Wicket prima di essere trasmessa al browser effettua il “pre-rendering” tramite l'evento before render che viene gestito nel metodo onBeforeRender della classe WebPage. Sovrascrivendo questo metodo possiamo ulteriormente modificare l'aspetto e i componenti della pagina. Abbiamo detto che se un Link Wicket non causa il cambiamento di pagina viene restituita la pagina che lo contiene, senza creare una nuova istanza. Dopo il click il costruttore della pagina non verrà invocato mai il metodo onBeforeRender si! Pagina di esempio sui Link ● ● Nella pagina di esempio abbiamo due timestamp, uno aggiunto nel costruttore della pagina e l'altro nel metodo onBeforeRender(). Sono presenti anche due link che non fanno cambiare pagina. Cliccando sui link si aggiorna solo il timestamp costruito su onBeforeRender(). Codice della pagina di esempio public class HomePage extends WebPage { public HomePage(){ super(); add(new Label("label", "Hello World")); add(new Label("timeStamp", "" + new Date())); add(new Link("link"){ @Override public void onClick() { } }); add(new Link("spanLink"){ @Override public void onClick() { } }); .... @Override protected void onBeforeRender() { super.onBeforeRender(); addOrReplace(new Label("timeStampFresh", "" + new Date())); } } JUG – Ancona Italy Il framework Wicket Il model dei componenti Wicket ● Il ruolo del “model” in wicket 1 ● ● Tutti i componenti Wicket (input form, radio button, select ecc...) memorizzano il valore ad essi associato in un'istanza dell'interfaccia Imodel e dispongono di un metodo getModel() per accedervi dall'esterno. Attraverso il model Wicket offre uno strumento universale per leggere i valori dei suoi componenti, come il contenuto di un campo testuale o l'opzione scelta in una select. public interface IModel { public Object getObject(); public void setObject(final Object object); } ● Anche se ogni componente Wicket può avere un model il suo significato e la sua utilità diventano chiari quando si usano i componenti di una form (campi testo, select, radio button...) che memorizzano nel model il valore inserito in input dall'utente. Il ruolo del “model” in wicket 2 ● Anche le Label hanno un model associato, ed è il secondo parametro stringa del costruttore che viene visualizzato nella label: add(new Label("label", "Hello World")); ... ● ● Nel caso della Label il modello è creato implicitamente dal costruttore e contiene la stringa passata come secondo argomento. Le classi model possono contenere una generica istanza di object ma di solito si ricorre al meccanismo dei generics per esplicitare il tipo di istanza in essi contenuta new Model<String>("label"); … new Model<Persona>(new Persona(“Mario”, “Rossi”)); … I Form di Wicket ● Wicket fornisce una sua classe Form per mappare i form HTML in oggetti Java ed associare a loro uno stato. Il mapping tra classe e tag avviene sempre attraverso l'attributo wicket:id: <form wicket:id="form"> ... ● ● Così come il form HTML anche il corrispondente Wicket si caratterizza in base ai controlli di input che contiene (text field, select, radio button, ecc...) e i pulsanti di submit (di solito uno solo) che avviano la trasmissione dei dati al server. Quindi anche il Form Wicket così come le pagine ed i pannelli è un componente container, ossia può contenere altri componenti al suo interno che devono però essere sottoclassi di FormComponent. Il componente TextField ● ● ● I campi testuali di una form vengono mappati in Wicket con la classe TextField e di solito è utile associare un model String che memorizza il valore inserito dall'utente. Purtroppo i TextField di Wicket non hanno un costruttore che crea in automatico il model secondo il tipo desiderato e dobbiamo farlo a mano :-(: Esempio di mapping di un campo testo di una form: Classe Java new TextField<String>(“username”,new Model<String>(“Inserisci il tuo username”)); Codice HTML Username: <input type="text" wicket:id="username"/> ● Ora vediamo un esempio di model più chiaro, realizzando una pagina di login con due campi testuali “username” e “password”. Prototipo maschera di login Il pannello di login (HTML) <html> … <wicket:panel> <div style="margin: auto; width: 40%" class="crop_content_contenuti"> <form id="ricerca" method="get" wicket:id="form"> <fieldset id="ricerca1" class="center"> <legend wicket:id="message">Ricerca</legend> <p wicket:id="loginStatus" style="font-weight: bold;color: red;textalign: left"></p> <span>Username: </span> <input wicket:id="username" type="text" id="username" /><br/> <span >Password: </span> <input wicket:id="password" type="password" id="password" /> <p> <input type="submit" name="login" value="login"/> </p> </fieldset> </form> </div> </wicket:panel> … </html> ● Nella pannello di login oltre ai campi username/password abbiamo inserito anche un titolo attraverso il tag <legend> e abbiamo inserti un paragrafo che contiene eventuali messaggi di errore se il login non va a buon fine. Il pannello e la form di login (Java) 1 public class LoginPanel extends Panel { public LoginPanel(String id) { super(id); init(); } private void init(){ Form loginForm = new LoginForm("form"); add(loginForm); } } ● Il pannello di login serve esclusivamente da “wrapper” per l'oggetto form vero e proprio che contiene i campi username e epassword e che gestisce il login. Il pannello e la form di login (Java) 2 public class LoginForm extends Form { private TextField usernameField = null; private PasswordTextField passwordField = null; private Label loginStatus = null; public LoginForm(final String componentName) { super(componentName); usernameField = new TextField("username", new Model<String>("")); passwordField = new PasswordTextField("password", new Model<String>("")); loginStatus = new Label("loginStatus"); Wicket dispone di un componente apposito per i campi password. add(usernameField); add(passwordField); add(new Label("message", "Login")); L'evento onSubmit della form scatta add(loginStatus); } prima di inviare i parametri della form al server. public final void onSubmit() { String username = usernameField.getValue(); String password = passwordField.getValue(); if((username.equals("Mario") && password.equals("Rossi"))) loginStatus.setDefaultModel(new Model<String>("Complimenti!")); else loginStatus.setDefaultModel(new Model<String>("Username o password errate!")); } Il pannello di login (Java) public class LoginPanel extends Panel { public LoginPanel(String id) { super(id); init(); } private void init(){ Form loginForm = new LoginForm("form"); add(loginForm); } public final private private private class LoginForm extends Form { TextField usernameField = null; PasswordTextField passwordField = null; Label loginStatus = null; public LoginForm(final String componentName) { super(componentName); Wicket dispone di un componente apposito per i campi password. usernameField = new TextField("username", new Model<String>("")); passwordField = new PasswordTextField("password", new Model<String>("")); loginStatus = new Label("loginStatus"); add(usernameField); add(passwordField); add(new Label("message", "Login")); add(loginStatus); } L'evento onSubmit della form scatta prima di inviare i parametri della form al server. public final void onSubmit() { String username = usernameField.getValue(); String password = passwordField.getValue(); if((username.equals("Mario") && password.equals("Rossi"))) loginStatus.setDefaultModel(new Model<String>("Complimenti!")); else loginStatus.setDefaultModel(new Model<String>("Username o password errate!")); ... La pagina di login ● Visto che ci siamo usiamo il layout di Jug4Tenda..... package helloWorld.layoutTenda; import helloWorld.LoginPanel; import org.apache.wicket.markup.html.basic.Label; public class LoginPage extends JugTemplate { public LoginPage(){ super(); } addOrReplace(new LoginPanel("contentComponent")); } Demo JUG – Ancona Italy Il framework Wicket Gestione avanzata dello stato ● I repeat viewer ● I bean come model Wicket ● ● ● Abbiamo visto cosa è il model in Wicket e lo abbiamo applicato nel modo più semplice possibile agli oggetti TextField Tuttavia la vera utilità del model si comprende usando funzioni più raffinate che permettono di associare a form e controlli delle semplici istanze di Java Bena da usare come model. Ciò permette di ragionare in maniera completamente object oriented e di usare il model di wicket in maniera molto più naturale. Esempio: Vogliamo realizzare un pannello e con una form con dei semplici dati anagrafici (nome, cognome, indirizzo, email). Premendo submit la form: 1) crea un'istanza della classe Person (il nostro JavaBean) che contiene i dati anagrafici 2) la aggiunge alla lista di istanze precedentemente create 3) visualizza le istanze della lista. Il JavaBean Person package helloWorld; import java.io.Serializable; public class Person implements Serializable { private private private private String String String String name; sureName; address; email; public public public public String getAddress() {return address;} void setAddress(String address) {this.address = address;} String getEmail() {return email;} void setEmail(String email) {this.email = email;} public public public public String getName() {return name;} void setName(String name) {this.name = name;} String getSureName() {return sureName;} void setSureName(String sureName) {this.sureName = sureName;} } La form class CreatePerson extends Form{ private Person person = new Person(); public Person getPerson() { return person; } public CreatePerson(String id) { super(id); setDefaultModel(new CompoundPropertyModel<Person>(this)); add(new add(new add(new add(new TextField<String>("person.name")); TextField<String>("person.sureName")); TextField<String>("person.address")); TextField<String>("person.email")); } @Override protected void onSubmit() { super.onSubmit(); personsArray.add(person); person = new Person(); } } La form class CreatePerson extends Form{ private Person person = new Person(); Uso la stessa form come modello per i public Person getPerson() { miei dati ed avrò accesso ai sui campi return person; } public CreatePerson(String id) { super(id); setDefaultModel(new CompoundPropertyModel<Person>(this)); add(new add(new add(new add(new TextField<String>("person.name")); TextField<String>("person.sureName")); TextField<String>("person.address")); TextField<String>("person.email")); } Si traduce in @Override getPerson().getPerson().set/getName, quindi il protected void onSubmit() { TextFiled legge/scrive direttamente sui campi super.onSubmit(); dell'istanza di Person personsArray.add(person); person = new Person(); } La form sarà una inner class del pannello è } vedrà la sua variabile personsArray Il pannello <wicket:panel> <div style="text-align: center;float: left;"> <form wicket:id="form" id="form"> ... </form> </div> <br style="clear: both;"></br> <span wicket:id="persons" > <div style="background-color: rgb(255, 245, 236); border: "> <b>Nome:</b> <span wicket:id="personName"></span><br></br> <b>Cognome:</b> <span wicket:id="personSurename"></span><br></br> <b>Indirizzo:</b> <span wicket:id="address"></span><br></br> <b>Cognome:</b><span wicket:id="email"></span> </div><br/> </span> </wicket:panel> ● Nel pannello oltre alla form c'è un componente con id persons. E'un nuovo tipo di componente Wicket che visualizza collezioni di oggetti ripetendo il codice HTML al suo interno per ogni elemento della collezione. Il pannello public class PersonsManager extends Panel { private List<Person> personsArray = new ArrayList<Person>(); Il codice nell'ellisse è il repeat viewer public PersonsManager(String id, List<Person> personsArray) { che visualizza la lista di persone create super(id); add(new CreatePerson("form")); PageableListView<Person> persons = new PageableListView<Person>("persons", personsArray,30){ @Override protected void populateItem(ListItem<Person> personHtml) { personHtml.add(new Label("personName", personHtml.getModel().getObject().getName())); personHtml.add(new Label("personSurename", personHtml.getModel().getObject().getSureName())); personHtml.add(new Label("address", personHtml.getModel().getObject().getAddress())); personHtml.add(new Label("email", personHtml.getModel().getObject().getEmail())); } }; add(persons); this.personsArray = personsArray; } Demo Conclusioni Pro: Permette di applicare la programmazione OO dal backend alla GUI, quindi... Rende le applicazioni web testabili molto più facilmente. Gestione dello stato trasparente. Grande varietà di componenti pronti all'uso. ... Contro: Cambio di prospettiva radicale! Documentazione utente e tutorial frammentari e scarni. ... JUG – Ancona Italy Grazie ! Andrea Del Bene JUG Marche - www.jugancona.it