La programmazione RIA con GWT/GXT

III parte: Semplificare lo sviluppo di applicazioni complesse grazie al MVCdi

Grazie a un potente meccanismo di MVC integrato nel framework GXT, è possibile realizzare applicazioni complesse senza che la complessità renda difficoltosa la stesura del codice e la sua successiva comprensione.

Una delle caratteristiche più importanti di un moderno framework grafico è la possibilità di organizzare il flusso delle varie pagine e quindi di separare i vari contesti logici dell'applicativo. In tal senso, una delle soluzioni ormai standard de facto è la possibilità di dividere la logica applicativa (Model) dalla logica di presentazione (View) e da quella che gestisce il flusso operativo (Controller). Il pattern MVC in questo ambito ha dimostrato di essere una soluzione ottimale, che consente di razionalizzare (e quindi di semplificare) lo sviluppo di applicazioni web.

MVC in GXT

Il framework GXT offre una implementazione di questo pattern completamente integrata nel sistema grafico, in un modo che risulta molto semplice da utilizzare; il meccanismo permette di realizzare la parte applicativa semplicemente costruendo le varie componenti, in maniera molto immediata. Di seguito sono riportati due esempi che mostrano come realizzare un semplice meccanismo di MVC sia per implementare una semplice funzionalità di logica (un login) sia per realizzare un sistema di gestione della messaggistica verso l'utente centralizzando la gestione degli alert (finestre di popup).

In GXT il modello MVC permette la gestione di scambio di messaggi basati su eventi con i quali si possono veicolare all'interno dell'applicazione informazioni e dati di vario tipo.

Il controller

Per realizzare un MVC partiamo dalla realizzazione di un controller, cosa che può essere realizzata in modo relativamente semplicemente implementando la classe astratta com.extjs.gxt.ui.client.mvc.Controller e il relativo metodo handleEvent().

Nota: per comodità e per migliorare la pulizia del codice, è buona norma suddividere l'applicazione in sottoparti, una per ogni use case o area applicativa. Per ogni unità di lavoro si potranno quindi creare un Controller, una View, un set di eventi e i componenti grafici corrispondenti.

Di seguito è mostrato un esempio che implementa il controller relativo all'operazione di login:

public class LoginController extends Controller {
    private LoginView loginView;
    public LoginController() {
        // registra su quali eventi si deve mettere in ascolto
        registerEventTypes(LoginEvents.doLogin);
        registerEventTypes(LoginEvents.showForm);
     }
    public void initialize() {
        loginView = new LoginView(this);
    }
    public void handleEvent(AppEvent event) {
        EventType type = event.getType();
        if (type == LoginEvents.showForm) {
            // inoltra l'evento alla view per visualizzare la form di login
            forwardToView(loginView, event);
        }
        else if(type == LoginEvents.doLogin){
            // esegue il login
            String userName = event.getData("userName");
            String password = event.getData("password");
            this.doLogin(userName, password);
        }
    }

Notiamo la presenza del metodo handleEvent che di fatto serve per intercettare il tipo di evento generato. All'interno del metodo andremo a controllare la tipologia del parametro Event passato direttamente dal sistema in modalità di callback. Normalmente all'interno di tale metodo, in funzione dell'evento arrivato al controller, verranno eseguite le operazioni di logica corrispondente, il che tipicamente viene realizzato per mezzo di una invocazione remota basata su RPC-GWT.

Nell'esempio che stiamo analizzando, dopo che il login Controller ha verificato che l'evento è effettivamente di tipo "doLogin", passa il controllo al metodo di invocazione remota

    public void doLogin(final String usname, final String passwd){
        final RemoteProviderAsync remoteProvider
        = (RemoteProviderAsync) Registry.get("remoteProvider");    
        AsyncCallback callback = new AsyncCallback(){    
            public void onSuccess(Object result) {    
                UserVO user = (UserVO) result;
                if (user == null){
                    AppEvent event = new AppEvent(LoginEvents.showForm);
                     event.setData("message", "Username o password non validi");
                    forwardToView(loginView, event);
                }
                else{
                    // login con successo. Mette l'oggetto nel registry e
                    // manda alla view un evento per la chiusura
// della finestra di login    
                    Registry.register("loggedUser", user);    
                    AppEvent event = new AppEvent(LoginEvents.hideForm);    
                    forwardToView(loginView, event);
                }
            }
        }
        public void onFailure(Throwable ex) {    
            System.out.println("Error "+ex.getMessage());
        }
    };
    remoteProvider.login(usname, passwd, callback);
}

