as3signals – An Awesome Solution to Events/Signals in AS3

There has been quite a lot of buzz lately in the community about a new library by Robert Penner called as3-signals. John Lindquist recently posted a video tutorial on using it, and I thought I’d follow up with a nice text-based tutorial explaining the common ins-and-outs of the library.

Let’s introduce a real-world example, since any library can only be useful if it can be used to solve problems in the real world. We’ll pose a hypothetical that you’re trying to build a socket-based library for using a particular protocol, perhaps even one you invented yourself. The service class you’re creating wraps flash.net.Socket and dispatches custom events when particular things happen. We’ll say that you have two custom events you need to dispatch, “writeResult” and “writeFailed,” in addition to other events pertaining natively to the Socket instance for other objects to listen for error events and the like.

Here’s the code you would need to write in order to accomplish this using Flash Player’s native event system.

CustomSocket.as

package com.mycompany.socket {

	import flash.events.EventDispatcher;
	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.net.Socket;	

	[Event(name="connect",type="flash.events.Event")]

	[Event(name="close",type="flash.events.Event")]

	[Event(name="ioError",type="flash.events.IOErrorEvent")]

	[Event(name="writeResult",type="com.mycompany.CustomSocketEvent")]

	[Event(name="writeFault",type="com.mycompany.CustomSocketEvent")]
	public class CustomSocket extends EventDispatcher {

		private var socket:Socket;

		public function CustomSocket() {
			this.socket = new Socket();
			this.socket.addEventListener(Event.CONNECT, dispatchEvent);
			this.socket.addEventListener(Event.CLOSE, dispatchEvent);
			this.socket.addEventListener(IOErrorEvent.IO_ERROR, dispatchEvent);
		}

		public function connect(host:String, port:int):void {
			this.socket.connect(host, port);
		}

		public function write():void {
			var writeValue:Boolean = Math.random() < 0.5;
			this.socket.writeBoolean(writeValue);
			if (writeValue) {
				this.dispatchEvent(new CustomSocketEvent(CustomSocketEvent.WRITE_RESULT));
			} else {
				this.dispatchEvent(new CustomSocketEvent(CustomSocketEvent.WRITE_FAULT));
			}
		}
	}
}

CustomSocketEvent.as

package com.mycompany.socket {

	import flash.events.Event;

	public class CustomSocketEvent extends Event {

		public static const WRITE_RESULT:String = "writeResult";

		public static const WRITE_FAULT:String = "writeFault";

		public function CustomSocketEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false) {
			super(type, bubbles, cancelable);
		}

		public function clone():Event {
			return new CustomSocketEvent(type, bubbles, cancelable);
		}
	}
}

Client.as

package {

	import com.mycompany.socket.CustomSocket;
	import com.mycompany.socket.CustomSocketEvent;

        import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.IOErrorEvent;

	public class Client extends Sprite {

		public function Client() {
			var socket:CustomSocket = new CustomSocket();
			socket.addEventListener(Event.CONNECT, onConnect);
			socket.addEventListener(Event.CLOSE, onClose);
			socket.addEventListener(IOErrorEvent.IO_ERROR, onIoError);
			socket.addEventListener(CustomSocketEvent.WRITE_RESULT, onWriteResult);
			socket.addEventListener(CustomSocketEvent.WRITE_FAULT, onWriteFault);
			socket.connect("localhost", 12345);

			socket.write();
		}

		private function onConnect(e:Event):void {
			trace("connect!");
		}

		private function onClose(e:Event):void {
			trace("close!");
		}

		private function onIoError(e:IOErrorEvent):void {
			trace("ioerror!");
		}

		private function onWriteResult(e:CustomSocketEvent):void {
			trace("writeResult!");
		}

		private function onWriteFault(e:CustomSocketEvent):void {
			trace("writeFault!");
		}
	}
}

Ok, so we're done. Let's talk about some of the inherent problems with this code.

First, there is not necessarily any compile-time or runtime way of knowing if a component dispatches events, and knowledge of which events it dispatches is nonexistent. However, as anyone well-versed in AS3 will point out, we have metadata! The [Event] tags at the top of the CustomSocket class let us know that CustomSocket will probably dispatch a type of event, but we have no way of knowing this. And, since the metadata has little or nothing to do with the code and is not enforced by the compiler, we're really out in the dark. There is no compile-time enforcement of events.

