MokaByte 102 - Dicembre 2005
 
MokaByte 102 - Dicembre 2005 Prima pagina Cerca Home Page

 

 

 

Applicazioni Desktop
Persistenza delle preferenze

Dopo aver visto come creare una finestra delle preferenze per configurare una ipotetica applicazione per il gioco del Sudoku, vediamo una delle possibili alternative per gestire la persistenza ed il recupero della configurazione che non richieda troppa codifica e creando uno strato di software riutilizzabile.

Il problema che si desidera affrontare e risolvere è quello di implementare la persistenza ed il recupero della configurazione che è possibile impostare nella finestra delle preferenze. Questa può includere diversi componenti visuali, di tipi diversi. Si possono avere infatti checkbox, combobox, tabelle, campi di testo. Ciascuno di questi elementi permette di configurare un aspetto dell’applicazione. La finestra che prenderemo in considerazione è illustrata in Figura 1.



Figura 1 – La finestra delle preferenze presa in esame


Come si può notare, sono presenti diversi checkbox, combobox e campi di testo. Per ciascuno di questi è necessario, alla chiusura della finestra, memorizzare il contenuto da qualche parte. Alla sua apertura, invece, è necessario recuperare l’informazione salvata ed inizializzare i componenti visuali opportunamente.
Essendo componenti di tipo diverso, è necessario trattare ciascuno di questi in modo specifico: per le checkbox è necessario salvare un valore booleano, per i campi di testo il contenuto testuale e per i combobox l’elemento selezionato. Per altri elementi, come liste o tabelle, sarebbe necessario supportare diverse modalità. Inoltre, è necessario notare che ciascuno di questi componenti visuali ha una interfaccia diversa. Per ciascuno è presente un diverso metodo che estrae il valore corrente.

 

L’oggetto Preferenze
Tra i diversi possibili approcci per la soluzione della problematica che ci siamo posti, invece che implementare i classici pezzi di codice che prendono e impostano i valori attuali dei singoli componenti, si è tentato un nuovo approccio. Si è pensato al concetto di collegare i componenti visuali ad un identificativo. Ad esempio, il campo di testo emailTextField, che contiene l’indirizzo e-mail del mittente, viene associato all’identificativo ID_EMAIL (una costante che vale “emailAccount”). Quando tutti i componenti visuali che contengono elementi di configurazione sono collegati è possibile invocare il metodo che esegue la persistenza, che non farà altro che scorrere tutti i collegamenti e salvare ciascun ID con il valore del relativo componente. In questo modo si ottengono una serie di vantaggi:

  • E' necessario meno codice personalizzato per eseguire persistenza e recupero della configurazione.
  • Non è necessario porre attenzione al tipo di componente visuale che si sta memorizzando o recuperando, perché di questo si occupa la classe che gestisce la persistenza.
  • Sempre per lo stesso motivo, un cambiamento nella GUI utilizzata, e di conseguenza del componente visuale utilizzato, non richiede modifica al codice di persistenza, a meno che ovviamente il tipo del nuovo componente non sia supportato dalla classe che gestisce la persistenza. In questo caso però sarà sufficiente estendere quest’ultima per supportare il nuovo componente. In questo modo, automaticamente, la nuova tipologia sarà disponibile in tutte le finestre di preferenze che utilizzano la classe base.

Per gestire la persistenza delle preferenze è stata creata la nuova classe Preferenze, che contiene tre attributi:

  • Il nome del file su cui memorizzare la configurazione.
  • I collegamenti tra componenti visuali e gli ID
  • I valori di default, se presenti, per ciascun ID.

I collegamenti ed i valori di default sono memorizzati in Map dove la chiave è l’ID, di tipo String, mentre il valore contiene, nel primo caso, l’oggetto Swing utilizzato, nel secondo un oggetto che rappresenta il valore di default.
La classe prevede un costruttore di default ed uno che si aspetta il nome del file, oltre ad un metodo per impostare quest’ultimo in un secondo tempo.


/**
* Classe generica di gestione delle preferenze
* utente
* @author max
*/
public abstract class Preferenze {

  private String filename;
  private Map bindings = new HashMap();
  private Map defaultValues = new HashMap();

  public Preferenze() {
  }

