Building Blocks as simple as possible, but no simpler

14Feb/1030

Robotlegs, AS3-Signals and the SignalCommandMap Example

Robotlegs and AS3-Signals play really well together. Both apply solid object-oriented principles to accomplish their respective goals. Signals is extremely well suited for automated Dependency Injection. By combining Signals and Robotlegs you are able to eliminate the use of Flash Events in the framework layer of your application. Eliminating Events means eliminating the ambiguity that can accompany Events and their String registry based approach to the Observer pattern. Signals provides a strongly-typed object-oriented approach to this same pattern.

With the standard Robotlegs MVCS implementation you leverage the events provided by Actionscript 3 to communicate amongst the various actors of an application. From models and services dispatching notifications of their actions to triggering commands, events are a core piece of the implementation. To facilitate the use of Signals within MVCS it was necessary to create an extension to allow for Signals to be registered as Command triggers. This need spawned the SignalCommandMap utility.

The SignalCommandMap extends the normal MVCS context and creates a SignalContext. The SignalContext instantiates and provides access to the SignalCommandMap alongside the other maps that are standard to Robotlegs. The SignalCommandMap allows you to map Signal classes and instances to commands that will be executed when the Signal dispatch() method is called. The value objects that are passed in the dispatch are then injected into the command alongside any other mapped injections you have created.

Let's take a look at a simple example that makes use of the SignalCommandMap and discuss some of the underlying code to see how it works:

Get Adobe Flash player

Source available on Github (zip)

The example is a menu that allows you to add food which is displayed in a list. The total cost of your selected items is displayed and you can remove items from your order. First let's get started by taking a look at the context of the Application, which extends SignalContext:

SignalCafeContext.as Bootstraps the Application
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SignalCafeContext extends SignalContext
{
    override public function startup():void
    {
      injector.mapSingleton(FoodOrderModel);
      injector.mapSingleton(FoodOrderUpdated);
      injector.mapSingleton(FoodItemAddedToOrder);
 
      signalCommandMap.mapSignalClass(AddFoodItemToOrder, AddFoodItemToOrderCommand);
      signalCommandMap.mapSignalClass(FoodItemSelected, FoodItemSelectedCommand);
      signalCommandMap.mapSignalClass(RemoveAllOfSelectedItem, RemoveAllSelectedItemCommand);
      signalCommandMap.mapSignalClass(NoFoodItemSelected, NoFoodItemSelectedCommand);
 
      mediatorMap.mapView(FoodSelectionView, FoodSelectionViewMediator);
      mediatorMap.mapView(CurrentOrderView, CurrentOrderViewMediator);
      mediatorMap.mapView(FoodOrderSummaryView, FoodOrderSummaryViewMediator);
      mediatorMap.mapView(FoodItemRemovalView, FoodItemRemovalViewMediator);
    }
}

A SignalContext is structured exactly the same as a standard Robotlegs MVCS Context class. You override the startup() method and bootstrap your application. The major difference here is that you are mapping signals to commands instead of mapping events to commands. With the SignalCommandMap you are not restricted to just using Signals for triggering commands. You could mix events and Signals liberally as your needs or requirements dictated. As a warning, this could quickly became confusing and choosing one or the other might be a saner choice.

SignalCafeContext is mapping 4 Signals to 4 commands. The Signals are typed extensions of the base Signal class. This is necessary to provide Robotlegs (and more specifically in this case SwiftSuspenders) object types to differentiate for the purposes of injection. You could use named injections, but this would rapidly riddle your application with strings that would need to be laboriously checked for accuracy. Avoiding strings in favor of compiler-checked object types is always a good choice.

The strongly typed Signals are relatively simple. They contain a constructor and no additional methods or properties are added to the Signal. You can of course add properties and methods to the extended Signals,, but it wasn't necessary for this example. The SignalCommandMap will accept any class that implements the ISignal interface. Let's take a look at one of the commands triggered by a Signal mapped in the SignalCafeContext:

AddFoodItemToOrder.as triggers the AddFoodItemToOrderCommand
1
2
3
4
5
6
7
public class AddFoodItemToOrder extends Signal
{
	public function AddFoodItemToOrder()
	{
		super(FoodType);
	}
}

That is the whole of the extended Signal. As mentioned, it is not very complex at all. You will notice that super(FoodType) is being called. Signals take constructor arguments consisting of classes or interfaces that will be used as the value objects transmitted via the dispatch() method. In this case we are using FoodType as this value to ensure that our Signal will carry the payload that the mapped command is expecting.

Command signals are named a bit differently than standard reaction Signals. A Signal that is triggering a command is "requesting action" or specifying an action that needs to occur. Standard signals are typically past tense, or informative, describing an action that has occurred. By using this standard it is much easier to differentiate quickly what each Signal's purpose is. This can help with overall clarity within your application.

The AddFoodItemToOrder signal is dispatched by the FoodSelectionViewMediator which is mediating the DropDownList of FoodTypes and the "Add Some Food" button:

FoodSelectionViewMediator.as mediates a view and dispatches a Signal when the user has clicked the "Add Some Food" button
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	public class FoodSelectionViewMediator extends Mediator
	{
		[Inject]
		public var view:FoodSelectionView;
 
		[Inject]
		public var addItem:AddFoodItemToOrder;
 
		override public function onRegister():void
		{
			view.itemTypeAdded.add(handleItemTypeAdded);
		}
 
		protected function handleItemTypeAdded(itemType:FoodType):void
		{
			addItem.dispatch(itemType);
		}
	}