Second, the CustomSocketEvent class is almost entirely useless. Even if we were passing some information along with the event, we had to write all of those boilerplate lines of code for almost no reason.While it is not necessary to write CustomSocketEvent, best practices tell us that we should, and if we're expecting to pass values attached to events, we must. In the spirit of keeping with "best practices," which may or may not even exist according to some, we've defined this event class.

Third, since the event system works on a String-key basis, there is really no way to distinguish between a flash.events.Event.CONNECT and a com.mycompany.socket.CustomSocketEvent.CONNECT if the constants define the same String literal "connect."

These are just some of the flaws plaguing Flash Player's built-in event system. Does it work? Yes! Does it work well? You decide. This is where as3-signals comes into play.

Let's rewrite this same example using as3-signals.

CustomSocket.as

package com.mycompany.socket {

	import flash.events.Event;
	import flash.events.IOErrorEvent;
	import flash.net.Socket;

	import org.osflash.signals.Signal;
	import org.osflash.signals.natives.NativeSignal;

	public class CustomSocket {

		public var connect:NativeSignal;

		public var close:NativeSignal;

		public var ioError:NativeSignal;

		public var writeFault:Signal;

		public var writeResult:Signal;

		private var socket:Socket;

		public function CustomSocket() {
			this.socket = new Socket();

			this.connect = new NativeSignal(this.socket, Event.CONNECT, Event);
			this.close = new NativeSignal(this.socket, Event.CLOSE, Event);
			this.ioError = new NativeSignal(this.socket, IOErrorEvent.IO_ERROR, IOErrorEvent);

			this.writeFault = new Signal();
			this.writeResult = new Signal();
		}

//		because we already have a member named "connect," 
//		we'll name this guy "connectSocket" (thanks Robert :] )
		public function connectSocket(host:String, port:int):void {
			this.socket.connect(host, port);
		}

		public function write():void {
			var writeValue:Boolean = Math.random() < 0.5;
			this.socket.writeBoolean(writeValue);
			if (writeValue) {
				this.writeResult.dispatch();
			} else {
				this.writeFault.dispatch();
			}
		}
	}
}

Client.as

package {

	import com.mycompany.socket.CustomSocket;

	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.IOErrorEvent;

	public class Client extends Sprite {

		public function Client() {
			var socket:CustomSocket = new CustomSocket();
			socket.connect.add(onConnect);
			socket.close.add(onClose);
			socket.ioError.add(onIoError);
			socket.writeResult.add(onWriteResult);
			socket.writeFault.add(onWriteFault);
			socket.connectSocket("localhost", 12345);

			socket.write();
		}

		private function onConnect(e:Event):void {
			trace("connect!");
		}

		private function onClose(e:Event):void {
			trace("close!");
		}

		private function onIoError(e:IOErrorEvent):void {
			trace("io error!");
		}

		private function onWriteResult():void {
			trace("write result!");
		}

		private function onWriteFault():void {
			trace("write fault!");
		}
	}
}

Now, let's talk about why this code is better than the first example.

First, as you can see, we completely eliminated the event class that we created in the first example. There's simply no need to create new Event subclasses when using as3-signals. The framework itself is very flexible and lightweight as it can be.

Next, as you can see, we have established a contract between the client and CustomSocket, binding the notion of "events" to members of the class. There's no key-based guessing-game here. From the layout of the class itself, you can see that CustomSocket dispatches certain types of events. There isn't really a way to add a listener for an event that isn't defined. All "events" (signals?) must be defined explicitly in the class itself. You may also have noticed that in the client class, our listener for "writeResult" doesn't define any parameters. It doesn't have to, since signals by default are empty. No more bloat than needed. Theoretically, you could even rewrite the example to make simple flash.events.Event signals not even pass the event along, since who actually uses the simple Event instance anyway?

Now, let's discuss the code. In as3-signals, there are basically three types of signals. There is Signal, a default implementation of the notion of an event or signal, DeluxeSignal, which adds some advanced functionality to Signal, and NativeSignal, a signal which wraps native events, allowing you to "port" your events into signals.

The signal classes sport a few methods that are fairly important: "add(listener:Function):void", "addOnce(listener:Function):void", "remove(listener:Function):void",  "removeAll():void", and "dispatch(...parameters):void". As you can see, complete control is established over event/signal flow. You can listen for a signal once or indefinitely. You can remove one listener, or all of them at once. Finally, using "dispatch(...parameters)" you can send a signal to all registered listeners.

