MokaByte Numero 05 - Febbraio 1997

 
LA GESTIONE DEGLI EVENTI 
CON IL JDK 1.1 
 
di
 Massimo Carli 
 

 


 




Come detto nell'articolo introduttivo delle nuove caratteristiche del JDK1.1, la gestione degli eventi ha subito una grossa modifica rispetto alle versioni precedenti. Le modifiche sono state introdotte per ottenere una migliore gestione degli eventi in applicazioni Java che iniziano ad essere di una certa consistenza, e per adattarsi a quella che è la filosofia di modularità sottolineata nelle specifiche JavaBean a cui la JDK1.1 si riferisce. Differenze JDK 1.02 e JDK 1.1 Ricordiamo brevemente come avveniva la gestione degli eventi nelle versioni del JDK precedenti la 1.1. Avevamo che ogni oggetto che derivava dalla classe Component, ereditava da essa i metodi action e handleEvent più una serie di metodi per la gestione di particolari azioni quali quelle relative al mouse. La gestione degli eventi avveniva in modo gerarchico. Se un oggetto, per esempio un Button, voleva gestire gli eventi in locale, cioè non lasciarlo fare al Component da cui deriva, doveva definire i metodi opportuni.
Per esempio si definiva la action o, in generale, la handleEvent distinguendo i comportamenti successivi all'evento in dipendenza dell'evento stesso. Sappiamo che i metodi relativi alla gestione degli eventi ritornano un valore boolean. Se il valore ritornato era true, significava che l'evento era stato completamente gestito e non si voleva che anche oggetti genitori lo gestissero. Se, invece, si voleva che l'evento del Button, per esempio, fosse gestito anche dal Component bastava ritornare il valore false.
Come notiamo la gestione degli eventi avveniva in modo gerarchico. Si può subito vedere che una gestione degli eventi in quel modo è un po' limitata. Vi potrebbero essere, infatti, altri oggetti che vogliono "ascoltare" gli eventi di un Button, per esempio, senza essere Component, Container o altro. Vogliono solo sapere se il Button è stato premuto. Da qui la necessità di una gestione migliore. Inoltre si esigeva un modello che permettesse una più completa modularità degli oggetti, ovvero fare in modo che ciascun Bean offrisse una interfaccia il più possibile standard con l'esterno.
Diciamo, inoltre, che la gestione attraverso i metodi action ed handleEvent necessita, talvolta, di numerosi test per l'identificazione dell'oggetto che ha generato l'evento con, quindi, maggiori possibilità di errore. La versione 1.1 permette, inoltre, anche il filtraggio degli eventi, cosa problematica nella gestione precedente.
Sottolineo che la gestione degli eventi utilizzata nella versione 1.1 non offre nulla di nuovo rispetto alle precedenti nel senso del funzionamento. Non esiste, cioè, cosa che si può realizzare adesso che non si poteva realizzare prima. I vantaggi, notevoli e fondamentali, stanno nel modo in cui questo si può ottenere. Si ha una maggiore facilità di gestione degli eventi e si rendono i programmi aperti a modifiche grosse nella funzionalità con poche modifiche nel codice. La cosa vi assicuro non è da poco. Le nuove specifiche sono solo una gestione degli eventi conseguenza della esperienza maturata dai programmatori Sun. Quando ho letto queste specifiche mi sono chiesto perchè non le avevo utilizzate prima. In verità lo avevo fatto.
Ricordate che nelle versioni precedenti, la classe Dialog aveva un problema legato alla sua modalità. Il fatto che una finestra di dialogo sia modale significa che fino a che essa è aperta, o in funzione, non si poteva accedere alle altre. Questo non funzionava completamente in quanto non si poteva sì accedere agli altri Frame, ma il programma non si fermava in attesa che la finestra si chiudesse, ma proseguiva eseguendo le istruzioni seguenti la show(). Se la finestra di dialogo doveva ritornare dei valori inseriti attraverso di essa, tali valori erano elaborati subito dopo la visualizzazione della finestra di dialogo e quindi senza dare il tempo per l'inserimento. Bisognava, allora, utilizzare un altro stratagemma.
Ho allora creato una interfaccia che ho chiamato Risultati con un unico metodo raccogli. Tale interfaccia era implemetata da ogni oggetto che utilizzava la finestra di dialogo per la raccolta di dati. Si procedeva così. Quando si volevano inserire i valori attraverso la finestra di dialogo, la si visualizzava semplicemente. Quando i dato erano inseriti, si premeva un pulsante OK. La Dialog, allora, non faceva altro che chiamare il metodo raccogli del padre che implementava l'interfaccia Risultati passando il nuovo oggetto i cui valori erano stati inseriti attraverso la Dialog. Era, allora, nella raccogli del frame padre che gestivo i dati. Come vedremo questo procedimento si avvicina molto alla gestione degli eventi che descriveremo. Il tutto sarà legato all'uso delle interfacce ed alla definizione di opportuni metodi che le interfacce prevedono. Quello che si passerà attraverso questi metodi, sarà un oggetto di un nuovo tipo che conterrà le informazioni legate all'evento, alla sua sorgente ed altre. Andiamo, comunque, per ordine.
La gestione degli eventi con il JDK 1.1 Iniziamo, quindi, l'esposizione della gestione degli eventi prevista nel JDK 1.1 e nelle specifiche JavaBean. Questo modello ha le seguenti caratteristiche:
E' molto semplice e facile da imparare Permette una separazione tra quella che è la progettazione del programma e la stesura del codice utilizzando la potenza delle interfacce. Permette una gestione degli eventi più robusta, meno portata ad errori. Permette una gestione degli eventi più flessibile. Permette una definizione più accurata delle interfacce che il Bean offre all'esterno E' compatibile con le versioni precedenti. La gestione degli eventi secondo le specifiche JavaBean si rifanno ad un modello frequente in Java: quello di produttore-consumatore o sorgente-ascoltatore. In questo caso si utilizza la seconda versione in quanto l'evento è generato da una sorgente di eventi, ed è sentito ed elaborato da uno o più ascoltatori. I termini di sorgente e di ascoltatore, come vedremo, non sono a caso. Descriviamo il modello attraverso una serie di domande risposte: Come fa una sorgente di un evento a comunicare ad un ascoltatore che l'evento si è verificato?
Semplice, chiamerà un particolare metodo dell'oggetto ascoltatore che lo avvisa dell'accaduto e che farà le opportune azioni relative all'ascoltatore stesso. Come si fa a distinguere i vari eventi? Come si fa a dire ad un ascoltatore che tipo di evento si è verificato? Ciascun tipo di evento prevede la chiamata di un particolare metodo, implementato dall'ascoltatore, associato all'evento stesso.
Ma come si fa a sapere che l'ascoltatore ha tale metodo? Ogni oggetto ascoltatore di un evento o di un gruppo di eventi, implementa una particolare interfaccia che deriva dalla interfaccia java.util.EventListener e che prevede la gestione di un certo gruppo di eventi. Notiamo che ognuno di questi oggetti ascoltatori è un oggetto di tipo EventListener perchè implementa tale interfaccia. Quindi, per dire che il mio oggetto è interessato ad un particolare insieme di eventi, devo implementare l'interfaccia che prevede la gestione degli eventi associati? Si. Saranno create interfaccie, che derivano dalla EventListener, per ogni gruppo di eventi che si vogliono ascoltare.
Il criterio del legame di tali eventi è, per esempio, il fatto che tutti si riferiscano a movimenti del mouse. L'oggetto che implementa tale interfaccia realizzerà i metodi previsti dall'interfaccia stessa, con le operazioni da eseguire quando tali eventi si verificheranno.
Ma dove racchiudo le informazioni relative all'evento che si è verificato? Tutti i metodi previsti dalle interfacce che derivano dalla EventListener, devono avere un solo argomento. Questo argomento racchiude tutte le informazioni associate all'evento che si è verificato.
Ognuno di questi è un oggetto che deriva dalla classe java.util.EventObject. Ma se metto più di un argomento nei metodi relativi ad un evento, ho un errore di compilazione? NO. Come vedremo anche per le sorgenti di eventi, le JavaBean danno delle regole cui gli sviluppatori devono adattarsi per mantenere una certa uniformità. Queste regole sono importanti nel caso della introspection dei Bean. Per instrospection si intende quel procedimento che permette di conoscere le interfacce che il Bean mette a disposizione, osservando il Bean stesso.
Esistono due modi per fare questo. Uno è detto esplicito e prevede l'implementazione di una interfaccia che prevede un metodo per la descrizione delle proprietà del Bean stesso. La seconda, invece, avviene osservando le regole di cui si parlava prima. Per esempio, supponiamo di avere un Bean con una proprietà che chiamiamo numero. Se volessimo dire ad un tool che edita il Bean, che numero è una proprietà editabile, esistono due metodi. Il primo, esplicito, consiste nel descrivere la proprietà numero come editabile nel metodo descrittivo del Bean (semplice no?).
La seconda è quella legata alle convenzioni. Se la proprietà numero è editabile avrà due metodi i cui nomi saranno getNumero() e setNumero(). La presenza della versione get ci dice che numero può essere letto. La presenza della versione set ci dice che numero può essere scritto. Se vi fosse solo la get significherebbe che numero non potrà essere modificato. La stessa cosa avviene per la gestione degli eventi. Se non si seguono determinate regole nella programmazione si rinuncia a questo secondo metodo di introspection. Conviene, quindi, stare alle regole.
Notiamo, anche, che tutte le informazioni che si metterebbero come secondo o terzo parametro, possono essere inglobate in un unico oggetto che deriva da EventObject, quindi il problema di un argomento in più è un falso problema. Finora si è parlato solo degli ascoltatori di un evento.
Ma come si definiscono le sorgenti? Anche qui ritorniamo al concetto precedente. Qui non esiste una interfaccia che permetta di dire che un oggetto è sorgente per un certo gruppo di eventi. Supponiamo di voler creare una sorgente per un gruppo di eventi che chiamiamo succede. Creeremo l'interfaccia SuccedeListener, dalla EventListener, con la gestione degli eventi relativi a succede. Il parametro che utilizziamo lo deriviamo dalla EventObject e lo chiamiamo SuccedeEvent. Eccoci alla sorgente. Per dire che essa è sorgente per il gruppo di eventi SuccedeListener, dobbiamo prevedere i seguenti due metodi di registrazione:

        addSuccedeListener( SuccedeListener sl);

        removeSuccedeListener(SuccedeListener sl);
