MokaByte 58 - Dicembre 2001 

Java Beans
La programmazione per componenti in Java
III parte - la gestione degli eventi

di
Andrea Gini
l meccanismo di proprietà bound descritto il mese scorso è un ottimo protocollo di comunicazione intra-component. Esso tuttavia è limitato alla notifica del cambiamento di proprietà, e non si presta ad un uso più generale. Questo mese vedremo il protocollo ad eventi Bean, pensato per permettere la diffusione di eventi con un contenuto informativo superiore rispetto al semplice ChangeEvent.

Eventi Bean
La notifica del cambiamento di valore delle proprietà bound è un meccanismo di comunicazione tra Beans. Se si vuole che un Bean sia in grado di propagare eventi di tipo più generico, o comunque eventi che non sia comodo rappresentare come un cambiamenti di stato, è possibile utilizzare un meccanismo di eventi generico, del tutto simile a quello presente nei componenti grafici Swing ed AWT. I prossimi paragrafi servono ad illustrare le tre fasi dell'implementazione: creazione dell'Evento, definizione dell'ascoltatore e infine la creazione della sorgente di eventi.

 

Creazione di un Evento
Per implementare un meccanismo di comunicazione basato su eventi, occorre anzitutto definire un opportuna sottoclasse di EventObject, che racchiuda tutte le informazioni relative all'evento da propagare:

public class <EventType> extends EventObject {
  private <ParamType> param
  public <EventType>(Object source,<ParamType> param) {
    super(source);
    this.param = param;
  }

  public <ParamType> getParameter() {
    return param;
  }
}

La principale variazione sul tema si ha sul numero e sul tipo di parametri: tanto più complesso è l'evento da descrivere, maggiori saranno i parametri in gioco. L'unico parametro che è obbligatorio fornire è un reference all'oggetto che ha generato l'evento: tale reference, richiamabile con il metodo getSource() della classe EventObject, permetterà all'ascoltatore di interrogare la sorgente degli eventi, qualora ce ne fosse bisogno.

 

Destinatari di Eventi
Il secondo passaggio è quello di definire l'interfaccia di programmazione degli ascoltatori di eventi. Tale interfaccia deve essere definita come sottoclasse di EventListener, per essere riconoscibile come ascoltatore dall'Introspector. Lo schema di sviluppo degli ascoltatori segue lo schema:

import java.awt.event.*;

public Interface <EventListener> extends EventListener {
  public void <EventType>Performed(<EventType> e);
}

Le convenzioni di naming dei metodi dell'interfaccia non seguono uno schema standard:: la convenzione descritta nell'esempio, <EventType>performed, può essere seguita o meno. L'importante è che il nome dei metodi dell'interfaccia Listener suggeriscano il tipo di azione sottostante, e che accettino come parametro un Evento del tipo giusto.

 

Sorgenti di Eventi
Se vogliamo aggiungere ad un Bean la capacità di generare eventi, dobbiamo implementare una coppia di metodi:

add<EventListenerType>(<EventListenerType> l)
remove<EventListenerType>(<EventListenerType> l)

La gestione della lista degli ascoltatori e l'invio degli eventi segue una formula standard, descritta nelle righe seguenti:

private Vector listeners = new Vector();

public void add<EventListenerType>(<EventListenerType> l) {
  listeners.add(l);
}

public void remove<EventListenerType>(<EventListenerType> l) {
  listeners.remove(l);
}

protected void fire<Eventype>(<EvenType> e) {
  Enumeration listenersEnumeration = listeners.elements();
  while(listenersEnumeration.hasMoreElements()) {
    <EventListenerType> listener=(<EventListenerType>)
                                 listenersEnumeration.nextElement();
    listener.<EventType>Performed(e);
  }
}

 

Sorgenti unicast
In alcuni casi occorre definire sorgenti di eventi capaci di servire un unico ascoltatore. Per implementare tali classi, che fungono da sorgenti unicast, si può seguire il seguente modello

private <EventListenerType> listener;

public void add<EventListenerType>(<EventListenerType> l)
                                   throws TooManyListenersException {
  if(listener==null)
    listener=l;
  else
    throw new TooManyListenerException();
}

public void remove<EventListenerType>(<EventListenerType> l) {
  listener=null;
}

protected void fire<Eventype>(<EvenType> e) {
  if(listener!=null)
    listener.<EventType>Performed(e);
}

 

 

Ascoltatori di Eventi: Event Adapter
Se vogliamo che un evento generato da un Bean scateni un'azione su un altro Bean, dobbiamo creare un oggetto che realizzi un collegamento tra i due. Tale classe, detta Adapter, viene registrata come ascoltatore presso la sorgente dell'Evento, e formula una chiamata al metodo destinazione ogni volta che riceve una notifica dal Bean sorgente.


Figura 5 - Un Adapter funge da ponte di collegamento
tra gli Eventi di un Bean e i metodi di un'altro

Gli strumenti grafici tipo JBuilder generano questo tipo di classi in maniera automatica: tutto quello che l'utente deve fare è collegare, con pochi click di mouse, l'Evento di un Bean sorgente ad un metodo di un Bean Target. Qui di seguito vediamo il codice di un Adapter, generato automaticamente dal Bean Box, che collega la pressione di un pulsante al metodo startJuggling(ActionEvent e) del Bean Juggler:

// Automatically generated event hookup file.

public class ___Hookup_172935aa26 implements java.awt.event.ActionListener, java.io.Serializable {

public void setTarget(sunw.demo.juggler.Juggler t) {
target = t;
}

public void actionPerformed(java.awt.event.ActionEvent arg0) {
target.startJuggling(arg0);
}

private sunw.demo.juggler.Juggler target;
}

 

 

