MVC Framework


Keywords
framework, html5, js, mvc
License
MIT
Install
haxelib install DomWires 1.0.0-alpha.14

Documentation

DomWires Build Status

Flexible and extensible MVC framework for projects written in Haxe.

haxelib install DomWires 1.0.0-alpha.14

Features

  • Splitting logic from visual part
  • Immutable interfaces are separated from mutable, for safe usage of read-only models (for example in mediators)
  • Possibility to use many implementations for interface easily
  • Fast communication among components using IMessageDispatcher
  • Object instantiation with dependencies injections using cool IAppFactory
  • Possibility to specify dependencies in config and pass it to IAppFactory
  • Easy object pooling management
  • Custom message bus (event bus) for easy and strict communication among objects

1. Hierarchy and components communication

Diagramm

On diagram we have main IContext in the center with 2 child contexts.

Lets take a look at right IContext.

Right IContext is mapped to AppContext implementation. You can see its hierarchy on the screen: IModelContainer with 2 child models, IUIMediator with IButtonView and IScreenMediator with 3 views.

IContext and its children all extend IMessageDispatcher and can listen or dispatch IMessage.

All messages in model hierarchy and from mediators bubble up to IContext. Also bubbled-up messages can be forwarded to parent contexts (by default forwarding message from child context to parent is disabled).

Also in default IContext configuration messages from models will be forwarded to mediators, messages from mediators will be forwarded to models and mediators.

IContext extends ICommandMapper and can map received messages (from model and mediators) to commands.

Creating context with default configuration
var factory:IAppFactory = new AppFactory();
factory.mapToValue(IAppFactory, factory);

var contextConfigBuilder:ContextConfigVoBuilder = new ContextConfigVoBuilder();
contextConfigBuilder.forwardMessageFromModelsToModels = false;
contextConfigBuilder.forwardMessageFromMediatorsToMediators = true;
contextConfigBuilder.forwardMessageFromMediatorsToModels = false;
contextConfigBuilder.forwardMessageFromModelsToMediators = true;

factory.mapToValue(ContextConfigVo, contextConfigBuilder.build());
factory.getInstance(AppContext);
Dispatching message from model
class AppModel extends AbstractModel implements IAppModel
{
    @PostConstruct
    private function init():Void
    {
        dispatchMessage(AppModelMessage.Notify);
    }
}

enum AppModelMessage
{
    Notify;
}
Listening message of model in mediator without having reference to model
class UIMediator extends AbstractMediator implements IUIMediator
{
    @PostConstruct
    private function init():Void
    {
        addMessageListener(AppModelMessage.Notify, handleAppModelNotify);
    }
    
    private function handleAppModelNotify(message:IMessage):Void
    {
        trace("Message received: ", message.type);
    }
}
Listening model message in mediator with hard reference to model
class UIMediator extends AbstractMediator implements IUIMediator
{
    @Inject
    private var appModel:IAppModelImmutable;
    
    @PostConstruct
    private function init():Void
    {
        appModel.addMessageListener(AppModelMessage.Notify, handleAppModelNotify);
    }

    override public function dispose():Void
    {
        appModel.removeMessageListener(AppModelMessage.Notify, handleAppModelNotify);
        
        super.dispose();
    }

    private function handleAppModelNotify(message:IMessage):Void
    {
        trace("Message received: ", message.type);
    }
}
Listening model message in mediator with hard reference to model
class UIMediator extends AbstractMediator implements IUIMediator
{
    @Inject
    private var appModel:IAppModelImmutable;
    
    @PostConstruct
    private function init():Void
    {
        appModel.addMessageListener(AppModelMessage.Notify, handleAppModelNotify);
    }

    override public function dispose():Void
    {
        appModel.removeMessageListener(AppModelMessage.Notify, handleAppModelNotify);
        
        super.dispose();
    }

    private function handleAppModelNotify(message:IMessage):Void
    {
        trace("Message received: ", message.type);
    }
}

2. Types mapping

Map 1 type to another
var factory:IAppFactory = new AppFactory();
factory.mapToType(IMyObj, MyObj);

//Will return new instance of MyObj
var obj:IMyObj = factory.getInstance(IMyObj);
Map type to value
var factory:IAppFactory = new AppFactory();
factory.mapToType(IMyObj, MyObj);

//Will return new instance of MyObj
var obj:IMyObj = factory.getInstance(IMyObj);
factory.mapToValue(IMyObj, obj);