Questi due metodi permettono di informare la sorgente di quelli che sono gli oggetti che ascoltano particolari tipi di eventi. Con add si aggiunge un oggetto ascoltatore a tale lista, e con remove si toglie. Notiamo che l'argomento è un oggetto di tipo SuccedeListener cioè che implementa la omonima interfaccia, e quindi che ha i metodi per la gestione degli eventi corrispondenti. La cosa fondamentale è la gestione dei nomi utilizzati per la creazione di tutte le classi ed interfacce.
A questo punto le cose dovrebbero essere abbastanza chiare.
Per approfondire i concetti esposti diamo un occhiata alla documentazione delle classi del JDK1.1.
Nel package java.util notiamo la interfaccia EventListener. Notiamo come essa non abbia nessun metodo da definire, serve solo come raccoglitore delle classi che implementano interfacce per ascoltare eventi. L'uso di interfacce senza metodi, con lo scopo di marcare classi, non è rara in Java. Se guardiamo la serializzazione, notiamo che la classe Serializable ha una funzione analoga, stessa cosa per l'interfaccia Remote per la gestione del RMI (Remote Method Invocation). Andiamo nel package java.awt.event. Notiamo la presenza di interfacce il cui nome finisce per Listener.
Queste sono tutte interfacce che derivano dalla interfaccia java.util.EventListener. Infatti, la gestione dei gruppi di eventi più comuni, sono state realizzate e messe nel JDK. Se andiamo a vedere ciascuna di queste classi, notiamo la presenza di metodi per la gestione degli eventi. La cosa ancora più importante è il parametro che ciascuno di essi ha. Notiamo che esso è uno solo ed ha nome che soddisfa alle regole precedentemente dette. Esaminiamo l'interfaccia più comune e forse più utile in quanto gestisce i movimenti del mouse : MouseListener. I metodi dell'interfaccia sono i seguenti:

        mouseClicked(MouseEvent me) ;

        mouseEntered(MouseEvent  me);

        mouseExited(MouseEvent  me) ;

        mousePressed(MouseEvent  me) ;

        mouseReleased(MouseEvent  me) ;
