Nella nostra analisi della programmazione con Google Web Toolkit / GXT, prosegue la trattazione sul binding dei componenti con set di dati. Questo mese vediamo alcune tecniche di ottimizzazione della paginazione e di buffering.
Introduzione
Nelle puntate precedenti abbiamo visto i concetti base legati all’uso di componenti databound, entrando nello specifico delle tecniche di caricamento dati tramite proxy e loader e al relativo uso all’interno del modello MVC di GXT. Questo mese completiamo la trattazione andando a vedere come sia possibile utilizzare alcune varianti di proxy e loader al fine di creare delle versioni ottimizzate di binding eseguendo paginazioni e bufferizzazioni dei dati.
Prima di vedere questi aspetti, affrontiamo il caso di semplici oggetti databound e vediamo come sia possibile visualizzare all’interno di una griglia o di una tabella un set di dati contenuti all’interno di un liststore (il cui caricamento, visto nell’articolo precedente, verrà dato qui per già fatto).
Oggetti databound: liste combo box, griglie
Proseguendo la trattazione del mese scorso, la prima cosa che resta da vedere è come sia possibile associare uno store a un oggetto di tipo lista (list, combo box,…) per la rappresentazione nella GUI. Il codice qui riportato verrà inserito nel pannello che fa parte della view del modello MVC di GXT (vedi articolo precedente); si ipotizza quindi che lo store con i dati passato al pannello sia stato già popolato con i dati secondo lo schema visto nell’articolo precedente: all’interno del controller viene eseguita una invocazione a un metodo dello strato di business logic per ricavare il set di dati che verranno usati per riempire lo store, store che poi viene passato alla view per la visualizzazione.
Il primo semplice esempio che possiamo prendere in considerazione è quello relativo al binding di una combo box con l’elenco dei corsi disponibili (l’esempio è quello di una applicazione per la gestione di un corso di studi) in modo da consentire la successiva visualizzazione in una griglia degli studenti di quel corso:
comboBoxCorsi = new ComboBox(); comboBoxCorsi.setEmptyText("Seleziona il corso"); comboBoxCorsi.setFieldLabel("Corso"); comboBoxCorsi.setDisplayField("siglaCorso"); corsiTutorListStore = new ListStore(); comboBoxCorsi.setStore(corsiTutorListStore); comboBoxCorsi.setTriggerAction(TriggerAction.ALL);
Figura 1 – La combo box popolata con l’elenco dei corsi disponibili in modo da consentire la successiva visualizzazione in una griglia degli studenti di quel corso.
Per quanto riguarda invece la visualizzazione dei dati in griglie si usa anche in questo caso uno store associato tramite una operazione di bind alla Grid:
// crea il column model ColumnModel columnModel = this.createGridColumnModel(0); // la lista deve essere vuota alla prima visualizzazione // qui si associa una array vuoto ArrayList users = new ArrayList(); store = new ListStore(); store.add(users); grid = new EditorGrid(store, columnModel); grid.setBorders(true); grid.setStripeRows(true); grid.getView().refresh(true);
Si noti la presenza di un oggetto ColumnModel con il quale è possibile specificare sia i nomi delle colonne della griglia che il binding con i dati presenti nello store; tale oggetto potrebbe essere così configurato:
List configs = new ArrayList(); // imposta il nome della colonna e a quale attributo // deve essere associato fra quelli disponibili nell'oggetto // presente nello store ColumnConfig studentIdColumn = new ColumnConfig("nomeStudente", "Nome studente", 70); configs.add(studentIdColumn); ColumnConfig studentNameColumn = new ColumnConfig("cognomeStudente", "Cognome studente", 130); configs.add(studentNameColumn);
La coppia griglia-form
Una caratteristica interessante che il famework mette a disposizione è la gestione dei form in maniera automatica agganciando i valori presenti in una griglia con quelli dei campi del form. Questo meccanismo permette ad esempio di eseguire modifiche a strutture dati complesse, tenendo in buffer tutte le modifiche e inviarle in un sol colpo allo strato server per il salvataggio.
Per capire esattamente cosa si intende si può fare riferimento alla immagine riportata in figura 2: dopo aver popolato una griglia con alcuni dati (in questo caso si tratta di dati relativi a studenti di un corso) si associa tale griglia al form che sta sulla destra per le operazioni di modifica. La griglia infatti, esclusivamente per motivi di spazio, non potrebbe visualizzare tutti i campi (colonne) della entità Studente (oltretutto il risultato visivo potrebbe risultare poco gradevole o poco usabile): in questo caso ci si limita alla visualizzazione delle due colonne più significative (nome e cognome). Ogni volta che utente seleziona una riga, i valori dell’entità selezionata popolano il form sulla destra, permettendone la modifica.
Figura 2 – Grazie all’uso di un oggetto Formbindings è possibile associare una griglia ad un form per la modifica.
L’oggetto che permette tale collegamento è Formbindings che automaticamente collega ogni campo del form con un attributo dell’entità usata per popolare lo store che è associato alla griglia:
// crea l'oggetto GXT Grid con nomi delle colonne // configurati opportunamente Grid grid = this.createGrid(); // crea un form che verrà visualizzato alla destra della griglia // in un pannello contenitore FormPanel form = createForm(); // crea un oggetto formbindings da associare al form // il parametro true permette l'auto-binding fra i campi del // form e gli attributi delle entità presenti nello store formBindings = new FormBinding(form, true); // associa lo store della griglia a quello presente formBindings.setStore((Store) grid.getStore()); // infine aggiunge la griglia e lo store al pannello // per la visualizzazione contentPanel.add(grid, new RowData(.4, 1)); contentPanel.add(form, new RowData(.6, 1));
Di tutto questo passaggio la riga fondamentale è la seguente:
formBindings = new FormBinding(form, true);
che permette di legare gli attributi degli oggetti databound contenuti nello store con i campi del form: ovviamente tale collegamento può essere eseguito in modo automatico solo se ogni field del form ha lo stesso nome del corrispondente attributo dell’entità. Ad esempio, volendo inserire nel form un campo che permetta di modificare il valore dell’attributo “valutazione” dello studente, si dovrà scrivere:
NumberField numberFieldVal = new NumberField(); numberFieldValSchede.setName("valutazione"); numberFieldValSchede.setFieldLabel("Valutazione"); fieldSet1.add(numberFieldValSchede, formData);
L’associazione fra le n righe della griglia (n oggetti di tipo Studente) e i dati visualizzati nel form (uno Studente per volta) potrà essere eseguita ogni volta che l’utente seleziona una particolare riga della griglia, associando un listener sintonizzato sull’evento di cambio dell’item selezionato, che consenta di associare l’elemento correntemente selezionato con i dati visualizzati:
grid.getSelectionModel().addListener(Events.SelectionChange, new Listener<SelectionChangedEvent>() { public void handleEvent(SelectionChangedEvent be) { if (be.getSelection().size() > 0) { ModelData selectedModel = (ModelData) be.getSelection().get(0); formBindings.bind(selectedModel); } else { formBindings.unbind(); } } });
Modifiche massive
L’utilizzo di uno store “dietro” la griglia permette di realizzare interessanti meccanismi di modifica in batch: in pratica è possibile eseguire molteplici variazioni ai dati degli studenti utilizzando il form sulla destra, e poi eseguire un aggiornamento in un solo colpo andando a farsi dare dallo store solamente i dati che sono cambiati.
Il codice per eseguire questa semplice operazione è molto semplice: aggiungiamo infatti due pulsanti, uno per l’annullamento l’altro per il commit delle modifiche:
contentPanel.addButton(new Button("Annulla", new SelectionListener() { @Override public void componentSelected(ButtonEvent ce) { grid.getStore().rejectChanges(); } })); contentPanel.addButton(new Button("Salva", new SelectionListener() { @Override public void componentSelected(ButtonEvent ce) { // lancia un evento per il salvataggio dei dati AppEvent event = new AppEvent(Events.saveStudenti); event.setData("studentiStore", grid.getStore().getModifiedRecords()); Dispatcher.forwardEvent(event); } })); return contentPanel; }
Si noti in questo caso l’operazione di annullamento sullo store (“rejectChanges()”) che forza a rimettere i valori precedenti nella griglia.
La gestione della commit sui dati segue quanto detto nella puntata precedente: si prelevano i dati dallo store (notare che in questo caso ci facciamo dare solo i record della griglia effettivamente modificati), si inseriscono in un evento e li si inviano al controller specifico per le operazioni di salvataggio.
Ovviamente è possibile eseguire una operazione di ripristino: in tal caso tutti i valori della griglia verranno rimessi ai valori originali prima delle modifiche fatte come mostrato nel seguente pezzo di codice (in questo caso si usa un pulsante tramite il quale forzare l’operazione di annullamento:
contentPanel.addButton(new Button("Annulla", new SelectionListener() { @Override public void componentSelected(ButtonEvent ce) { grid.getStore().rejectChanges(); } }));
Si tenga presente che questo approccio alla modifica/salvataggio massiva dei dati si adatta necessariamente a tutti gli scenari: infatti in caso di errore su una singola operazione di salvataggio, il sistema esegue una operazione di ripristino su tutto il dataset reimpostando la griglia nello stato precedente, annullando anche le modifiche per quei valori buoni, che invece potevano essere correttamente salvate.
Caricamenti ottimizzati
In tema di tecniche di databounding GXT offre alcune interessanti varianti volte a ottimizzare e rendere più performante il processo di caricamento e binding dei dati. Sul sito di GXT si possono trovare a tal proposito alcuni esempi piuttosto interessanti che mostrano nell’ordine:
- il caricamento bufferizzato in una griglia di un set di dati (tramite JSON) di considerevole dimenzione
- la paginazione lato server dei dati
- la paginazione lato client
In tutti e tre questi esempi (che riporteremo qui tali e quali per semplificare il lavoro del lettore che potrà in tal modo verificarne il funzionamento direttamente online sul sito della casa [GXT]) sono strutturati in modo simili: tramite un proxy si esegue una connessione a una sorgente dati, con un reader si esegue la lettura dei dati (nel caso di JSON, i dati devono essere opportunamente decodificati, operazione resa possibile grazie alla configurazione specifica di un reader).
Il primo esempio che andiamo ad analizzare è quello che mostra come eseguire una paginazione dei dati da mostrare in una griglia; il risultato finale è illustrato nella figura 3.
Figura 3 – Popolamento di una griglia con paginazione.
Il codice che permette questo risultato è il seguente:
// Si utilizza un proxy paginabile RpcProxy<PagingLoadResult> proxy = new RpcProxy<PagingLoadResult>() { @Override public void load(Object loadConfig, AsyncCallback<PagingLoadResult> callback) { service.getPosts((PagingLoadConfig) loadConfig, callback); } }; // analogamente anche il loader deve essere di tipo speciale e consentire la paginazione final PagingLoader<PagingLoadResult> loader ; loader = new BasePagingLoader<PagingLoadResult>( proxy); // il caricamento deve essere eseguito lato server loader.setRemoteSort(true); // infine associa lo store al loaader ListStore store = new ListStore(loader); // si crea una toolbar di paginazione che viene bindata allo store per forzare il // caricamento dei dati della pagina successiva final PagingToolBar toolBar = new PagingToolBar(50); toolBar.bind(loader);
Si noti la presenza della toolbar “bindata” con lo store, cosa che permette di scatenare, ove necessario, ulteriori caricamenti dei dati lato server (ricordo che si tratta di una paginazione lato server, il client visualizza tutti i dati che riceve). Pezzo forte di questo passaggio è la definizione della gestione degli eventi legati al cambio pagina: essendo in atto una paginazione lato server, il client visualizza i dati e ne richiede altri quando l’utente supera la dimensione del buffer memorizzato lato client.
grid.addListener(Events.Attach, new Listener<GridEvent>() { public void handleEvent(GridEvent be) { PagingLoadConfig config = new BasePagingLoadConfig(); config.setOffset(0); config.setLimit(50); // dalla griglia ci facciamo dire in che stato ci troviamo in funzione dell'evento // generato: l'oggetto state viene reimpostato di conseguenza e passato // al loader Map<String, Object> state = grid.getState(); if (state.containsKey("offset")) { int offset = (Integer)state.get("offset"); int limit = (Integer)state.get("limit"); config.setOffset(offset); config.setLimit(limit); } if (state.containsKey("sortField")) { config.setSortField((String)state.get("sortField")); config.setSortDir(SortDir.valueOf((String)state.get("sortDir"))); } loader.load(config); } });
Il secondo esempio invece, visibile in esecuzione in figura 4, esegue una paginazione dei dati lato client: in questo caso arrivano tutti i dati in un sol colpo e si paginano nella griglia per poterli vedere un po’ per volta:
// aggiunge il proxy con funzionalità di paginazione lato client PagingModelMemoryProxy proxy = new PagingModelMemoryProxy(TestData.getStocks()); // loader PagingLoader<PagingLoadResult> loader = new BasePagingLoader<PagingLoadResult>(proxy); loader.setRemoteSort(true); ListStore store = new ListStore(loader); final PagingToolBar toolBar = new PagingToolBar(10); toolBar.bind(loader); loader.load(0, 10);
Anche in questo caso il punto fondamentale è l’uso di un loader apposito (per la paginazione) unitamente a un proxy che stavolta esegue la paginazione direttamente in memoria.
Figura 4 – Caricamento dati con paginazione lato client in memoria.
Infine il terzo caso in cui si mette in contatto la griglia con un set di dati strutturati in XML (si usa JSON per il trasferimento); al solito nella figura 5 si può vedere il risultato finale di tale soluzione.
Figura 5 – Caricamento dati strutturati in JSON tramite un proxy-reader apposito.
La differenza rispetto a prima è la presenza di un proxy adatto alla connessione JSON (si preoccupa della connessione e del trasporto dei dati) e di un reader che opportunamente configurato consente la trasformazione/lettura dei dati XML in un formato stampabile nella griglia (in questo caso è essenziale l’utilizzo anche di specifici renderer che formattano i dati in modo opportuno all’interno di ogni cella).
String url = "http://www.extjs.com/forum/topics-browse-remote.php"; ScriptTagProxy<PagingLoadResult> proxy = new ScriptTagProxy<PagingLoadResult>(url); ModelType type = new ModelType(); type.setRoot("topics"); type.setTotalName("totalCount"); type.addField("title"); type.addField("forumtitle"); type.addField("forumid"); type.addField("author"); type.addField("replycount"); type.addField("lastposter"); type.addField("excerpt"); type.addField("replycount"); type.addField("threadid"); DataField datefield = new DataField("lastpost"); datefield.setType(Date.class); datefield.setFormat("timestamp"); type.addField(datefield); JsonPagingLoadResultReader<PagingLoadResult> reader ; reader = new JsonPagingLoadResultReader<PagingLoadResult>(type); final PagingLoader<PagingLoadResult> loader ; loader = new BasePagingLoader<PagingLoadResult>(proxy, reader); loader.addListener(Loader.BeforeLoad, new Listener() { public void handleEvent(LoadEvent be) { BasePagingLoadConfig m = be. getConfig(); m.set("start", m.get("offset")); m.set("ext", "js"); m.set("lightWeight", true); m.set("sort", (m.get("sortField") == null) ? "" : m.get("sortField")); m.set("dir", (m.get("sortDir") == null || (m.get("sortDir") != null && m. get("sortDir").equals(SortDir.NONE))) ? "" : m.get("sortDir")); } }); // imposta le modalità di ordinamento loader.setSortDir(SortDir.DESC); loader.setSortField("lastpost"); loader.setRemoteSort(true); ListStore store = new ListStore(loader); final PagingToolBar toolBar = new PagingToolBar(500); toolBar.bind(loader); List columns = new ArrayList(); RowNumberer rn = new RowNumberer(); rn.setWidth(30); columns.add(rn); ColumnConfig title = new ColumnConfig("title", "Topic", 100); title.setRenderer(new GridCellRenderer() { public Object render(ModelData model, String property, ColumnData config, int rowIndex, int colIndex, ListStore store, Grid grid) { return " href="http://extjs.com/forum/showthread.php?t=" + model.get("threadid") + "" target="_blank">" + model.get("title") + " href="http://extjs.com/forum/forumdisplay.php?f=" + model.get("forumid") + "" target="_blank">" + model.get("forumtitle") + " Forum"; }
Conclusioni
Con questa quinta parte della serie si conclude l’analisi delle tecniche di databining disponibili all’interno del framework GXT. Nei prossimi numeri affronteremo gli aspetti legati alla gestione della visualizzazione e del rendering tramite CSS e HTML.
Riferimenti
[GXT] Alcuni esempi di GXT