Awesome
Overview
MassiveMVC is a light but powerful IOC framework utilizing Signals and macro based injection
MassiveMVC is a port of the excellent AS3 RobotLegs framework, optimised to better leverage features of the Haxe language like generics, macros. It also does away with event based command mapping, instead favouring signal based mapping based on Joel Hooks SignalCommandMap concept. This enables portability away from the Flash platform.
Installation
MMVC is available from Haxelib
haxelib install mmvc
Dependencies
MassiveMVC depends on two other Massive libraries also available from haxelib: msignal and minject.
Examples
If you are familiar with RobotLegs, you may want to jump directly into the example reference app. This contains a simple todo list application demonstrating the main components of MMVC, running across JS, Flash and Neko targets.
Otherwise read on :)
Getting Started
What is MMVC
MMVC is a Dependency Injection framework based off RobotLegs (see https://github.com/robotlegs/robotlegs-framework/wiki/Best-Practices for more information).
It provides a robust, modular, testable pattern for the Model-View-Controller design pattern.
It's recommended to first read the above RobotLegs documentation if you are unfamiliar with the concepts outlined below.
- Injectors
- Context
- Actors and Models
- Signals
- Commands
- View Mediators
Injectors
Injectors provide a dependency injection mechanism for framework classes.
@inject something:Something;
Injection is performed by MassiveInject. For more information checkout the documentation and examples.
Context
The context provides the central wiring/mapping of common elements within a contextual scope (eg application).
Creating a Context
Contexts must extend mmvc.impl.Context and override the startup method to map appropriate actors, commands and mediators
Generally a context is defined at an application level
class ApplicationContext extends mmvc.impl.Context
{
public function new(?contextView:IViewContainer=null, ?autoStartup:Bool=true)
{
super(contextView, autoStartup);
}
override public function startup():Void
{
//map commands/models/mediators here
}
}
Wiring the Context
Models are mapped via the injector:
injector.mapSingleton(DanceModel);
Commands are mapped to actions (Signals) using the commandMap in the Context
commandMap.mapSignalClass(Dance, DanceCommand);
Standalone actions (Signals) without a corresponding Command can also be mapped via the injector
injector.mapSingleton(ClapHands);
Mediators are mapped to Views via the mediatorMap
mediatorMap.mapView(DanceView, DanceViewMediator);
Initializing the Context
Usually an application context is instantiated within the main view of an application:
class ApplicationView implements mmvc.api.IViewContainer
{
static var context:ApplicationContext;
public function new()
{
context = new ApplicationContext(this, true);
}
}
Or externally in the Main haxe file
class Main
{
public static function main()
{
var view = new ApplicationView();
var context = new ApplicationContext(view);
}
}
Important Caveat
The IViewContainer (ApplicationView) should be the last mapping in the Context.startup function otherwise other mappings may not be configured:
function startup()
{
//map everything else
mediatorMap.mapView(ApplicationView, ApplicationViewMediator);
}
Actors (Models and Services)
Actor is a generic term for an application class that is wired into the Context. Generally these take the form of models or services
Mapping Actors
Actors are mapped via the injector:
injector.mapSingleton(DanceModel);
Example
By default actors don't require any interface or inheritance to be mapped in the context.
However if a actors requires references to other application parts it should extend mmvc.impl.Actor
class DanceModel extends mmvc.impl.Actor
{
public inline static var STYLE_WALTZ = "waltz";
public inline static var STYLE_FOXTROT = "foxtrot";
@inject something:Something;
public var dancer:String;
public var style:String;
public function new()
{
...
}
}
Signal
Signals are highly scalable and lightweight alternative to Events. MassiveSignal leverages Haxe generics to provide a strictly typed contract between dispatcher (Signal) and it's listeners.
See MassiveSignals's documentation and examples for more details and examples on working with Signals.
Application signals represent unique actions or events within an application.
Mapping Signals
The Signal can be mapped to an associated Command via the Context
commandMap.mapSignalClass(Dance, DanceCommand);
They can also be mapped as a standalone actor
injector.mapSingleton(DoSomething);
Example
A simple example Signal with a signal dispatcher argument:
class Dance extends msignal.Signal1<String>
{
inline static public var FOX_TROT = "foxtrot";
inline static public var WALTZ = "waltz";
public function new()
{
super(String);
}
}
And to dispatch the signal
dance.dispatch(Dance.FOX_TROT);
To add a listener to the signal
dance.add(danceHandler);
...
function danceHandler(style:String)
{
trace("Dance style: " + style);
}
Responder Signals
Within an application is is often useful to be able to receive callbacks once a signal has finished or completed.
A good way to achieve this is through child signals.
This example adds a completed signal to dispatch once the dance has been completed.
class Dance extends msignal.Signal1<String>
{
public var completed:Signal1<String>;
inline static public var FOX_TROT = "foxtrot";
inline static public var WALTZ = "waltz";
public function new()
{
super(String);
completed = new Signal1<String>();
}
}
To listen for completion of the Dance
dance.completed.addOnce(this.danceCompleted);
dance.dispatch();
Command
Commands represent the controller tier of the Application. Commands are generally stateless, short lived objects that provide a single, granular activity within an application.
Mapping Commands
Commands are mapped to actions (Signals) using the commandMap in the Context
commandMap.mapSignalClass(Dance, DanceCommand);
Triggering Commands
Commands are triggered by dispatching the associated Signal from elsewhere within the application (generally a Mediator or other Command)
dance.dispatch(Dance.FOX_TROT);
Example
class DanceCommand extends mmvc.impl.Command
{
@inject
public var danceModel:DanceModel;
@inject
public var dance:Dance;
@inject
public var style:String;
public function new()
{
super();
}
override public function execute():Void
{
//some application logic here
//dispatch completed once done
dance.completed.dispatch(style);
}
}
Views & Mediator
Mediators are used to handle framework interaction with specific View classes, and decouple views from other application components.
This includes:
- listening and responding to application Signals
- listening to the view instance and dispatching application signals in response to user input
- injecting external actors and models into the view during registration
Mapping Mediators
Mediators are mapped to Views via the mediatorMap
mediatorMap.mapView(DanceView, DanceViewMediator);
Mediating Views
Mediator instances are created automatically when the IViewContainer (generally an ApplicationView) calls the added handler.
Generally a base view class will handle bubbling of added and removed events from the target platform's display heirachy.
See the examples for a reference implementation.
To manually do this call the handler directly
applicationView.added(viewInstance);
Accessing a Mediator's View
The associated view instance can be accesed view the 'view' property
this.view.doSomething();
Example
This is an example demonstrating integration with both application and view events within a mediator
class DanceViewMediator extends mmvc.impl.Mediator<DanceView>
{
@inject model:DanceModel;
@inject dance:Dance;
public function new()
{
super();
}
override function onRegister()
{
super.onRegister();
view.changeStyle.add(styleChanged);
view.start(model.style);
}
override public function onRemove():Void
{
super.onRemove();
view.changeStyle.remove(styleChanged);
}
function styleChanged(style:String)
{
dance.completed.addOnce(danceCompleted);
dance.dispatch(style);
}
function danceCompleted(newStyle:String)
{
view.start(newStyle);
}
}
And the view class:
class DanceView
{
public var changeStyle:Signal1<String>
var style:String;
public function new()
{
changeStyle = new Signal1<String>();
}
public function start(style:String)
{
this.style = style;
//do something
}
public function restart()
{
start(style);
}
function internallyChangeStyle(newStyle:String)
{
changeStyle.dispatch(newStyle);
}
}
Release
Update version information (lets say 1.2.3) in:
- CHANGES.md
- project.json
- src/haxelib.json
- src/haxelib.xml
Commit and push updates using git commands:
git add CHANGES.md project.json src/haxelib.json src/haxelib.xml
git commit -m "Version 1.2.3"
git tag 1.2.3
git push origin master --tags
Submit updated version into haxelib:
haxelib submit
package: src