Dopo aver parlato nei mesi scorsi della architettura MVC di una applicazione GXT tipica e della relativa gestione degli eventi, questo mese vediamo un‘altra parte piuttosto interessante legata al meccanismo di visualizzazione ed editing dei dati tramite il legame (binding) di oggetti visuali ai corrispettivi oggetti di modello (JavaBeans opportunamente adattati).
Premessa
Nello scenario che andiamo a proporre, si immagina di caricare dei dati da un layer di persistenza tramite una qualche logica resa disponibile da un opportuno layer (p.e., session bean in esecuzione in un container EJB). I dati verranno travasati dallo strato di persistenza (JPA, Entities, Hibernate, Spring) in oggetti di trasporto (pattern DTO) per atterrare all’interno del layer di persistenza. L’uso di DTO è in questo caso particolarmente indicato a causa della modalità con la quale GXT permette il binding con oggetti grafici (come avremo modo di vedere più avanti).
Negli esempi che seguono si faranno vedere come collegare questi insiemi di dati con liste e con griglie (per la visualizzazione e per l’editing), nonche’ come agganciare il singolo dato a un form per la creazione/modifica.
Come pretesto in questo articolo immaginiamo di dover scrivere una applicazione che gestisca gli studenti di una scuola con le relative associazioni alle classi; l’obiettivo non è ovviamente quello di mostrare tutti gli aspetti di logica della applicazione ma piuttosto vedere come sia possibile collegare gli oggetti di dominio (tipicamente dei JavaBeans) agli oggetti grafici della GUI per una rapida visualizzazione (il binding appunto).
Oggetti di modello per il binding
Volendo visualizzare in GUI un dato o un set di dati, GXT mette a disposizione, come molti altri framework Java e non, alcuni componenti piuttosto potenti con funzionalità di databounding, ovvero particolari widget che, se associati a particolari JavaBeans, permettono la visualizzazione dei loro dati, consentendone eventualmente anche la modifica.
Per permettere tale associazione, non è possibile utilizzare un JavaBean qualsiasi, ma devono essere rispettate alcune semplici regole: il lavoro per trasformare una classe in un oggetto adatto al binding non è complesso; sicuramente il vantaggio è quello di non dover poi scrivere nemmeno una riga di codice nella gestione bidirezionale dei dati da e verso il widget grafico.
Nel caso si vogliano collegare liste di valori con widget GXT di tipo lista, combo o tabella, è necessario usare un oggetto di tipo listStore che è il punto di partenza della trattazione. Un listStore, come suggerisce il nome, è un magazzino di dati che verranno successivamente collegati al widget. Uno store permette interessanti funzionalità fra cui il caricamento differito, la paginazione e il caching dei dati (aspetti di cui parleremo approfonditamente in uno dei prossimi articoli).
Prima di procedere con la spiegazione su quali sono i modi con cui popolare uno store, cerchiamo di capire per prima con che tipo di oggetti si può riempire tale “magazzino”: essendo lo store infatti direttamente connesso con il componente widget databound (una lista o una griglia) è necessario usare oggetti opportunamente predisposti per la visualizzazione automatica in GUI (detto in altro modo, si richiede un piccolo sforzo di implementazione/preparazione in cambio del binding automatico fra oggetti di dominio e componenti grafici).
Supponiamo di avere un JavaBean che rappresenta un determinato oggetto di dominio: ad esempio uno studente della nostra applicazione immaginaria; la parte di definizione degli attributi della classe Studente potrebbe essere la seguente:
public class Studente implements Serializable { private Integer id; private String nome; private String cognome; private Integer idAnagrafica; private String siglaCorso; private Integer stato; private Date dataSelezione; private Date dataIsrizione; private Integer annoDiCorso; private Integer annoInCorso; private String eMail; ... }
Questa entità potrebbe essere un oggetto che all’interno del layer di persistenza verrebbe reso persistente tramite Hibernate o con l’inserimento di opportune annotazioni con JPA/EJB3.0. Lo stesso oggetto, o una sua diversa versione, potrebbe poi essere spostato sul layer di presentation per essere usato per visualizzarne gli attributi in GUI.
A tal proposito si potrebbe immaginare di collegare il bean lato persistenza (che per semplicità chiameremo entity) con il corrispettivo lato presentation tramite un oggetto di trasporto (pattern DTO) il quale avrebbe più o meno la stessa struttura dell’entity. In diversi casi, sul layer di presentation il DTO viene nuovamente tradotto in un JavaBean (detto a volte Value Object, VO)il cui scopo è esclusivamente quello di vivere nello strato di visualizzazione a stretto contatto con gli oggetti grafici.
Questo approccio spesso è considerato da alcuni troppo prolisso: per far passare una informazione da uno strato all’altro è necessario scrivere troppo codice che alla fine è tutto un po’ simile a se’ stesso (entity, DTO e VO sono del tutto simili fra loro, anche se hanno funzioni diverse). Personalmente, anche se riconosco l’eccessiva formalizzazione di questo approccio, ritengo che in alcuni casi seguire questo formalismo possa risultare molto utile e, come avremo di vedere fra poco, ciò vale particolarmente in GWT/GXT.
In questo caso infatti, dal punto di vista del binding, il DTO una volta arrivato sul layer di presentation (ovvero all’interno della applicazione GXT) dovrà essere convertito per poterne consentire l’associazione con il widget; per creare l’oggetto destinazione, GXT fornisce diverse soluzioni:
- usare un oggetto che deriva dalla classe base BeanModel;
- associare al bean semplice una classe marker che lo “abilita” a essere usato come oggetto di modello del widget.
La prima soluzione obbliga alla scrittura di un ulteriore oggetto nel quale travasare i dati che arrivano tramite il DTO. Questa soluzione, come si diceva poco sopra, obbliga a scrivere molto codice dato che richiede la creazione di una ulteriore classe (il VO) clone del DTO; il Value Object, in questo caso, non sarà un semplice JavaBean, ma deriva dalla classe BeanModel di GXT:
public class StudenteModel extends BeanModel{ private Integer id; private String nome; private String cognome; private Integer idAnagrafica; private String siglaCorso; private Integer stato; private Date dataSelezione; private Date dataIsrizione; private Integer annoDiCorso; private Integer annoInCorso; private String eMail; ... }
La seconda soluzione è più sbrigativa e sintetica e permette di usare direttamente il DTO (o comunque un semplice POJO) per effettuare l’assocazione al widget: è sufficiente infatti affiancare al POJO una classe che funzioni da marcatore del bean; ad esempio si potrebbe pensare di scrivere:
@BEAN(it.mokabyte.miopackage.Studente.class) public interface StudenteMarker extends BeanModelMarker { }
Si potrebbe pensare adesso al popolamento del liststore con istanze di oggetto Studente e il framework penserà automaticamente al binding con il widget.
A prima vista, la seconda soluzione appare più veloce, dato che permette di risparmiare tempo in scrittura di codice; si potrebbe usare il progetto EJB (dove abbiamo definito gli entity persistenti) e usare direttamente i DTO o addirittura i POJO persistenti per eseguire il travaso in una grid o combox.
Questa ipotesi non tiene conto però di come è regolato il funzionamento di una applicazione GWT (e quindi anche GXT): dato che stiamo parlando di associare il bean al widget, stiamo parlando del ramo client della applicazione GWT (si veda il primo articolo di questa serie per un rapido ripasso), ramo che prevede la trasformazione in JS di codice Java tramite l’apposito compilatore GWT. Le classi che derivano da progetti esterni (in questo caso il POJO o il DTO del progetto EJB) sono viste come classi server e non possono essere tramutate in oggetti JS-client senza le opportune inclusioni dei moduli esterni (i package con le classi del progetto esterno) nel modulo principale GWT (operazione che personalmente non amo particolarmente) e che rende il lavoro non più tanto semplice come si poteva immaginare inizialmente.
C’è un altro aspetto che rema contro la seconda soluzione e che deriva da considerazioni tipiche del ruolo del progettista: usare un oggetto DTO o peggio ancora POJO come oggetto di binding nel widget lega con un filo molto forte e rigido i due layer (peggio ancora i due corrispondenti progetti) per cui questa dovrebbe essere una soluzione da evitare se possibile.
Il mio personale consiglio, per quello che può valere, è di usare l’entity bean sul layer di persistenza, di travasare e quindi esportare i dati da tale layer tramite un DTO e di ritravasare nuovamente i dati in un oggetto di valore (pattern Value Object, VO) che risiede nel layer di presentation e lì solamente: se presentation è GXT, travaseremo il DTO in un bean semplice (POJO) utilizzando comunque il meccanismo del marker, quindi facendo un mix delle due tecniche. A parte i gusti personali, si possono apportare le dovute variazioni sul tema: importante è rispettare le imposizioni di GWT nella separazione dei layer e nel cercare di limitare l’accoppiamento fra i vari layer.
Quindi, ricapitolando, possiamo pensare di avere:
- un bean persistente (inserito nel progetto e nel layer server/EJB);
- un DTO per il trasporto dati;
- un oggetto VO popolato tramite i dati che arrivano dal DTO;
- un marker del VO che lo “abilita” al binding con il widget grafico.
È consigliabile usare una apposita classe di utilità (Helper Class) il cui compito specifico è quello di effettuare i travasi: ciò allo scopo di semplificare il lavoro e renderlo più agile, agevolando il travaso DTO-VO e, viceversa, per il viaggio di ritorno. Eccone un esempio:
public class StudenteHelper{ ... // metodo che riceve in input un DTO e resituisce il corrispondente VO public static StudenteVO getStudenteVO(StudentiDTO studenteDTO){ StudenteVO studenteVO = new StudenteVO (studentiDTO.getIdStudente(), studentiDTO.getNome(), studentiDTO.getCognome(),etc...); return studenteVO; } // metodo che crea un DTO partendo dal VO di partenza public static StudentiDTO getStudenteDTO(StudenteVO studenteVO){ ... }
Oggetto Store: cosa è, come si popola (caricamento differito: store, loader, proxy)
Ora che abbiamo chiarito cosa possiamo usare come oggetto di binding, possiamo tornare alla disamina dello store; come si diceva poco sopra, si tratta di un oggetto il cui scopo è funzionare come magazzino per la memorizzazione di dati da associare al widget; di fatto è un tramite fra gli oggetti di dominio da associare al componente grafico e il componente stesso.
Per popolare uno store, si possono seguire differenti soluzioni, delle quali la più semplice (sarebbe meglio dire la più rudimentale) prevede di inserire manualmente uno o più oggetti di modello (scelti in base a una delle tecniche mostrate nel paragrafo precedente) tramite una semplice operazione di add; in questo caso, dopo aver ricavato dal layer di business logic una lista di oggetti VO, la aggiunge allo store:
List studentVOList = ... listStore.add(studentVOList);
Il secondo modo è quello di popolare lo store tramite l’ausilio di un proxy e di un loader: l’uso combinato di questi tre oggetti consente di ottenere molti interessanti risultati dalla paginazione on demand, al caching dei dati. Per il momento ci limitiamo ad analizzare il caso più semplice in cui lo store viene popolato tramite l’ausilio di un loader apposito con il quale rimane connesso per mezzo di un proxy (in un prossimo articolo analizzeremo alcune interessanti evoluzioni dell’uso di questi oggetto):
// ricava il remote provider RPC-GWT che era stato preventivamente // istanziato e caricato nel registro di sessione final RemoteProviderAsync remoteProvider = (RemoteProviderAsync) Registry.get("remoteProvider"); // crea un oggetto proxy il cui metodo di load è associato // alla invocazione del metodo remoto del provider; in questo caso // verrà caricata la lista degli studenti in base al nome del corso RpcProxy proxy = new RpcProxy() { public void load(Object loadConfig, AsyncCallback callback) { remoteProvider.findStudentiPerCorso(siglaCorso, callback); } }; // crea un reader di oggetti BeanModel: lo scopo è quello di // consentire automaticamente la lettura dei dati // che arrivano da remoto BeanModelReader reader = new BeanModelReader(); // associa il reader al proxyListLoader loader = new BaseListLoader(proxy, reader); // istanzia lo store associandolo al loaderstudent ListStore = new ListStore(loader); // definisce un ascoltatore associato al listener // tale listener entra in funzione nel momento in cui // verrà dato ordine al loader di caricare i dati: // in questo caso si sfrutta il meccanismo degli eventi di MVC // per inoltrare un evento applicativo che contiene i dati // caricati e inseriti nel listStore loader.addLoadListener(new LoadListener() { public void loaderLoad(LoadEvent le) { AppEvent event = new AppEvent(MyEvents.loadStudenti); event.setData("studentListStore", studentListStore); forwardToView(psicoterapiaView, event); } public void loaderLoadException(LoadEvent le) {
}
});
I commenti riportati nel codice dovrebbero chiarire in modo piuttosto esaustivo il significato di ogni passaggio.
Infine si può procedere a caricare i dati tramite metodo load() del loader:
// carica i dati nello store loader.load();
L’esecuzione del metodo load() provoca il caricamento dei dati tramite l’ausilio del reader per l’operazione fisica di lettura dei vari oggetti bean model e il proxy per l’associazione (ma disaccoppiata) con lo store.
A questo punto la variabile studentListStore può essere “passata” alla parte di presentazione visuale per la associazione con i widget specifici. Per fare questo lo store viene impacchettato in un evento applicativo che viene poi a sua volta inoltrato al Controller dell’MVC di GXT. Questo passaggio viene spiegato meglio nel paragrafo successivo.
Chi fa cosa? La gestione del data binding in un contesto MVC
Nella documentazione ufficiale di GXT, per quanto possa sembrare strano vista la sua importanza, non sono specificati in modo chiaro e univoco il ruoli dei vari componenti in un contesto MVC: chi invoca il caricamento dei dati? chi popola lo store? chi associa lo store a widget? chi esegue il refresh grafico del componente?
Proponiamo quindi una soluzione (forse la più semplice) che ha l’obiettivo di permettere il corretto caricamento dei dati e la successiva visualizzazione minimizzando le dipendenze fra i vari componenti come loader, store, oggetti grafici e in generale i vari componenti dell’MVC. Ogni variante a quello che andremo a mostrare dovrebbe comunque tenere sempre a mente questi aspetti.
Per semplificare la trattazione e la sequenza delle operazioni elenchiamo qui di seguito la sequenza delle operazioni da svolgere:
Step 1
Nella definizione dei componenti di una GUI si deve inserire una qualche logica che consenta la generazione di eventi che scatenino il caricamento dei dati, il binding e poi la visualizzazione. Per fare questo possiamo ipotizzare di inserire un pulsante alla cui pressione da parte dell’utente si generi un evento applicativo:
buttonCancel = new Button("Cerca"); buttonCancel.addSelectionListener(new SelectionListener() { public void componentSelected(ButtonEvent ce) { AppEvent event = new AppEvent(LoadStudents); // immette nell'evento il nome del corso da usare nella ricerca; // la variabile courseName viene ricavata dalla GUI // tramite un campo a selezione dell'utente event.setData("courseName", courseName); // lancia l'evento che verrà intercettato dal controller // per il caricamento dei dati relativi al corso in oggetto Dispatcher.forwardEvent(event); } });
Step 2
L’evento generato verrà intercettato da un opportuno controller (in ascolto su tale evento) il quale provvede a eseguire l’invocazione remota al provider RPV-GWT per il popolamento dello store tramite loader reader e proxy.
public void handleEvent(AppEvent event) { EventType type = event.getType(); if (type == MyEvents.loadStudents) { // esegue l'invocazione asincrona // verso il layer server per // ricavare la lista degli studenti // lo store viene caricato secondo una delle tecniche viste // in precedenza loadStudents(event.get("courseName")); } }
Qui il metodo loadStudents, oltre a ricavare il parametro del nome del corso dall’evento applicativo, esegue la chiamata asincrona e popola lo store; in questo caso si usa una tecnica di caricamento dei dati nello store, probabilmente più semplice e immediata (tecnica spesso usata per il binding su liste e combobox):
final RemoteProviderAsync remoteProvider = (RemoteProviderAsync) Registry.get("remoteProvider"); AsyncCallback callback = new AsyncCallback() { public void onSuccess(Object result) { List corsoVOs = (List) result; // crea il factory per creare l'oggetto datamodel BeanModelFactory modelFactory = BeanModelLookup.get().getFactory(StudentVO.class); // crea lo store per memorizzare i datamodel object ListStore corsiTutorListStore = new ListStore(); List studentModels; studentsModels = modelFactory.createModel(studentVOs); studentsListStore.add(corsoModels); AppEvent event = new AppEvent(MyEvents.loadStudents); event.setData("studentsListStore", studentsListStore); forwardToView(myView, event); } public void onFailure(Throwable ex) { ... } }; // invoca il proveder asincrono RPC-GWT remoteProvider.findStudents(courseName, callback);
In questo caso la lista ricevuta dopo l’invocazione remota da parte del provider RPC-GWT contiene dei semplici bean che vengono tramutati in bean di modello per mezzo del metodo modelFactory.createModel() e poi associati allo store.
Step 3
Una volta che lo store è stato caricato, verrà inserito in un nuovo evento applicativo per il trasferimento verso la View; si crea quindi l’evento, vi si immette lo store e si esegue un forward dell’evento verso la view specifica:
AppEvent event = new AppEvent(MyEvents.loadStudents); event.setData("studentsListStore", studentsListStore); forwardToView(myView, event);
Step 4
La view, nel metodo handleEvent intercetta tale evento e ne estrapola lo store
public class StudentsView extends View{ // definisce la variabile studentsPanel che // conterrà la griglia con gli studenti StudentsPanel studentsPanel; ... protected void handleEvent(AppEvent event) { ... else if (event.getType() == MyEvents.loadStudenti) // verifica se nell'evento c'è uno store valido per // popolare la lista degli studenti. // in tal caso lo associa al componente grafico ListStore studentsListStore = (ListStore) event.getData("studentsListStore"); if (studentsListStore != null){ studentsPanel.getGrid().reconfigure(studentsListStore, studentsPanel.getGrid().getColumnModel()); } return; } }
Si noti che la view ha diretto accesso agli oggetti presenti in GUI dato che, è bene ricordare che è la view stessa che crea gli oggetti della parte di GUI; quindi la view ricava l’oggetto di cui effettuare il binding e vi assegna lo store; in questo caso si pensa a popolare una griglia. Infine si eseguono le operazioni per forzare il refresh grafico degli oggetti.
Conclusione
Per questo mese ci fermiamo qui; nella prossima puntata vedremo nel dettaglio come si definisce un oggetto di tipo grid o list e come si associa un form di input con uno store per l’inserimento o la modifica di dati.