Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

Découverte du framework Robotlegs

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Par frangois (frangois), le 31 octobre 2010

Ce tutoriel présente les grandes lignes de la micro-architecture Robotlegs, un projet open-source de framework architectural de plus en plus répandu.

Les fichiers du tutoriel : tuto-robotlegs.zip

Prérequis:

• Connaître la programmation orientée objet.

• Usage obligatoire de FlashDevelop ou Flash Builder

Retour sur les fondamentaux de l’architecture logicielle orienté objet.

Les applications Flash ou Flex sont constituées d’objets, qui communiquent entre eux par des événements. Le paradigme orienté-objet existant depuis les temps immémoriaux du langage Smalltalk, certaines structures de classes récurrentes répondant à des problèmes précis ont émergées au fil des années. Ces structures récurrentes ont été listées dans l’ouvrage “Design Patterns” du GoF, en voici quelques-uns biens connus :

Factory : encapsule la création d’un objet complexe de classe A dans une autre classe B.

Adapter : encapsule un objet A dotée d’une interface inadaptée au projet dans une classe B.

Command : encapsule une méthode dans une classe indépendante A, dont le rôle est d’agir sur des classes B,C,D.

Les design-patterns ne sont pas un code-source unique et précis. Ils sont indépendants du langage, un design-pattern est une pratique. Il y a autant d’implémentation que de langages de programmation, et plusieurs implémentations possibles au sein d’un même langage.

On trouve des design-patterns simples, répondant à une tâche précise (Factory, Adapter; Command, …) et des design-patterns composés, répondant à des problèmes plus vastes et plus complexes en réunissant plusieurs patterns. Par exemple, pour structurer une application qui affiche des données via une IHM permettant de filtrer/modifier les données, on utilise le plus souvent le pattern Model-View-Controller (dit “MVC”).

Dans le cas précis du développement Flash, ce dernier type d’applications est évidemment le plus courant, la nature même de Flash étant le développement de RIA. Pour structurer ce type d’applications, plusieurs angles d’attaques existent :

ne pas utiliser de MVC : pour une petite application, on code tout rapidement dans une classe.

le framework “maison” : on produit une implémentation personnelle de MVC, qu’on réutilise ensuite.

le framework Cairngorm : développé par Adobe, c’est l’implémentation de référence censée être utilisée et connue, en particulier par tous les développeurs Flex.

les frameworks MVC alternatifs : issus de la communauté, les plus courants sont Robotlegs, PureMVC, Parsley.

les frameworks non-MVC alternatifs : basés sur d’autres patterns, comme Swiz et Mate.

Ce tutoriel aborde Robotlegs. Ses points forts par rapport aux autres frameworks sont son utilisation astucieuse de l’injection de dépendances, sa simplicité d’utilisation et sa légèreté (c’est une micro-architecture plus qu’un framework).

Le MVC et son implémentation par Robotlegs

Vue générale

Attardons-nous maintenant sur le pattern MVC, et son implémentation particulière au sein de Robotlegs. MVC scinde l’application en 3 couches :

la couche Model : regroupe l’ensemble des classes chargeant et stockant les données. Une classe du Model émet des événements en direction des autres couches, et est pilotée par des méthodes publiques.

la couche Controller : écoute la couche Model et la couche View, et agit sur l’un ou l’autre en appelant des méthodes publiques, en fonction des actions de l’utilisateur, ou de l’évolution des données.

la couche View : affiche les données du Model, et émet des événements lors des actions utilisateurs.

Ce schéma donne une vue globale de l’implémentation du MVC par Robotlegs.
On remarque deux types de flèches bien distincts :

• les flèches pointillées : ce sont des événements.
• les flèches pleines : ce sont des appels de méthodes publiques - par exemple : applicationModel.load(‘content.xml’); ou applicationView.refresh();

Si le Controller est capable de passer ce type d’appel, on en déduit que le Controller possède une référence vers les classes de la View, et les classes du Model. Or, c’est assez négatif, puisque cela sous-entend d’avoir “en dur” dans le Controller des variables pointant vers toutes les instances existantes des classes du Model et de la View. Ce qui constituerait une “dépendance” du Controller envers la View et le Model. Comment supprimer cette dépendance ?