Notiamo come tutti gli eventi legati al mouse siano raggruppati in questa interfaccia. Una cosa da andare a vedere è la classe MouseEvent. Essa appartiene al package java.awt.event e deriva, ovviamente, dalla classe java.util.EventObject. Se guardiamo la gerarchia delle classi notiamo che quella relativa agli eventi del mouse deriva dalla EventObject attraverso una lunga serie di classi. E' spesso utile, infatti, raggruppare classi con funzioni simili, e questo è quello che si fa. Ma esaminiamo bene la classe EventObject che è comune a tutti gli oggetti relativi ad un evento. Quando succede qualcosa è bene saperne la causa. Ogni oggetto EventObject dispone di un campo source che conterrà la sorgente dell'evento. Notiamo come essa sia di tipo Object, cioè del tipo più generale possibile. Saranno poi, in relazione alla applicazione specifica, realizzate le opportune conversione con operazioni di cast. Una cosa che, ai più attenti, appare strana è la presenza di un nuovo modificatore applicato a source. Esso è il modificatore transient.
Vedremo meglio questo modificatore quando parleremo della serializzazione degli oggetti oppure dell'uso del RMI. Diamone, comunque, una descrizione. Supponiamo di voler rendere un oggetto persistente (altro nuovo termine in Java). Rendere un oggetto persistente significa fare in modo che esso esista anche dopo il termine dell'esecuzione del programma che lo ha creato o che lo utilizza.

