Les passions sont un moteur, essences de la vie..

Logo flux syndication RSS

Conception et développement d’un annuaire Flex 4.5 – Architecture MVC Robotlegs + Service Zend AMF [Partie3]

11 juillet 2011
Adobe Flex

Voici enfin le dernier article de cette trilogie, nous allons pouvoir commencer le développement de notre application Flex à l’aide du framework MVC Robotlegs.

Pour rappel, dans le premier article nous avons abordé l’implémentation du modèle MVC par Robotlegs et travaillé sur l’étude de cas de notre annuaire. Dans le second article, nous avons mis en place le service web Zend AMF permettant à l’application Flex de dialoguer avec la base de données MySQL.

Attention : pour simplifier la compréhension, l’ensemble du code source ne sera pas détaillé dans cet article.  Pour plus d’informations, vous pouvez télécharger les sources de l’application.

Nous allons créer un projet d’application web Flex à l’aide de Flash Builder et utiliser la version 4.5 du SDK. Nous ajoutons le binaire de Robotlegs dans le répertoire « libs » de notre projet, rendez-vous sur le site http://www.robotlegs.org/ pour télécharger les sources du framework.

Context

Le contexte est le coeur de l’application, il permet de créer les dépendances entre le contrôleur, le modèle et la vue.  Nous allons créer une classe MainContext qui hérite de org.robotlegs.mvcs.Context. Lors du démarrage d’une application Robotlegs, la classe de contexte doit être la première à être instanciée. Le constructeur de cette classe attend en paramètre « contextView », la vue principale de notre application.

<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/mx"
               creationComplete="creationCompleteHandler(event)">
    <fx:Script>
        <![CDATA[
            import mx.events.FlexEvent;

            private var _context:MainContext;

            protected function creationCompleteHandler(event:FlexEvent):void
            {
                _context = new MainContext(this);
            }

        ]]>
    </fx:Script>

    <fx:Declarations>
        <!-- Placer ici les éléments non visuels (services et objets de valeur, par exemple). -->
    </fx:Declarations>

</s:Application>

Model

Nous allons commencer par développer la couche la plus basse, le modèle. Il permet le stockage des données au sein de l’application, le chargement sera opéré par l’intermédiaire d’un service. Notre application regroupe 3 types de données : les sites, les tags et les catégories. Pour chaque type de donnée, une classe sera créée permettant le stockage des objets dans une structure de type liste. Le pattern Value Object permettra d’encapsuler les données retournées par le service.

Vous vous rappelez surement lors de la création de notre service PHP, l’implémentation de la méthode getASClassName().

namespace jf\services\directory;

class Site
{
    public function getASClassName()
    {
        return 'SiteVO';
    }
}

La balise de métadonnées [RemoteClass] permet de spécifier le nom de classe distante à utiliser, dans notre cas SiteVO.

package jf.directory.models.vo
{
    [Bindable]
    [RemoteClass(alias="SiteVO")]
    public class SiteVO
    {
        public var id:int;
        public var categoryId:int;
        public var name:String;
        public var url:String;
        public var description:String;
        public var createdAt:String;
    }
}

Le rôle de la classe SiteModel est de stocker une collection de value objects de type SiteVO. Le modèle doit signaler son changement d’état à la vue, un événement SiteModelEvent.SITE_COLLECTION permettra d’informer que la collection a été mise à jour. Pour diffuser des événements, la classe SiteModel étend org.robotlegs.mvcs.Actor et utilise la méthode dispatch().

package jf.directory.models
{
    import jf.directory.models.events.SiteModelEvent;
    import jf.directory.models.vo.SiteVO;

    import org.robotlegs.mvcs.Actor;

    public class SiteModel extends Actor
    {
        private var _sites:Vector.<SiteVO> = new Vector.<SiteVO>;

        public function updateCollection(collection:Array):void
        {
            _sites.splice(0, _sites.length);

            for each (var site:Object in collection)
            {
                _sites.push(site as SiteVO);
            } 

            dispatch(new SiteModelEvent(SiteModelEvent.SITE_COLLECTION, collection));
        }
    }
}

Service

Une bonne pratique de développement consiste à concevoir dans des interfaces.

package jf.directory.models.services
{
    public interface IDirectoryService
    {
        function getSites( categoryID:int, tagsID:Array = null ):void
    }
}

Nous allons créer une classe RemoteService pour encapsuler le comportement permettant d’accéder à des classes sur un serveur d’applications distant.

