Continuiamo l‘uso del Visual Editor di Eclipse passando a un argomento più avanzato. In questo articolo analizzeremo l‘utilizzo delle viste approfondendo gli aspetti relativi all‘interazione tra le views.
Introduzione
In questo articolo introdurremo l’utilizzo di due viste all’interno di una prospettiva, ognuna con una funzione precisa; una di esse ha un ruolo cardine nella gestione delle entità, vedremo che viene indicata con il termine navigator. Vedremo poi come sia possibile far interagire fra di loro le viste di una prospettiva attraverso metodi di lookup capaci di localizzare una vista attiva nella pagina partendo dal suo identificatore. Arricchiremo inoltre il livello di controllo sul campo “codice” in modo che l’utente sia vincolato inserire un valore numerico.
Interazione tra Views
L’obiettivo che ci poniamo con questo articolo è quello di utilizzare due views per la gestione completa dell’entità Libro. Arricchiremo la view di dettaglio (LibroView) già esaminata nei precedenti articoli e ne inseriremo un’altra (LibroListView) capace di visualizzare la lista degli elementi e applicare filtri di ricerca. Nell’architettura RCP, una tale vista viene identificata con il termine di “Navigator”, dato che permette di navigare la lista degli elementi ed individuarne uno determinato fra quelli registrati.
Nella figura 1 riportiamo uno schema disegnato a mano, pubblicato nella wiki del sito di eclipse[1], che dà un’idea dell’architettura che sta alla base della UI di una applicazione RCP.
Figura 1 – Schema applicazione RCP
Supponendo che quellasviluppata nei precedenti articoli sia la nostra “Editor Area”, iniziamo a vedere come è possibile arricchire l’applicazione con un’altra view che ci permetta di vedere l’elenco dei libri presenti su DB ed eventualmente selezionarne uno per modificarlo.
Come prima cosa dobbiamo creare una view che contenga la lista dei libri e poi ci occuperemo di posizionarla a sinistra rispetto alla view di dettaglio che già esiste.
In un secondo momento dovremo preoccuparci di implementare l’interazione tra le due views, da un lato provocando il caricamento dei dati di dettaglio dell’elemento selezionato dal Navigator e dall’altro aggiornando la lista mostrata dal Navigator a seguito di un salvataggio nella form di dettaglio.
Creazione della vista “Navigator”
Ripetiamo velocemente i passi per creare una View utilizzando Visual Editor. Cliccando con il tasto sul package che contiene le view e selezionando dal menu contestuale la voce “New” e poi “Other”, si aprirà uno wizard dal quale si dovrà scegliere il tipo”View Visual Class”, come mostrato in figura 2.
Figura 2 – Creazione “View Visual Class”
Inserisci un nome per la nuova classe (p.e. “LibroListView”) e premi “Finish”. Da questo momento è possibile utilizzare la palette che si trova sulla destra di per aggiungere i componenti alla view, come abbiamo già spiegato nel precedente articolo.
Quello che dobbiamo fare è aggiungere una label con la scritta “Filtro” e un Text che chiameremo txtFiltro, customizzandone il layout in modo che occupi tutto lo spazio (grabEccess_HorizontalSpace e Fill_Horizontal a true).
A questo punto inseriamo sotto la label un componente Table (che troviamo sempre sulla palette), specificando nella popup del Layout i valori riportati in figura 3; tali opzioni gli permetteranno di posizionarsi sotto ai componenti sopra definiti, come mostrato in figura.
Figura 3 – Layout della tabella
A questo punto la creazione della view è completa e possiamo occuparci delle azioni da associarle. Cliccando con il tasto destro del mouse sulla tabella e selezionando la voce “Event” -> “Add event …” si apre la dialog in figura 4.
Figura 4 – Aggiungere un listener
Questa finestra mostra tutti gli eventi che possono essere utilizzare per gestirne le azioni: selezioniamo widgetSelected e clicchiamo su finish. A questo punto è necessario aggiungere manualmente all’interno del listener il seguente codice:
import org.eclipse.ui.IViewPart; import org.eclipse.ui.PlatformUI; ... TableItem[] sel= ((Table)e.widget).getSelection(); if(sel!=null && sel.length==1){ String codice = sel[0].getText(0); Libro libro = LibroBusiness.load(codice); IViewPart view = PlatformUI.getWorkbench() .getActiveWorkbenchWindow() .getActivePage() .findView(LibroView.ID); if (view instanceof LibroView) { LibroView libroView = (LibroView) view; libroView.assignCurrent(libro); } }
Vedrete che l’istruzione libroView.assignCurrent(libro) darà un errore di compilazione, dato che non abbiamo ancora modificato la classe LibroView; ma, state tranquilli, lo faremo tra un attimo!
Aggiungiamo un altro listener, questa volta sul campo di testo txtFiltro. Cliccando con il tasto destro del mouse e selezionando la voce “Event” -> “modify text”, verrà subito aggiunto al text il listener all’interno del quale sarà sufficiente richiamare il metodo aggiornaLista() (anche questa riga darà errore di compilazione).
Ultimiamo la preparazione di questa classe con l’aggiunta di due metodi che ne completeranno l’implementazione:
import java.util.Collection; import java.util.Iterator; ... public void aggiornaLista() { Collection lista = LibroBusiness.findByTitolo(txtFiltro.getText()); collection2ListWidget(lista); } private void collection2ListWidget(Collection libri) { table.removeAll(); for (Iterator iterator = libri.iterator(); iterator.hasNext();) { Libro libro = iterator.next(); TableItem item = new TableItem(table, SWT.NONE); item.setText(new String[] { libro.getCodice(), libro.getTitolo() }); } }
Infine l’ultimo passo per completare il processo di creazione della view è l’inserimento nel file plugin.xml dell’extension in configurazione. Non ripetiamo tutti i passi che sono già stati presentati negli articoli precedenti, ma ci limitiamo a riportare in figura 5 la configurazione corretta.
Figura 5 – Configurazione dell’extension della view “LibroListView”
Rivediamo la perspective
Riprendiamo e approfondiamo la parte iniziale dell’articolo per parlare della costruzione della perspective.
Avevamo costruito, negli articoli precedenti, la perspective in modo che potesse ospitare una sola view. Ora dobbiamo portare qualche modifica per permettere alle due views di dividersi lo spazio a disposizione. Per far ciò è sufficiente modificare il metodo createInitialLayout della classe Perspective.java, come riportato qui sotto.
public void createInitialLayout(IPageLayout layout) { String editorArea = layout.getEditorArea(); layout.setEditorAreaVisible(false); layout.setFixed(true); layout.addStandaloneView(LibroListView.ID, false, IPageLayout.LEFT, 0.40f, editorArea); layout.addStandaloneView(LibroView.ID, false, IPageLayout.LEFT, 0.60f, editorArea); }
Come è facilmente intuibile abbiamo semplicemente indicato alle due view, che devono dimensionarsi in modo che la prima occupi il 40% dello spazio a disposizione, mentre la seconda il restante 60%. Non è questo il modo più elegante per disegnare una perspective, ma per lo scopo che ci poniamo in questo articolo sarà più che sufficiente.
Figura 6 – Componenti del workbench di eclipse
Nella figura 6 riportiamo un esempio concreto, che non è altro che una prospettiva standard di Eclipse: sono evidenziate tutte le componenti del workbench, non solo le 3 view e l’editor che permette la modifica dei file di testo, ma anche alcune aree particolari che arricchiscono l’interazione con l’utente. La tool bar, la shortcut bar e la status line infatti sono a nostra disposizione per essere utilizzate anche nello sviluppo di applicazioni per il cliente finale.
Nella parte seguente dell’articolo infatti vedremo come utilizzare la status line per senalare eventuali problemi di validazione di un campo di input.
I nuovi metodi di business
A questo punto dobbiamo aggiungere nella classe LibroBusiness il metodo “findByTitolo” che ci fornirà la lista di oggetti Libro il cui titolo inizia con la sottostringa passata come parametro:
import java.util.List; ... public static List findByTitolo(String titoloRicerca) { List ret = null; EntityManager em = emf.createEntityManager(); ret = em.createQuery( "select libro from Libro as libro" + " where libro.titolo like :ricercaLike") .setParameter("ricercaLike", titoloRicerca+"%") .getResultList(); em.close(); return ret; }
Si noti come venga utilizzato il concatenamento dei metodi (method chaining) per l’impostazione del parametro per l’operatore LIKE e per il recupero del result set.
Completiamo la classe LibroView
Per quanto riguarda il codice della view che gestisce il dettaglio di un oggetto Libro dovrà invece essere modificato in diversi punti.
Innanzitutto sposteremo il codice responsabile del databinding dal metodo “createPartControl” in un metodo privato specifico
private void bind() { DataBindingContext dbc = new DataBindingContext(); dbc.bindValue(SWTObservables.observeText(txtCodice,SWT.Modify), BeansObservables.observeValue(libro,"codice"),null,null); dbc.bindValue(SWTObservables.observeText(txtTitolo,SWT.Modify), BeansObservables.observeValue(libro,"titolo"),null,null); }
Figura 7 – Metodo bind()
In questo modo, il metodo può essere invocato, oltre che qui (durante l’inizializzazione della view), anche dal nuovo metodo “assignCurrent”, che invece sarà pubblico in quanto richiamato dal Navigator nel momento in cui cambia l’elemento selezionato nella tabella.
public void assignCurrent(Libro libro) { this.libro=libro; bind(); }
Grazie a questa operazione infatti l’oggetto libro passato come parametro dal Navigator (cioè l’elemento selezionato in tabella) verrà collegato agli widget della view con tutti i vantaggi già descritti nei precedenti articoli.
Inoltre tale metodo ci permette di sperimentare un modo alternativo di effettuare un caricamento di un oggetto Libro sulla view quando si esce dal campo titolo con un valore presente nel database (evento lostFocus). La riga
BeanUtils.copyProperties(libro, libroDB);
può infatti essere sostituita da
assignCurrent(libroDB);
provocando ora il re-bind del nuovo oggetto caricato in luogo della precedente operazione di copia delle proprietà del nuovo oggetto su quelle dell’oggetto corrente.
Dato che abbiamo a che vedere con due view (il Navigator e quella di dettaglio) visualizzate contemporaneamente, dobbiamo preoccuparci di aggiornare il Navigator in seguito al salvataggio di dati sulla view di dettaglio. Questo può essere realizzato creando il seguente metodo “updateListView”, da richiamare subito dopo il salvataggio di un dato.
Figura 8 – Aggiornamento dei dati presenti nella tabella del Navigator
import org.eclipse.ui.IViewPart; import org.eclipse.ui.PlatformUI; ... private void updateListView() { IViewPart view = PlatformUI.getWorkbench() .getActiveWorkbenchWindow() .getActivePage() .findView(LibroListView.ID); if (view instanceof LibroListView) { LibroListView libroListView = (LibroListView) view; libroListView.aggiornaLista(); } }
Tale metodo non fa altro che localizzare l’istanza del Navigator correntemente utilizzata (attraverso il suo ID) e provocarne l’invocazione del metodo delegato all’aggiornamento della lista, con la rilettura dei dati dal database.
Utilizzo della statusline
Per poter utilizzare la statusline è necessario attivarla, cosa che è possibile fare semplicemente modificando da false a true l’attributo nell’istruzione all’interno della classe ApplicationWorkbenchWindowsAdvisor. Riportiamo di seguito il codice del metodo preWindowOpen() modificato.
public void preWindowOpen() { IWorkbenchWindowConfigurer configurer = getWindowConfigurer(); configurer.setInitialSize(new Point(600, 300)); configurer.setShowCoolBar(false); configurer.setShowStatusLine(true); configurer.setTitle("Biblioteca"); }
Nella classe LibroView dobbiamo inserire il controllo sul campo codice in modo che accetti solo input numerici. Aggiungiamo un ModifyListener (ormai abbiamo già visto come) e inseriamo il codice qui sotto riportato.
import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; ... txtCodice.addModifyListener(new ModifyListener(){ @Override public void modifyText(ModifyEvent e) { if(!"".equals(txtCodice.getText())){ try { Integer.parseInt(txtCodice.getText()); } catch (NumberFormatException e1) { ApplicationWorkbenchAdvisor.getStatusLine() .setErrorMessage( "Attenzione il valore inserito non è numerico"); PlatformUI.getWorkbench() .getDisplay().asyncExec(new Runnable(){ @Override public void run() { txtCodice.setFocus(); } }); return; } } ApplicationWorkbenchAdvisor.getStatusLine().setErrorMessage(""); } });
Questa parte di codice non fa altro che un parse del valore inserito dall’utente, cercando di ottenere un integer. Nel caso il metodo dia un eccezione NumberFormatException, si visualizza il messaggio di errore nella statusline e si riporta il focus sul campo. Quest’ultima operazione in particolare utilizza le funzioni del MultiThreading (argomento che non verrà affrontato in questo articolo) ma che comunque necessita di un piccolo approfondimento: richiamiamo infatti la piattaforma per recuperare il display ed eseguire l’istruzione in un thread asincrono. L’effetto che otteniamo è che l’esecuzione dell’istruzione setFocus() viene accodata all’esecuzione attuale. In questo modo siamo sicuri che sia l’ultima cosa che viene eseguita dal thread che gestisce l’interfaccia.
Nella figura 9 riportiamo la prova di esecuzione dell’applicazione, evidenziando la funzionalità della statusline per la senalazione degli errori.
Figura 9 – Esecuzione con la statusline
Prepariamo il DB per l’esecuzione
A titolo di esempio, eseguiamo un caricamento di dati di prova, aggiungendo il seguente blocco di codice come ultime istruzioni del metodo “createInitialLayout” della classe Perspective:
for (int i = 0; i < 10; i++) { Libro libro= new Libro(); libro.setCodice("00"+i); libro.setTitolo("A"+i); LibroBusiness.save(libro); } for (int i = 0; i < 10; i++) { Libro libro= new Libro(); libro.setCodice("01"+i); libro.setTitolo("B"+i); LibroBusiness.save(libro); }
Otterremo così una certa quantità di record il cui titolo inizia con la lettera “A” e altri che iniziano con la lettera “B”; digitando l’una o l’altra lettera nel campo di ricerca vedremo apparire la lista dei record corrispondenti, mentre nessun record apparirà digitando una lettera diversa.
Naturalmente si potrà sperimentare il funzionamento della ricerca inserendo valori più plausibili di titoli di libri. Oppure si potrebbe effettuare la ricerca della sottostringa all’interno del campo titolo invece che all’inizio.
In ogni caso tenete presente che il valore “create” della proprietà “hibernate.hbm2ddl.auto” definito nel file persistence.xml ha l’effetto di ricreare le strutture del database ad ogni esecuzione, con evidente perdita dei dati eventualmente presenti. Se questo meccanismo può risultare utile nelle fasi di testing automatico e durante la realizzazione dei primi prototipi, visto che riporta le medesime condizioni di partenza ad ogni esecuzione, diventa poco pratico in tutti gli altri casi. Per conservare i dati ad ogni esecuzione, pur consentendo al motore di persistenza di aggiornare le strutture del database quando necessario, è sufficiente impostare la proprietà “hibernate.hbm2ddl.auto” al valore “update”.
Conclusioni
Abbiamo visto come completare la gestione di un’entità semplice utilizzando il concetto di Navigator e nell’implementazione di questo abbiamo visto come gestire un nuovo fondamentale widget: l’oggetto table. Si noti come l’utilizzo di un Navigator rappresenti solo uno tra gli approcci possibili (in realtà fra i più utilizzati). Un modo alternativo potrebbe ad esempio basarsi sull’uso di pop-up di ricerca oppure sull’utilizzo di viste ad elenco da “attraversare” prima di arrivare alla vista di dettaglio.
Riferimenti
“Using Perspectives in the Eclipse UI”
http://www.eclipse.org/articles/using-perspectives/PerspectiveArticle.html