Un esempio di Bean con Eventi
Il prossimo esempio è un Bean Timer, che ha il compito di generare battiti di orologio ad intervalli regolari. Questo Componente è un tipico esempio di Bean non grafico.
La prima classe che andremo a definire è quella che implementa il tipo di Evento:

package com.mokabyte.mokabook.javaBeans.timer;

import com.mokabyte.mokabook.javaBeans.*;
import java.io.*;
import java.util.*;

public class TimerEvent extends EventObject implements Serializable{
  public TimerEvent(Object source) {
    super(source);
  }
}

Come si può vedere, l'implementazione di un nuovo tipo di Evento è questione di poche righe di codice. L'unico particolare degno di nota è che il costruttore del nuovo tipo di evento deve invocare il costruttore della superclasse, passando un reference alla sorgente dell'Evento.
L'interfaccia che rappresenta l'ascoltatore deve estendere l'interfaccia EventListener; a parte questo al suo interno si può definire un numero arbitrario di metodi, la cui unica costante è quella di avere come parametro un reference all'Evento da propagare.

package com.mokabyte.mokabook.javaBeans.timer;

import com.mokabyte.mokabook.javaBeans.timer.*;

public interface TimerListener extends java.util.EventListener {
  public void clockTicked(TimerEvent e);
}

Per finire, ecco il Bean vero e proprio. Come si può notare, esso implementa l'interfaccia Serializable che rende possibile la serializzazione.

package com.mokabyte.mokabook.javaBeans.timer;

import com.mokabyte.mokabook.javaBeans.*;
import java.io.*;
import java.util.*;

public class TimerBean implements Serializable {

  private int time = 1000;
  private transient TimerThread timerThread;
  private Vector timerListeners = new Vector();

  public void addTimerListener(TimerListener t) {
    timerListeners.add(t);
  }
  public void removeTimerListener(TimerListener t) {
    timerListeners.remove(t);
  }
  
  protected void fireTimerEvent(TimerEvent e) {
    Enumeration listeners = timerListeners.elements();
    while(listeners.hasMoreElements())
      ((TimerListener)listeners.nextElement()).clockTicked(e);
    
}

  public synchronized void setMillis(int millis) {
    time = millis;
   }
  
  public synchronized int getMillis() {
    return time;
  }

  public synchronized void startTimer() {
    if(timerThread!=null)
      forceTick();
    timerThread = new TimerThread();
    timerThread.start();
  }

  public synchronized void stopTimer() {
    if(timerThread==null)
      return;

    timerThread.killTimer();
    timerThread = null;
  }

  public synchronized void forceTick() {
    if(timerThread!=null) {
      stopTimer();
      startTimer();
    }
    else
      fireTimerEvent(new TimerEvent(this));
  }

  class TimerThread extends Thread {
    private boolean running = true;

  public synchronized void killTimer() {
    running = false;
  }
  

  private synchronized boolean isRunning() {
    return running;
  }
  
  public void run() {
    while(true)
      try {
        if(isRunning()) {
          fireTimerEvent(new TimerEvent(TimerBean.this));
          Thread.sleep(getMillis());
        }
        else
          break;
      }
      catch(InterruptedException e) {}
    }
  }
}

I primi tre metodi servono a gestire la lista degli ascoltatori. Il terzo e il quarto gestiscono la proprietà 'millis', ossia la il tempo, in millisecondi, tra un tick e l'altro. I due metodi successivi, startTimer, stopTimer, servono ad avviare e fermare il timer, mentre forceTick lancia un tick e riavvia il timer, se questo è attivo. Il timer vero e proprio viene implementato grazie ad una classe interna TimerThread, sottoclasse di Thread. Si noti il metodo killTimer, che permette di terminare in modo pulito la vita del Thread: questa soluzione è da preferire al metodo stop (deprecato a partire dal JDK 1.1), che in certi casi può provocare la terminazione del Thread in uno stato inconsistente.

Per compilare le classi del Bean, bisogna usare la seguente riga di comando:

javac com\mokabyte\mokabook\javaBeans\timer\*.java

Per impacchettare il Bean in un file Jar, dobbiamo per prima cosa creare con un editor di testo il file timerManifest.tmp, con le seguenti righe:

Name: com/mokabyte/mokabook/javaBeans/timer/TimerBean.class
Java-Bean: True

Per creare l'archivio dobbiamo quindi digitare il seguente comando:

jar cfm timer.jar timerManifest.tmp com\mokabyte\mokabook\javaBeans\timer\*.class

Per testare classe TimerBean, possiamo usare il seguente programma, che crea un oggetto TimerBean e registra un TimerListener che stampa a video una scritta ad ogni tick del timer.

package com.mokabyte.mokabook.javaBeans.timer;

import com.mokabyte.mokabook.javaBeans.timer.*;

public class TimerTest {
  public static void main(String argv[]) {
    TimerBean t = new TimerBean();
    t.addTimerListener(new TimerListener() {
      public void clockTicked(TimerEvent e) {
      System.out.println("Tick");
      }
    });
  t.startTimer();
  }
}


Conclusioni
Questo mese abbiamo studiato il meccanismo di propagazione degli eventi Bean, senza dubbio lo strumento più importante per la comunicazione tra componenti. Il mese prossimo studieremo l'introspezione, ed impareremo a curare l'integrazione dei Beans all'interno di strumenti grafici.

Gli esempi allegati si possono trovare qui

 


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems; tutti i diritti riservati 
E' vietata la riproduzione anche parziale 
Per comunicazioni inviare una mail a info@mokabyte.it