package jf.directory.models.services
{
    import mx.rpc.remoting.mxml.RemoteObject;

    import org.robotlegs.mvcs.Actor;

    public class RemoteService extends Actor
    {
        private var _remoteObject:RemoteObject;

        public function get remoteObject():RemoteObject
        {
            return _remoteObject;
        }

        public function set remoteObject(value:RemoteObject):void
        {
            _remoteObject = value;
        }

        public function RemoteService()
        {
            _remoteObject = new RemoteObject();
            _remoteObject.showBusyCursor = true;
        }
    }
}

L’implémentation de la classe RemoteDirectoryService peut maintenant commencer. Les propriétés destination, source et endpoint du RemoteObject sont configurées au moment de l’initialisation de l’application à l’aide d’un fichier XML.

L’utilisation d’une propriété publique et du meta-tag [Inject] permet à Robotlegs de réaliser une injection de dépendance, notre service dispose maintenant d’une référence à SiteModel. Le contrôleur appellera les méthodes du service. Lorsque les résultats seront retournés, le service mettra à jour le modèle.

package jf.directory.models.services
{
    import jf.directory.models.SiteModel;

    import mx.rpc.events.FaultEvent;
    import mx.rpc.events.ResultEvent;

    public class RemoteDirectoryService extends RemoteService implements IDirectoryService
    {
        [Inject]
        public var siteModel:SiteModel;

        public function RemoteDirectoryService()
        {
            super();

            remoteObject.getSites.addEventListener(ResultEvent.RESULT, getSites_resultHandler); 

            remoteObject.addEventListener(FaultEvent.FAULT, faultHandler);
        }

        public function getSites( categoryId:int, tagsId:Array = null ):void
        {
            remoteObject.getSites( categoryId, tagsId );
        }

        /*
        * Handlers
        */

        protected function getSites_resultHandler(event:ResultEvent):void
        {
            siteModel.updateCollection(event.result as Array);
        }

        protected function faultHandler(event:FaultEvent):void
        {
            trace(event.toString());
        }
    }
}

Controller

La logique de l’application doit être implémentée dans les commandes, classes héritant de org.robotlegs.mvcs.Command. La déclaration d’une commande implique de surcharger la méthode execute(). Les commandes sont appelées lors de la diffusion d’un événement spécifique, toujours à l’aide du modèle événementiel de Flash.

package jf.directory.controllers.events
{
    import flash.events.Event;

    public class LoadSiteEvent extends Event
    {
        public static const LOAD_SITE:String = "loadSite";

        private var _categoryId:int;

        public function get categoryId():int
        {
            return _categoryId;
        }

        private var _tagsId:Array;

        public function get tagsId():Array
        {
            return _tagsId;
        }

        public function LoadSiteEvent(type:String, categoryId:int, tagsId:Array = null, bubbles:Boolean=false, cancelable:Boolean=false)
        {
            _categoryId = categoryId;
            _tagsId = tagsId;

            super(type, bubbles, cancelable);
        }

        public override function clone():Event
        {
            return new LoadSiteEvent( type, categoryId, tagsId);
        }
    }
}

Dans notre cas l’événement LoadSiteEvent.LOAD_SITE appellera la commande LoadTagCommand. L’injection de dépendance utilisée dans la classe de commande permettra de faire appel à la méthode getSite() du service RemoteDirectoryService. L’événement diffusé pour faire appel à la commande peut également être injecté pour accéder à ses informations.

package jf.directory.controllers.commands
{
    import jf.directory.controllers.events.LoadSiteEvent;
    import jf.directory.models.services.IDirectoryService;

    import org.robotlegs.mvcs.Command;

    public class LoadSiteCommand extends Command
    {
        [Inject]
        public var event:LoadSiteEvent;

        [Inject]
        public var service:IDirectoryService;

        override public function execute() : void
        {
            service.getSites(event.categoryId, event.tagsId);
        }
    }
}

View

Les composants visuels ne doivent contenir aucune référence au framework Robotlegs, ils pourront ainsi être réutilisés dans n’importe quelle application. Pour communiquer avec le reste de l’application, les composants utiliseront des médiateurs. Par exemple, la vue principale de notre annuaire s’appellera DirectoryView et son médiateur DirectoryMediator qui hérite de org.robotlegs.mvcs.Mediator. Pour rappel, voici la maquette correspondant à la vue en question.

DirectoryView est composé d’une liste de catégories représentée sous la forme d’un arbre et d’un composant permettant de sélectionner une liste de mots clés. En fonction de la catégorie et des mots clés sélectionnés, une liste de sites est affichée.