Questo significa che una volta spento il programma, l'oggetto esiste ancora e può essere ricreato, uguale, in una esecuzione successiva dell'applicazione. Per fare questo vedremo come utilizzare la serializzazione. Diciamo solo che essa mi permetterà di salvare su uno stream, in generale, e quindi su un file, un oggetto per poi ripristinarlo successivamente. Un oggetto con queste caratteristiche si dice serializzabile e implementerà, come vedremo, la classe java.io.Serializable.

Però non sempre si vogliono salvare tutte le proprietà dell'oggetto stesso. Alcune dovranno, all'atto del ripristino, essere ricreate esattamente come erano, ma altre possono essere create anche assumendo dei valori di default. Queste ultime si dicono transient. Se guardiamo la classe EventObject, notiamo che essa implementa Serializable.
Il campo source sarà transient per il semplice fatto che non ha molto senso memorizzare la sorgente di un evento per gestirlo in un'altra sessione. Sarebbe come premere il bottone del mouse e gestire l'evento in una esecuzione futura dell'applicazione. La cosa è, peraltro, difficile dato che un evento viene subito gestito o quasi. Esistono poi i due metodi getSource() e toString() il cui significato è ovvio. Come visto le informazioni generali a tutti gli eventi sono legate alla sorgente dell'evento verificato. Ora proviamo a creare un semplice oggetto che vuole gestire gli eventi del mouse. Supponiamo di estendere un oggetto Frame ma esplicitiamo il tutto relativamente agli eventi. Supponiamo di creare un oggetto che ascolta il mouse e che stampa un messaggio in corrispondenza dell'evento verificato e che lo smista agli oggetti che lo vogliono ascoltare da lui. La cosa è molto semplice ma permette di vedere bene il funzionamento degli eventi nel JDK1.1.

Chiamiamo la nostra classe Sensor. Si ha:

Notiamo, inizialmente, come esso debba implementare l'interfaccia MouseListener per cui deve definire i metodi relativi. Notiamo come essi non ritornino nessun valore a differenza di quelli quasi omonimi nelle versioni precedenti. Infatti questi metodi gestiranno gli eventi localmente e sarà un altro metodo, che vedremo poi, che avrà il compito di informare anche altri oggetti. Se ci fermiamo qui la cosa non funziona. Potete cliccare fin che volete ma nulla si vede. Allora cosa dobbiamo fare? Notiamo che la nostra classe estende la classe Frame ma non è detto, come avveniva nelle versioni precedenti, che essa senta quello che succede nel Frame. Non esiste una handleEvent che per default ritorna false. Ora bisogna registrarsi al Frame come ascoltatore. Usiamo allora la seguente istruzione:

        super.addMouseListener(this);
