vendredi 29 mars 2013

MVC dans un form Master-Detail


Ce billet fait suite au billet posté sur MVC le 4 Janvier 2011

MVC est un design pattern relativement vieux, mais il faut reconnaître qu’il a été remis au goût du jour depuis quelques années déjà et ce en particulier grâce ou à cause de Java.
Dans ce billet je vais m’intéresser à : 
-A)          Pertinence de MVC
Un des problèmes majeurs du développement est que les possibilités de résoudre un problème sont généralement multiples.  Souvent a priori, il n’y a pas de bonne ou de mauvaise méthode, les dev se laissent aller à leur habitudes, leurs logiques propre (parfois très différentes d’un dev à l’autre, en fonction de leur expérience). Il est souvent difficile lorsque l’on s’attaque à un problème non connu de trouver la solution ou la stratégie qui sera la plus payante, sur le court ou sur le long terme. Souvent on se laisse aller à son bon sens, ce qui peut parfois être payant à court terme mais décevant sur le long terme ou pire, inutilisable lorsque le système grossit. Heureusement, nombre de développeurs et d’architectes ont formalisé leur expérience, leur stratégie applicable sur des problèmes concret de développement. Ce formalisme définit les design patterns.  L’un des avantages de MVC est qu’il est lui-même un design pattern s’appuyant sur le pattern Observer (View)-Subject (Model). Remarque que java.util définit aussi la notion d’observer : Observable (Model) – Observer(View) - Listener (Controller). 
Dans ce cas précis nous allons nous intéresser à la manière de modéliser les réponses d’un utilisateur à une interface graphique servant de vue et d’entrée sur les données contenues dans une base.

Un problème auquel tout développeur, qui se respecte,  a été confronté au moins une fois dans sa vie.
Dans le courant des années nonante et même plus tard, les IDE graphique permettait d’associer aux contrôles (widgets) d’une fenêtre, un code de réponse à un évènement. Ce formalisme, bien connu des utilisateurs VB, Delphi, .. est souvent connu sous le nom de programmation Event Driven.

Le  problème de cette approche est le couplage fort entre l’interface et le contrôle de flux. En effet, tant que le contrôle de flux ne dépend que d’une fenêtre cette approche fonctionne plus ou moins bien.
Toutefois, quand le contrôle de flux impacte plusieurs fenêtres, lorsque l’on veut, par exemple, répercuter les modifications provoquées par l'action sur une fenêtre dans les autres, on est souvent amené à faire des bidouilles de programmation pas très propres (euphémisme pour ne pas dire dégueulasses) qui au final transforme le code de contrôle des interfaces en magnifique spaghetti peu appétissant.

B)       L’approche MVC (pattern observer)
      Définition : Model View Controller.

Pour faire simple le controlleur répond aux évènements activés dans les vues, en réponse à cela il modifie le status du modèle, celui-ci répercute les changements sur les vues.

Avantage : Contrôle de flux est indépendant des vues,  ajouter de nouvelles vues n’impacte pas toujours  le contrôle de flux. Les vues ne se connaissent pas entre elles, ce qui évite des couplages entre vues et entre code de contrôle.  Le modèle n’update les vues que lorsqu’il a été modifié par le controlleur.

D'un point de vue design pattern, le model contient donc un pattern observer, au sens Java, il est un observable, tandisque la vue est un observer. On peut résumer cela par le formalisme UML.

Diagramme de Classe:
- La responsabilité de la vue est l'affichage du modèle et la capture des évènements liés à la vue (clic de souris, clavier, ...)
- La responsabilité du contrôleur est de répondre aux évènements engendrés dans la vue.
- La responsabilité du modèle est double d'une part récupérer et gérer les données en provenance d'une source de donnée (une DB par exemple) ET notifier les vues d'un changement d'état:

Diagramme de Sequence


Ce diagramme donne les séquences de création par rapport à l'application. On crée le contrôleur, puis le modèle, puis les vues en leur passant une référence sur le contrôleur qui contient le code de gestion des événements, ensuite on appelle la fonction registerObserver du modèle pour assigner une nouvelle vue.

