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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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.

1
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:

1
2
3
4
5
6
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.

Share This:
  • RSS
  • Twitter
  • del.icio.us
  • StumbleUpon
  • Digg
  • Facebook
  • Technorati
  • Reddit