Il programma diventa allora il seguente:

Notiamo che ora la cosa finalmente funziona. In sintesi ci siamo registrati in modo da sentire quello che accade nel Frame padre. Notiamo che vengono chiamati i metodi relativi all'azione che si è verificata. Ora togliamo la riga aggiunta e supponiamo di voler raccogliere gli eventi senza farseli passare dal parent. Utilizziamo, cioè, la classe Sensor come sorgente. Dobbiamo dotare la classe Sensor dei metodi di registrazione ma introduciamo anche un nuovo metodo:

        protected void processEvent(AWTEvent e);
La funzione di questo metodo è molto semplice ma importante. Esso ha quasi la stessa funzione del vecchio handleEvent(). Il suo compito è quello di raccogliere gli eventi che si sono verificati e di smistarli agli oggetti ascoltatori che si sono registrati. Questo metodo è ridefinibile. Nel caso lo facessimo, dobbiamo comunque ricordarci di chiamare sempre quello del parent perchè il lavoro di smistamento sia effettivamente fatto oltre al lavoro introdotto nella ridefinizione. Consideriamo allora il seguente listato

Ora eseguiamo il programma e notiamo che ancora non si ha nulla. Infatti la nuova gestione degli eventi permette di sentire un particolare gruppo di eventi solo se lo si è abilitato. Esistono, infatti, i metodi

        protected final void enableEvents(long eventsToEnable);

        protected final void disableEvents(long eventsToDisable);
che permettono di abilitare o meno un gruppo di eventi. Notiamo cone essi siano final nel senso che non si possono ridefinire. Infatti non ve ne è la necessità. L'argomento dei due metodi è un long che si ottiene esaminando la classe java.awt.AWTEvent. Essa dispone di tutte le maschere per la abilitazione o meno dei gruppi di eventi. Nel nostro caso basterà aggiungere, nel costruttore, la seguente riga

        enableEvents(AWTEvent.MOUSE_EVENT_MASK);
in modo da abilitare gli eventi del mouse. La nostra classe diventa allora:

Ora anche se clicchiamo l'unica cosa che si verifica è la scritta "SIAMO IN PROCESSEVENT". Questo significa che quando si verifica uno degli eventi del tipo abilitato, viene chiamata la processEvent(). Notiamo come non vengano chiamati i metodi della interfaccia MouseListener. Essi sono chiamati, infatti, dalla sorgente di MouseListener a cui il nostro oggetto si è eventualmente registrato come ascoltatore.
Diciamo ora che il metodo processEvent è relativo a tuti gli eventi abilitati ma che esistono anche altri metodi process relativi alle varie interfaccie MouseListener ecc. Se un altro oggetto si registra a noi come MouseListener, con il precedente codice, esso non ascolterà nulla. Infatti dobbiamo ricordarci nel processEvent di chiamare il super.processEvent.
Nel JDK1.1 esiste anche un meccanismo di creazione di una coda di eventi di sistema. La cosa non è ancora definita bene specialmente per quello che riguarda la gestione delle code di eventi negli applet a causa di problemi di sicurezza per cui ne parlerò, eventualmente, in un successivo articolo.
 

CONCLUSIONI

Penso che gli esempi introdotti siano una buona base per apprendere i nuovi concetti del JDK 1.1. E' bene,fin da subito, adottare questa gestione durante la programmazione.
Uno svantaggio si potrebbe verificare negli applet. Essi, infatti, possono gestire gli eventi in questo modo solo se interpretati con appletviewer. Infatti i browser non sono ancora adattati alla JDK1.1 e molte delle classi per tale gestione appartengono solo al JDK1.1.
Comunque questo è un problema che si risolverà presto, o almeno lo spero.
 
 
 
  

 

MokaByte rivista web su Java

MokaByte ricerca nuovi collaboratori
Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it