<?xml version="1.0" encoding="utf-8"?>
<s:HGroup xmlns:fx="http://ns.adobe.com/mxml/2009"
          xmlns:s="library://ns.adobe.com/flex/spark"
          xmlns:mx="library://ns.adobe.com/flex/mx"
          xmlns:components="jf.directory.views.components.*">

    <fx:Script>
        <![CDATA[
            import jf.directory.models.vo.CategoryVO;
            import jf.directory.views.events.TagSelectionEvent;

            import mx.collections.IList;
            import mx.controls.Alert;

            private var _categoryProvider:IList;

            public function get categoryProvider():IList
            {
                return _categoryProvider;
            }

            [Bindable]
            public function set categoryProvider(value:IList):void
            {
                _categoryProvider = value;
            }

            private var _siteProvider:IList;

            public function get siteProvider():IList
            {
                return _siteProvider;
            }

            [Bindable]
            public function set siteProvider(value:IList):void
            {
                _siteProvider = value;
            }

            public function get selectedCategoryId():int
            {
                return categoryTree.selectedItem == null ? 0 : categoryTree.selectedItem.id;
            }

            public function get selectedTagsId():Array
            {
                var tagsID:Array = new Array();

                for each(var tag:Object in tagSelection.selectedTags)
                {
                    tagsID.push(tag.id);
                }

                return tagsID;
            }

            public function showHelp(message:String):void
            {
                Alert.show(message, "Aide");
            }

        ]]>
    </fx:Script>

    <s:VGroup>
        <mx:Tree id="categoryTree" labelField="name"
                 dataProvider="{categoryProvider}"
                 width="200" height="300" />
        <s:Label text="Mots clés :" fontWeight="bold" />
        <components:TagSelection id="tagSelection" width="200" />
    </s:VGroup>
    <s:List id="siteList" dataProvider="{siteProvider}"
            itemRenderer="jf.directory.views.components.renderers.SiteListItemRenderer"
            width="100%" height="100%" horizontalScrollPolicy="off"/>

</s:HGroup>

Les médiateurs doivent surcharger la méthode onRegister(), elle est appelée automatiquement lorsque la vue liée au médiateur est ajoutée à l’application. Le médiateur va écouter les événements envoyés par les composants visuels et/ou par Robotlegs. La propriété « eventDispatcher » du médiateur permet d’écouter les événements envoyés par le framework.

package jf.directory.views.mediators
{
    import jf.directory.controllers.events.LoadSiteEvent;
    import jf.directory.models.events.CategoryModelEvent;
    import jf.directory.models.events.SiteModelEvent;
    import jf.directory.views.components.DirectoryView;
    import jf.directory.views.events.TagSelectionEvent;

    import mx.collections.ArrayCollection;
    import mx.events.ListEvent;

    import org.robotlegs.mvcs.Mediator;

    public class DirectoryMediator extends Mediator
    {
        [Inject]
        public var view:DirectoryView;

        override public function onRegister():void
        {
            //view
            eventMap.mapListener(view.categoryTree, ListEvent.CHANGE, categoryTree_changeHandler);
            eventMap.mapListener(view.tagSelection, TagSelectionEvent.SELECT_TAG, tagSelection_changeHandler);
            eventMap.mapListener(view.tagSelection, TagSelectionEvent.UNSELECT_TAG, tagSelection_changeHandler);
            //framework
            eventMap.mapListener(eventDispatcher, SiteModelEvent.SITE_COLLECTION, siteCollectionHandler);
        }

        protected function validate():Boolean
        {
            return view.selectedCategoryId == 0 ? false : true;
        }

        protected function load(showHelp:Boolean = true):void
        {
            if(validate())
            {
                dispatch(new LoadSiteEvent(LoadSiteEvent.LOAD_SITE, view.selectedCategoryId, view.selectedTagsId));
            }
            else if(showHelp)
            {
                view.showHelp("Veuillez sélectionner une catégorie de l'annuaire pour lancer une recherche.");
            }
        }

        protected function categoryTree_changeHandler(event:ListEvent):void
        {
            load();
        }

        protected function tagSelection_changeHandler(event:TagSelectionEvent):void
        {
            load(event.type == TagSelectionEvent.SELECT_TAG ? true : false);
        }

        protected function siteCollectionHandler(event:SiteModelEvent):void
        {
            view.siteProvider = new ArrayCollection(event.collection);
        }
    }
}

Le médiateur est informé lorsqu’une nouvelle catégorie est sélectionnée via l’événement ListEvent.CHANGE. Cette action permet de lancer le chargement des sites en diffusant un événement LoadSiteEvent.LOAD_SITE. Une fois le chargement des sites terminé, le modèle est modifié. Le médiateur est à son tour notifié avec un événement SiteModelEvent.SITE_COLLECTION et la vue est mise à jour.