  public Preferenze( String filename ) {
    setFilename(filename);
  }

  public void setFilename( String filename ) {
    this.filename = filename;
  }

La classe definisce anche il metodo astratto getDescrizione() che deve essere implementato nelle sottoclassi di Preferenze per fornire una descrizione da associare al file di configurazione:

/**
* Le sottoclassi forniscono la descrizione
* del file di configurazione
* @return descrizione del file di configurazione
*/
public abstract String getDescrizione();

Il resto dell’implementazione prevede due versioni del metodo bind(), che permette di collegare un JComponent con un ID di tipo String. La seconda versione si aspetta anche un valore di default:

/**
* Collega un componente ad un elemento di configurazione
* @param component
* @param id
*/
public void bind( JComponent component, String id ) {
  bindings.put( id, component );
}

/**
* Valore di default
* @param component
* @param id
* @param defaultValue
*/
public void bind( JComponent component, String id, Object defaultValue ) {
  bind( component, id );
  defaultValues.put( id, defaultValue );
}

Il metodo che si occupa della memorizzazione della configurazione si chiama store() e può sollevare una IOException, nel caso ci siano problemi nella scrittura della configurazione. Questo metodo crea una Map chiamata state che viene utilizzata per contenere i valori di configurazione ed itera per tutti i collegamenti. Per ciascun valore viene estratto il componente Swing ed in base al suo tipo viene estratto il valore attuale, che viene memorizzato nello stato. Questo viene poi riversato in un oggetto di tipo Properties. Questo viene poi scritto nel file di configurazione in precedenza aperto attraverso un oggetto FileOutputStream.


/**
* Memorizza la configurazione
* @throws IOException
*/
public void store() throws IOException {
  checkState();
  Map state = new HashMap();

  for( Iterator iter = bindings.keySet().iterator(); iter.hasNext(); ) {
    String id = (String)iter.next();
    JComponent c = (JComponent)bindings.get(id);

    if (c instanceof JCheckBox) {
      JCheckBox cc = (JCheckBox)c;
      state.put( id, String.valueOf(cc.isSelected()) );
    }
    
else if (c instanceof JTextField) {
      JTextField cc = (JTextField)c;
      state.put( id, cc.getText() );
    }
    else if (c instanceof JComboBox) {
      JComboBox cc = (JComboBox)c;
      String s = (String)cc.getSelectedItem();
      state.put( id, s );
    }
  }

  FileOutputStream out = new FileOutputStream(filename);

  Properties p = new Properties();
  p.putAll(state);
  p.store( out, getDescrizione() );
}

Il metodo di recupero della configurazione è paritetico e si chiama load(). Questo utilizza un oggetto Properties che legge dal file di configurazione tramite un oggetto di tipo FileInputStream. Il codice è il seguente:

/**
* Carica la configurazione
* @throws IOException
*/
public void load() throws IOException {
  checkState();

  Properties p = new Properties();
  p.load( new FileInputStream(filename) );

  for( Iterator iter = bindings.keySet().iterator(); iter.hasNext(); ) {
    String id = (String)iter.next();
    JComponent c = (JComponent)bindings.get(id);

    String defaultValue = (String)defaultValues.get(id);
    String value = p.getProperty(id, defaultValue);
    if (c instanceof JCheckBox) {
      JCheckBox cc = (JCheckBox)c;
      cc.setSelected( "true".equals(value) );
    } else if (c instanceof JTextField) {
      JTextField cc = (JTextField)c;
      cc.setText(value);
    } else if (c instanceof JComboBox) {
      JComboBox cc = (JComboBox)c;
      cc.setSelectedItem(value);
    }
  }
}

Entrambi questi metodi, per prima cosa, verificano che siano presenti collegamenti, utilizzando il metodo checkState():

/**
* Verifica che siano stati eseguiti i collegamenti
* @throws IllegalArgumentException
*/
private void checkState() throws IllegalArgumentException {
  if (bindings.size() == 0) {
    throw new IllegalArgumentException("E' necessario prima fare bind");
  }
}


Evolvere la classe base
L’approccio mostrato ha due limitazioni:

  • memorizza la configurazione sempre e comunque su un file di testo. Sebbene questo sia lo standard per le applicazioni desktop (e quelle Mac OS X), si potrebbe desiderare di memorizzare questi dati in un database relazionale, un file XML oppure il registro di Windows. In questi casi è necessario scorporare la gestione della memorizzazione. Si può pensare anche di utilizzare package open source come Jakarta Commons Configurations che offre proprio questo tipo di funzionalità;
  • l’accesso in lettura e scrittura sui diversi componenti Swing utilizza un elenco inelegante e poco manutenibile di if. Si può pensare di sostituire questa parte con una interfaccia, implementazioni concrete per ciascun componente ed una factory. Un esempio di implementazione è il seguente:

Il metodo load() non fa altro che richiedere un ComponentAccessor all’oggetto ComponentAccessorFactory e poi impostare il valore recuperato dal file di configurazione:

public void load() throws IOException {
  checkState();

  Properties p = new Properties();
  p.load( new FileInputStream(filename) );

  for( Iterator iter = bindings.keySet().iterator(); iter.hasNext(); ) {
    String id = (String)iter.next();
    JComponent c = (JComponent)bindings.get(id);

    String defaultValue = (String)defaultValues.get(id);
    String value = p.getProperty(id, defaultValue);

    ComponentAccessor ca = ComponentAccessorFactory.getComponent(c);
    ca.setValue(value);
  }
}

L’interfaccia contiene un metodo per ottenere il valore ed uno per impostarlo:

interface ComponentAccessor {
  String getValue();
  void setValue(String value);
}

La factory non fa altro che ritornare una istanza concreta di tipo ComponentAccessor in funzione del tipo di parametri (in realtà la fila di if è stata spostata qui, ma è sempre possibile pensare ad utilizzare la reflection per eliminare questa “ultima dipendenza”):

class ComponentAccessorFactory {
  public static ComponentAccessor getComponent( JComponent component ) {
    if (component instanceof JCheckBox) {
      return new CheckBoxComponentAccessor((JCheckBox)component);
    }
    //altri
    throw new IllegalArgumentException("non supportato");
  }
}

L’implementazione dei singoli ComponentAccessor è poi banale. Qui di seguito è illustrata l’implementazione dell’accessor per l’oggetto JCheckBox:

class CheckBoxComponentAccessor implements ComponentAccessor {
  JCheckBox checkBox;
  public CheckBoxComponentAccessor(JCheckBox checkBox) {
    this.checkBox = checkBox;
  }
  public String getValue() {
    return String.valueOf( checkBox.isSelected() );
  }
  public void setValue(String value) {
    checkBox.setSelected( "true".equals(value) );
  }
}

Il secondo approccio è sicuramente più elegante ed orientato ai pattern, ma richiede molto più codice.

 

Preferenze personalizzate
La classe Preferenze però non è sufficiente per implementare la persistenza della finestra vista in Figura 1. È necessario infatti creare una sottoclasse che fornisca l’implementazione del metodo getDescrizione(). La classe SudokuPreferenze qui illustrata include anche un costruttore ed un elenco di costanti che definiscono gli ID di configurazione utilizzati in questa applicazione.

import java.io.File;
import javax.swing.*;

public class SudokuPreferenze extends Preferenze {

/** nome del file di default */
private final String FILENAME = ".mySudoku.properties";

//pannello generale
public static final String ID_SHOW_TIMER = "showTimer";
public static final String ID_HIDE_WHEN_PAUSE = "hideWhenPause";
public static final String ID_PAUSE_WHEN_HIDE = "pauseWhenHide";
public static final String ID_SHOW_ERRORS = "showErrors";
public static final String ID_ALLOW_HINTS = "allowHints";
public static final String ID_USE_TIMER = "useTimer";

//pannello nuovo gioco
public static final String ID_GRID_SIZE = "gridSize";
public static final String ID_LEVEL = "level";

//pannello posta elettronica
public static final String ID_EMAIL = "emailAccount";
public static final String ID_EMAIL_NOME = "emailName";
public static final String ID_SMTP_SERVER = "smtpServer";

/**
* Inizializza costruendo il nome del file delle
* proprietà mettendo FILENAME nella user.home
*/
public SudokuPreferenze() {
  String filename = System.getProperty("user.home") +File.separator +FILENAME;
  setFilename( filename );
}

/**
*
*/
public String getDescrizione() {
  return "File di configurazione di mySudoku";
}

}
Si noti come la classe Preferenze e SudokuPreferenze suddividano opportunamente le parti comuni, e quindi riutilizzabili in altre applicazioni, e quelle specifiche ad un determinato programma.

 

Modificare la finestra delle preferenze
È possibile includere la gestione della memorizzazione e del recupero delle preferenze direttamente nella classe AbstractPreferencesFrame. D’altra parte, il suo stesso nome indica come questa sia una finestra di preferenze, dunque avrà sicuramente l’esigenza di memorizzare e recuperare la configurazione. Per questo motivo:
è stato aggiunto un attributo di tipo Preferenze.
Nel costruttore viene chiamato il metodo load() per recuperare la configurazione, dopo che la GUI è stata inizializzata e prima di visualizzare la finestra all’utente.
Viene installato un WindowListener che, quando l’utente chiude la finestra, chiama il metodo store() definito in seguito. Su Windows lo standard è avere un pulsante di salvataggio (OK), mentre su Mac OS X (la finestra riprende, per gioco, questo sistema operativo) è sufficiente chiudere la finestra.
Sono stati aggiunti i metodi load() e store(), che non fanno altro che richiamare i metodi omonimi sull’oggetto Preferenze.
Il codice è il seguente:


public abstract class AbstractPreferencesFrame extends JFrame {

