Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Design Pattern Model-View-Controller

Compatible ActionScript 2. Cliquer pour en savoir plus sur les compatibilités.

Introduction

Ce Pattern est une sorte d'extension au Pattern Observer à quelques différences prêt :

  • les évènements sont spécialisés (plusieurs méthodes évènementielles)
  • les vues sont gérées par leur contrôleur (un par vue généralement)
  • l'affichage, le traitement et le stockage des données se font dans des classes spécifiques (accentue la notion POO)

Comment tout cela fonctionne-t-il ? Tout d'abord voyons un peu la définition de chacun de ces 3 mots :

  • le modèle : c'est la que l'on stocke les données ou, plus généralement, là ou se passent les évènements (tels qu'on le verra ci-dessous)
  • la vue : c'est la partie visuelle, basée sur les données du modèle et qui va agir différemment en fonction des évènements envoyés par celui-ci. Il peut y avoir plusieurs vues ayant pour un seul modèle (c'est bien le but !) et chacune d'entre elles adopteronts des comportements spécifiques lorsqu'elles recevront les évènements générés par les modèles.
  • le contrôleur : les vues interagissant avec le modèle, elles ont parfois (souvent) besoin de modifier les données. Le contrôleur sert à faire la liaison entre le modèle et la vue. C'est notamment lui qui, dans l'exemple ci-dessous, s'occupe du traitement des données avant de les transmettre aux modèle. Le traitement des données étant souvent spécifique à chaque vue, il y en a généralement un par vue.

Un exemple simple de MVC utilisé dans windows : ouvrez 2 fenêtres d'explorer sur le même répertoire et, dans une des deux, créer un nouveau fichier. Vous le verrez aussitôt apparaître sur la 2ème fenêtre ! Ce Design Pattern est en général utilisé pour gérer l'interface d'une application.

A la base, le MVC a été développé pour tracer les opérations input (contrôleur), processing (modèle) et output (vue). C'est typiquement ce qui ce passe avec l'explorer de Windows : le contrôleur saisis les données de l'utilisateur (donc la création du fichier), il dit au modèle (donc à l'explorer) de créer le fichier, le modèle le crée et notifie toutes les fenêtres ouvertes sur ce dossier qu'un nouveau fichier a été créé. Ensuite chacune de ces fenêtres réagit pour afficher le nouveau fichier !

Modélisation

Voici le schéma UML du Mailer utilisé ci-dessous. Tout est expliqué en détail dans la partie suivante si vous ne le comprenez pas !
mvc.jpg

Mailer POO

Cet exemple est très proche de la gestion d'explorer (si si vous allez voir) ! Ci-dessous, une liste des fichiers utilisés pour réaliser l'exemple :

  • Classe DataMail : c'est la classe qui fera office de modèle. Elle se chargera d'envoyer et de réceptionner les données entre l'application flash et le fichier php
  • Interface DataMailListener : interface pour les écouteurs d'évènements de la classe DataMail.
  • Classe Mailer : c'est notre vue tout simplement. Elle gère tout ce qui est visuel et écoute les évènement de la classe DataMail
  • Classe MailerController : le controleur qui va se charger que les données entrées sont valides et il les transmettra au modèle pour l'envoi des données.

Le modèle

Comment tout cela est-il imbriqué ensemble ? Commençons par notre modèle ! Dans notre exemple, il s'occupe de la transmission de données entre flash et php. Il va donc gérer un LoadVars et stockera les écouteurs qui recevront la notification des évènements (qui correspondent à l'interface DataMailListener). Rapidement, voici sa structure :

class DataMail
{
	public static function get URL_FILE():String { return "php/mailer.php"; } //url du fichier php
	public static function get METHOD():String { return "POST"; } //méthode de transmission des variables
 
	private var _listeners:Array; //ce seront les écouteurs
	private var _data:LoadVars; //le loadvars contenant les données à envoyer
 
	public function DataMail(Void) //constructeur
 
	public function addListener(l:DataMailListener):Void //ajouter un écouteur
	public function removeListener(l:DataMailListener):Void //supprimer un écouteur
	public function addData(name:String, value:Object):Void //ajouter une donnée à envoyer
	public function release(Void):Void //supprimer toutes les données à envoyer
	public function send(Void):Void //envoyer les données
 
	private function dispatchEvent(name:String, params:Array):Void //envoyer un évènement
}

et les classes qui voudront écouter les évènements de cette classe, devront implémenter l'interface suivante :

interface DataMailListener
{
	public function onDataSended(Void):Void; //appelé lorsque les données sont envoyées
	public function onDataReceived(code:Number):Void; //appelé losrque les données sont recues
}

Si vous voulez approfondir vos connaissances sur la programmation évènementielle, rendez-vous sur les Pattern d'évènements par délégation ici et ici. On reviendra sur la classe DataMail plus bas.

La vue

Passons à présent à la vue et à son contrôleur puisque c'est là que se passent les choses intéressantes ! Tout d'abord, la vue doit toujours être un écouteur du modèle. Elle doit donc, dans notre cas, implémenter l'interface DataMailListener afin de pouvoir recevoir les évènements générés par le modèle. Par contre elle ne possède pas de référence à son modèle, mais uniquement à son contrôleur. Pas clair ? Voyez plutot :

dynamic class Mailer extends MovieClip implements DataMailListener
{
	private var _controller:MailerController; //le controleur de cette vue
 
	public function Mailer(Void) //constructeur
	public function getModel(Void):DataMail //retourne le modèle depuis le contrôleur
	public function setModel(model:DataMail):Void //attribue le modèle au contrôleur et s'ajoute comme écouteur
 
        //fonctions de gestion des évènements
        public function onDataSended(Void):Void
	public function onDataReceived(code:Number):Void
 
        //fonctions de gestions de la vue
        public function setEnabled(value:Boolean):Void
	public function release(Void):Void
	public function setStatus(status_mailer:String):Void
}

