RobotSignals: Combining RobotLegs with Robert Penner’s Signals
Robotlegs is an AS3 dependency injection micro-framework.
Signals is a new approach for AS3 events, inspired by C# events and signals/slots in Qt.
Put them together and you’ve got an elegant, simple & completely decoupled solution to the problem of Flex & AIR development.
I’ve thrown together a really quick example (which you can download below) based upon a couple of things I’ve seen recently; firstly Richard Lord’s framework comparison talk at FlashBrighton a couple of weeks ago, and secondly, Owen Bennett’s blend of RobotLegs and Signals he showed me last week. Seeing what Owen had put together inspired me to have a go myself. I wondered whether it was possible to create a RobotLegs/Signals hybrid that was even more decoupled than the system Owen was working on. So I created a short (less then 100 lines) class called ‘SignalBox’, named after a similarly named class in Owen’s system.
RobotLegs & Signals are great bits of work but I won’t discuss them here, because you can either go here & here to do that, or if you’re feeling confident - or just plain impatient - download the RobotSignals example here and just have a play with it. It’s a FlexBuilder project that contains both the Robotlegs & Signals source code (from mid-December 2009), so should contain all you need.
The following swf is what the example project compiles down to:
As simple an example I could conceive of. This Flex app conatins two View elements - the TextInput & Button to the left, and the TextArea to the right - both of which are controlled by their own Mediators and an Actor based Proxy Model class in the background. These three elements are entirely decoupled from each other but communicate through the SignalBox class in the following way:
- The TextSubmit component dispatches user input into the Signalbox;
- The TextProxy Model instance hears the Signal and stores said user input, before dispatching it again into the SignalBox;
- The TextDisplay component instance hears the signal and displays the user input in its TextArea component
The decoupling is extreme, to the point that the only public method they definitely require is the onRegister(); method they use to register with the RobotLegs framework. All communications go through the SignalBox. RobotLeg’s Context class injects the SignalBox into every class in you application, meaning they all have access to the same single instance of said SignalBox, meaning that it’s Signals can propigate through every part of the application at will. This is an example of a singleton-with-a-small-s pattern; as in a single instance and not a global property.
The ISignalBox interface defines the following seven, reasonably self-explanatory methods:
function addListener (signalName:String, listener:Function, valueClass:Class = null):void; function addListenerOnce (signalName:String, listener:Function, valueClass:Class = null):void; function deleteSignal (signalName:String):void; function dispatchSignal (signalName:String, valueObject:Object, andDeleteIfLastListener:Boolean = true):void; function hasSignal (signalName:String):Boolean; function removeAllListeners (signalName:String, andDelete:Boolean = true):void; function removeListener (signalName:String, listener:Function, andDeleteIfLastListener:Boolean = true):void;
SignalBox keeps a private collection of Signals, meaning that Robert Penner’s Signals themselves are abstracted away and you never interect with them. By calling addListener or addListenerOnce you can listen for a Signal, and calling dispatchSignal from elsewhere sends that listened for Signal. All seven methods in SignalBox take the following param as their first parameter:
signalName:StringCalling either addListener or addListenerOnce ensures that a Signal with the passed signalName is created, if it doesn’t already exist. Each signalName must be unique. Calling addListener or addListenerOnce multiple times with the same signal name results in additional listeners being added to a single Signal:
signalBox.addListener("UserInput", someMethodName); signalBox.addListener("UserInput", anotherMethodName);
Once you’ve created a Signal like this, you can dispatch signals through it:
signalBox.dispatchSignal("UserInput", "A Message");
Calling dispatchSignal on a signal that has yet to be created results in an error being thrown.
Adding a Class as the optional third parameter of either addListener or addListenerOnce results in listeners to that Signal expecting instances of that type:
signalBox.addListener("LoginAttempt", handleLogin, Boolean); signalBox.dispatchSignal("LoginAttempt", true);
If this third parameter is left undefined, the class String is used by default.
I want to keep this post as short and simple as possible so I’ll stop here and if you’ve got any questions please fire away. You can download the RobotSignals example here.
Tags: as3signals, RobotLegs, Signals
December 20th, 2009 at 8:21 pm
Nice work Rich!
One thing that occurs to me is that by removing the concrete Signal definitions you are swapping a dependency on a function defined by an interface for a dependency on a static string in a concrete class.
So, rather than:
signalBox.textDisplaySignal.add(onDisplayText);
you are using:
signalBox.addListener(TextProxy.DISPLAY_TEXT, onDisplayText);
Which means that the TextProxy class would need to be included. You also lose the benefit of strong typing for the Signals. I think that my system is far from perfect though, so it’d be interesting to see if there is a way to combine the two :)
December 20th, 2009 at 10:21 pm
yeah, i guess that’s true, somewhat irritatingly. One solution that suggested itself was the idea that as SignalBox & ISignalBox have to imported, to put the constants in there. But not in SignalBox itself, but extend SignalBox into a new class that only features said constants, like this:
package model
{
import uk.co.richtextformat.signals.SignalBox;
import uk.co.richtextformat.signals.ISignalBox;
public class MyAppsSignalBox extends SignalBox implements
ISignalBox
{
public static const DO_SUMMAT:String = ‘DoSummat’;
public static const DO_SUMMAT_ELSE:String = ‘DoSummatElse’;
}
}
altho equally, that’s not ideal either…