//obj2 will equal obj1
var obj2:IMyObj = factory.getInstance(IMyObj);
Apply mapping at runtime via configuration
{
    "mock.mvc.models.IDefault$def": {
        "implementation": "mock.mvc.models.Default",
        "newInstance": true
    },
    "mock.mvc.models.ISuperCoolModel": {
        "implementation": "mock.mvc.models.SuperCoolModel"
    },
    "Int$coolValue": {
        "value": 7
    },
    "Bool$myBool": {
        "value": false
    },
    "Int": {
        "value": 5
    },
    "Dynamic$obj": {
        "value": {
            "firstName": "nikita",
            "lastName":"dzigurda"
        }
    },
    "Array<String>": {
        "value": ["botan","sjava"]
    }
}
var config:MappingConfigDictionary = new MappingConfigDictionary(json);

factory.appendMappingConfig(config.map);
var m:ISuperCoolModel = factory.getInstance(ISuperCoolModel);

Assert.areEqual(m.getCoolValue, 7);
Assert.areEqual(m.getMyBool, false);
Assert.areEqual(m.value, 5);
Assert.areEqual(m.def.result, 123);
Assert.areEqual(m.object.firstName, "nikita");
Assert.areEqual(m.array[1], "sjava");
Default value of interface

If no mapping is specified, IAppFactory will try to find default implementation on the interface.

Default implementation should be in the same package with interface and without this first "I" char.

var factory:IAppFactory = new AppFactory();

//Will try to return instance of MyObj class 
var obj:IMyObj = factory.getInstance(IMyObj);

3. Message bubbling

By default, when message is dispatched it will be bubbled-up to top of the hierarchy. But you can dispatch message without bubbling.

Dispatch message without bubbling it up
//set the 3-rd parameter "bubbles" to false
dispatchMessage(UIMediatorMessage.UpdateAppState, {state: AppState.Enabled}, false);

It is also possible to stop bubbling up received message from bottom of hierarchy

Stop message propagation
override public function onMessageBubbled(message:IMessage):Bool
{
    super.onMessageBubbled(message);

    //message won't propagate to higher level of hierarchy
    return false;
}

To stop forwarding redirected message from context (for ex. mediator dispatcher bubbling message, context receives it and forwards to models), you can do that way:

override public function dispatchMessageToChildren(message:IMessage):Void
{
    /*
    * Do not forward received messages to children.
    * Just don't call super.dispatchMessageToChildren(message);
    */
}

4. Mapping messages to commands in IContext

IContext extends ICommandMapper and can map any received message to command.

Mapping message to command
class AppContext extends AbstractContext implements IContext
{
    override private function init():Void
    {
        super.init();
        
        map(UIMediatorMessage.UpdateAppState, UpdateAppStateCommand);
    }
}

In code screen above, when context receive message with UIMediatorMessage.UpdateAppState type, it will execute UpdateAppStateCommand. Everything that is mapped to IContext factory will be injected to command.

Inject model to command
class AppContext extends AbstractContext implements IContext
{
    private var appModel:IAppModel;
    
    override private function init():Void
    {
        super.init();

        appModel = factory.getInstance(IAppModel);
        addModel(appModel);
        
        factory.mapToValue(IAppModel, appModel)
        
        map(UIMediatorMessage.UpdateAppState, UpdateAppStateCommand);
    }
}

class UpdateAppStateCommand extends AbstractCommand
{
    @Inject
    private var appModel:IAppModel;

    override public function execute():Void
    {
        super.execute();

        //TODO: do something
    }
}

Also IMessage can deliver data, that will be also injected to command.

Injecting IMessage data to command
class UIMediator extends AbstractMediator implements IUIMediator
{
    @PostConstruct
    private function init():Void
    {
        dispatchMessage(UIMediatorMessage.UpdateAppState, {state: AppState.Enabled});
    }
}


class UpdateAppStateCommand extends AbstractCommand
{
    @Inject
    private var appModel:IAppModel;

    @Inject("state")
    private var state:AppState;

    override public function execute():Void
    {
        super.execute();

        appModel.setCurrentState(state);
    }
}

5. Command guards

It is possible to add “guards“, when mapping commands. Guards allow doesn’t allow to execute command at current application state.

Adding guards to command mapping
class AppContext extends AbstractContext implements IContext
{
    private var appModel:IAppModel;

    override private function init():Void
    {
        super.init();

        appModel = factory.getInstance(IAppModel);
        addModel(appModel);
        
        factory.mapToValue(IAppModel, appModel)

        map(UIMediatorMessage.UpdateAppState, UpdateAppStateCommand)
            .addGuards(CurrentStateIsDisabledGuards);
    }
}

class CurrentStateIsDisabledGuards extends AbstractGuards
{
    @Inject
    private var appModel:IAppModel;

    override private function get_allows():Bool
    {
        super.get_allows();
        
        return appModel.currentState == AppState.Disabled; 
    }
}