Lo scopo di un controller è quello quindi di eseguire le operazioni di business (in proprio oppure invocando un corrispondente metodo remoto) e poi passare il controllo alla view opportuna per la relativa visualizzazione delle modiche o dei risultati dell'operazione. La comunicazione avviene rilanciando un evento opportuno grazie al metodo forwardToView(): il quale consente di passare il controllo alla view specificata. Nell'esempio di cui sopra, nel caso in cui il login sia eseguito con successo, si crea e si invia un evento di tipo hideForm alla view in modo che la finestra della finestra di login scompaia.

Prima di passare all'analisi della view, quindi, conviene prima fare una breve analisi sugli eventi e su come questi sono gestiti. In una applicazione GWT/GXT ci sono due tipologie di eventi, quelli applicativi e quelli di GUI: i secondi sono tutti quegli eventi generati a fronte di una operazione di input dell'utente (click del mouse, drag&drop, mouseover etc...) e che verranno intercettati dal sistema per poter catturare le intenzioni dell'utente. Non ci interessano in questa fase, dove invece ci concentriamo sugli eventi di tipo applicativo (derivano dalla classe com.extjs.gxt.ui.client.mvc.AppEvent); per creare un evento è sufficiente eseguire un'operazione del tipo:

AppEvent event = new AppEvent(LoginEvents.hideForm);    

dove si nota che al costruttore è passata una istanza di EventType per specificare il tipo esatto di evento che si vuole creare. Nell'applicazione che presentiamo, si è sfruttato un semplice trucco di utilizzare dei contenitori di tipi eventi da usare all'occorenza in funzione delle necessità. In questo caso, come accennato nella nota precedente, oltre a creare per ogni use caso o area applicativa del programma, un apposito controller e una view, creiamo un set di event-type che verranno inglobati per comodità all'interno di una apposita classe come nel seguente esempio:

public class LoginEvents {
    public static final EventType doLogin = new EventType();
    public static final EventType showForm = new EventType();
    public static final EventType hideForm = new EventType();
}

Avere a disposizione un set di tipi di evento già pre-istanziati è sicuramente una cosa utile e semplifica il lavoro, ad esempio quando si deve andare a intercettare il tipo di evento in arrivo (vedi metodo handleEvent di cui sopra).
Gli eventi applicativi sono quindi dei marcatori di tipologia di operazione che si vuole eseguire, all'interno dei quali si possono inserire informazioni (stringhe) oppure oggetti generici di vario tipo se si rende necessario notificare una qualche parte della applicazioni con informazioni che non vogliamo condividere con il resto del codice: ad esempio, dopo il login, potremmo voler dire alla view di login (che è di fatto il componente che serve per la gestione della comunicazione visuale con l'utente di tutte le operazioni di login) che il login con username XYZ è avvenuto con successo. Si possono quindi inserire nell'evento appena creato tutte le variabili necessarie (al solito si veda il metodo doLogin() di cui sopra).

In sintesi quindi possiamo dire che un controller è un oggetto che si sintonizza sulla gestione di particolari tipologie di eventi applicativi e che in genere funziona da tramite con la parte applicativa server side (esegue le invocazioni RPC) e passa il controllo per la visualizzazione dei risultati alla corrispondente View (come vedremo in seguito). Normalmente la view associata al controller viene creata direttamente dal controller all'interno del metodo init(). Per permettere al controller di attivarsi una particolare tipologia di evento è necessario procedere alla relativa registrazione (in accordo a quanto descritto nel pattern Observer), cosa che normalmente viene fatta all'interno del costruttore del Controller stesso:

public LoginController() {
    // registra su quali eventi si deve mettere in ascolto
    registerEventTypes(LoginEvents.doLogin);
    registerEventTypes(LoginEvents.showForm);
}

Infine la creazione di tutti i controller usualmente avviene al momento del caricamento del modulo nella classe Entry Point nel seguente modo:

public class SchoolmanagerEP implements EntryPoint {
    public void onModuleLoad() {
        Dispatcher dispatcher = Dispatcher.get();
        dispatcher.addController(new AppController());
        dispatcher.addController(new OperationsController());    
        dispatcher.addController(new LoginController());
...
}

La gestione della View

Come si è avuto modo di dire, il compito della view è quello di gestire la parte di visualizzazione dei vari componenti grafici modificandone il comportamento in funzione delle variazioni di  stato all'interno del controller.

La view quindi procede in genere alla creazione, visualizzazione e rimozione dei vari pannelli grafici (è comodo organizzare le varie fasi di esecuzione della logica di comunicazione con l'utente in pannelli, finestre, dialog etc… e modificarne lo stato in modo diretto).

Di seguito è riportato un esempio di view relativa alla gestione del login: quando la view riceve un evento di tipo showForm (in genere lanciato al momento della inizializzazione della applicazione) procederà alla visualizzazione della dialog modale di login. Se invece dal controller viene lanciato un evento di tipo hideForm significa che il login è andato bene e che quindi possiamo procedere a chiudere la finestra di login:

public class LoginView extends View {
    // la finestra di login è una  dialog modale
    LoginDialog loginDialog;
    public LoginView(Controller controller) {
        super(controller);
    }
    @Override
    protected void initialize() {
        loginDialog = new LoginDialog();
    }
    @Override
    protected void handleEvent(AppEvent event) {
        if (event.getType() == LoginEvents.showForm) {
            // mostra la finestra di login
            loginDialog.show();
        }
        else if (event.getType() == LoginEvents.hideForm) {
            // nscaonde la finestra di login
            loginDialog.hide();
        }
    }
}

La view serve anche per popolare gli oggetti grafici con i risultati delle interrogazioni fatte sullo strato server. Come avremo modo di vedere in uno dei prossimi articoli, nel caso in cui si vogliano utilizzare componenti databound (ovvero direttamente collegati a una sorgente di dati) si scoprirà che in GXT esiste un interessante meccanismo che collega i componenti visuali a un oggetto detto "store" con il quale è possibile eseguire prevaricamenti dei dati, caricamenti parziali o intelligenti e altre cose piuttosto utili quando si ha a che fare con grandi quantità di dati. Normalmente il caricamento di tale store avviene tramite dati che arrivano direttamente dallo strato remoto (attraverso una invocazione RPC, vedi la puntata precedente) e viene popolato per mezzo di particolari oggetti di supporto (RPC-Proxy e Loader). Una volta che lo store è stato riempito con i dati (visto che il popolamento è dinamico, ed è possibile associare una cache o la paginazione di grosse quantità di dati, forse sarebbe meglio parlare di binding fra la sorgente dati e lo store) è sufficiente "agganciare" lo store al componente grafico per consentire la visualizzazione dei dati.

In MVC questo giro può essere svolto nel seguente modo:

  • Il controller riceve un evento che gli comunica che è giunto il momento di effettuare un caricamento di nuovi dati dallo strato server (ad esempio, l'utente ha cliccato su un pulsante della GUI che ha prodotto un evento applicativo per il controller).
  • Il controller che, secondo l'alberatura del progetto vista nella puntata precedente, è un oggetto GWT-client, esegue la chiamata GWT-RPC sulla controparte GWT-server.
  • All'interno delle classi lato server i dati vengono ricavati in qualche modo (p.e. tramite una chiamata diretta sul DB oppure più realisticamente tramite una invocazione a un oggetto di business come un session bean o un POJO Spring).
  • Il lato GWT-client riceve i dati che sono passati al controller, il quale più o meno automaticamente si ritrova con lo store popolato.
  • A questo punto il controller deve passare lo store alla view affinche' questa provveda al relativo collegamento con l'oggetto grafico che dovrà visualizzare i dati contenuti nello store stesso (p.e. una Grid).

Tutti questi passaggi sono persenti nella porzione di codice riportata qui sotto: il metodo handleEvent deve per prima cosa intercettare il tipo di evento

public void handleEvent(AppEvent event) {
    EventType type = event.getType();
    if (type == MyEvents.viewForm) {
        loadDataListStore();
    }
}

Il metodo loadDataListStore esegue quindi la chiamata asincrona GWT-RPC per ricavare i dati dalla controparte server:

protected ListStore loadDataListStore(){
    final RemoteProviderAsync remoteProvider
= (RemoteProviderAsync) Registry.get("remoteProvider");
    RpcProxy  proxy = new RpcProxy(){
        public void load(Object loadConfig, AsyncCallback callback) {
            // esegue la chiamata remota con cui popolare il proxy
            // che verrà poi associato allo store tramite il loader
            remoteProvider.findData();
        }
    };
    BeanModelReader reader = new BeanModelReader();
    ListLoader  loader = new BaseListLoader(proxy, reader);
    listStore = new ListStore (loader);
    
    // crea un listener embedded per poter ricevere la notifica che la chiamata remota
    // ha avuto termine con esito positivo
    loader.addLoadListener(new LoadListener(){
        public void loaderLoad(LoadEvent le) {
            AppEvent event = new AppEvent(AppEvents.showData);
            event.setData("listStore", listStore);
            forwardToView(myView, event);
        }
        // eventualmente si può gestire la notifica con errore
        public void loaderLoadException(LoadEvent le) {
        }
    });
    // carica i dati nello store
    loader.load();
}

Si noti come il passaggio dello store alla view avviene all'interno di un listener "agganciato" all'invocazione remota: solo quando tale listener riceve notifica che la chiamata remota ha avuto termine con successo, lo store sarà disponibile per essere passato alla view. Il passaggio avviene tramite la creazione di un evento che conterrà al suo interno lo store, e propagando tale evento alla view corrispondente. La generazione della chiamata remota viene innescata tramite l'esecuzione del metodo load() del load cui l'oggetto RPC-Proxy è collegato (vedremo meglio questi aspetti nella prossima puntata di questa serie).

La view quando riceve l'evento ed ricava lo store dal suo interno e lo associa all'oggetto grafico di pertinenza (la grid):

public void handleEvent(AppEvent event) {
     if (event.getType() == MyEvents.loadStudenti){
        ListStore listStore = (ListStore) event.getData("listStore ");
        if (listStore!= null){
            myPanel.getGrid().reconfigure(listStore, myPanel.getGrid().getColumnModel());
        }
        return;
    }
}

La gestione della notifica dell'errore

In una moderna applicazione multistrato multicanale la gestione dell'errore è una cosa molto complessa e, per certi versi, probabilmente vicina a misteriose pratiche di divinazione esoterica. L'esperienza di un buon architetto potrà infatti consentire di capire quale sia il modo migliore per gestire l'errore e propagare in modo opportuno i messaggi al passare dei vari layer. Senza entrare nel dettaglio di questi aspetti, concentriamo l'attenzione al solo strato web client GXT. In questo contesto possiamo immaginare di ricevere diverse comunicazioni dai layer sottostanti (sotto forma di Exception sia di sistema sia applicative). Grazie all'uso degli eventi in ogni punto del nostro programma dove si verificheranno errori di vario tipo, possiamo pensare di lanciare un evento che verrà quindi intercettato da un apposito controller, all'interno del quale potremo inserire la logica relativa alla gestione dell'errore: dal non fare nulla allo stampare un messaggio di log al far apparire una dialog di allarme.

È quello che andiamo a vedere: supponiamo che in concomitanza di una invocazione GWT-RPC si debba intercettare l'errore che si potrebbe verificare; secondo la convenzione Java dovremmo gestire il tutto tramite un blocco try-catch, come mostrato di seguito.

Primo passo: si intercetta l'errore nella catch si rilancia l'evento:

try{
... Esegue una chiamata GWT-RPC
}
catch(Excpetion ex) {
    AppEvent errorEvent = new AppEvent(AppEvents.error);
    errorEvent.setData("errorMessage","Errore nella fase di caricamento dei dati");
    Dispatcher.forwardEvent(errorEvent);
}

L'evento verrà quindi intercettato dal controller applicativo (ovvero un controller non associato a nessun caso d'uso particolare ma il cui scopo è quello di gestire tutta l'applicazione nel suo contesto):

public class AppController extends Controller {
    private AppView appView;
    public AppController() {
        registerEventTypes(AppEvents.init);
        registerEventTypes(AppEvents.login);
        registerEventTypes(AppEvents.error);
    }
    public void handleEvent(AppEvent event) {
        EventType type = event.getType();
        if (type == AppEvents.error) {
            forwardToView(appView, event);
        }
    }
}

Il corrispondente metodo handleEvent della View a questo punto potrà gestire in modo centralizzato l'errore esempio visualizzando un dialog:

public void handleEvent(AppEvent event) {
    EventType type = event.getType();
    if (type == AppEvents.error) {
        // visualizza un messaggio di errore nella status bar
        FooterPanel footerPanel = (FooterPanel) Registry.get("footer");    
        if (footerPanel != null){
            String errorMessage = event.getData("errorMessage"    );
            footerPanel.setMessage(errorMessage);
            MessageBox.alert("Attenzione", errorMessage, null);
        }
    }
}

Conclusione

Come si è potuto vedere, il modello MVC è un ottimo strumento che permette di risolvere brillantemente la gestione di applicazioni con interfaccia grafica complessa, suddividendo e organizzando in maniera pulita e precisa la gestione delle varie parti della applicazione. Nel prossimo numero vedremo come in GXT questo si sposi con la gestione dei componenti databound per la visualizzazione di complesse strutture dati ricavate dal database.

Condividi

Pubblicato nel numero
150 aprile 2010
Giovanni Puliti lavora come consulente nel settore dell’IT da oltre 20 anni. Nel 1996, insieme ad altri collaboratori crea MokaByte, la prima rivista italiana web dedicata a Java. Da allora ha svolto attività di formazione e consulenza su tecnologie JavaEE. Autore di numerosi articoli pubblicate sia su MokaByte.it che su…
Articoli nella stessa serie
Ti potrebbe interessare anche