Terzo articolo su Wicket Web Beans, un toolkit che combina insieme le potenzialità del framework Wicket con quelle dei Java Web Beans. In questa terza parte viene fatta una carrellata di altri importanti strumenti messi a disposizione di questo framework e viene posto l‘accento sulla semplicità del loro uso e sulla loro versatilità.
Proseguiamo nella presentazione del framework Wicket Web Beans. Per la comprensione del presente articolo valgono le raccomandazioni fatte nei primi due della serie (webbeans-7 e webbeans-8, vedi menu alla sinistra) riguardo le conoscenze e le letture preliminari. La versione di Wicket Web Beans a cui faremo riferimento è sempre la 1.1.
Tabs
In una applicazione (web e non), in tutti quei casi in cui per un form bisogna trattare un numero notevole di informazioni che quindi non possono essere contenute in un’unica pagina, si ricorre ai tabs. Anche Wicket Web Beans (WWB) mette a disposizione questo component, molto semplice da trattare. Prendendo in esame il solito TestBean di esempio utilizzato nei due precedenti articoli, il codice Java della Page che contiene il tabs basato su tale Bean rimane esattamente identico a quello della SimpleBeanPage vista nel primo articolo:
public class TabBeanPage extends WebPage { public TabBeanPage() { TestBean bean = new TestBean(); BeanMetaData meta = new BeanMetaData(bean.getClass(), null, this, null, false); add( new BeanForm("beanForm", bean, meta) ); } public void save(AjaxRequestTarget target, Form form, TestBean bean) { info("Saved - thank you"); } public void cancel(AjaxRequestTarget target, Form form, TestBean bean) { info("Canceled - thank you"); } public void clearLastName(AjaxRequestTarget target, Form form, TestBean bean) { bean.setLastName(""); } }
Quello che cambia è il beanprops file associato:
# Tabs Example TestBean { props: -number; tabs: name { props: firstName, lastName, action.clearLastName }, calculator { props: operand1, operand2, result }; }
In questo caso abbiamo utilizzato l’attributo props per indicare le proprietà di TestBean che non vogliamo visualizzare. Quindi abbiamo utilizzato l’attributo tabs per specificare due tab groups (name e calculator) e le proprietà da visualizzare in ciascuno dei due e l’ordine di visualizzazione. In figura 1 viene mostrato il risultato che si ottiene a runtime:
Figura 1 – Il tab per TestBean.
Bean Contexts
WWB consente di implementare casi d’uso (context) diversi utilizzando lo stesso codice di un form e di una page. Supponiamo per esempio che, in un’applicazione che implementa il CRUD, si voglia consentire a un utente di editare tutti i field al momento dell’aggiunta di un nuovo record, vedere un record senza poter modificare alcuni field e modificarne solo alcuni in caso di update. Facendo riferimento al solito TestBean di esempio, il codice dell’unica Page da implementare è il seguente:
import java.math.BigDecimal; import net.sourceforge.wicketwebbeans.containers.BeanForm; import net.sourceforge.wicketwebbeans.examples.simple.TestBean; import net.sourceforge.wicketwebbeans.model.BeanMetaData; import org.apache.wicket.markup.html.WebPage; public class ContextBeanPage extends WebPage { public ContextBeanPage() { TestBean bean = new TestBean(); bean.setFirstName("Dan"); bean.setLastName("Syrstad"); bean.setOperand1(BigDecimal.valueOf(123.0)); bean.setOperand2(BigDecimal.valueOf(456.0)); BeanMetaData meta = new BeanMetaData(bean.getClass(), "view", this, null, false); add( new BeanForm("beanForm", bean, meta) ); } }
e il beanprops file associato è:
# Context Example TestBean { props: firstName, lastName, EMPTY, operand1, operand2, result, -number; } TestBean[view] { viewOnly: true; } TestBean[limitedEdit extends view] { props: firstName{ viewOnly: false }; }
In questo caso abbiamo passato al costruttore di BeanMetaData il valore “view” per indicare a runtime tutti i field che per questa pagina sono visibili in sola lettura. Per cambiare context e passare, per esempio in modalità “limitEdit”(tutti i field in sola lettura, tranne firstName), basta passare in ingresso al costruttore di BeanMetaData il valore “limitEdit” (il quale estende “view”):
BeanMetaData meta = new BeanMetaData(bean.getClass(), "limitedEdit", this, null, false);
senza alcuna necessità di dover implementare una nuova Page.
Nested Beans
Supponiamo di avere il seguente Bean Address:
import java.io.Serializable; public class Address implements Serializable { private String address1; private String address2; private String city; private String state; private String zip; public Address() { } public String getAddress1() { return address1; } public void setAddress1(String address1) { this.address1 = address1; } public String getAddress2() { return address2; } public void setAddress2(String address2) { this.address2 = address2; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getState() { return state; } public void setState(String state) { this.state = state; } public String getZip() { return zip; } public void setZip(String zip) { this.zip = zip; } }
e un secondo Bean, Customer, avente due attributi di tipo Address:
import java.io.Serializable; public class Customer implements Serializable { private String firstName; private String lastName; private Address billToAddress; private Address shipToAddress; public Customer() { } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = (firstName == null ? null : firstName.toUpperCase()); } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public Address getBillToAddress() { return billToAddress; } public void setBillToAddress(Address billToAddress) { this.billToAddress = billToAddress; } public Address getShipToAddress() { return shipToAddress; } public void setShipToAddress(Address shipToAddress) { this.shipToAddress = shipToAddress; } }
La WebPage che conterrà il Bean Customer sarà fatta così:
import net.sourceforge.wicketwebbeans.containers.BeanForm; import net.sourceforge.wicketwebbeans.model.BeanMetaData; import org.apache.wicket.markup.html.WebPage; public class NestedBeanPage extends WebPage { public NestedBeanPage() { Customer bean = new Customer(); BeanMetaData meta = new BeanMetaData(bean.getClass(), null, this, null, false); add( new BeanForm("beanForm", bean, meta) ); } }
Come si può notare dal codice, viene creata solo un’istanza di Customer. Le istanze di Address relative agli attributi billToAddress e shipToAddress vengono create automaticamente da WWB e visualizzate in base a quanto descritto nel beanprops file associato alla pagina:
# Nested Bean Example Customer { props: firstName, lastName, billToAddress{colspan: 3}, shipToAddress{colspan: 3}; } Address { props: address1{colspan: 3}, address2{colspan: 3}, city, state, zip; }
Questo è l’unico punto in cui ci si riferisce esplicitamente ai due Bean, Customer e Address. In WWB i Nested Bean per default vengono visualizzati all’interno di BeanGridPanel e i suoi field sotto forma di BeanGridField. La proprietà colspan serve ad indicare al Panel quante colonne deve occupare al suo interno un singolo field. In figura 2 si può osservare il risultato finale della Page a runtime.
È possibile cambiare il tipo di field con cui vengono rappresentati i nested Beans semplicemente ricorrendo alla proprietà fieldType all’interno del beanprops file. Supponendo di voler utilizzare, per esempio, field di tipo net.sourceforge.wicketwebbeans.fields.BeanInCollapsibleField, nel beanprops file va specificato quanto segue:
Customer { props: firstName, lastName, billToAddress{colspan: 3; fieldType: BeanInCollapsibleField }, shipToAddress{colspan: 3; fieldType: BeanInCollapsibleField }; }
Il risultato finale di questo secondo caso è mostrato in figura 3.
Figura 3 – WebPage con Nested Beans e BeanInCollapsibleField.
Enumerated types
Talvolta una proprietà rappresenta una selezione all’interno di una lista di valori. Se tale proprietà restituisce una enumeration Java, tramite WWB è semplice rappresentarla come drop-down list. Riprendiamo in esame il Bean Customer. A questo aggiungiamo una nuova proprietà, customerType
private CustomerType customerType; public CustomerType getCustomerType() { return customerType; } public void setCustomerType(CustomerType customerType) { this.customerType = customerType; }
di tipo CustomerType
public enum CustomerType { Consumer, Business, Government, International }
Il codice Java della Page relativa al Bean non differisce da quello delle altre che abbiamo già visto:
import net.sourceforge.wicketwebbeans.containers.BeanForm; import net.sourceforge.wicketwebbeans.model.BeanMetaData; import org.apache.wicket.markup.html.WebPage; public class EnumBeanPage extends WebPage { public EnumBeanPage() { Customer bean = new Customer(); BeanMetaData meta = new BeanMetaData(bean.getClass(), null, this, null, false); add( new BeanForm("beanForm", bean, meta) ); } }
È nel beanprops file associato l’unico punto in cui viene specificato che il field customerType deve essere visualizzato come drop-down list:
# Enum Bean Example Customer { cols: 1; props: firstName, lastName, customerType{default: Government}; }
Tramite la proprietà default abbiamo indicato Government come valore di default. Qualora non venga indicato un valore di default, la drop-down mostrerà come default la selezione vuota. In figura 4 il risultato che si ottiene a runtime per la EnumBeanPage.
Figura 4 – EnumBeansPage layout.
Tables
Nei casi in cui sia necessario visualizzare delle liste, WWB mette a disposizione le tables. Vediamo un esempio relativo ad una Page in cui dobbiamo visualizzare una lista di Nested Bean. Il Bean a cui facciamo riferimento è Invoice:
import java.io.Serializable; import java.util.ArrayList; import java.sql.Date; import java.util.List; import net.sourceforge.wicketwebbeans.examples.nested.Address; public class Invoice implements Serializable { private String invoiceNumber; private String customerName; private Date invoiceDate; private Address shipToAddress; private List lines = new ArrayList(); public Invoice() { } public String getInvoiceNumber() { return invoiceNumber; } public void setInvoiceNumber(String invoiceNumber) { this.invoiceNumber = invoiceNumber; } public String getCustomerName() { return customerName; } public void setCustomerName(String customerName) { this.customerName = customerName; } public Date getInvoiceDate() { return invoiceDate; } public void setInvoiceDate(Date invoiceDate) { this.invoiceDate = invoiceDate; } public Address getShipToAddress() { return shipToAddress; } public void setShipToAddress(Address shipToAddress) { this.shipToAddress = shipToAddress; } public List getLines() { return lines; } public void setLines(List lines) { this.lines = lines; } }
il quale, fra le altre proprietà, ha una lista di InvoiceLine:
import java.beans.PropertyChangeListener; import java.beans.PropertyChangeSupport; import java.io.Serializable; import java.math.BigDecimal; import java.math.RoundingMode; public class InvoiceLine implements Serializable { public enum ItemCode { Gears, Sprockets, Chains, Wheels, Gizmos }; private Integer quantity; private ItemCode itemCode; private BigDecimal cost; private PropertyChangeSupport listeners = new PropertyChangeSupport(this); public InvoiceLine() { } // JavaBeans compliant method to add a PropertyChangeListener. public void addPropertyChangeListener(PropertyChangeListener listener) { listeners.addPropertyChangeListener(listener); } // JavaBeans compliant method to remove a PropertyChangeListener. public void removePropertyChangeListener(PropertyChangeListener listener) { listeners.removePropertyChangeListener(listener); } public Integer getQuantity() { return quantity; } public void setQuantity(Integer quantity) { this.quantity = quantity; fireTotalChange(); } public ItemCode getItemCode() { return itemCode; } public void setItemCode(ItemCode itemCode) { this.itemCode = itemCode; } public BigDecimal getCost() { return cost; } public void setCost(BigDecimal cost) { // Ensure two decimal places this.cost = (cost == null ? null : cost.setScale(2, RoundingMode.HALF_UP)); fireTotalChange(); } private void fireTotalChange() { listeners.firePropertyChange("total", null, getTotal()); } public BigDecimal getTotal() { return cost == null ? null : cost.multiply( BigDecimal.valueOf((long)quantity)); } }
Nel codice della Page viene istanziato, come abbiamo visto in precedenza parlando dei Nested Beans, solo quello più esterno (in questo caso Invoice). Le istanze di InvoiceLine associate a quella di Invoice vengono create al momento in cui viene eseguita la action di addLine (e aggiunte alla tabella). La action removeLine rimuove invece una linea dalla table.
import net.sourceforge.wicketwebbeans.containers.BeanForm; import net.sourceforge.wicketwebbeans.model.BeanMetaData; import org.apache.wicket.ajax.AjaxRequestTarget; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.form.Form; public class TableBeanPage extends WebPage { private Invoice bean = new Invoice(); public TableBeanPage() { // Start with one empty line. bean.getLines().add( new InvoiceLine() ); BeanMetaData meta = new BeanMetaData(bean.getClass(), null, this, null, false); add( new BeanForm("beanForm", bean, meta) ); } public void addLine(AjaxRequestTarget target, Form form, Invoice bean) { this.bean.getLines().add( new InvoiceLine() ); } public void removeLine(AjaxRequestTarget target, Form form, InvoiceLine line) { bean.getLines().remove(line); info("Removed line with item code " + (line.getItemCode() == null ? "" : line.getItemCode()) ); } }
Il codice del beanprops file associato è il seguente:
# Table Bean Example Invoice { props: invoiceNumber, invoiceDate, EMPTY, customerName, shipToAddress{colspan: 3}, action.addLine{colspan: 3}, lines{colspan: 3}; } InvoiceLine { props: action.removeLine{labelImage: "remove.gif"}, quantity, itemCode, cost, total; } Address { props: address1{colspan: 3}, address2{colspan: 3}, city, state, zip; }
Con la pseudo-proprietà EMPTY si indica che nel BeanGridPanel una cella deve rimanere vuota. La proprietà labelImage utilizzata nella action addLine consente di visualizzare un’immagine al posto di una label. In figura 5 viene mostrato il layout della TableBeanPage.
Figura 5 – TableBeanPage layout.
Nella figura 5 si può notare che il field invoiceDate viene visualizzato con un pop-up calendar. Questo tipo di visualizzazione viene fatta in automatico da WWB per tutte le proprietà di tipo java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp e java.util.Calendar.
Conclusioni
In questo ultimo articolo della serie sono stati descritti gli altri principali components di WWB. Da quanto riportato si evince che l’implementazione della struttura di una web application tramite WWB è molto rapida e ci si può quindi concentrare sul modello e sulle funzionalità di business.
Riferimenti
[1] Karthik Gurumurthy, “Pro Wicket”, Apress, 2006
[2] Guglielmo Iozzia, serie di articoli su Wicket framework pubblicati su MokaByte a partire da
https://www.mokabyte.it/cms/article.run?articleId=XIU-464-LXV-CM2_7f000001_11994537_f9c9053a
[3] Guglielmo Iozzia, serie di articoli su Web Beans pubblicati su MokaByte a partire da
https://www.mokabyte.it/cms/article.run?articleId=2U2-E3P-NNC-6VT_7f000001_10911033_0a4a63e2
[4] Specifiche Sun JavaBeans
http://java.sun.com/javase/technologies/desktop/javabeans/docs/spec.html
[5] Sito ufficiale di Wicket Web Beans presso Google
http://code.google.com/p/wicket-web-beans/
[6] Wicket Web Beans SVN repository
http://wicket-web-beans.googlecode.com/svn/trunk/