L’injection de dépendance

Robotlegs, et c’est là son principal point positif, utilise “l’injection de dépendance”. Lorsqu’un appel d’une méthode publique de la View et du Model est nécessaire, la dépendance est injectée dans la partie du Controller qui en a besoin. Elle n’existe que le temps nécessaire, puis disparaît.

Au niveau code, cela se présente ainsi, au sein du Controler ici :

public class LoadConfigurationCommand extends Command
{
	// l'injection de dépendances a lieu ici
        [Inject]
	public var applicationData : ApplicationData;
 
	override public function execute() : void
	{
	      // applicationData est peuplée! pourtant elle est simplement déclarée plus haut
              // c'est l'injecteur qui a injecté cette classe !
              applicationData.load('swf/slideshow-conf.xml');
	}
 
}

On note l’usage très astucieux du meta-tag [Inject]. En effet, il est tout à fait possible pour un développeur de créer de nouveaux meta-tags. Et ce meta-tag particulier à Robotlegs va en fait déclencher en arrière-plan l’injection de la dépendance. La variable publique va automatiquement être peuplée par Robotlegs.

Derrière ce tag custom se cache en réalité toute une couche d’injection dénommée SwiftSuspenders, qui est indépendante de Robotlegs et peut parfaitement être utilisée sur des projets non-Robotlegs, il suffit d’en récupérer le .swc sur le site du projet SwiftSuspenders.

L’implémentation Robotlegs du MVC

On l’a vu plus haut, le MVC est un pattern composé. On connaît maintenant la structure générale de Robotlegs et sa manière particulière de conserver un couplage lâche via l’injection de dépendances. Examinons maintenant les différents patterns constituants son implémentation de MVC :

la couche Model : elle est implémentée via un pattern Actor. Pattern qui n’en est pas vraiment un, en réalité, c’est simplement une classe capable d’émettre un événement.

la couche Controller : elle est implémentée via un pattern Command. Chaque événement de l’application est mappé sur une Command. La Command est déclenchée quand l’événement survient. Au sein de la Command sont injectés les classes Model et View à modifier.

la couche View : elle est implémentée via un pattern Mediator. Pour chaque composant visuel important, une classe Mediator est créée. Cette classe écoute le framework et agit sur la View, et écoute la View et émet vers le framework.

Actor et Mediator émettent des événements, la couche Controller en écoute. Par souci de clarté, il est important voire capital de bien assimiler que les événements du framework ne doivent pas cohabiter avec les événements issus de la View : le moindre MouseEvent.ROLL_OVER n’a pas forcément vocation à remonter jusqu’au coeur du framework!

Robotlegs utilise pour ça une 2ème instance de l’EventDispatcher, pour bien séparer événements du framework et les événements Flash de basse importance.

Tout ça est très compliqué. Heureusement voilà le gros exemple détaillé, pas de panique.

Diaporama sous Robotlegs : la structure générale.

Robotlegs est disponible sous forme d’un .zip contenant sources, .swc et documentation, à cette adresse : http://www.robotlegs.org/

Commençons par créer un projet AS3 sous FlashDevelop, puis créer la structure de packages que nous allons remplir :

On voit bien les 3 couches à développer :

controller : il contient les différentes Command qui implémentent cette couche. Un package séparé pour les events qui sont parfois nombreux.

model : contient les différents Actors, un package est prévu pour les V.O. (“Value Objects”) qui sont des objets de données simples.

view : un sous-package pour les Mediators, un autre pour les classes graphiques - on a vu plus haut qu’une classe graphique communique toujours par un Mediator.

le Context, coeur de l'application

La structure globale d’une application Robotlegs est définié dans une classe dite “contexte”, qui étend org.robotlegs.mvcs.Context. Cette classe est la façade de l’application, c’est là que sont définis les différents paramètres de l’application.

Au runtime, une application Robotlegs démarre de la manière suivante :

• point d’entrée

• instantiation du Context

• appel de la méthode startup() du Context.

La première classe de notre application sera son point d’entrée de compilation, nommé Diaporama :