In above example command won’t be executed, if appModel.currentState != AppState.Disabled.

6. Object pooling

IAppFactory has API to work with object pools.

Register pool
class AppContext extends AbstractContext implements IContext
{
    override private function init():Void
    {
        super.init();

        //Registering pool of MyObj with capacity 5 and instantiate them immediately
        factory.registerPool(MyObj, 5, true);

        for (i in 0...100)
        {
            //Will return one of objects in pool
            factory.getInstance(MyObj);
        }
    }
}

There are other helpful methods to work with pool in IAppFactory

7. Handling multiple implementations of one interface

It is possible to dynamically map different implementations to one interface.

Mapping specific implementation according platform
class AppContext extends AbstractContext implements IContext
{
    override private function init():Void
    {
        super.init();

        #if js
        factory.mapToType(INetworkConnector, JSNetworkConnector);
        #elseif native
        factory.mapToType(INetworkConnector, NativeNetworkConnector);
        #else
        throw Error.Custom("There is no default implementation of INetworkConnector");
        #end
    }
}

There are even possibilities to remap commands.

Remapping command
factory.mapToType(
    com.crazyflasher.app.commands.UpdateModelsCommand,
    com.mycompany.coolgame.commands.UpdateModelsCommand
);
        
/*
* Will execute com.mycompany.coolgame.commands.UpdateModelsCommand instead of
* com.crazyflasher.app.commands.UpdateModelsCommand
*/
commandMapper.executeCommand(com.crazyflasher.app.commands.UpdateModelsCommand);

Also you can map extended class to base

Map extended class to base
//GameSingleWinVo extends SingleWinVo
factory.mapToType(SingleWinVo, GameSingleWinVo);
        
//Will return new instance of GameSingleWinVo
factory.getInstance(SingleWinVo);

8. Immutability

DomWires recommends to follow immutability paradigm. So mediators have access only to immutable interfaces of hierarchy components. But feel free to mutate them via commands. To handle this way, it’s better to have separate factories for different types of components. At least to have separate factory for context components (do not use internal context factory, that is used for injecting stuff to commands and guards).

Mapping mutable and immutable interfaces of model
class AppContext extends AbstractContext implements IContext
{
    override private function init():Void
    {
        super.init();

        var appModel:IAppModel = factory.getInstance(IAppModel);
        addModel(appModel);
        
        map(UIMediatorMessage.UpdateAppState, UpdateAppStateCommand)
            .addGuards(CurrentStateIsDisabledGuards);

        var mediatorFactory:IAppFactory = new AppFactory();

        //mutable interface will be available in commands
        factory.mapToValue(IAppModel, appModel);

        //immutable interface will be available in mediators
        mediatorFactory.mapToValue(IAppModelImmutable, appModel);
        
        var uiMediator:IUIMediator = mediatorFactory.getInstance(IUIMediator);
        addMediator(uiMediator);
    }
}

class AppModel extends AbstractModel implements IAppModel
{
    public var currentState(get, never):EnumValue;
    
    private var _currentState:EnumValue;

    public function setCurrentState(value:EnumValue):IAppModel
    {
        _currentState = value;
        
        dispatchMessage(AppModelMessage.StateUpdated);
    }

    private function get_currentState():EnumValue
    {
        return _currentState;
    }
}

interface IAppModel extends IAppModelImmutable extends IModel
{
    function setCurrentState(value:EnumValue):IAppModel;
}

interface IAppModelImmutable extends IModelImmutable
{
    var currentState(get, never):EnumValue;
}

enum AppModelMessage
{
    StateUpdated;
}

class UIMediator extends AbstractMediator implements IUIMediator
{
    @Inject
    private var appModel:IAppModelImmutable;

    @PostConstruct
    private function init():Void
    {
        addMessageListener(AppModelMessage.StateUpdated, appModelStateUpdated);

        dispatchMessage(UIMediatorMessage.UpdateAppState, {state: AppState.Enabled});
    }

    private function appModelStateUpdated(message:IMessage):Void
    {
        //possibility to access read-only field
        trace(appModel.currentState);
    }
}

enum UIMediatorMessage
{
    UpdateAppState;
}

class UpdateAppStateCommand extends AbstractCommand
{
    @Inject
    private var appModel:IAppModel;

    @Inject("state")
    private var state:AppState;

    override public function execute():Void
    {
        super.execute();

        appModel.setCurrentState(state);
    }
}

class CurrentStateIsDisabledGuards extends AbstractGuards
{
    @Inject
    private var appModel:IAppModel;

    override private function get_allows():Bool
    {
        super.get_allows();

        return appModel.currentState == AppState.Disabled;
    }
}

Minimum requirements

  • Haxe 4.0.1 or higher