Dynamically Handling Events with Function Expression - actionscript-3

I have a class which exposes literally dozens of events(before you get of on a tangent about whether that's good/bad design, just know that I didn't make that class). The event object of each event(eventParam in the below code) always has a toDebugString function, that basically creates a string containing all of the event object's property values:
propertyName1: propertyValue1
propertyName2: propertyValue2
propertyName3: propertyValue3
It works so far as creating all of the panels, with the title of each panel being the name of the event. However, the big problem is that all of events end up in the TextArea of the last panel. So there is something I don't understand about the anonymous method. It's as if each iteration of the loop uses the same function, and on the last iteration of the loop it decides that the debugPanel that was just created will be the one that all instances of that function will reference. In other words, a new unique debugSubPanel and TextArea is created in each iteration of the loop, but there is only one debugResponseListener event handler shared by all iterations of the loop. So my question is, how can I dynamically create the event handler function dynamically so that it stays associated with the debugSubPanel that I want it to?
public function debugPanelCreated(event:FlexEvent)
{
//iterate through all of the events exposed by mClient.ResponsesDispatcher
//where key is the name of the event
for (var key:String in mClient.ResponsesDispatcher.respMap)
{
//for each event, create a panel containing a text box
var debugSubPanel:Panel = new Panel();
debugSubPanel.title = debugSubPanel.label = key;
var debugSubPanelTextArea:TextArea = new TextArea();
debugSubPanel.addChild(debugSubPanelTextArea);
var debugResponseListener:Function =
function (eventParam :Object) : void
{
//use debugString function to write the properties
//of eventParam to the text box
debugSubPanelTextArea.text = eventParam .toDebugString();
};
//listen to this event:
mClient.ResponsesDispatcher.addEventListener(key,debugResponseListener);
//add the panel for this event
debugPanel.addChild(debugSubPanel);
}
}

Actionscript includes a feature called closures, which means that when you create an inner function and call it, the variables of its parent function are still available. (This is how debugResponseListener = function() ... works at all.) The issue is that a closure is only created when that function is called, and it uses the variable values from their last setting.
You can get around this by making a function that returns the listener function you want.
function makePanelListener(debugSubPanelTextArea:TextArea) : Function
{
return function(eventParam :Object) : void {
//use debugString function to write the properties
//of eventParam to the text box
debugSubPanelTextArea.text = eventParam .toDebugString();
}
}
and in your original code:
var debugResponseListener:Function = makePanelListener(debugSubPanelTextArea);
(There's a little explanation of what's going on in Explaining JavaScript scope and closures, look for the section called "The Infamous Loop Problem". More on closures at jibbering.)

This is the hack I came up with. I really don't like it, but it'll work for now. Open to suggestions still.
public class ResponseDispatcherToDebugStringHelper
{
public var textArea:TextArea;
public function responseToDebugStringHandler(eventParam:Object) : void
{
//use debugString function to write the properties
//of eventParam to the text box
textArea.text = eventParam.toDebugString();
}
}
public function debugPanelCreated(event:FlexEvent)
{
//iterate through all of the events exposed by mClient.ResponsesDispatcher
//where key is the name of the event
for (var key:String in mClient.ResponsesDispatcher.respMap)
{
//for each event, create a panel containing a text box
var debugSubPanel:Panel = new Panel();
debugSubPanel.title = debugSubPanel.label = key;
var debugSubPanelTextArea:TextArea = new TextArea();
debugSubPanel.addChild(debugSubPanelTextArea);
var helper:ResponseDispatcherToDebugStringHelper =
new ResponseDispatcherToDebugStringHelper();
helper.textArea = debugSubPanelTextArea;
//listen to this event:
mClient.ResponsesDispatcher.addEventListener(key,helper.responseToDebugStringHandler);
//add the panel for this event
debugPanel.addChild(debugSubPanel);
}
}

Related

Fire an event when a TextField text property is set