package 
{
	import flash.display.Sprite;
 
	/**
	 * point d'entrée de l'application
	 * 
	 */
	public class Diaporama extends Sprite 
	{
 
		public function Diaporama() : void 
		{
			new DiaporamaContext(this);
		}
 
	}
 
}

Jusque-là c’est plutôt simple, on instantie DiaporamaContext en lui passant l’objet graphique racine de l’application. Voyons maintenant DiaporamaContext :

package  
{
	import org.robotlegs.mvcs.Context;
 
	import flash.display.DisplayObjectContainer;
 
	/**
	 * Contexte de l'application Diaporama
	 * 
	 */
	public class DiaporamaContext extends Context 
	{
 
		public function DiaporamaContext( 	contextView : DisplayObjectContainer,
							autoStartup : Boolean = true ) 
		{
			// passage explicite des arguments à la classe mère
                        super(contextView, autoStartup);
		}
 
		override public function startup() : void
		{
 
		}
 
	}
 
}

DiaporamaContext étend Context. il reçoit bien en argument l’objet graphique racine. Il passe cette objet à sa classe mère, puis la méthode startup() est appelée automatiquement puisque nous passons autoStartup = true à la classe mère.

La méthode startup est vide pour l’instant. C’est là que toute la configuration de l’application va se faire. Commençons par définir notre Controller.

définition du Controller

C’est un diaporama classique, avec 2 boutons “précédent” et “suivant”, une barre de progression du chargement par image, et un XML de contenu.

Commençons par lister l’intégralité des événements générant un changement d’état lors du cycle de vie de l’application :

DiaporamaEvent.CONFIG_COMPLETE : on charge un XML, on aura donc un événement de fin de chargement.

DiaporamaEvent.NEXT : on a la possibilité d’un clic sur ce bouton.

DiaporamaEvent.PREVIOUS : on a la possibilité d’un clic sur ce bouton.

Comme indiqué plus haut, les Events d’implémentation Flash classiques n’ont pas leur place dans le contrôleur s’ils ne concernent pas toute l’application. On notera l’absence des événements du type LOAD_START, LOAD_PROGRESS, LOAD_COMPLETE. Ceux-là ne concernant que le composant d’affichage, ils y restent.

Nous allons mapper comme il se doit chacun de ces événements vers une Command :

override public function startup() : void
{
	// définition du Controller
	commandMap.mapEvent( DiaporamaEvent.STARTUP, StartupCommand );
        commandMap.mapEvent( DiaporamaEvent.CONFIG_COMPLETE, ConfigCompleteCommand );
	commandMap.mapEvent( DiaporamaEvent.NEXT, NextCommand );
	commandMap.mapEvent( DiaporamaEvent.PREVIOUS, PreviousCommand );
}

C’est un peu l’équivalent d’un addEventListener, sauf qu’au lieu d’appeler une fonction, on appelle la classe Command adaptée, plus particulièrement sa méthode execute().

définition du Model

Pour cette application nous n’avons qu’un seul type de données à charger, en une seule fois au début du cycle de vie, c’est le XML de configuration.

<?xml version="1.0" encoding="utf-8" ?>
<data>
	<entry file="img/placeholder-00.png">
		<title><![CDATA[lorem ipsum]]></title>
	</entry>
	<entry file="img/placeholder-01.png">
		<title><![CDATA[sit dolor est]]></title>
	</entry>
	<entry file="img/placeholder-02.png">
		<title><![CDATA[ consectetur adipiscing elit]]></title>
	</entry>	
</data>

Nous aurons donc une seule classe dans notre Model, appelons-là par exemple DiaporamaData. Modifions DiaporamaContext pour en informer le framework :

override public function startup() : void
{
	// définition du Controller
	commandMap.mapEvent( DiaporamaEvent.STARTUP, StartupCommand );
        commandMap.mapEvent( DiaporamaEvent.CONFIG_COMPLETE, ConfigCompleteCommand );
	commandMap.mapEvent( DiaporamaEvent.NEXT, NextCommand );
	commandMap.mapEvent( DiaporamaEvent.PREVIOUS, PreviousCommand );
 
	// définition du Model
        injector.mapSingleton( DiaporamaData );
}

