Jason Crist posted a thought provoking request for his upcoming presentation comparing Robotlegs and Swiz. He’s got a clever knack for stirring the framework ant pile and getting developers eyes off their IDEs long enough to discuss their passions. In this case the developers include Ben Clinkinbeard, Shaun Smith, Jesse Warden and myself.
These conversations are always good natured. While we work on (or use) “competing” frameworks there is always a sense of mutual respect. We like our tools but have obvious inclinations towards the projects that we’ve invested our hearts and souls into.
Ben Clinkinbeard has pointed out (several times!) that Robotlegs is all about extending the MVCS classes. My stock answer is that there is a clear separation between the framework and the concrete MVCS implementation. To paraphrase Ben, “Well show it to me then!”
All of the “official” Robotlegs examples are making use of the MVCS implementation. Why? Because it is solid, recognizable, and fairly easy to get one’s head around. It provides a common ground for developers and a set architectural structure which is a huge advantage in any team environment. It is important to make the distinction between the framework and the MVCS implementation. What does that even mean? At the core, Robotlegs is a modular set of tools to provide a convenient mechanism for wiring applications. Robotlegs is not doing class reflection. Robotlegs is not an automated dependency injection solution. Robotlegs is an adapter to a dependency injection solution,by default the lightweight SwiftSuspenders library. Through a set of tools, namely the MediatorMap, CommandMap, ViewMap, and the injection adapter Robotlegs provides a robust starting place to begin coding your application.
The MVCS implementation is a set of base classes loosely modeled on PureMVC. At the heart of the implementation is the Context. The Context creates instances of the various mapping classes, the injection adapter, and gives the developer a centralized IEventDispatcher that can be used for messaging between application tiers.. The other three classes, Actor, Mediator, and Command, reduce boiler plate and provide convenient access to injected dependencies typically used in the the MVCS tiers.
What if you hate PureMVC, don’t want to extend any framework classes, or generally just want to work in a different way outside of a prescribed MVCS architecture?
No problem.
Here’s the deal. Robotlegs, the framework, is a set of interfaces. You can effectively do whatever you want with these interfaces. You can make use of the base concrete implementations of the core interfaces, use a class from the MVCS implementation, implement your own concrete classes based on the framework core, bring in other libraries or any combination you can think of. In terms of a framework, Robotlegs can be whatever you want/need it to be.
Man Joel, that was a long intro. Where’s the freaking code??!?
So, to that end, I sat down to rework my first (and still favorite) Robotlegs example application and demonstrate this concept a bit.
I’ve stripped the example down slightly, using just the XMLImageService and not connecting to Flickr (in an effort to keep it simple).
Get the source on GitHub
Download the full source as a Zip
This example requires:
Robotlegs
AS3-Signals
SignalsCommandMap library
Flex 4 SDK (a fairly recent version)
The example should be familiar if you’ve looked at the Image Gallery previously. It loads images from a service and displays them. This version is much different on the code level however. Instead of mediators and data models, the gallery is using presentation models and AS3-Signals. It still uses the Context from the MVCS implementation. it’s so f’n handy and I don’t want to manually instantiate the maps.
ImageGalleryContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public class ImageGalleryContext extends Context { private const VIEW_PACKAGE:String = "org.robotlegs.examples.imagegallery.view.components"; private const LOAD_GALLERY:String = "loadGallery"; override public function startup():void { //map the views viewMap.mapPackage(VIEW_PACKAGE); //map the presentation models injector.mapSingletonOf( IGalleryViewPresentationModel, GalleryViewPresentationModel ); injector.mapSingletonOf( IGalleryThumbnailsPresentationModel, GalleryThumbnailsPresentationModel ); //map the services and their factories injector.mapSingletonOf( IGalleryImageService, XMLImageService ); injector.mapSingletonOf( IGalleryFactory, XMLGalleryFactory ); //map the signals injector.mapSingleton(GalleryUpdatedSignal); injector.mapSingleton(GalleryImageSelectedSignal); //map the command commandMap.mapEvent( LOAD_GALLERY, LoadGalleryCommand ); //go!! eventDispatcher.dispatchEvent(new Event(LOAD_GALLERY)); } } |
As you can see here the context is performing the duty of mapping the injections the application will use.
Automated Dependency Injected View
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <s:Group xmlns:fx="https://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/halo" width="100%" height="100"> <fx:Script> <![CDATA[ import org.robotlegs.examples.imagegallery.view.models.IGalleryThumbnailsPresentationModel; private var _model:IGalleryThumbnailsPresentationModel; public function get model():IGalleryThumbnailsPresentationModel { return _model; } [Inject] [Bindable] public function set model(value:IGalleryThumbnailsPresentationModel):void { _model = value; } ]]> </fx:Script> <s:Rect width="100%" height="100%"> <s:fill> <s:SolidColor color="#111111"/> </s:fill> </s:Rect> <s:Group width="100%" height="100%"> <s:layout> <s:VerticalLayout gap="0"/> </s:layout> <s:DataGroup id="dgThumbnails" clipAndEnableScrolling="true" dataProvider="{model.dataProvider}" width="100%" height="85" itemRenderer="org.robotlegs.examples.imagegallery.view.components.renderers.GalleryImageThumbnailItemRenderer"> <s:layout> <s:HorizontalLayout gap="0"/> </s:layout> </s:DataGroup> <s:HScrollBar id="thumbScrollBar" viewport="{dgThumbnails}" width="100%" smoothScrolling="true"/> </s:Group> </s:Group> |
The viewMap is set to map the entire components folder. This is a convenient way to provide automated dependency injection to a large number of views. It also maps views in any sub-package of the mapped package.
GalleryViewThumbnailsPresentationModel
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | [Bindable] public class GalleryThumbnailsPresentationModel implements IGalleryThumbnailsPresentationModel { [Inject] public var updated:GalleryUpdatedSignal; [Inject] public var imageSelected:GalleryImageSelectedSignal; [PostConstruct] public function mapSignalListeners():void { updated.add(galleryUpdated); imageSelected.add(updateImageSelectionState); } private var _dataprovider:ArrayCollection; public function get dataProvider():ArrayCollection { return _dataprovider; } public function set dataProvider(v:ArrayCollection):void { _dataprovider = v; } private function galleryUpdated(gallery:Gallery):void { dataProvider = gallery.photos; if(gallery.photos[0]) { imageSelected.dispatch(gallery.photos[0]); } } private function updateImageSelectionState(image:GalleryImage):void { for each(var galleryImage:GalleryImage in dataProvider) { galleryImage.selected = galleryImage == image; } } } |
IGalleryViewThumbnailsPresentationModel
1 2 3 4 5 6 7 8 | /** * This interface is simple because the application is simple. Obviously in * a large application you'd get more complex interfaces. */ public interface IGalleryThumbnailsPresentationModel { function get dataProvider():ArrayCollection; } |
The presentation models are provided as singletons. The application will only display one of each view that requires the model. They are also mapped as interfaces with the interfaces being injected into the views. With the presentation model, the view does not update the model directly. By using interfaces we can supply read-only contracts between the presentation models and the views they control:
XMLImageService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | public class XMLImageService implements IGalleryImageService { protected static const BASE_URL:String = "assets/gallery/"; [Inject] public var galleryFactory:IGalleryFactory; [Inject] public var galleryUpdated:GalleryUpdatedSignal; public function XMLImageService() { super(); } public function loadGallery():void { var service:HTTPService = new HTTPService(); var responder:Responder = new Responder(handleServiceResult, handleServiceFault); var token:AsyncToken; service.resultFormat = "e4x"; service.url = BASE_URL+"gallery.xml"; token = service.send(); token.addResponder(responder); } protected function handleServiceResult(event:Object):void { var gallery:Gallery = galleryFactory.createGallery(event.result.image, BASE_URL); galleryUpdated.dispatch( gallery ); } protected function handleServiceFault(event:Object):void { trace(event); } } |
IGalleryImageService
1 2 3 4 | public interface IGalleryImageService { function loadGallery():void; } |
The service and its factory class are also mapped as singleton interfaces. This makes it really easy to swap out services (to add a Flickr service for example).
GalleryUpdatedSignal
1 2 3 4 5 6 7 | public class GalleryUpdatedSignal extends Signal { public function GalleryUpdatedSignal() { super(Gallery); } } |
The signals are simple classes that merely extend the Signal class and declare their payload type in the super() of the constructor. Signals are a really marvelous concept and provide a lot of very nice functionality. They make a great companion to Robotlegs and I think you will see some really cool stuff being done with Signals and Robotlegs in the very near future.
LoadGalleryCommand
1 2 3 4 5 6 7 8 9 10 | public class LoadGalleryCommand { [Inject] public var service:IGalleryImageService; public function execute():void { service.loadGallery(); } } |
The application has a single command, LoadGalleryCommand, that is used to actually load the initial images from service and is only called once. In fact, the event that is fired to launch this command in the context startup method is the only event that this application uses (outside of the Flex events on the UI items). That is awesome!
Something you will notice right away looking at the source is that the ImageGalleryContext is the only class that extends a Robotlegs class. You could do without that too, frankly, and create your own context that created the maps and injector instances. I’m not a masochist however, and I will use the provided context for this example. There is a huge amount of potential there for implementing your own custom contexts. I think an interesting implementation might pitch Flash Events altogether and use only Signals for communication between application actors. This example is essentially doing that, but I’d like to get rid of that single event in startup too!
Download the full source as a Zip
Let me know if you make something cool with Robotlegs and post details about it, I’ve got a stack of these to give away:
The Robotlegs Image Gallery Example using AS3-Signals and the Presentation Model by Joel Hooks, unless otherwise expressly stated, is licensed under a Creative Commons Attribution 3.0 United States License.