I have a TextField instance on stage. The copy within the field is updated from outside my project by a 3rd party system that I have no access to. I don't know exactly how this is achieved but I need to know when it has happened.
I want a way of telling when the text is updated within the field so I can then format and position the field based on its new content. I've tried
myTextField.addEventListener(Event.CHANGE, updateMethod);
but it seems that this only fires when the text is changed by direct user interaction and not the kind of injection from outside that I am getting.
Is there any way that I can listen for an assignment to myTextField.text rather that only for changes made by the user via mouse/keyboard?
Appreciate any advice. Thanks in advance :)
Instead of directly accessing the text field, you can wrap it with your own class, and dispatch events when the setter is fired: (untested code)
public class MyTextField extends EventDispatcher {
private var textField:TextField;
public MyTextField(t:TextField) {
textField = t;
}
public function set text(s:String) {
textField.text = s;
var e:Event = new Event(Event.CHANGE);
dispatchEvent(e);
}
public function get text():String {
return textField.text;
}
}
And else where in your code:
var mytf:MyTextField = new MyTextField(stage.myTextField); //stage.myTextField is the reference to the text field on the stage
mytf.addEventListener(Event.CHANGE, function(e:Event) { /* get event */ });
// don't use stage.myTextField.text to set text, instead, use:
mytf.text = "setting text!"; //this will trigger the event callback above

Passing arguments into mouseEvent function

This seems simple in other languages, but I don't understand the error. I have 7 buttons that I would like to each take my gallery movieclip to a certain frame when you click on them.
Error: 1067: Implicit coercion of a value of type int to an unrelated type flash.events:MouseEvent.
Error: 1136: Incorrect number of arguments. Expected 2.
Error: 1067: Implicit coercion of a value of type void to an unrelated type Function.
Any help?
function gotoImage(event:MouseEvent, frameParam:int):void
{
MovieClip(this.root).gallery.gotoAndStop(frameParam);
}
t1.addEventListener(MouseEvent.CLICK, gotoImage(1));
t2.addEventListener(MouseEvent.CLICK, gotoImage(2));
t3.addEventListener(MouseEvent.CLICK, gotoImage(3));
t4.addEventListener(MouseEvent.CLICK, gotoImage(4));
t5.addEventListener(MouseEvent.CLICK, gotoImage(5));
t6.addEventListener(MouseEvent.CLICK, gotoImage(6));
t7.addEventListener(MouseEvent.CLICK, gotoImage(7));
You've got two things off with your code:
First, in ActionScript, event handlers always have the same signature:
function someHandler(e:Event):void { .. }
Sometimes the Event argument is a more specific subclass of Event, such as MouseEvent, but there is always just one argument.
The addEventListener method needs a function itself, not the result of invoking a function.
// Here's a function:
function multiply(i1:int, i2:int):int { return i1 * i2; }
// Here's assigning the result of **invoking** a function:
var result:int = multiply(2,3);
// Here's assigning a **function itself** to a variable:
var f:Function = multiply;
// You can invoke the function via the variable f in two different ways:
var result1 = f(2,3);
var result2 = f.apply(null, [2,3]);
So, you'll need to change your code to follow the above to points. You'll have to associate the buttons with jumping to a specific frame one of two ways:
Simple but repetitive: Use a separate handler for each button, with the frame hard coded into each handler.
1a. Named functions (most verbose):
function onT1Click(e:MouseEvent):void {
MovieClip(this.root).gallery.gotoAndStop(1);
}
t1.addEventListener(MouseEvent.CLICK, onT1Click);
// etc. etc.
1b. Anonymous functions:
t1.addEventListener(MouseEvent.CLICK, function(e:Event):void {
MovieClip(this.root).gallery.gotoAndStop(1);
});
// etc. etc.
More elegant: Use the same handler, and store the association between button and frame elsewhere, such as in a Dictionary. If you stick with your naming convention you could even fill the Dictionary in a for loop getting the buttons by name:
var buttonToFrame:Dictionary = new Dictionary();
for(var i:int = 1; i < 8; i++) {
var btn:Button = this["t" + i.toString()];
buttonToFrame[btn] = i;
btn.addEventListener(MouseEvent.CLICK, onClick);
}
function onClick(e:MouseEvent):void {
var btn:Button = Button(e.currentTarget);
var frameNum:int = buttonToFrame[btn];
MovieClip(this.root).gallery.gotoAndStop(frameNum);
}
Just change this
t1.addEventListener(MouseEvent.CLICK, function(me:MouseEvent):void{ gotoImage(me, 1)});
t2.addEventListener(MouseEvent.CLICK, function(me:MouseEvent):void{ gotoImage(me, 2)});
and so on...
This is possible with a roundabout approach. For the event handler, use a function that returns a nested anonymous function.
private var textFieldA:TextField = new TextField;
private var textFieldB:TextField = new TextField;
public function setParameterizedTextWhenTextFieldsAreClicked ():void {
addChild(textFieldA);
textFieldA.text = 'Text field A';
textFieldA.addEventListener(MouseEvent.CLICK, showCustomMessage("One"));
addChild(textFieldB);
textFieldB.text = 'Text field B';
textFieldB.y = 20;
textFieldB.addEventListener(MouseEvent.CLICK, showCustomMessage("Two"));
// NOTE: We must use strongly referenced listeners because weakly referenced
// listeners **will get garbage collected** because we're returning
// an anonymous function, which gets defined in the global namespace and
// thus, the garbage collector does not have anything pointing to it.
}
private function showCustomMessage (message:String):Function {
// NOTE: You can store the following function to a class variable
// to keep it in memory, which would let you use weakly referenced
// listeners when using this as an event handler. Many people
// would find that awkward. I would discourage that.
return function (e:MouseEvent):void {
var textField:TextField = e.target as TextField;
textField.text = message; // "message" argument is available because
// this function's scope is kept in memory.
}
}
Bear in mind, the use of anonymous functions and reliance on function scope being kept in memory seem to present garbage collection complications.