Nous indiquons simplement à l’injecteur que lorsque nous demanderons l’accès à un objet DiaporamaData, notez qu'un Model est fatalement un Singleton.

Définition de la View

Pour cette application nous n’avons que 2 composants visuels à l’écran :

DiaporamaDisplay : C’est simplement le container destiné à afficher l’image chargée. Concrétement, c’est un MovieClip.

DiaporamaControl : C’est simplement le container des 2 boutons “précédent” et “suivant”.

Modifions DiaporamaContext pour en informer le framework :

override public function startup() : void
{
	// définition du Controller
	commandMap.mapEvent( DiaporamaEvent.STARTUP, StartupCommand );
        commandMap.mapEvent( DiaporamaEvent.CONFIG_COMPLETE, ConfigCompleteCommand );
	commandMap.mapEvent( DiaporamaEvent.NEXT, NextCommand );
	commandMap.mapEvent( DiaporamaEvent.PREVIOUS, PreviousCommand );
 
	// définition du Model
        injector.mapSingleton( DiaporamaData );
 
	// définition de la View
	mediatorMap.mapView( DiaporamaDisplay, DiaporamaDisplayMediator );
	mediatorMap.mapView( DiaporamaControls, DiaporamaControlsMediator );
}

On a vu plus haut q’un composant = un Mediator. Ici nous mappons simplement un composant vers son Mediator. Robotlegs écoute l’événement ADDED_TO_STAGE en permanence, quand il détectera l’ajout d’un composant DiaporamaDisplay, il créera automatiquement son pendant DiaporamaDisplayMediator.

Récapitulatif

package  
{
	import controller.events.*;
	import controller.commands.*;
	import view.mediators.*;
	import view.components.*;
	import model.*;
 
	import org.robotlegs.mvcs.Context;
 
	import flash.display.DisplayObjectContainer;
 
	/**
	 * Contexte de l'application Diaporama
	 * 
	 */
	public class DiaporamaContext extends Context 
	{
 
		public function DiaporamaContext( contextView : DisplayObjectContainer,
						  autoStartup : Boolean = true ) 
		{
			super(contextView, autoStartup);
		}
 
		override public function startup() : void
		{
			// définition du Controller
			commandMap.mapEvent( DiaporamaEvent.STARTUP, StartupCommand );
			commandMap.mapEvent( DiaporamaEvent.CONFIG_COMPLETE, ConfigCompleteCommand );
			commandMap.mapEvent( DiaporamaEvent.NEXT, NextCommand );
			commandMap.mapEvent( DiaporamaEvent.PREVIOUS, PreviousCommand );
 
			// définition du Model
			injector.mapSingleton( DiaporamaData );
 
			// définition de la View
			mediatorMap.mapView( DiaporamaDisplay, DiaporamaDisplayMediator );
			mediatorMap.mapView( DiaporamaControls, DiaporamaControlsMediator );
 
			// démarrage de l'application
			dispatchEvent( new DiaporamaEvent( DiaporamaEvent.STARTUP ) );
		}
 
	}
 
}

Voilà la classe DiaporamaContext qui définit l’intégralité des acteurs en jeu dans notre application, ainsi que leurs relations. On constate que c’est très simple d’ajouter des fonctionnalités et des états complémentaires à cette application, voire un chargement d’autres données, une nouvelle vue, etc.

On y a ajouté une dernière ligne qui dispatche l’événement de départ STARTUP.

La couche Model

Commençons le développement par la couche la plus basse et la plus basse, la couche de Model, qui charge les données et les stocke pour leur usage au sein de l’application. Voici le XML de contenu :

<?xml version="1.0" encoding="utf-8" ?>
<data>
	<entry file="img/placeholder-00.png">
		<title><![CDATA[lorem ipsum]]></title>
	</entry>
	<entry file="img/placeholder-01.png">
		<title><![CDATA[sit dolor est]]></title>
	</entry>
	<entry file="img/placeholder-02.png">
		<title><![CDATA[ consectetur adipiscing elit]]></title>
	</entry>	
</data>