Let's take a moment to talk about the "dispatch(...parameters)" method. The flash.events.IEventDispatcher.dispatchEvent method takes one parameter of type flash.events.Event. So how do we use "dispatch(...parameters)", then? It's actually quite simple. When creating the signal implementation, you can pass Class objects to the constructor. Let's wire up an example real quick to demonstrate this.

var signal:Signal = new Signal(String, Object);

The above code tells your signal object that, when the dispatch() method is called, the first parameter should be of type String and the second parameter should be of type Object. On the other side, the listener can take typed arguments:

signal.add(myListener);
signal.dispatch("key", {a: "b"});

function myListener(key:String, value:Object):void {

}

As you can probably tell, this eliminates almost any need to create custom Event classes to contain values. All of this is accomplished really only using one class which has already been created and won't need to be reinvented indefinitely as in the AS3 event system. Signal will check and see if types match when "dispatch(...parameters)" is run, and throw an Error if types don't match.

So that's a pretty thorough explanation and intro to using as3-signals. The only downside I have found with as3-signals is since it reinvents the Event system (word pun intended), it won't "just work" with Flex, since Flex is so tied to AS3's event system. Unfortunately, that means I won't be able to use it as often as I would like (ie: all of the time). And that sums up as3-signals! If you have any questions or comments, please feel free to reply below in the comments.