  //...

  /** oggetto preferenze collegato alla finestra */
  protected Preferenze preferenze;

  public AbstractPreferencesFrame( String title, int panelCount ) {
  //...

  //carica la configurazione
  try {
    load();
  } catch (IOException e) {
    e.printStackTrace();
  }

  //aggancio salvataggio preferenze alla chiusura della finestra.
  addWindowListener( new WindowAdapter() {
    public void windowClosing(WindowEvent event) {
      try {
        store();
      } catch (IOException e) {
         e.printStackTrace();
      }
      setVisible(false);
      dispose();
    }
    } );
  }

  /**
  * Carica la configurazione
  * @throws IOException
  */
  protected void load() throws IOException {
  preferenze.load();
  }

  /**
  * Salva la configurazione
  * @throws IOException
  */
  protected void store() throws IOException {
    preferenze.store();
  }


Il tocco finale si trova nella classe PreferencesFrame, che rappresenta la finestra delle preferenze concreta per il gioco del Sudoku e che contiene tutti gli elementi GUI presenti nella Figura 1. Il metodo bind() esegue il collegamento tra i singli componenti Swing e gli ID:

private void bind() {
  preferenze = new SudokuPreferenze();

  //pannello generale
  preferenze.bind(useTimerCheckBox, SudokuPreferenze.ID_USE_TIMER);
  preferenze.bind(showTimerCheckBox, SudokuPreferenze.ID_SHOW_TIMER);
  preferenze.bind(hideWhenPauseCheckBox, SudokuPreferenze.ID_HIDE_WHEN_PAUSE);
  preferenze.bind(pauseWhenHideCheckBox, SudokuPreferenze.ID_PAUSE_WHEN_HIDE);
  preferenze.bind(showErrorsCheckBox, SudokuPreferenze.ID_SHOW_ERRORS);
  preferenze.bind(allowHintsCheckBox, SudokuPreferenze.ID_ALLOW_HINTS);

  //pannello nuovo gioco
  preferenze.bind(gridSizeComboBox, SudokuPreferenze.ID_GRID_SIZE);
  preferenze.bind(levelComboBox, SudokuPreferenze.ID_LEVEL);

  //pannello posta elettronica
  preferenze.bind(emailTextField, SudokuPreferenze.ID_EMAIL);
  preferenze.bind(nomeTextField, SudokuPreferenze.ID_EMAIL_NOME,
                  System.getProperty("user.name"));
  preferenze.bind(smtpServerTextField, SudokuPreferenze.ID_SMTP_SERVER);
}

 

Conclusioni
In questo articolo abbiamo visto un possibile approccio alla persistenza della configurazione dell’applicazione. Sebbene migliorabile, mostra un modo comodo per leggere e salvare lo stato di componenti Swing.