On a un seul type de données à stocker dans une structure séquentielle de type liste. Nous allons stocker chaque entrée dans un type personnalisé correspondant à cette structure. C’est un pattern de Value Object, nommons ce type le type Entry.

package model.vo 
{
	public class Entry 
	{
 
		public var file : String;
		public var title : String;
 
	}
 
}

Construisons maintenant la classe DiaporamaData, dont le rôle sera :

charger un fichier XML,

stocker les données : ici dans un Vector.<Entry>

passer à l’image suivante : dispatcher le nom du fichier de l’image suivante à charger, et cycler à l’infini

passer à l’image précédente : dispatcher le nom du fichier de l’image précédente à charger, et cycler à l’infini

package model 
{
	import controller.events.DiaporamaEvent;
	import model.vo.Entry;	
 
	import org.robotlegs.mvcs.Actor;
 
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.events.Event;
	import flash.events.IOErrorEvent;	
 
	public class DiaporamaData extends Actor 
	{
 
		private var _entries : Vector.<Entry>;
		private var _current : uint = 0;
 
		public function DiaporamaData()
		{
			super();
		}
 
		public function load( url : String = 'diaporama-content.xml' ) : void
		{
			var content : URLLoader = new URLLoader();
			content.addEventListener( Event.COMPLETE, onComplete );
			content.addEventListener( IOErrorEvent.IO_ERROR, onIOError );
			content.load( new URLRequest(url) );
		}
 
		private function onComplete( e : Event ) : void 
		{
			_entries = new Vector.<Entry>();
 
			var content : XML = new XML((e.target as URLLoader).data);
			for each( var entryData : XML in content.entry ) {
				var entry : Entry = new Entry();
				entry.file = entryData.@file;
				entry.title = entryData.title;
				_entries.push(entry);
			}
 
			eventDispatcher.dispatchEvent( new DiaporamaEvent( DiaporamaEvent.CONFIG_COMPLETE, false, false, _entries[_current] ) );
		}
 
		public function next() : void
		{
			if ( _current + 1 < _entries.length ) {
				_current++;
			} else {
				_current = 0;
			}
 
			eventDispatcher.dispatchEvent( new DiaporamaEvent( DiaporamaEvent.REFRESH, false, false, _entries[_current] ) );
		}
 
		public function previous() : void
		{
			if ( _current - 1 >= 0 ) {
				_current--;
			} else {
				_current = _entries.length - 1;
			}
 
			eventDispatcher.dispatchEvent( new DiaporamaEvent( DiaporamaEvent.REFRESH, false, false, _entries[_current] ) );
		}
 
		private function onIOError( e : IOErrorEvent ) : void 
		{
			throw new Error( 'content could not be loaded' );
		}
 
		public function get entries() : Vector.<Entry>
		{
			return _entries;
		}
 
	}
 
}

C’est là tout ce dont nous avons besoin pour cette application. Le Model est généralement ce qu’il y a de plus facile : on expose quelques méthodes publiques, on émet simplement un/des événements selon l’état du Model.

La couche View

Les composants visuels

Les composants visuels doivent être réutilisables de projet à projet. Ils sont donc agnostiques au projet, et ne doivent contenir aucune référence au framework. On doit littéralement pouvoir les importer dans un projet vide, et les lancer seuls - ils sont autonomes.

Pour ce projet nous aurons deux composants visuels :

DiaporamaDisplay : c’est un UILoader en plus léger et plus classe. On lui passe une URL et il charge cette image puis l’affiche. Et c’est tout.