22 thoughts on “as3signals – An Awesome Solution to Events/Signals in AS3

  1. Hi TK, I like your practical example and your explanation is really clear.

    One small thing: it looks like your CustomSocket class has two members named “connect”: var and function. That’s why I like to use past tense with Signal names.

  2. In your first example, there was no need to create a custom Event class either, since the event itself had no custom properties. You could have defined the constants anywhere, or not at all. Frequently in the Flex Framework if all they want to do is define constants they’ll just define an EventKind class that sets out what event kinds are possible. You could also just make the constants part of the CustomSocket class itself.

    I don’t see anything in your code that handles situations where you _actually_ need to extend Event. By that, I mean where your Event carries a “payload” of additional information that needs to be used elsewhere in the program. Possibly this is because usually when we do this, we’ve attached the payload to a bubbling event and the event is coming from an object we don’t care about–instead we care about the payload. Is this a situation that’s different from what signals is designed to handle?

  3. @Robert, I’ll definitely address that, time for an edit :) And thanks so much for the library, it’s so simple and yet solves so many problems, a goal every good developer reaches for, so thanks so much for the library!

    @Amy, I understand where you’re coming from on not REALLY having to define my own Event subclass for CustomSocketEvent since it’s not carrying any significant data, but if we try and apply the last example where we pass a String and an Object with the Event/Signal, we would need to define CustomSocketEvent while using the default AS3 Event system. A good point to make about the library is that not too much of your code changes when you’d like to attach information to your Event/Signal. So I really do like the library and find it very useful.

    Also, Amy, the whole bubbling scenario CAN be handled by as3-signals, though it’s definitely a little different than AS3′s native event system. Since AS3′s native event system uses some secret, hidden magic behind the scenes in flash.events.EventDispatcher instances, it allows for bubbling events from EventDispatcher to EventDispatcher. As far as I know, (correct me if I’m wrong, Robert) bubbling in as3-signals can be accomplished in a few ways. First, I think that if you use the NativeSignal class, you’ll be able to bubble using the traditional AS3 secret system of event propagation. Second, I believe that as3-signals tries to address bubbling by using the flash.display.DisplayObject.parent property to climb the display list.

    As far as the whole capture/target/bubble phase of native events in AS3, I don’t know if as3-signals tries to handle it. To be honest, I’ve worked 8 hours, 5 days a week for the last 2 years with AS3 and I’ve really only had to use the capture phase of events once or twice in my whole experience. And even in those cases, there were workarounds I could have employed to not need to use a capture phase.

    Just a few thoughts. I’ll update the post :)

  4. Pingback: TK Kocheran

  5. Pingback: robpenner

  6. Pingback: wach

  7. Thanks for the tutorial. Signals does look cool but the replacement for Event bubbling is a little unclear to me. But I am sure that will come with time.

  8. Pingback: Ben

  9. Pingback: Bart

  10. @Robert, fixed it :) I just hope people know that this is meant as pseudo-code. Perhaps I’ll later create a compilable project so people can play around with some real code.

  11. Here is my implementation in Flex, without comment:

    CompareFlex.as:
    package {

    import com.mycompany.socket.CustomSocketFlex;
    import com.mycompany.socket.CustomSocketEvent;

    import flash.display.Sprite;
    import flash.events.Event;
    import flash.events.IOErrorEvent;

    public class CompareFlex extends Sprite {

    public function CompareFlex() {
    var socket:CustomSocketFlex = new CustomSocketFlex();
    socket.addEventListener(Event.CONNECT, onConnect);
    socket.addEventListener(Event.CLOSE, onClose);
    socket.addEventListener(IOErrorEvent.IO_ERROR, onIoError);
    socket.addEventListener(CustomSocketEvent.WRITE_RESULT, onWriteResult);
    socket.addEventListener(CustomSocketEvent.WRITE_FAULT, onWriteFault);
    socket.connect(“localhost”, 80); //was 12345

    socket.write();
    }

    private function onConnect(e:Event):void {
    trace(“onConnect: connect!”);
    }

    private function onClose(e:Event):void {
    trace(“close!”);
    }

    private function onIoError(e:IOErrorEvent):void {
    trace(“onIoError: ioerror!”);
    }

    private function onWriteResult(e:CustomSocketEvent):void {
    trace(“writeResult!”);
    }

    private function onWriteFault(e:CustomSocketEvent):void {
    trace(“writeFault!”);
    }
    }
    }

    CustomSocketFlex.as:
    package com.mycompany.socket {

    import flash.events.EventDispatcher;
    import flash.events.Event;
    import flash.events.IOErrorEvent;
    import flash.net.Socket;

    public class CustomSocketFlex extends EventDispatcher {

    private var socket:Socket;

    public function CustomSocketFlex() {
    this.socket = new Socket();
    /*
    this.socket.addEventListener(Event.CONNECT, dispatchEvent);
    this.socket.addEventListener(Event.CLOSE, dispatchEvent);
    this.socket.addEventListener(IOErrorEvent.IO_ERROR, dispatchEvent);
    */
    }

    public function connect(host:String, port:int):void {
    this.socket.connect(host, port);
    return;
    }

    public function write():void {
    var writeValue:Boolean = Math.random() < 0.5;
    this.socket.writeBoolean(writeValue);
    if (writeValue) {
    this.dispatchEvent(new CustomSocketEvent(CustomSocketEvent.WRITE_RESULT));
    } else {
    this.dispatchEvent(new CustomSocketEvent(CustomSocketEvent.WRITE_FAULT));
    }
    }
    }
    }

  12. CustomSocketEvent.as
    ackage com.mycompany.socket {

    import flash.events.Event;

    public class CustomSocketEvent extends Event {

    public static const WRITE_RESULT:String = “writeResult”;

    public static const WRITE_FAULT:String = “writeFault”;

    public function CustomSocketEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false) {
    super(type, bubbles, cancelable);
    }

    override public function clone():Event {
    return new CustomSocketEvent(type, bubbles, cancelable);
    }
    }
    }

  13. CustomSocketEvent.as;
    This example can do without defining custom events, but here it is anyway:
    package com.mycompany.socket {

    import flash.events.Event;

    public class CustomSocketEvent extends Event {

    public static const WRITE_RESULT:String = “writeResult”;

    public static const WRITE_FAULT:String = “writeFault”;

    public function CustomSocketEvent(type:String, bubbles:Boolean = false, cancelable:Boolean = false) {
    super(type, bubbles, cancelable);
    }

    override public function clone():Event {
    return new CustomSocketEvent(type, bubbles, cancelable);
    }
    }
    }

  14. Pingback: Flexeando - Fx{do}

  15. Pingback: vic c

  16. Pingback: AS3 Signals: A Better Solution to Events | The Programmer's Challenge

  17. Pingback: マツカワトモヒロ(曲げるわかめ)

  18. Nice article there…

    I have a quick question though about using Signals to dispatch events from Item Renderers… Basically, is it a good idea? It seems that there is a bind between the Signal dispatcher component and the Signal handler.

    If we have a List or DataGrid with a custom renderer and the Renderers dispatched events using Signals, the parent component would have to bind to the renderers properties to listen for the Signal.

    This, I believe would cause memory leaks because there will always be a ‘handle’ from the parent to the renderer.

    Signals seems pretty cool to solve certain issues but I’m curious to your thoughts for it’s use with Item Renderers?

    Thanks,
    Nick

    • @Nick, yes you should be very careful. If you want to do it right, then you should look into the flash.utils.Dictionary class, as you can use it to effectively create weak references. If you need to use events at all with you item renderers, make sure things are weakly references, from event listeners to Signal handlers.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>