Cours interface graphique java : MVC et listes

Cours interface graphique java : MVC et listes
...
n le contrôleur est le coeur de l'application. Toutes les demandes du client transitent par lui. C'est une servlet générique [ActionServlet] fournie par STRUTS. On peut dans certains cas être amené à la dériver. Pour les cas simples, ce n'est pas nécessaire. Cette servlet générique prend les informations dont elle a besoin dans un fichier le plus souvent appelé struts-config.xml.
n la demande du client vient par l'intermédiaire d'une requête HTTP. L'URL cible est l'action demandée par le client.
n si la requête du client contient des paramètres de formulaire, ceux-ci sont mis par le contrôleur dans un objet [ActionForm].
n dans le fichier de configuration struts-config.xml, à chaque URL (=action) devant être traitée par programme (ne correspondant donc pas à une vue JSP qu'on pourrait demander directement) on associe certaines informations :
o le nom de la classe de type Action chargée d'exécuter l'action
o si l'URL demandée est paramétrée (cas de l'envoi d'un formulaire au contrôleur), le nom de l'objet [ActionForm] chargé de mémoriser les informations du formulaire
n muni de ces informations fournies par son fichier de configuration, à la réception d'une demande d'URL par un client, le contrôleur est capable de déterminer s'il y a un objet [ActionForm] à créer et lequel. Une fois instancié, cet objet [ActionForm] peut vérifier que les données qu'on vient de lui injecter et qui proviennent du formulaire, sont valides ou non. Une méthode de [ActionForm] appelée validate est appelée automatiquement par le contrôleur. L'objet [ActionForm] étant construit par le développeur, celui-ci a mis dans la méthode validate le code vérifiant la validité des données du formulaire. Si les données se révèlent invalides, le contrôleur n'ira pas plus loin. Il passera la main à une vue dont il trouvera le nom dans son fichier de configuration. L'échange est alors terminé.
n si les données de l'objet [ActionForm] sont correctes, ou s'il n'y a pas de vérification ou s'il n'y a pas d'objet [ActionForm], le contrôleur passe la main à l'objet de type [Action] associé à l'URL. Il le fait en demandant l'exécution de la méthode execute de cet objet à laquelle il transmet la référence de l'objet [ActionForm] qu'il a éventuellement construit. Les objets [Action] sont construits par le développeur. C'est là qu'il place le code chargé d'exécuter l'action demandée. Ce code peut nécessiter l'utilisation de la couche métier ou modèle dans la terminologie MVC. Les objets [Action] sont les seuls objets en contact avec cette couche. A la fin du traitement, l'objet [Action] rend au contrôleur une chaîne de caractères représentant le résultat de l'action.
n dans son fichier de configuration, le contrôleur trouvera l'URL de la vue associée à cette chaîne. Il envoie alors cette vue au client. L'échange avec le client est terminé.
...
Les éléments de M2VC
M2VC a été construit avec JBuilder. Le plus simple est peut-être de partir de la structure du projet JBuilder utilisé pour construire [M2VC] puis de détailler un à un ses différents éléments :
Le projet est formé des éléments suivants :
- trois interfaces : IAction, IVue, IControleur
- deux classes dérivables : BaseControleur, BaseVueJFrame
- une classe : InfosAction
Passons en revue le rôle de ces différents éléments sans encore entrer dans les détails. Les explications ci-dessous doivent être lues à la lumière de l'architecture présentée dans le paragraphe précédent :
IControleur dit ce que doit savoir faire un contrôleur générique
implémente l'interface [IControleur]. Assure toute la synchronisation [traitement actions] - affichage vues. Le développeur peut dériver cette classe si le contrôleur générique ne lui suffit pas.
BaseControleur
IAction dit ce que doit savoir faire un objet [Action]. Aucune implémentation n'est proposée. dit ce que doit savoir faire une vue
IVue
BaseVueJFrame implémente l'interface [Ivue] précédente. Est dérivée de l'objet [JFrame]. Les formulaires d'une application windows utilisant [M2VC] seront dérivés de cette classe de base. Il est possible de construire d'autres implémentations de l'interface [IVue]...
Nous décrivons maintenant ces éléments un à un. Ils sont tous placés dans l'espace de noms [istia.st.m2vc.core].
4.1 L'interface [IControleur]
Son code est le suivant :
package istia.st.m2vc.core;
/**
* @author [email protected], juin 2005
*
*/
public interface IControleur {
// lance le contrôleur
public void run();
}
On ne demandera qu'une chose à un contrôleur, c'est de s'exécuter. C'est la méthode [run] qui nous permettra de lancer le contrôleur de l'application windows.
4.2 L'interface [IAction]
Son code est le suivant :
package istia.st.m2vc.core;
/**
* @author [email protected], juin 2005
*
*/
public interface IAction {
// exécute l'action
public String execute();
}
On ne demandera qu'une chose à un objet [Action], c'est d'exécuter l'action pour laquelle il a été construit. Rappelons ici que c'est le développeur qui fournira les classes [Action] implémentant l'interface [IAction]. Pour exécuter une action, on appellera donc la méthode [execute] de l'objet [Action] associée à cette action.
4.3 L'interface [IVue]
Son code est le suivant :
package istia.st.m2vc.core;
public interface IVue {
// affiche la vue
public void affiche();
// cache la vue
public void cache();
// récupère le nom de l'action demandée par la vue
public String getAction();
// fixe le nom de la vue
public void setNom(String nom);
// récupère le nom de la vue
public String getNom();
}
Que peut-on demander à une vue ?
- qu'elle s'affiche (méthode affiche)
- qu'elle se cache (méthode cache)
- qu'elle donne son nom (propriété nom). Cette propriété n'a jamais paru indispensable mais elle a été néanmoins conservée. Je l'ai utilisée dans des phases de débogage
- qu'elle donne le nom de l'action demandée par l'utilisateur (propriété action). Rappelons qu'une vue sera une fenêtre windows. Celle-ci va réagir toute seule à certains événements. Ce sera la grande différence avec les vues web. Elle peut ainsi réagir à un événement drag'n drop. Elle s'interdira de faire appel elle-même aux couches métier dont elle n'est pas sensée avoir connaissance.. Dans ce cas, elle passera plutôt la main au contrôleur en lui donnant le nom de l'action demandée par l'utilisateur.
4.4 La classe [Barriere]
Lorsque le contrôleur fait afficher une vue, il se met ensuite en attente d'un événement. Une vue répond aux événements qui concernent son apparence. Lorsque l'utilisateur demandera une action qui nécessite un accès à la couche métier, la vue s'interdira de l'exécuter elle-même. Elle la fera exécuter par le contrôleur. Cette demande constitue ce que nous avons appelé ci-dessus un événement pour le contrôleur. Pour permettre au contrôleur d'être averti de celui-ci, on utilise la classe [Barriere] suivante :
- package istia.st.m2vc.core;
- /**
- * @author [email protected], juin 2005
- *
- */
- public class Barriere {
- // état ouvert - fermé de la barrière
- private boolean ouverte;
- /**
- * @return Returns the ouverte.
- */
- public boolean isOuverte() {
- return ouverte;
- }
- /**
- * @param ouverte The ouverte to set.
- */
- public void setOuverte(boolean ouverte) {
- this.ouverte = ouverte;
- }
- // fermeture barrière
- public synchronized void reset() {
- ouverte = false;
- }
- // ouverture barrière
- public synchronized void set() {
- if (! ouverte){
- ouverte = true;
- this.notify();
- }
- }
- // attente barrière
- public synchronized void waitOne() {
- if (!ouverte) {
- try {
- this.wait();
- }
- catch (InterruptedException ex) {
- throw new RuntimeException(ex.toString());
- }
- }
- } 51.}
- ligne 11, la classe gère une propriété [ouverte] pour indiquer si la barrière est ouverte ou non
- lignes 41-50, la méthode [waitOne] permet d'attendre (ligne 44) que ce booléen passe à vrai si on l'a trouvé à faux
- lignes 33-38, la méthode [set] ouvre la barrière et en avertit un éventuel thread en attente de cette ouverture (ligne 36).
- lignes 28-30, la méthode [reset] ferme la barriere.
Note : cette classe n'a pas fait l'objet d'une étude de synchronisation poussée. Il faudrait voir si elle passe bien tous les cas de synchronisation possible entre le contrôleur et les vues.
4.5 La classe [BaseVueJFrame]
[BaseVue] est une classe de base implémentant l'interface Ivue. Outre qu'elle implémente les caractéristiques de l'interface [IVue], elle sait également se synchroniser avec le contrôleur générique
- faire appel à la méthode [affiche] de la vue
- attendre que celle-ci envoie le nom d'une action à exécuter. Il y a donc une synchronisation [controleur - vue] à organiser.
Le code de la classe [BaseVueJFrame] est le suivant :
- package istia.st.m2vc.core;
- import javax.swing.JFrame;
- /**
- * @author [email protected], juin 2005
- *
- */
- public class BaseVueJFrame
- extends JFrame implements IVue, Runnable {
- // champs privés
- private String nom; // nom de la vue
- private String action; // nom de l'action que le contrôleur doit exécuter
- private Barriere synchro; // objet de synchronisation controleur - vues
- private Thread threadVue; // objet de synchronisation de la vue
- /**
- * @return Returns the action.
- */
- public String getAction() {
- return action;
- }
- /**
- * @param action
- * The action to set.
- */
- public void setAction(String action) {
- this.action = action;
- }
- /**
- * @return Returns the nom.
- */
- public String getNom() {
- return nom;
- }
- /**
- * @param nom
- * The nom to set.
- */
- public void setNom(String nom) {
- this.nom = nom;
- }
- /**
- * @return Returns the synchro.
- */
- public Barriere getSynchro() {
- return synchro;
- }
- /**
- * @param synchro The synchro to set.
- */
- public void setSynchro(Barriere synchro) {
- this.synchro = synchro;
- }
- // affiche la vue
- public void affiche() {
- // on s'affiche dans le thread d'affichage
- if (threadVue == null) {
- // on crée le thread d'affichage
- threadVue = new Thread(this);
- threadVue.start();
- } else {
- // on s'affiche
- this.setVisible(true);
- }
- }
- // cache la vue
- public void cache() {
- // on se cache
- this.setVisible(false);
...
Les points à comprendre sont les suivants :
- la classe [BaseVueJFRame] dérive de [JFrame] (ligne 10).
- la classe implémente l'interface [IVue] - ligne 10
- la classe implémente l'interface [Runnable] - ligne 10. En effet, elle contient la méthode [run] d'un thread qu'elle crée pour s'afficher.
- elle implémente la propriété [nom] de l'interface [Ivue] - lignes 34-47
- elle implémente la propriété [action] de l'interface [Ivue] - lignes 19-32
- elle implémente la méthode [affiche] de l'interface [Ivue] - lignes 64-74
- elle implémente la propriété [cache] de l'interface [Ivue] - lignes 77-80
- elle a un objet de synchronisation [synchro] de type [Barriere] - lignes 50-61. La propriété [synchro] va permettre à la vue de signaler au contrôleur qu'elle demande l'exécution d'une action.
- la fenêtre graphique va être affichée dans un thread différent de celui dans lequel s'exécute le contrôleur. Ce thread est déclaré en ligne 16
...
Seules les classes dérivées le sauront. Elles procèderont à l'affichage du formulaire de la façon suivante :
- initialisation des composants du formulaire
- appel de la méthode [affiche] de la classe de base [BaseVueJFrame] ci-dessus. C'est elle qui doit terminer l'affichage. Que fait-elle ?
X elle regarde si le formulaire a déjà été affiché. Si c'est le cas, le thread d'affichage [threadVue] a été initialisé - ligne 3
X si le thread d'affichage n'existe pas, alors on le crée (ligne 4) et on le lance (ligne 6). Le thread a été lancé pour exécuter la méthode privée [run] de la classe. La méthode [run] (lignes 28-31) s'exécute donc. Que fait-elle ? Elle affiche le formulaire (ligne 30). La méthode [affiche] est terminée.
...
X si le thread d'affichage existe déjà, alors on se contente d'afficher la vue (ligne 9). La méthode [affiche] est terminée.
X dans quelle situation sommes-nous lorsque la méthode [affiche] a été entièrement exécutée ?
X l'utilisateur voit le formulaire. Il peut agir dessus. Le formulaire réagit aux événements.
X celui-ci est affiché dans un thread [threadVue] séparé du thread du contrôleur
X le contrôleur est en attente d'un événement : que la vue lui dise qu'une action lui est demandée. Elle signalera cet événement grâce à la variable de synchronisation [synchro] qui sera partagée entre le contrôleur et toutes les vues de l'application. Pour attendre, le contrôleur a émis la séquence d'instruction suivante :
// on affiche la vue en se synchronisant avec elle synchro.reset(); vue.affiche(); synchro.waitOne();
X la vue débloquera le contrôleur en faisant :
synchro.Set()
X quand la situation se débloque-t-elle ? L'utilisateur interagit avec le formulaire. Celui-ci réagit avec les gestionnaires d'événements qu'on lui a mis. Pour certains événements, le développeur de la vue décide de passer la main au contrôleur. Pour cela il appelle la méthode [execute] de la classe de base [BaseVueJFrame] en lui passant le nom de l'action à exécuter.
- la méthode [execute] Son code est le suivant :
- // action asynchrone
- public void exécute(String action) {
- // on note l'action à exécuter
- this.setAction(action);
- // on passe la main au contrôleur
- synchro.set();
- }
X l'action à eéxcuter est notée dans la propriété [action] - ligne 4. C'est ici que le contrôleur la récupèrera.
X on débloque le contrôleur - ligne 6. Celui-ci va pouvoir exécuter l'action qu'on lui demande
X on notera et c'est très important de le comprendre qu'en ligne 6, la méthode [execute] est terminée. Son exécution sera demandée à la suite d'un événement du formulaire affiché. La fin de [exécute] entraîne la fin de la gestion de l'événement. De nouveaux événements du formulaire peuvent alors survenir et être traités. Cela signifie qu'alors que le contrôleur est en train d'accomplir une action pour le compte de la vue, celle-ci continue à répondre aux événements qui la concernent. Nous n'avons pas voulu prendre de décision sur ce point. C'est au développeur de le faire. Celui-ci doit savoir qu'à chaque fois qu'il fait appel au contrôleur, il déclenche une action asynchrone. A lui de savoir comment son formulaire doit se comporter. Dans les exemples étudiés plus loin, nous proposons plusieurs réponses simples à cette situation.
- la méthode [cache]
Le contrôleur gère plusieurs vues. Après avoir exécuté l'action demandée par une vue, il peut vouloir afficher une autre vue. Il demande alors à la vue précédente de se cacher. Le code est simple :
- // cache la vue
- public void cache() {
- // on se cache
- this.setVisible(false);
- }
Les vues de l'application windows, dérivées de [BaseVueJFrame], seront instanciées par configuration avec [Spring IoC].
...
Le contrôleur sera intancié par configuration à l'aide de [Spring Ioc]. Commençons par présenter les propriétés publiques du contrôleur.
actions lignes 18-31, ligne 13
un dictionnaire faisant le lien entre le nom d'une action et l'instance [InfosAction] à utiliser pour cette action. Initialisé par fichier de configuration.
firstActionName lignes 33-46, ligne 14
le nom de la première action à exécuter. En effet, les noms des actions sont normalement donnés par les vues. Or au départ, il n'y a pas de vue. Initialisé par fichier de configuration. lignes 48-61, ligne 15
le nom de la dernière action à exécuter. Lorsqu'une vue envoie cette action au contrôleur, celui-ci s'arrête. Initialisé par fichier de configuration.
lastActionName
synchro lignes 63-76, ligne 16
l'instance [Barriere] qui permet au contrôleur de se synchroniser avec les vues. Initialisé par fichier de configuration.
Le contrôleur implémente l'interface [IControleur] (ligne 10). Il a donc une méthode [run] (ligne 79). C'est cette méthode qui lance le contrôleur. Celui-ci, après quelques initialisations, passe son temps dans une boucle où il va exécuter les actions demandés par les vues. Il va commencer par la première, qui aura été positionnée par configuration (ligne 89). Il terminera par celle ayant été déclarée comme la dernière (ligne 90).
...
Application 3
Nous abordons ici une application à trois vues. L'application commence par afficher la vue [Saisir] suivante :
L'utilisateur entre des textes dans le champ de saisie. Le bouton [Mémoriser] les mémorise au fur et à mesure. Le bouton [Consulter] sert à voir la liste des textes saisis. L'action associée a été construite pour échouer une fois sur deux.
Lorsque l'action [Consulter] réussit, on a la vue [Consulter] suivante :
Un bouton permet de revenir à la saisie des textes.
Lorsque l'action [Consulter] échoue, on a la vue [Erreurs] suivante :
On peut alors soit revenir à la saisie des textes, soit quitter l'application. En cas d'erreurs, l'action [Consulter] a été construite pour attendre trois secondes pour rendre son résultat. Pendant cette attente, la vue [Saisir] est complètement gelée. Elle affiche un message pour faire patienter l'utilisateur :
La structure de l'application
Le projet JBuilder a la structure suivante :
La session
Dans les exemples étudiés précédemment, les actions [IAction] et les vues [IVue] n'échangeaient pas d'informations. Ce n'est pas le cas courant. Dans cette application, la vue [VueSaisir] remplit un objet [ArrayList] avec les textes qu'elle a saisis. Lorsque la vue [VueConsulter] va vouloir afficher les textes saisis, elle devra avoir accès à cet objet [ArrayList]. Le contrôleur [M2VC] ne donne aucune aide pour ces échanges d'informations entre vues et actions. C'est au développeur d'organiser ceux-ci. Nous proposons ici une méthode simple qui devrait suffire pour beaucoup d'applications. Il s'agit de créer un unique objet qui contiendra toutes les informations à partager entre les vues et les actions. Il n'y a pas de problème de synchronisation. Les objets du contrôleur ne sont jamais amenés à accéder à cet objet partagé de façon simultanée. Nous appellerons cet objet [Session] par similitude avec l'objet [Session] des applications web dans lequel on stocke tout ce qu'on veut garder au fil des échanges client-serveur. Tous les objets d'une application web ont accès à cet objet [Session] comme ce sera le cas ici.
Notre classe [Session] sera la suivante :
package istia.st.m2vc.appli; import java.util.ArrayList;
public class Session {
// liste de textes
private ArrayList textes=new ArrayList();;
// liste d'erreurs
private ArrayList erreurs=new ArrayList();
// getters-setters
public ArrayList getErreurs() {
return erreurs;
}
public ArrayList getTextes() {
return textes;
}
public void setErreurs(ArrayList erreurs) {
this.erreurs = erreurs;
}
public void setTextes(ArrayList textes) { this.textes = textes;
}
}
Elle contiendra la liste des textes mémorisés par la vue [VueSaisir] et les erreurs à afficher par la vue [VueErreurs].
7.3 Les vues
Il ya trois vues. Elles sont implémentées par les fichiers [BaseVueAppli.java, VueConsulter.java, VueErreurs.java, VueSaisir.java].
7.3.1 La vue [BaseVueAppli]
...