package view.components 
{
	import model.vo.Entry;
	import controller.events.DiaporamaEvent;
 
	import com.gskinner.motion.GTween;	
	import com.gskinner.motion.easing.Quadratic;	
 
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.display.Loader;
	import flash.display.Bitmap;
	import flash.net.URLRequest;
	import flash.display.LoaderInfo;
 
	/**
	 * composant visuel basique, 
	 * un afficheur d'image simple avec une transition animée
	 */	
	public class DiaporamaDisplay extends Sprite 
	{
		private var _front : Sprite;
		private var _back : Sprite;
 
		private var _backContent : Bitmap;
		private var _frontContent : Bitmap;
 
		private var _current : Sprite;
 
		public function DiaporamaDisplay()
		{
			_back = new Sprite();
			_front = new Sprite();
 
			addChild(_back);
			addChild(_front);
		}
 
		public function refresh( entry : Entry ) : void
		{
			var l : Loader = new Loader();
			l.contentLoaderInfo.addEventListener( Event.COMPLETE, onLoadComplete );
			l.load( new URLRequest(entry.file) );
		}
 
		private function onLoadComplete( e : Event ) : void
		{
			_backContent = (e.target as LoaderInfo).loader.content as Bitmap;
			_back.alpha = 0;
			_backContent.addEventListener( Event.ADDED_TO_STAGE, onAdded );
			_back.addChild(_backContent);
		}
 
		private function onAdded(e:Event):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, onAdded);
			new GTween( _back, .5, { alpha: 1 }, { onComplete: onFadeComplete, ease : Quadratic.easeOut } );
			new GTween( _front, .5, { alpha: 0 }, { ease : Quadratic.easeIn } );			
		}
 
		private function onFadeComplete( g : GTween ) : void
		{
			var frontBuffer : Sprite = _front;
			_front = _back;
			_back = frontBuffer;
 
			dispatchEvent( new DiaporamaEvent( DiaporamaEvent.LOAD_COMPLETE ) );
		}
 
	}
 
}

DiaporamaControls : constitué d’une flêche vers la droite (btNext) et d’une flèche vers la gauche (btPrevious). il n’a que 2 méthodes, enable() et disable(), pour freezer les boutons, par exemple le temps d’un chargement.

package view.components 
{
	import controller.events.DiaporamaEvent;
 
	import com.gskinner.motion.GTween;
 
	import flash.display.MovieClip;
	import flash.events.MouseEvent;
 
	/**
	 * composant visuel basique, 2 flêches que l'on peut "geler"
	 * par exemple le temps d'un chargement
	 */
	public class DiaporamaControls extends DiaporamaControls_skin
	{
 
		public function DiaporamaControls() 
		{
			disable();
		}
 
		/**
		 * rend actif les flêches
		 */
		public function enable() : void
		{
			for each( var button : MovieClip in [ btPrevious, btNext ] ) {
				if ( button.alpha != 1 )
					new GTween( button, .5, { alpha : 1 } );
				button.addEventListener( MouseEvent.CLICK, onClick );
				button.buttonMode = true;
			}
		}
 
		/**
		 * rend inactif les flêches
		 */
		public function disable() : void
		{
			for each( var button : MovieClip in [ btPrevious, btNext ] ) {
				if ( button.alpha != .5 )
					new GTween( button, .5, { alpha : 0 } );
				button.removeEventListener( MouseEvent.CLICK, onClick );
				button.buttonMode = false;
			}
		}
 
		/**
		 * émission d'un Event simple au click
		 * 
		 * @param	e
		 */
		private function onClick( e : MouseEvent ) : void
		{
			dispatchEvent( new DiaporamaEvent(
				{
					btNext : DiaporamaEvent.NEXT,
					btPrevious : DiaporamaEvent.PREVIOUS
				}[e.currentTarget.name]
			) );
		}
 
	}
 
}

Nous avons bien 2 composants agnostiques et autonomes, sans aucune référence à un quelconque objet issu de l’application. Pour être parfait, nous aurions dû utiliser un Event séparé par composant, et faire un .swc par composant + événement attitré.

Les Mediators

Le rôle des Mediators est :

• d’écouter l’application et agir sur leur composant,

• d’écouter leur composant et d’en informer l’application.

Commençons par DisplayControlsMediator, qui va s’occuper de DisplayControls