How to call removeEventListener when using addEventListener with parameters passing in AS3

I am trying to make a object, which is a movieclip, move up and down continuously and print its y-axis value whenever it finished a move. The below code works fine.
var tweenUp:Tween = null, tweenDown:Tween = null;
function up():void {
tweenUp = new Tween(person,"y",None.easeNone,person.y,person.y+20,1,true);
tweenUp.addEventListener(TweenEvent.MOTION_FINISH, finishedUp);
}
function down():void {
tweenDown = new Tween(person,"y",None.easeNone,person.y,person.y-20,1,true);
tweenDown.addEventListener(TweenEvent.MOTION_FINISH, finishedDown);
}
function finishedUp(event:TweenEvent):void {
trace(person.y);
tweenUp.removeEventListener(TweenEvent.MOTION_FINISH, finishedUp);
tweenUp = null;
down();
}
function finishedDown(event:TweenEvent):void {
trace(person.y);
tweenDown.removeEventListener(TweenEvent.MOTION_FINISH, finishedDown);
tweenDown = null;
up();
}
up();
However, I am looking for a solution to pass a object to the callback function of listener. I try to use the way shown below but it does not work.
var tweenUp:Tween = null, tweenDown:Tween = null;
var functionFinishedUp:Function = null, functionFinishedDown:Function = null;
function up(object:MovieClip):void {
tweenUp = new Tween(object,"y",None.easeNone,object.y,object.y+20,1,true);
functionFinishedUp = finishedUp(object);
tweenUp.addEventListener(TweenEvent.MOTION_FINISH, functionFinishedUp);
tweenUp.removeEventListener(TweenEvent.MOTION_FINISH, functionFinishedUp);
tweenUp = null;
}
function down(object:MovieClip):void {
tweenDown = new Tween(object,"y",None.easeNone,object.y,object.y-20,1,true);
functionFinishedDown = finishedDown(object);
tweenDown.addEventListener(TweenEvent.MOTION_FINISH, functionFinishedDown);
tweenDown.removeEventListener(TweenEvent.MOTION_FINISH, functionFinishedDown);
tweenDown = null;
}
function finishedUp(object:MovieClip):Function {
return function(event:TweenEvent):void {
trace(object.y);
down(object);
}
}
function finishedDown(object:MovieClip):Function {
return function(event:TweenEvent):void {
trace(object.y);
up(object);
}
}
up(person);
It just goes up and then do nothing because it seems that the listener is removed just after added.
Is there any good solution to remove the listener which has parameters passing, after the listener finished its task?
Thanks in advance for any help you are kind enough to provide!
If you're looking for an easy way to remove an event listener right after the event was received, you can do this:
function listener(event:Event):void
{
// stop listening to the dispatcher for this event type
EventDispatcher(event.target).removeEventListener(event.type, arguments.callee);
// ...and do whatever else you need to do here
}
That line can be used in any event listener.
Another option would be to use Signals (https://github.com/robertpenner/as3-signals). They have an addOnce method that will only listen once and then remove themselves.
First, I advise against using object as the name of a variable! Although technically allowed it is semantically meaningless and unhelpfully similar to the reserved class name Object.
The reason your code isn't working is that you are removing the event handler immediately after adding -- before it ever has a chance to get invoked. You really don't ever have to remove the listeners, because you want them to be invoked every time the respective tweens are complete. If you insist on attaching and detaching the listener functions every iteration you'll have to remove the event listener in the listener function itself. Of course this gets rather tricky with all those closures you're creating and tossing.
In fact you don't need the closures at all, since the tween objects have a reference to the tweened object in the obj property. In addition, you only need to create one up tween and one down tween for each target person, and can then just rewind() and start() each tween back and forth. You can keep track of which tween goes with which person using a couple of Dictionary objects, which use object references as the keys.
Here's a proof of concept that generates twenty persons all with their own set of tweens -- but only two handlers are defined and neither of them are closures. I've also combined up with finishedDown and down with finishedUp:
var upTweens:Dictionary = new Dictionary();
var downTweens:Dictionary = new Dictionary();
for(var i:uint = 0; i < 20; i++) {
// Make a new Person
var person:Person = new Person();
person.y = 100;
person.x = i * 20;
addChild(person);
// Create the tweens but stop them before they can play at all
var top = person.y - 20; // Up is -y in Flash
var bottom = person.y;
var upTween:Tween = new Tween(person, "y", None.easeNone, bottom, top, 1, true);
upTween.stop();
upTween.addEventListener(TweenEvent.MOTION_FINISH, onUpFinished);
var downTween:Tween = new Tween(person, "y", None.easeNone, top, bottom, 1, true);
downTween.stop();
downTween.addEventListener(TweenEvent.MOTION_FINISH, onDownFinished);
// Associate tweens with this person, and start it up!
upTweens[person] = upTween;
downTweens[person] = downTween;
upTween.start();
}
function onUpFinished(e:TweenEvent):void {
var upTween:Tween = Tween(e.currentTarget);
var person:Person = Person(upTween.obj);
var downTween:Tween = Tween(downTweens[person]);
downTween.rewind();
downTween.start();
}
function onDownFinished(e:TweenEvent):void {
var downTween:Tween = Tween(e.currentTarget);
var person:Person = Person(downTween.obj);
var upTween:Tween = Tween(upTweens[person]);
upTween.rewind();
upTween.start();
}

AS3 Event Race Condition

I'm having some trouble solving a race condition that exists when I instantiate new MovieClip classes with some custom events that fire back before I can store the instances in an associative object.
Main Class
var pages:Object = {
"page1":"page1",
"page2":"page2"
};
for(var pageName:String in pages)
{
pages[pageName] = buildPage(pageName, onReady);
}
function buildPage(pageName:String, onReady:Function)
{
var newPage:MovieClip = new (getDefinitionByName(pageClass) as Class)();
newPage.addEventListener("PAGE_READY", onReady);
newPage.dispatchEvent(new Event("PAGE_CREATE"));
return newPage;
}
function onReady(e:Event)
{
for(var pageName:String in pages)
{
trace(typeof pages[pageName]);
}
}
Page 1 & 2 Classes extends MovieClip
function pageX()
{
this.addEventListener("PAGE_CREATE",this.onCreate);
}
function onCreate(e:Event)
{
this.graphics.beginFill(0xFF0000);
this.graphics.drawRect(0,0,200x200);
this.graphics.endFill();
this.dispatchEvent(new Event("PAGE_READY"));
}
Unfortunatley when this all fires off I get:
string
string
object
string
The original string object still exists before I can store the MovieClip instance for later reference. Any suggestions on how to beat this?
The code you posted does not depict a standard class.
Is this code on the movieclip frames or in separate files?
But as far as posted code I can see your issue is with the PAGE_CREATE event firing before the object has been added to the array.
your are doing
newPage.dispatchEvent(new Event("PAGE_CREATE"));
before you return the object.
You did not post the full classes so I really can't help you rewrite your code but, from what you posted I would remove the buildPage method completely and put the code in loop then dispatch the event after the object has been added to the array.
Also, another reason, best practice would be not to call a function from a loopif that function does not have a lot of code in it.
for(var pageName:String in pages)
{
var newPage:MovieClip = new (getDefinitionByName(pageClass) as Class)();
newPage.addEventListener("PAGE_READY", onReady);
pages[pageName] = newPage;
newPage.dispatchEvent(new Event("PAGE_CREATE"));
}

Actionscript3: add and remove EventListeners (with dynamic name and dynamic variables)

I am making a boardgame in flash Action Script 3. Each position on the board is a buttons like this: button_1_1, button_1_2 etc. Whenever a character is selected you want to move it so the script has to add event listeners for positions around the selected unit
// This function adds or deletes an event listener
function listentoButton (isTrue:int, position_x:int, position_y:int):void {
var myFunction:Function = new Function;
myFunction = function ():void {userClickedPosition(position_x, position_y)};
if (isTrue == 1) {
this["button_position_"+(position_x)+"_"+(position_y)].addEventListener(MouseEvent.CLICK, myFunction);
} else {
this["button_position_"+(position_x)+"_"+(position_y)].removeEventListener(MouseEvent.CLICK, myFunction);
}
}
In the rest of the code I have:
function userClickedPosition(position_x:int, position_y:int)
it selects or deselect a unit
function selectUnit(position_x:int, position_y:int):
it uses the listentoButton(1) function to add 8 listeners (the positions around the clicked unit)
function deselectUnit(position_x:int, position_y:int):
it uses the listentoButton(0) function to delete 8 listeners (the positions around the clicked unit)
My question: adding eventlisteners is no problem but removing them dont seem to work? What did I do wrong?
When you go to remove the event, you are using a new instance of myFunction, not the same one you added it with. You either need to declare the function like you would any other function, and use the event args to examine the button's position like. I Think you want the stageX and stageY properties:
http://www.adobe.com/livedocs/flex/3/langref/flash/events/MouseEvent.html
// This function adds or deletes an event listener
function listentoButton (isTrue:int, position_x:int, position_y:int):void {
if (isTrue == 1) {
this["button_position_"+(position_x)+"_"+(position_y)].addEventListener(MouseEvent.CLICK, myFunction);
} else {
this["button_position_"+(position_x)+"_"+(position_y)].removeEventListener(MouseEvent.CLICK, myFunction);
}
}
function myFunction(eventArg:MouseEvent):void {
//use MouseEvent
};
Or you can create a little MyFunctionParameters class to hold the coordinate information and create a new instance of that class, add it to a collection indexed by the x and y coordinates, and later when you go to remove the event, you would lookup the MySpaceParameters instance in the collection, based on x and y coordinates, then use that to remove function.
class MyFunctionParameters
{
public x:int;
public y:int;
function myFunction(eventArg:MouseEvent):void {
userClickedPosition(x,y);
};
}