TikeSwing è un framework open source che permette di implementare agevolmente il pattern MVC nelle nostre applicazioni desktop che fanno uso della libreria grafica Swing. Il framework semplifica e velocizza lo sviluppo permettendo una “copia” diretta dei dati dal model alla view e viceversa senza il classico intervento del programmatore. In questa seconda parte, vedremo gli aspetti inerenti la personalizzazione.
Nel precedente articolo abbiamo fatto una breve introduzione a questo framework open source. Abbiamo anche visto come creare un semplice MVC [1], come collegare le varie entità tra di loro e fatto una breve introduzione alla gestione degli eventi. In questa seconda parte cominceremo da quello che abbiamo lasciato in sospeso, ovvero il pattern Observer, e impareremo a trattare più a fondo gli eventi che il framework ci mette a disposizione per, ad esempio, tracciare le modifiche dell‘utente e gestire l‘applicazione in modo interattivo. Infine vedremo come costruire dei componenti personalizzati e integrarli con TikeSwing [ 2].
Ancora Pattern: Observer
Questo pattern è alla base del meccanismo di “dialogo” tra i componenti. A dire il vero è anche alla base della libreria Swing, infatti i concetti che seguono saranno familiari per chi ha un po‘ di dimestichezza con Swing. Come ogni design pattern[3], nasce per risolvere uno specifico problema. In questo caso si pone come soluzione al seguente problema: realizzare una dipendenza uno-a-molti nella quale “molti” oggetti rivolgono la propria attenzione su un altro oggetto per “ascoltarne” i cambiamenti. L‘entità oggetto delle “attenzioni” si identifica come Observable, quelli in ascolto come Observer (o Listener). L‘idea è che ogni ascoltatore si “registri” presso l‘oggetto da monitorare e sia aggiornato di conseguenza ad ogni cambiamento di stato. Per notificare un oggetto ascoltatore di solito si usa un metodo di interfaccia (tipo update()) che viene invocato dall‘Observable in caso di cambiamento di stato. L‘Observer è quindi un oggetto che implementa una determinata interfaccia per permettere all‘Observable di notificarlo. La figura dovrebbe chiarire quanto detto.
Figura 1 – Il pattern Observer
In realtà il pattern non è molto rigido e i metodi register() e update() si possono implementare secondo le proprie esigenze passando ad esempio uno o più parametri dentro update().
Per chiarire definitivamente il tutto facciamo un esempio concreto noto a tutti. Pensiamo ad una comune applicazione con un bottone di tipo JButton. Per essere notificati quando l‘utente clicca il bottone si scrive più o meno quanto segue:
myButton.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
//codice gestione
}
}
In questo caso il bottone è il nostro Observable, la classe anonima che implementa ActionListener il nostro Observer. Invocando il metodo addActionListener non facciamo altro che “registrare” la nostra classe che implementa ActionListener presso l‘Observable in qualità di ascoltatore. Nel momento in cui il bottone sarà premuto allora il bottone cercherà tutti i listener registrati presso di lui (ovvero tutti i listener passati con addActionListener), e invocherà su di essi il metodo
actionPerformed(ActionEvent e);
che non è altro che il metodo di interfaccia di cui parlavamo prima. Nel nostro caso non solo non abbiamo la necessità di creare nuove classi, anche se anonime, quanto il vantaggio principale è che il codice è già diviso per competenze (codice business nel controller), e non abbiamo “sporcato” la view con codice che esula dalla semplice presentazione dei dati. A questo punto dovrebbe essere tutto chiaro…
Tracciare le modifiche dell‘utente
Spesso scriviamo un‘infinità di codice per capire se un utente ha modificato un dato qualsiasi di quelli a video per proporre, ad esempio, una finestra di salvataggio solo se i dati sono effettivamente stati modificati. Tutto questo con TikeSwing non ha senso poiché dispone di un metodo per sapere se l‘utente ha modificato i dati.
boolean hasViewChanges()
Il metodo ritornerà true se l‘utente ha modificato uno qualsiasi dei campi a video, false altrimenti. Quello che fa non è altro che andare a scoprire tutti i componenti che sono nella view e verificare che non abbiano subito cambiamenti rispetto al modello tramite il metodo di YController
boolean hasChanges(YIModelComponent comp)
che verifica se un determinato componente ha subito cambiamenti rispetto al suo campo mappato nel modello. In questo modo abbiamo bisogno di una singola istruzione per sapere se uno qualsiasi dei campi disponibili all‘utente è stato cambiato.
Interazione real-time con l‘utente
A volte abbiamo il bisogno che l‘applicazione comunichi con l‘utente, in modo interattivo, appena finisce di inserire un campo (ad esempio un indirizzo email). Prendiamo il caso in cui abbiamo un componente di tipo YTextField con MVC_NAME=”email” e relativo campo mappato nel modello di tipo String e nome “email”. Volendo comunicare all‘utente se l‘indirizzo appena inserito è valido (almeno sintatticamente), una prima soluzione è:
- Aggiungere un FocusListener sull‘oggetto YTextField tramite implementazione dell‘interfaccia FocusListener o estensione della classe astratta FocusAdapter.
- Implementare il metodo che controlla la correttezza dell‘indirizzo.
Con TikeSwing, è possibile implementare dei metodi che corrispondano a “mvc_name”+Changed che verranno invocati a runtime appena un campo cambierà il suo valore nel modello. Questo meccanismo, che prende spunto dall‘Observer visto sopra, permette al controllore di essere notificato appena una proprietà cambia il suo valore nel modello. Per invocare questi metodi si usa la Reflection[4], quindi si procede all‘invocazione SOLO nel caso in cui siano state implementi i metodi. Per l‘esempio visto sopra il codice del controllore è il seguente:
public void emailChanged() {
// controllo email
}
Quello visto sopra è un esempio banale, ma è possibile sapere se, ad esempio, è cambiato un elemento di una JTable, cosa non altrettanto banale. Per tutti i possibili casi si rimanda alla lettura della JavaDoc disponibile con il download, o ai commenti nei sorgenti che sono veramente ben fatti. Una nota riguarda il caso appena trattato della validazione dell‘input dell‘utente. Nel framework sono disponibili diversi componenti in grado di validare “al volo” l‘input dell‘utente e assumere un bordo differente o non permettere che il focus passi al componente successivo in caso di input non valido. Se invece siamo interessati a tutti i cambiamenti che possono avere luogo nel model basta implementare il seguente metodo:
public void modelChanged(YModelChangeEvent event);
dove all‘interno dell‘oggetto event ci sono tutte le informazioni per capire cosa è cambiato e gestire il tutto in un unico metodo.
Dialogo tra controller
Veniamo ora ad una delle cose più comode di TikeSwing, ovvero il dialogo tra i controllori. Quando si ha la necessità di informare uno o più controller di qualche evento si può usare un meccanismo interno che permette di registrarsi su determinati eventi o inviare un messaggio in Broadcast a tutti i controllori interessati ad un certo evento. Vediamo prima come lanciare i messaggi, successivamente vedremo come registrare ogni singolo controllore per uno o più eventi. Per effettuare una notifica si invia un oggetto ApplicationEvent a tutti i controllori in ascolto tramite il metodo di YController
sendApplicationEvent(ApplicationEvent e);
I costruttori di ApplicationEvent permettono di specificare fino a 3 parametri che sono:
- Nome dell‘evento (meglio se si specifica come costante pubblica dentro il controllore e accedere sempre a quella istanza di String)
- Sorgente (cioè quale oggetto ha generato l‘evento)
- Valore (un valore che si desidera passare agli ascoltatori)
In questo modo si hanno diverse possibilità per notificare altri controllori di un determinato evento come meglio si desidera.
Per ricevere una notifica bisogna prima registrarsi per l‘evento tramite il metodo
public void register(String nomeEvento);
Successivamente si implementa il metodo.
public void receiveApplicationEvent(YApplicationEvent e);
e si gestisce. Ipotizziamo ora che un controllore (diciamo C1) voglia permettere ad un altro (C2) di essere notificato quando l‘utente salva i dati presenti nella view di C1. C1 dovrà avere una costante pubblica di tipo String per identificare l‘evento, successivamente al salvataggio dei dati invocherà il metodo per lanciare la notifica a tutti i controllori interessati. C2 a sua volta dovrà registrarsi per quell‘evento e implementare il metodo receiveApplicationEvent. Vediamo un esempio di codice per chiarire le idee…
//C1
public class C1 extends YController{
public static final String DATI_SALVATI="dati_salvati";//nome dell‘evento
public void salva(){
//salva i dati...
sendApplicationEvent(new YApplicationEvent(DATI_SALVATI,model.getData(),this));
}
}
//C2
public class C2 extends YController {
public C2() {
super();
register(C1.DATI_SALVATI);//Si registra per l‘evento
}
@Override
public void receivedApplicationEvent(YApplicationEvent event) {
if(event.getName().equals(C1.DATI_SALVATI)){
//Codice di gestione
}
}
}
Per non essere più notificati basta semplicemente invocare
public void unregister(String event);
Tutto questo ricorda l‘Observer, infatti l‘idea è la stessa. La differenza è che tutto è gestito da un‘unica classe, quindi abbiamo solo oggetti simili che comunicano tra di loro, mentre con l‘Observer si dispone di una interfaccia che tutti possono implementare. In questo caso però si desidera che il meccanismo di eventi abbia luogo solo tra controllori e non tra entità eterogenee.
Un componente personalizzato
Vediamo ora come implementare un componente personalizzato per modificare particolari tipi di dati come, ad esempio, numeri interi positivi. Per prima cosa implementeremo il componente per visualizzare e modificare solo numeri interi, poi lo collegheremo al modello integrandolo in TikeSwing. Fatto questo avremo il nostro nuovo componente da usare come abbiamo imparato fino ad adesso. Creiamo innanzitutto una classe che estende JTextField. Nel costruttore imposteremo un Document (che gestisce le stringhe visualizzate) personalizzato per permettere unicamente l‘inserimento di caratteri corrispondenti a numeri interi. Vediamo come:
public class IntegerTextField extends JTextField {
/*
* classe per costringere l‘utente a inserire solo numeri
*/
private class IntegerDocument extends PlainDocument {
char[] validi = new char[] { ‘1‘, ‘2‘, ‘3‘, ‘4‘, ‘5‘, ‘6‘, ‘7‘, ‘8‘, ‘9‘, ‘0‘ };
@Override
public void insertString(int offs, String str, AttributeSet a)
throws BadLocationException {
if (valida(str))
super.insertString(offs, str, a);
}
/*
* Controlla se la stringa contiene solo caratteri numerici
*/
private boolean valida(String str) {
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
boolean valido = false;
for (int j = 0; j < validi.length && !valido; j++) {
if (validi[j] == c)
valido = true;
}
if (!valido)
return false;
}
return true;
}
}
/*
* costruttore di default
*/
public IntegerTextField() {
super();
setDocument(new IntegerDocument());
}
}
In realtà bisognerebbe controllare meglio la correttezza del testo inserito in modo da non superare il valore Integer.MAX_VALUE e gestire le eventuali eccezioni generate dalla conversione dell‘oggetto String in Integer. Fatto questo procediamo all‘integrazione. Per integrare i componenti bisogna semplicemente implementare l‘interfaccia YIModelComponent. I relativi metodi da implementare per il collegamento con il model sono i seguenti:
// YProperty per impostare MVC_NAME e altri parametri
private YProperty yProperty = new YProperty();
// Getter per YProperty
public YProperty getYProperty() {
return yProperty;
}
/*
* Questo è il valore che sarà copiato nel modello.
*/
public Object getModelValue() {
String text = getText();
if (text.equals("")) {
return null;
} else {
// Converto la stringa in Integer
return new Integer(text);
}
}
/*
* Riceve il valore dal modello e lo visualizza al suo interno
*/
public void setModelValue(Object obj) {
if (obj == null || !(obj instanceof Integer)) {
this.setText("");
} else {
if (obj instanceof Integer)
// Ottengo la stringa da visualizzare
this.setText(((Integer) obj).toString());
}
}
/*
* Qui va aggiunto il listener che aggiorna il modello e eventualmente
* notifica il controllore
*/
public void addViewListener(final YController controller) {
this.addFocusListener(new FocusAdapter() {
public void focusLost(FocusEvent ev) {
// quando perde il fuoco aggiorno il modello
controller.updateModelAndController(IntegerTextField.this);
}
});
}
A questo punto possiamo usare il nostro componente come qualsiasi altro di TikeSwing, facendo ovviamente attenzione a mappare i componenti con le opportune proprietà nel modello.
Conclusioni
Abbiamo fatto una panoramica su questo framework e visto come può semplificare e modularizzare alcune componenti di una tipica applicazione desktop, in particolare la parte di presentazione dei dati all‘utente. Tuttavia TikeSwing mette a disposizione molti altri strumenti a disposizione per lo sviluppo come, ad esempio, il supporto multithreading per eseguire processi costosi in un Thread separato ed evitare il classico freeze delle GUI. Inoltre, come qualcuno avrà notato, è perfettamente integrato con Log4j [5] per tenere traccia, e risolvere agevolmente, qualsiasi errore. Spero con questa miniserie di articoli avere quantomeno stuzzicato l‘interesse di qualcuno verso questo framework e, indirettamente, verso i pattern [3] che costituiscono un enorme bagaglio che qualsiasi sviluppatore dovrebbe avere.
Riferimenti
[1] MVC
http://java.sun.com/blueprints/patterns/MVC-detailed.html
[2] Tomi Tuomainen , “Use high-level MVC and POJOs with Swing”, JavaWorld.com, 06/20/05
http://www.javaworld.com/javaworld/jw-06-2005/jw-0620-tikeswing.html
[3] Gamma E. – Helm R. – Johnson R. – Vlissides I., “Design Patterns: Elements of Reusable Object-Oriented Software”
[4] http://java.sun.com/docs/books/tutorial/reflect/index.html
[5] Log4j
http://logging.apache.org/log4j/docs/index.html
Emanuele Tomeo è laureando in Ingegneria Informatica presso l‘Unical. Da due anni lavora come programmatore Java presso un‘azienda di sviluppo software. Si è occupato di sviluppo di interfacce grafiche, LookAndFeel personalizzati e applicazioni CRM. Attualmente si occupa di sviluppo software in Java e personalizzazione CMS.