package view.mediators 
{
	import controller.events.DiaporamaEvent;
	import view.components.DiaporamaControls;
 
	import org.robotlegs.mvcs.Mediator;
 
	/**
	 * Agit sur diaporamaDisplay en fonction des événements reçus
	 * du composant ou de l'application
	 * 
	 */
	public class DiaporamaControlsMediator extends Mediator 
	{
		[Inject]
		public var controls : DiaporamaControls;
 
		override public function onRegister() : void
		{
			// événements issus de l'application
			eventMap.mapListener( eventDispatcher, DiaporamaEvent.LOAD_COMPLETE, onLoadComplete );
 
			// événements issus du composant
			controls.addEventListener( DiaporamaEvent.NEXT, onClick );
			controls.addEventListener( DiaporamaEvent.PREVIOUS, onClick );
		}
 
		private function onClick( e : DiaporamaEvent ) : void 
		{
			controls.disable();
			dispatch(e);
		}
 
		private function onLoadComplete( e : DiaporamaEvent ) : void
		{
			controls.enable();
		}
 
	}
 
}

On constate que son rôle est d’appeler les méthodes enable() et disable() au bon moment, et forwarder son événement de clic, tout simplement.

Occupons-nous de DiaporamaDisplayMediator, qui s’occupe de DiaporamaDisplay:

package view.mediators 
{
	import controller.events.DiaporamaEvent;
	import view.components.DiaporamaDisplay;
 
	import org.robotlegs.mvcs.Mediator;
 
	/**
	 * Agit sur diaporamaDisplay en fonction des événements reçus
	 * du composant ou de l'application
	 * 
	 */
 
	public class DiaporamaDisplayMediator extends Mediator 
	{
		[Inject]
		public var display : DiaporamaDisplay;
 
		override public function onRegister() : void
		{
			// événements issus de l'application
			eventMap.mapListener( eventDispatcher, DiaporamaEvent.REFRESH, onRefresh );
 
			// événements issus du composant
			display.addEventListener( DiaporamaEvent.LOAD_COMPLETE, onLoadComplete );
		}
 
		private function onRefresh( e : DiaporamaEvent ) : void 
		{
			display.refresh(e.entry);
		}
 
		private function onLoadComplete( e : DiaporamaEvent ) : void 
		{
			dispatch(e);
		}
 
	}
 
}

On constate que son rôle est d’appeler la méthode refresh() au bon moment, et forwarder son événement de fin de chargement.

On remarque un DiaporamaEvent.REFRESH, qui n’est pas mappé sur une Command dans DiaporamaContext. En effet, seul ce Mediator est concerné par cet événement, il n’est pas obligatoire d’utiliser une Command.

L’utilité et le rôle d’un Mediator est souvent difficile à cerner pour un débutant. A la relecture de l’integralité du code que nous avons tapé pour l’instant, nous constatons que “c’est là que se font les addEventListener sur la Vue, et les appels de méthodes”. C’est aussi simple que ça.

Note : on voit que l’injecteur est utilisé pour injecter le composant. C’est une très mauvaise idée d’utiliser cet injecteur pour injecter directement dans le Mediator un Model. Cela nuit au découplage en couches. Si un Mediator a besoin d’une donnée, cette donnée doit lui parvenir via un Event reçu, et c’est tout.

La couche Controller

Les Events

Ici nous avons seulement un unique Event pour ce projet. C’est assez rare, le projet est vraiment petit.

package controller.events 
{
	import model.vo.Entry;
 
	import flash.events.Event;
 
	public class DiaporamaEvent extends Event 
	{
		// événements concernant l'application
		public static const STARTUP : String = 'startup';
		public static const CONFIG_COMPLETE : String = 'configComplete';
		public static const NEXT : String = 'next';
		public static const PREVIOUS : String = 'previous';
 
		// événements concernant la View
		public static const REFRESH : String = 'refresh';
		public static const LOAD_COMPLETE : String = 'loadComplete';		
 
		public var entry : Entry;
 
		public function DiaporamaEvent( type : String,
					    bubbles : Boolean = false,
					    cancelable : Boolean = false,
					    entry : Entry = null ) 
		{ 
			super(type, bubbles, cancelable);
 
			this.entry = entry;
		} 
 
		public override function clone():Event 
		{ 
			return new DiaporamaEvent( type, bubbles, cancelable, entry );
		} 
 
		public override function toString():String 
		{ 
			return formatToString("DiaporamaEvent", "type", "bubbles", "cancelable", "eventPhase"); 
		}
 
	}
 
}