When the AddFoodItemToOrder Signal's dispatch(aFoodTypeInstance) is called the AddFoodItemToOrderCommand is triggered:

AddFoodItemToOrderCommand.as is executed in response to AddFoodItemToOrder Signal dispatch
1
2
3
4
5
6
7
8
9
10
11
12
13
public class AddFoodItemToOrderCommand extends SignalCommand
{
	[Inject]
	public var itemType:FoodType;
 
	[Inject]
	public var model:FoodOrderModel;
 
	override public function execute():void
	{
		model.addItemToOrder(itemType);
	}
}

AddFoodItemToOrderCommand has two public properties that are marked for injection. The itemType property is a FoodType object. As you will recall, FoodType is the parameter that was passed to the AddFoodItemToOrder Signal super constructor. The AddFoodItemToOrder Signal dispatch() included an instance of a FoodType object which will be injected into the AddFoodItemToOrderCommand alongside any other injections that have been mapped and specified. In this case we are also injecting the FoodOrderModel instance that was mapped as a singleton in the SignalCafeContext.

It is important to note that the parameters passed via the Signals dispatch() method will be instantly mapped and unmapped when the command is executed. If you've previously mapped a class that is being delivered via a Signal command mapping, that mapping will be overwritten and removed. To avoid this, favor using typed value objects as Signal parameters.

The AddFoodItemToOrderCommand has a simple job. It accesses the FoodOrderModel instance and calls its addItemToOrder method adding the FoodType parameter that was dispatched with the Signal to trigger the command.

That covers the basics of using the SignalCommandMap within a Robotlegs application. There are options that this example doesn't cover. I would recommend looking through the SignalCommandMap unit test suite for a thorough review of its capabilities. There are several more commands and Signals in the example for you to look over as well. Additionally the example uses Signals within views and the model to dispatch notifications. Outside of binding and UI events, no events are being passed within the application.

Hopefully this will give you the basic understanding of using the SignalCommandMap in a Robotlegs application. Let me know if you have any criticisms, comments, suggestions, or ideas regarding the utility's implementation at http://knowledge.robotlegs.org

  • Nikos
    Hi again, if I run this with flex 4 I get this when I try and add a food item :(

    Error: Injector is missing a rule to handle injection into target [object FoodItemSelectedCommand]. Target dependency: org.robotlegs.examples.signalcommands.model.vo::FoodItem
  • Hi Nikos, I am traveling currently and can't trouble shoot your issue. Be sure to check your versions. It is likely some weird combination of older RL or SwiftSuspenders compiled into the utility.
  • Nikos
    fixed:
    had to add
    injector.mapSingleton(FoodItem);
    to context
  • That isn't proper, something is wrong with the versions. If I had to
    guess I would say the signal command map sec has RL classes compiled
    into it that are older
  • Nikos
    Im using
    robotlegs-framework-v1.1.2.swc
  • Nikos
    with SwiftSuspenders-v1.5.1.swc
  • Nikos
    No probs, I don't think you publish a swc for your own RL singal adabpter code?
  • Nikos
    Best RL eg YET :)
  • Will
    Should signals be used when a mediator simply needs to respond to a change in the model? I assume in order to do this the signal will need to be registered as a singleton using injector.mapSingleton(MySignal) as a Command is not needed in this use case.
  • sjhewitt
    I came across a small problem (caused by the use of the .constructor property) when trying to inject a ListCollectionView into a Command, so thought I'd write a post about the changes I made to SignalCommandMap to overcome it and try to improve the argument injection. Any thoughts are welcome!
    http://blog.sjhewitt.co.uk/2010/02/robotlegs-and-as3signals-injecting-interfaces-and-named-arguments/
  • Your first proposition sounds interesting. In practice we (CommandMap too) should be calling IReflector#getClass. Till's implementation actually has the same bug but it's already fixed in Injector#injectInto so I'll file a bug and/or patch right away. I recently proposed CommandMap#executedCommand and wonder whether it could be modified to accommodate SignalCommandMap#routeSignalToCommand too:

    https://gist.github.com/11597e33a30351a2e2ad

    Could your second proposal work without explicit naming injections? Maybe Joel could implicitly name repeated types ordinally. Thus your Command would be like:

    http://gist.github.com/310118
  • sjhewitt
    I thought about using a reflector, but i would still run into the problem of not being able to inject to interfaces. Personally, i think the contract created using Signal.valueClasses should be honoured when it is set, otherwise falling back to the usual method.

    Implicitly named injectors could be used, but does add quite a bit more complexity to the signal routing. It could also be possible to create an IInjectedSignal interface that extends ISignal and adds a command something like:
    mapValues(injector, valueObjects) and unmapValues(injector, valueObjects)
    Then the routeSignalToCommand works like this:
    http://gist.github.com/310237
  • Will
    Shouldn't the constructor for AddFoodItemToOrder accept a Class as a parameter since you are calling the super's constructor with super(FoodType)?
  • Will
    Wait... I get it... nevermind :/
  • it *is* kinda hidden in there, but at the same time I don't want they value type changed and it kicks a RTE quickly if a wrong type is passed.
  • Will
    When instantiating a custom class extended from SignalContext I am unable to pass the contextView & autoStartup parameters usually passable to a regular Context. I there a reason for this?
  • That's hot. Could SignalsCommandMap throw an error if it tries to map a dispatched payload that was already mapped in the Context? I think that might aver unpleasant surprises.
  • That would definitely be the way to go. Unfortunately until SwiftSuspenders and Robotlegs hit the next dot-release the IInjector API isn't there to accomplish it. It will be more robust in the near future.
blog comments powered by Disqus