Conclusion

Pour terminer nous allons lier l’ensemble des composants de l’application depuis son contexte. La méthode startup() de la classe MainContext va permettre de configurer l’application, les vues seront liées aux médiateurs, les commandes aux événements, etc.

package
{
    import flash.display.DisplayObjectContainer;

    import org.robotlegs.mvcs.Context;

    public class MainContext extends Context
    {
        public function MainContext(contextView:DisplayObjectContainer=null, autoStartup:Boolean=true)
        {
            super(contextView, autoStartup);
        }

        override public function startup():void
        {
            // Controllers
            commandMap.mapEvent(LoadSiteEvent.LOAD_SITE, LoadSiteCommand, LoadSiteEvent);

            //Services
            injector.mapSingletonOf(IDirectoryService, RemoteDirectoryService);

            //Models
            injector.mapSingleton(SiteModel);

            //Views
            mediatorMap.mapView(DirectoryView, DirectoryMediator);
         }
    }
}
Veuillez installer Flash 10.2 pour exécuter l'application.

Pour télécharger le code source, dans l’application clique droit « View Source » ou Code source application Directory

Logo flux syndication RSS

Laisser un commentaire

Tu me dis, j'oublie. Tu m'enseignes, je me souviens. Tu m'impliques, j'apprends..
Benjamin Franklin

Cette citation de Benjamin Franklin illustre parfaitement mes sentiments. Chaque jour, je cherche à progresser et acquérir de nouvelles connaissances. Cet enseignement se traduit par l'échange et la remise en question, alors n'hésitez pas à partager votre analyse en postant un commentaire.

*
*
*

Vous pouvez utiliser ces balises et attributs HTML : <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

5 Réponse(s) pour Conception et développement d’un annuaire Flex 4.5 – Architecture MVC Robotlegs + Service Zend AMF [Partie3]

    karma - Répondre
    9 mai 2012 à 13 h 25 min

    merci de partager ton expérience, j'utilise Flash depuis longtemps et j'ai eu une expérience de MVC qui s'est révélé fructueuse.
    Maintenant je me mets à Flex et je tente de comprendre la logique de l’implémentation de composants dans ce modele, et je penses que ce va l'aider.
    J'ai importer le dossier pour faire un projet Flex par contre dans la source il me demande Flex4.Components.swc et jfFlex.Core.swc.
    Les aurais-tu ?

    Merci

      Jérôme Fath - Répondre
      11 mai 2012 à 17 h 38 min

      Hello Karma, Flex4.Components.swc est une archive contenant des composants Flex partagés entre mes différents projets. Pour cette application, le composant AutoComplete est utilisé. Concernant jfFlex.Core.swc, c'est un ensemble de composants développés par mes soins, la vue "SubscribeView" exploite la classe jf.flex.validators.URLValidator. Je te laisse faire le nécessaire pour configurer ton projet correctement.

        karma - Répondre
        18 mai 2012 à 11 h 06 min

        Merci pour ta réponse

        Ok pas de problème si c'est tes sources.
        Par contre j'ai beau chercher quels paramètres je doit modifier pour que le projet compile, je ne trouves pas il me dit ne pas trouver le fichier Main.html (pourtant je n'ai vu nulle part où il y est fait mention)
        Pire je n'arrive pas à compiler un projet que j'ai fait en essayant de m'appuyer sur ton modèle

        Par exemple il me génère une erreur sur ma classe StartUp qui reprend ta classe du même nom mais avec un chemin différent. Il me dit que l'objet "xmlServiceProxy" quand je le lance en débug
        Effectivement sur ta classe il n'est pas instancié alors j'ai regardé dans Command mais rien non plus alors je suis un peu perdu.

        J'avoue que le fait de pouvoir compiler ton projet m'aider pour la logique car je pourrais mettre des point d'arrêts.

        Voilà si t'as du temps à me consacrer pourrais-tu me dire ce que je dois faire

    pitrack - Répondre
    13 août 2011 à 16 h 49 min

    Bonjour et merci pour ce tutoriel qui m'intéresse particulièrement.
    Cependant, je n'arrive pas à voir où l'on peut télécharger les sources ?
    Merci d'avance !

      Jérôme Fath - Répondre
      15 août 2011 à 9 h 44 min

      Bonjour, je viens de mettre un lien pour télécharger les sources directement depuis l'article. Normalement, il est possible de visualiser et télécharger le code source depuis l'application mais Flash 10.2 doit être installé (Adobe express install n'a pas l'air de fonctionner).