On distingue 2 catégories d’Event.TYPE dans cette classe. On a vu plus haut que REFRESH ne concerne qu’un Mediator, c’est la même chose pour LOAD_COMPLETE. J’ai donc bien distingué ces deux catégories dans mes commentaires. En effet, si tous les Event donnant lieu à une Command sont listés dans mon Context, les handlers présents dans les Mediators sont moins facilement repérables. Cela m’aidera à me relire que de le préciser dans ma classe DiaporamaEvent.

Les Commands

Les Commands contiennent la logique de l’application, ici nos seules fonctionnalités sont les clics sur suivant et précédent. On retrouve donc ces 2 Commands, prenons-en une sur les 2, elles sont très semblables :

package controller.commands 
{
	import model.DiaporamaData;
 
	import org.robotlegs.mvcs.Command;
 
	public class NextCommand extends Command
	{
		[Inject]
		public var diaporamaData : DiaporamaData;
 
		override public function execute() : void
		{
			diaporamaData.next();
		}
 
	}
 
}

Lorsque l’événement DiaporamaEvent.NEXT survient, la Command est éxecutée, le Model est injecté, et on agit sur le Model par un appel de méthode publique. Voilà qui colle plutôt bien à la définition du Controller qu’on a donnée plus tôt, et à notre petit schéma.

Dans une Command, on agit sur le Model.

Si on relis DiaporamaContext, on a une StartupCommand, dispatchée au lancement de l’application :

package controller.commands 
{
	import model.DiaporamaData;
 
	import org.robotlegs.mvcs.Command;
 
	public class StartupCommand extends Command
	{
		[Inject]
		public var diaporamaData : DiaporamaData;
 
		override public function execute() : void
		{
			diaporamaData.load();
		}
 
	}
 
}

Enfin on a une ConfigCompleteCommand, qui est appelée à la fin du chargement du XML. Pour l’instant, nous n’avons rien de visible sur la scène, notre appli est vide!

package controller.commands 
{
	import controller.events.DiaporamaEvent;
	import view.components.DiaporamaDisplay;
	import view.components.DiaporamaControls;
 
	import org.robotlegs.mvcs.Command;
 
	/**
	 * appelée à la fin du chargement du .XML
	 */
	public class ConfigCompleteCommand extends Command
	{
		// on récupére l'Event déclencheur
		[Inject]
		public var event : DiaporamaEvent;
 
		override public function execute() : void
		{
			// on crée la vue
			contextView.addChild( new DiaporamaDisplay() );
			contextView.addChild( new DiaporamaControls() );
 
			// on force le refresh de la vue, pour afficher la première image
			dispatch( new DiaporamaEvent( DiaporamaEvent.REFRESH, false, false, event.entry ) );
		}
 
	}
 
}

C’est donc là qu’est créé la couche visible de notre application! On notera 2 choses très importantes sur cette Command :

l’injection d’un event : en effet, une Command on l’a compris est une sorte de gros handler d’événement. Pour récupérer les paramètres de l’Event déclencheur (ici entry nous intéresse), on injecte l’event.

l’appel à contextView : pour récupérer une référence vers la Stage et ajouter des éléments à la DisplayList, RobotLegs fourni une référence nommée contextView vers la variable du même nom déclarée dans DiaporamaContext.

Note : plutôt qu’un appel à contextView, dans certains cas (menu clic-droit custom, resize de stage, curseur custom, multi-fenêtre sous AIR, etc), il est indiqué de créer un StageMediator.

Aller plus loin

Sur ce tutoriel en particulier, les choses suivantes peuvent être effectuées pour s'entraîner :

• séparer correctement DiaporamaEvent en plusieurs événements disctincts.

• ajouter un StageMediator et customiser le menu du clic-droit

• passer l'adresse du XML en FlashVars

• ajouter du son : un indice, un son est une View.

Sur un plan plus général :

• Beaucoup d'exemples ici : http://www.robotlegs.org/examples/

• RobotLegs a le site de support le mieux fichu qui soit : http://knowledge.robotlegs.org/

• Les best-practices sont ici : http://github.com/robotlegs/robotlegs-framework/wiki/Best-Practices

Les fichiers du tutoriel : tuto-robotlegs.zip