Notez aussi que le contrôleur n'est pas rendu publique mais ce serait tout à fait faisable si d'autres classes doivent intéragir avec cette vue. Héritant ici de MovieClip, il n'y a pas de possibilité de passer des arguments au constructeur, nous sommes donc contraints de passer par la méthode setModel / getModel. Voyons en détail leur implémentation :

/**
* Récupère le modèle de cette vue
*
* @return	Le modèle
*/
public function getModel(Void):DataMail
{
	return _controller.getModel();
}
 
/**
* Attribue un nouveau modèle à cette vue
*
* @param	model	Le nouveau modèle
*/
public function setModel(model:DataMail):Void
{
	//on récupère l'ancien écouteur et on s'enlève
	getModel().removeListener(this);
 
	//on s'ajoute en tant qu'écouteur
	model.addListener(this);
 
	//et on le passe au controller
	_controller.setModel(model);
}

Pas grand-chose à dire sur la méthode getModel : elle va chercher le modèle dans son contrôleur. La méthode setModel est plus intéressante. Lorsque l'on attribue un nouveau modèle à cette vue, il ne faut pas oublier qu'elle ne doit plus recevoir les évènements de l'ancien modèle ! Il faut donc l'enlever et la mettre en tant qu'écouteur du nouveau modèle reçu en paramètre.

Le contrôleur

Voyons maintenant la structure du contrôleur :

class MailerController
{
	private var _view:Mailer; //la vue
	private var _model:DataMail; //le modèle
 
	public function MailerController(view:Mailer, model:DataMail) //constructeur
	public function getModel(Void):DataMail //récupérer le modèle
	public function setModel(model:DataMail):Void //attribuer un modèle
	public function send(Void):Void //envoi des données
 
	private function isMailValid(mail:String):Boolean //traitement des email
}

Ici la méthode send(Void):Void est intéressante. Elle est appelée par le bouton 'send' qui se trouve dans la classe Mailer (qui est dynamic !). Cette méthode s'occupe du traitement des données insérées par l'utilisateur dans le Mailer. Voici son implémentation :

/**
* Vérification et envoi des données
*/
public function send(Void):Void
{ 
	//on récupère les valeurs des champs de texte
        //qui se trouvent dans le Mailer
	var s:String = _view.sender.text;
	var r:String = _view.receiver.text;
	var t:String = _view.subject.text;
	var m:String = _view.message.text;
 
	//vérification
	if (!isMailValid(s))
	{
                //affichage d'un message d'erreur
		_view.setStatus("email expéditeur invalide !");
		return;
	}
 
	if (!isMailValid(r))
	{
		_view.setStatus("email destinataire invalide !");
		return;
	}
 
	if (t.length == 0)
	{
		_view.setStatus("entrez un sujet !");
		return;
	}
 
	if (m.length == 0)
	{
		_view.setStatus("entrez un message !");
		return;
	}
 
	//si on arrive ici , tout est ok --> on ajoute les données
	_model.addData("sender", s);
	_model.addData("receiver", r);
	_model.addData("subject", t);
	_model.addData("message", m);
 
	//et on envoie le tout
	_model.send();
}

Gestion des évènements

On en arrive finalement à la gestion d'évènement de la classe DataMail ! Il y a 2 évènements envoyés à 2 moments différents (logique !). Le premier, onDataSended se fait dès l'appel à la méthode send. Cela signifie que que les données sont envoyées à php. La 2ème, onDataReceived, est appelée lorsque le fichier php a répondu et que les données sont revenues dans flash. Voici l'implémentation de la méthode send :

/**
* Cette fonction envoie les données
*/
public function send(Void):Void
{
	//lors du chargement
	var me:DataMail = this;
	_data.onLoad = function(ok:Boolean):Void
	{
		var code:Number = 2;
 
		if (ok)
		{
			//on récupère le code et on le caste
			code = Number(this.code);
		}
 
		//on libère le tout
		me.release();
 
		//dispatche l'évènement
		me.dispatchEvent("onDataReceived", [code]);
	}
 
	//dispatche l'évènement
	dispatchEvent("onDataSended", null);
 
	//envoi des données
	_data.sendAndLoad(DataMail.URL_FILE, _data, DataMail.METHOD);
}
 
/**
* Dispatche un évènement
*
* @param	name	Le nom de la méthode d'évènement
* @param	params	Les paramètres à passer à la méthode
*/
private function dispatchEvent(name:String, params:Array):Void
{
	var len = _listeners.length;
 
	for (var i:Number=0 ; i<len ; i++)
	{
		_listeners[i][name].apply(_listeners[i], params);
	}
}

Comme vous pouvez le voir, la classe DataMail ne s'occupe pas du tout du traitement des données ! Cela a pour avantage qu'elle peut être rétutilisable par n'importe quelle classe avec n'importe quelles données. De plus, le modèle est indépendant des vues, ce qui signifie que n'importe quel type de classe (même non visuelle) peut écouter ce modèle. En approfondissant le sujet, vous pouvez lier plusieurs MVC entre eux et avec encore d'autres Pattern.

Utilisation

Placer le clip containerMailer sur votre scène, donnez-lui un nom d'occurence (mainMailer dans mon exemple) et ensuite pour l'utiliser, il vous suffit d'instancier un modèle et de le lui indiquer via setModel !

//création d'un objet modèle
var dm:DataMail = new DataMail();
 
//on le met en tant que modèle
this.mainMailer.setModel(dm);

Sources

Ceci dit, vous trouverez ici les fichiers joints, commentés comme il se doit !