Lorsqu'un utilisateur effectue une action sur la vue, l'applic envoit un évènement, la réponse à cet évènement se fait dans le contrôleur qui va modifier l'état du modèle, la modification de l'état du modèle va obliger celui-ci à notifier les vues qui l'observent de son changement d'état. Ainsi quelques soit la modification effectuée dans une vue, cette modification, si elle a changé l'état du modèle se répercutera dans toutes les autres.

Traduisons cela en Java et pour se faire choisissons un exemple, tout d'abord une vue contenant une liste de studyInf contenue dans un JList.
La seconde vue contiendra la description des champs du studyInf ainsi qu'une liste de boutons permettant, la sauvegarrde, ou le passage à l'élément suivant ou précédent, il est à noter que les évènements du JList impacteront la vue contenant le descriptif et les boutons.

1. Crée les modèles:
dans ce cas nous avons besoin de deux modèles : StudyInf et StudyInfListModel

StudyInf : est le plus simple puisqu'il contiendra les données provenant de la DB. Remarquez que dans ce tutorial nous n'allons pas nous étendre trop loin sur la manière de récupérer les données DB.... Le mieux est de créer un service se chargeant de l'accès à la base de donnée et qui renverra un modèle ou une liste de modèles... Cela pourrait faire l'objet d'un autre billet... ou pas.


public class StudyInf {
protected String signalAction,
signalDescription,
assignment;
protected Long studyId;
protected int
protocolStatus,
signalStatus,
timeDimId;
protected float sizeN,
totalImbalance;
protected String protocolId,
protocolName;
...
// add getter/setter
}

Il s'agit d'une simple classe POJO. Intéressons nous maintenant à la classe StudyInfListModel qui sera utilisée par l'objet JList et par qui toutes les actions de rafraîchissements transiteront.


public class StudyInfListModel extends DefaultListModel {

/**

*
*/
private static final long serialVersionUID = -1561852405765263848L;
private static class MyObservable extends Observable {
@Override
public void setChanged() {
super.setChanged();
}

}

// embed the Observable (java does not support multiple inheritance)
public MyObservable observable;
public StudyInfListModel() {
super();
this.observable = new MyObservable();
}

public void addObserver(Observer observer) {

this.observable.addObserver(observer);
}

public void notifyObservers(Object object) {

this.observable.setChanged();
this.observable.notifyObservers(object);
}
}


Comme on le voit, la classe hérite de DefaultListModel, ce qui est obligatoire si on veut l'utiliser comme modèle pour un JList.. Comme Java ne supporte pas l'héritage multiple, on va simplement incorporer un Observable dans cette classe Model. Et on va ajouter les méthodes utiles à un Observable : 
- addObserver et notifyObserver(). Il faut noter qu'avant d'appeler notifyObserver il faut appeler la méthode setChanged(); Sinon l'appel ne se fait pas, si le modèle n'est pas passé à l'état changé.

Comme cette méthode est définie comme protected dans Observable, elle n'est donc pas directement accessible par notre classe wrapper. Pour pâlier à cela on va créer la classe interne : MyObserver qui va exposer publiquement la méthode setChanged(); Comme vous le savez une méthode protected peut être appelée par la classe fille, et celle-ci peut donc mettre le membre appelant public...

2.) Créer le contrôleur

Voici une partie du code de notre contrôleur:


public class StudyInfController implements ActionListener,
ListSelectionListener {
protected List studyInfList;
protected StudyInfListModel studyInfListModel;
public void initializeStudyInfModel() {
       //TODO add the code here
      //....
}

public StudyInfController(StudyInfDao studyInfDao,
StudyInfListModel studyInfListModel) {
this.studyInfListModel = studyInfListModel;

initializeStudyInfModel();

}


public void actionPerformed(ActionEvent e) {
StudyInf studyInf;
if (e.getSource() instanceof Component) {
Component source = (Component) e.getSource();
if (source.getName().equals("btCommit")) {
                             // TODO add the code here}
                            // ...
}

public void valueChanged(ListSelectionEvent e) {
if (!e.getValueIsAdjusting()) {
try {
// Retrieve the selected item.
StudyInf studyInf = (StudyInf) this.getModel().getSelected();
this.getModel().notifyObservers(studyInf);
} catch (Exception ex) {
}
}
}
}


Comme déjà expliqué le rôle de contrôleur est de répondre aux évènements générés par les vues et d'adapter le modèle en conséquence. C'est pourquoi la classe implémente deux interfaces : SelectionListener pour la JList et ActionListener pour les boutons.
Tout d'abord le constructeur va charger les données utiles à l'initialization du modèle. C'est durant la construction du controlleur que le modèle studyInfListModel sera chargé avec les données provenant de la DB.
Comme vous l'aurez remarqué les fonctions actionPerformed() et selectionChanged() ne font jamais directement un appel explicite à la vue étant donné que si les vues connaisssent le contrôleur celui-ci ne les connais pas. La plupart des actions se feront au travers du modèle qui lui impactera ses vue (notifyObservers()). Dans actionPerformed() les tests sur les contrôles cliqués se feront au moyen du nom de ceux-ci.

if (source.getName().equals("btCommit")) {...
}

Remarquez aussi que le modèle est passé en paramètre au contrôleur.

3.) Les vues:
D'une manière générale, nos vues ne sont rien d'autre que des JPanel.

3.1) La listview:
La première vue définie ici s'occuppe d'afficher la liste des studyInf et de permettre à l'utilisateur de les sélectionner.

public class StudyInfListView extends JScrollPane implements Observer {
private static final long serialVersionUID = 1L;
JList list;
public StudyInfListView(StudyInfController controller) {
super();

this.list =new JList(controller.getModel());
this.setViewportView(this.list);
controller.getModel().setListContainer(this.list);
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
list.addListSelectionListener(controller);
list.setFont(new Font("Courier",Font.BOLD, 12));

}
public void update(Observable observable, Object arg1) {

this.list.revalidate();
}
}

Comme déjà expliqué, la vue va implémenter une interface Observer qui va nous fournir la fonction update, qui sera automatiquement appelée par le modèle.
La vue est constituée d'un ScrollPane qui va nous servir de pattern decorator sur la JList afin de lui ajouter des scrollbars.

On va ensuite créer la JList en utilisant le modèle contenu dans le contrôleur, en effet la vue peut contenir une référence sur le contrôleur (notamment dans le but de l'assigner comme classe de réponse aux évènements reçu par ses composants). Comme par exemple pour la vue représentant le modèle:


btCommit.addActionListener(controller);
btClose.addActionListener(controller);
btNext.addActionListener(controller);
btPrev.addActionListener(controller);

Remarquez l'utilisation de "revalidate()" pour réafficher la fenêtre updatée.


4) L'application:
C'est là que l'on va gérer la création des entités MVC et lier le tout ensemble:

   StudyInfListModel listInfModel = new StudyInfListModel();

    this.studyInfController=new StudyInfController(listInfModel);

    JPanel panelEdt = new JPanel();
        panelEdt.setLayout(new BorderLayout());

     StudyInfListView panelEdtTop = new StudyInfListView(  studyInfController);

    panelEdtTop.setPreferredSize(new Dimension(640,240));
      listInfModel.addObserver(panelEdtTop);


On crée le modèle
On crée le contrôleur auquel on passe le modèle.
On crée le panel principal en border Layout (Center contiendra la liste et South les composants text edit, label, combo ainsi que les boutons).
On crée la première vue "StudyInfListView" et on lui passe le contrôleur en paramètre. 
On informe le modèle qu'une vue est prête à l'observer : addObserver()

On fait de même avec la seconde vue et puis on active le tout:

  listInfModel.notifyObservers(null);

      panelEdt.add(panelEdtTop, BorderLayout.CENTER);
      panelEdt.add(panelEdtBottom, BorderLayout.SOUTH);

      this.getContentPane().add(panelEdt);

    this.pack();

D'abord on demande au modèle de rafrâichir ses vues fraîchement crées, puis on place les deux vues dans le panneau principal que l'on attache au JFrame de l'application (getContentPane) et puis on fait un pack.


 Une fois ceci fait l'application devrait créer les vues sur les modèles et permettre à l'utilisateur d'influer sur l'état des modèles aux moyens des contrôles placés sur les vues...

Voilà, l'exemple donné ici, n'est pas fonctionnel tel quel mais le but est de donner une idée, un cannevas permettant d'implémenter du MVC dans Swing.



Aucun commentaire:

Enregistrer un commentaire