Is it okay to create clicklisteners (or other listeners) inside a viewmodelscope? - google-maps

I have a a fragment containing a googleMap where I am creating a bunch of Markers (which also is clickable). They are spiced with different information (colors, shapes and so on) from a room livedata query. In addition I have some MaterialButton buttons (which are styled as pushbuttons) where I toggle the Marker visible status on. At the moment, the "setup" of theese markers takes some time (200ms-2 secs, depends of amount of markers). To get out of that waiting, I was planning to use a viewmodelscope. Since there are some clicklisteners for theese buttons defined in there (they should do some action with the markers), will they still be alive when the viewmodelscope coroutine section ends, and If they are alive, do they still live in the correct coroutine-context, and do I need to do some housekeeping on the listeners when fragment and/or viewmodel ends?
I.E:
class MapsFragment:Fragment(){
private lateinit var mapsViewModel : MapsViewModel
private lateinit var googleMap : GoogleMap
//...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mapsViewModel = ViewModelProvider(requireActivity()).get(MapsViewModel::class.java)
_binding = FragmentMapsBinding.inflate(inflater, container, false)
val root:View = binding.root
//...
return root
}//onCreateView
//...
override fun onViewCreated(view: View, savedInstanceState:Bundle?){
super.onViewCreated(view, savedInstanceState)
//...
mapFragment?.getMapAsync(_googleMap->
_googleMap?.let{safeGoogleMap->
googleMap = safeGoogleMap
}?:let{
Log.e(TAG,"googleMap is null!!")
return#getMapAsync
}
//...
mapsViewModel.apply{
liveDataMapsListFromFiltered?.observe(
viewLifecycleOwner
){mapDetailList->
viewModelScope.launch{
binding.apply{
//...
siteMarkers.map{
siteMarker.remove() //removes existing markes from map on update
}
siteMarkers.clear() //empty the siteMarker array on update
//...
mapDetailList?.map{
it.apply{
//...
coordinateSiteLongitude?.let { lng->
coordinateSiteLatitude?.let { lat->
siteMarkerLatLng = LatLng(lat,lng)
siteLatLngBoundsBuilder?.include(siteMarkerLatLng)
}
}
//...
siteMarkerLatLng?.let { safeSiteMarkerLatLng ->
val siteMarkerOptions =
MarkerOptions()
.position(safeSiteMarkerLatLng)
.anchor(0.5f, 0.5f)
.visible(siteMarkerState)
.flat(true)
.title(setTicketNumber(ticketNumber?.toDouble()))
.snippet(appointmentName)//TODO: Consider build siteId instead
.icon(siteIcon[iconType])
siteMarkers.add(
googleMap.addMarker(siteMarkerOptions) //Here are the markers added
)
}//siteMarkerLatLng?.let
}//it.apply
}//mapDetailList?.map
onSiteCheckedChangeListener?.let{
fragmentMapsMapTagSelector
?.apTagSelectorMaterialButtonSite
?.removeOnCheckedChangeListener(it) //clearing listener on button before update
}
onSiteCheckedChangeListener = MaterialButton.OnCheckedChangeListener { siteButton, isChecked ->
siteMarkers.map {
it.isVisible = isChecked
}
}.also {
fragmentMapsMapTagSelector
?.mapTagSelectorMaterialButtonSite
?.addOnCheckedChangeListener(it)
}
//Will this onCheckedChangeListener still survive when this viewmodelscope runs to the end ?
}//binding.apply
}//viewModelScope.launch
}//liveDataMapsListFromFiltered.observe
}//mapsviewModel.apply
}//getMapAsync
}//onViewCreated
}//MapsFragment

I think you misunderstand what a CoroutineScope is. It determines the lifecycle of coroutines that it runs, but not of the objects created in the process of running those coroutines.
viewModelScope is a CoroutineScope that automatically cancels any coroutines it is running when the associated ViewModel is torn down. The coroutine doesn't know what you're doing with it. Cancelling a coroutine merely stops it from running to completion, like returning from a function early. In your code, you set your listeners and haven't stored references to them besides in the views they are set to, so their lives are tied to their respective view's lives.
If you were going to use a coroutine in your fragment to set up something for your UI, you would use the Fragment's lifecycleScope, not the ViewModel's viewModelScope. Like if you were fetching something to show in your UI, you would want that coroutine to be cancelled when the Fragment is destroyed, not the ViewModel which might be outliving the Fragment.
Your use of a coroutine in your example code looks pointless, because I don't see any blocking or asynchronous suspend functions being called. You mentioned setting up site markers is taking like 200ms. I'm not familiar with Google Maps since I haven't used it in the past several years, so I'm not sure which part is time-consuming. Usually, UI elements do not allow you to interact with them on background threads, so you might be out of luck. But maybe the time-consuming part is allowed to be done on background threads. You'll have to read the documentation. Using a coroutine for this won't make it take less time, but can prevent the UI from stuttering/freezing.
If you were going to do some long computation with a coroutine, you would need to switch dispatchers to do the blocking work and interact with the UI elements back on the main dispatcher. Simply putting something in a coroutine doesn't make it take less time, but it provides a convenient way to do something on another thread and then continue on the main thread after the result is ready. For example:
lifecycleScope.launchWhenStarted { // lifecycle coroutines launch on main thread by default
val result = withContext(Dispatchers.Default) { // switch to dispatcher for background work
doTimeConsumingCalculation()
}
// back on main thread:
applyResultsToMyViews(result)
}
By using launchWhenStarted instead of launch, a Fragment's lifecycleScope will pause the coroutine when the Fragment is not attached, which will prevent potential crashes from trying to update UI using requireContext() or requireActivity() when there is no Activity.

Related

How do you share a variable between scripts using the MovieClip variable?

I'm currently trying to code an interactive timeline for my Uni project (keep in mind im a new coder) and we go over basic actionscript stuff. I was taught to communicate between scripts using a movieclip variable and declaring this.parent.
I have 3 scripts, one that controls the button that is used to move forward in the timeline, one is main, and the other controls the text box which displays the timeline. I placed a number variable in main, initialised at 0(timeCount). In the button script, i have it linked to main using refToMain, my movieclip variable. Within the button script, if the user clicks on the button, it rises the number variable from main using refToMain(refToMain.timeCount). It was my ambition to have the text box script track the number and each number has a different bit of the timeline on. However, when I trace timeCount in the button script, the number seems fine and raises accordingly, however it doesnt change the number in any other script. How can I fix this using basic as3 code?
In Main:
var timeCount:Number = 0;
In Button:
public function mDown (mDown:MouseEvent){
refToMain.timeCount += 1;
if(refToMain.timeCount >= 10){
refToMain.timeCount = 10;
}
trace(refToMain.timeCount);
In timeline:
if(refToMain.timeCount == 0){
timelineText.text = "welcome"
}
if(refToMain.timeCount == 1){
timelineText.text = "hello"
}
Are you expecting the code in your timeline to run continuously instead of just once? A frame script will only run once each time the timeline reaches that frame. And if you only have one frame, the timeline won't advance at all. If that's the case, a simple fix would be to add another frame to your timeline with F5, and then your timeline will alternate between your two frames forever so that your script on frame 1 will execute every other frame.
A better option would be to call the script that updates the timeline text directly every time the button is clicked. So you would move the code from your timeline script to your button script like this:
public function mDown (mDown:MouseEvent) {
refToMain.timeCount += 1;
if(refToMain.timeCount >= 10) {
refToMain.timeCount = 10;
}
trace(refToMain.timeCount);
if(refToMain.timeCount == 0) {
MovieClip(root).timelineText.text = "welcome";
}
if(refToMain.timeCount == 1) {
MovieClip(root).timelineText.text = "hello";
}
}
There are several ways and approaches to access objects and variables across your application.
1) Traversing. The (probably) older and the most straightforward one is fully understanding and controlling the display list tree. If you understand where your current script is and where your target script is, you just traverse this tree with root to go straight to the top, parent to go level up and getChildByName or [] or dot notation to go level down.
Pros: it's simple. Contras: The weak point of this approach is its inflexibility. Once you change the structure of display list tree, the access would presumably be broken. Also, this way you might not be able to access things that are not on the display list. Also, there are cases the dot notation would not work, and there are cases getChildByName would not work. Not that simple, after all.
2) Bubbling events. These are events that bubble from the depths of display list to the root. Mouse events are bubbling: you can catch it anywhere from the deepest object that had some mouse event then all its parents right up to the stage. You can read about them here. So, you can send bubbles from whatever depth you want then intercept them at the any parent of the event target:
// *** TextEvent.as class file *** //
package
{
import flash.events.Event;
public class TextEvent extends Event
{
static public const TEXT_EVENT:String = "text_event";
public var text:String;
// Although it is not a very good practice to leave the basic Event
// parameters out of it, but it will do for this example.
public function TextEvent(value:String)
{
// Set type = "text_event" and bubbles = true.
super(TEXT_EVENT, true);
text = value;
}
}
}
// *** Button script *** //
import TextEvent;
// Dispatch the event.
dispatchEvent(new TextEvent("welcome"));
// *** Main timeline *** //
import TextEvent;
// Subscribe to catch events.
addEventListener(TextEvent.TEXT_EVENT, onText);
function onText(e:TextEvent):void
{
// Extract the passed text value.
timelineText.text = e.text;
}
Pros: it is good in an app architecture terms. Contras: you cannot catch the bubbling event at the point that is not parent of event source.
3) Static class members. Or singleton pattern, its basically the same. You can devise a class that shares certain values and references over the whole application:
// *** SharedData.as class file *** //
package
{
import flash.display.MovieClip;
public class SharedData
{
static public var MainTimeline:MovieClip;
}
}
// *** Main timeline *** //
import SharedData;
// Make root accessible from anywhere.
SharedData.MainTimeline = this;
// *** Button script *** //
import SharedData;
// You can access main timeline via shared reference.
SharedData.MainTimeline.timelineText.text = "welcome";
Pros: you are not limited by display list structure any more, you can also share non-visual instances this way, anything. Contras: careful with timelines, they tend to destroy and create timeline instances as playhead moves, so it is not impossible to end up with a reference to a removed object while timeline holds a new instance that is no longer shared.

Create delay in ActionScript 3 function

I have a function in Adobe Flex 4 (ActionScript 3) that accepts an object and returns an ArrayCollection...
If a certain global variable is set to true, I want the function to delay itself for 3 seconds before running. Otherwise I want the function to run as normal.
The problem is, if I use a Timer, that timer calls a separate function, and that function cannot return anything to my calling function, nor can the function it calls accept any parameters, so it's not like I can call my own function recursively after the TimerComplete event fires... And a recursive call wouldn't work anyway, because it would return the ArrayCollection to the timer-result function, not to the original calling function...
I need a delay within the function, not a delay that causes me to go outside that function. But I cannot figure out how to do it.
Something like this is what I need to do:
private function createArrayCollection(myObject:Object):ArrayCollection {
var myArrayCollection:ArrayCollection = new ArrayCollection();
if (globalWaitBoolean) {
//delay here for 3 seconds, somehow
}
//Here I do the stuff that uses the info in myObject to figure out what to
//put into the ArrayCollection I want to return
return (myArrayCollection);
}
So... Any ideas on how to accomplish this without calling an external Timer function that cannot return an object back to my original function?
Thanks,
The way you want it you will have your whole application to lag for 3 seconds, unresponsive to any user input and external events. But it is possible, sure:
import flash.utils.getTimer;
private function createArrayCollection(myObject:Object):ArrayCollection
{
var myArrayCollection:ArrayCollection = new ArrayCollection;
if (globalWaitBoolean)
{
var waitUntil:int = getTimer() + 3000;
// Method getTimer() returns time in ms passed since app start.
// So you just have to wait until it is greater than appointed time.
while (getTimer() < waitUntil)
{
// Do nothing.
}
}
return (myArrayCollection);
}
Still, if you want to do it in a correct way of doing it:
import flash.utils.setTimeout;
private function callerMethod():void
{
// Blah blah blah.
// ...
// Finally.
createArrayCollection(sourceData, asyncResult);
}
private function createArrayCollection(myObject:Object, handler:Function):void
{
var result:ArrayCollection = new ArrayCollection;
if (globalWaitBoolean) setTimeout(handler, 3000, result);
else handler(result);
}
private function asyncResult(source:ArrayCollection):void
{
// The rest of your processing code.
}
Normal (synchronous) code flow would not return until the value is prepared, so should you desire to actually wait for 3 seconds while not allowing your app to do anything, use getTimer() approach from #Organis's answer. If you'll go for an asynchronus result, you'll need to face and overcome some more problems.
First, when do you expect your returned ArrayCollection to actually arrive. Speaking of code design, asynchronous code requires a whole lot of assumptions, thread safety etc etc, and even while AS3/Flash does not have true multithreading unless you count Workers, the code flow with events is not as obvious. So, whoever called your createArrayCollection() MUST NOT expect value returned from it right away. So, speaking about your direct question, NO, you can't avoid timers of some sort if you desire a responsive application. But you can use them with an approach that would involve an indirectly returned result.
Second, whether there might be concurring requests for more array collections from objects if your app would require these - you have to prepare for any kind of interference that might be caused by this. Say your function is triggered by a button click - what if that button would get clicked more than once in 3 seconds?
Third, actual route to processing code is not direct with asynchronous return. You need either a callback, an event handler (which is essentially a semi-native callback), a code that periodically checks for value presence (enter frame handler, etc) or a similar trick to gather the value that's returned asynchronously, and then transfer it to any relevant code that would process it further. Therefore, you would need to design an interface capable of receiving complex data (source object forward, array collection backward) and then carefully test it against all the possible cases and flaws.
An example of implementing all that is very long, I'll try to outline it somehow. Ler's assume you have a sort of "server" class that accepts requests for data and processes it synchronously (no wait) or asynchronously (wait). It accepts a source object of type "T" and provides a newly created object of type ArrayCollection, supplied as a parameter to whatever callback function sent to it. Also it accepts a delay (a simple way to show sync/async return would be a boolean, but why not getting an int?) as a parameter, and guarantees (to the extent of event model limitations) that after this delay the callback will be called ASAP. The architecture will then look like this:
class Processor {
Dictionary requests; // here all the requests that are delayed will be stored
public function dpr(source:T,callback:Function,delay:int=0):void{...}
// creates requests and stores them
private function syncProcess(source:T):ArrayCollection {...}
// whatever routine you want to get variably delayed
private function processTimeout(e:Event=null):void {...}
// processes events from "setTimeout()" and calls callbacks
}
Note that asynchronous approach forced to create three more entities than a synchronous one. First is the request holding structure (the dictionary here), second is timeout event handler, third is whatever callback you'll desire to get called when the data is ready. The code flow would go like this:
Synchronous call would result in the callback directly called from within the class: request->processTimeout->syncProcess()->callback. Asynchronous call will have the callback called from within Timer::timerComplete event handler via setTimeout called within request, with data that originally came from request stored in requests.
You could use an embedded/inline function:
private function createArrayCollection(myObject:Object):ArrayCollection {
var myArrayCollection:ArrayCollection = new ArrayCollection();
if (globalWaitBoolean) {
var milliseconds:int = 3000;
//delay here for 3 seconds
setTimeout(function()
{
//Here I do the stuff that uses the info in myObject to figure out what to
//put into the ArrayCollection I want to return
return (myArrayCollection);
},
milliseconds);
}
else
{
//Here I do the stuff that uses the info in myObject to figure out what to
//put into the ArrayCollection I want to return
return (myArrayCollection);
}
}
The inner function will have access to all local vars of the outer function.

Deferring PropertyChanged events until view bindings setup complete

A number of our MVVMcross views depend remote services to fully display themselves. We typically kick this off a Task in ViewModel's Init() using to get it async. ViewModel properties are set in the Task upon completion, UI updated via PropertyChanged notifications.
Sometimes the remote data (and task) completes before the View has bound it's listeners and thus no property changed event is received.
This issue is touched on at async Init and Property Changed in MvvmCross but the solution feels like duplication of presentation logic.
We've had success buffering PropertyChanged notifications until the end of ViewDidLoad, but we'd like to turn below into a more generic solution by hooking into the MVX framework.
Is there a way to hook mvvmcross's view creation to fire our code off after viewDidLoad completes?
Base View Model
public abstract class BaseViewModel : MvxViewModel{
protected bool _deferPropertyChangedEvents = true;
private readonly List<PropertyChangedEventArgs> _deferedPropertyChangedEvents = new List<PropertyChangedEventArgs>();
public override void RaisePropertyChanged(PropertyChangedEventArgs changedArgs)
{
lock(_deferedPropertyChangedEvents){
if (!_deferPropertyChangedEvents)
{
base.RaisePropertyChanged(changedArgs);
}
else
{
// buffer it up
_deferedPropertyChangedEvents.Add(changedArgs);
}
}
}
public void EndDeferringPropertyChangedEvents()
{
lock(_deferedPropertyChangedEvents){
_deferPropertyChangedEvents = false;
// playback all buffered notifications
foreach (var e in _deferedPropertyChangedEvents)
{
RaisePropertyChanged(e);
}
_deferedPropertyChangedEvents.Clear();
}
}
}
Sample view
public class SomeView : MvxViewController
{
public override void ViewDidLoad()
{
base.ViewDidLoad();
var bindings = this.CreateBindingSet<StopView, SomeViewModel>();
.....
bindings.Apply();
// plays back any PropertyChanged() notifications that were buffered
// up while the view was initializing
// ---> want to find a way to have MVX call this
ViewModel.EndDeferringPropertyChangedEvents();
}
}
As a simple answer, I believe your own line can easily be called using a BaseViewModel cast:
// ---> want to find a way to have MVX call this
((BaseViewModel)ViewModel).EndDeferringPropertyChangedEvents();
However, on a more technical note, I think it might be useful to further examine and understand why this Deferring code is necessary - to further take a look at what the underlying threading problems are.
There are a number of factors that are puzzling me at present::
During the line bindings.Apply(); all current bound property values should be transferred from the ViewModel to the View - so calling EndDeferringPropertyChangedEvents(); in the next line should (in theory) only rarely get different values.
Further, the default MvvmCross RaisePropertyChanged method changed notifications across to the UI thread. Because ViewDidLoad is also invoked on the UI thread, this means that any RaisePropertyChanged calls made on background threads during ViewDidLoad should all be automatically deferred until after ViewDidLoad has finished and the UI thread becomes available.
Looking at the MvxNotifyPropertyChanged code, the only potential gap I can see where mutli-threading might find a way through this automatic RaisePropertyChanged deferral is in this optimisation check:
// check for subscription before potentially causing a cross-threaded call
if (PropertyChanged == null)
return;
(from https://github.com/MvvmCross/MvvmCross/blob/v3.1/Cirrious/Cirrious.MvvmCross/ViewModels/MvxNotifyPropertyChanged.cs#L76)
If your ViewModel Init method is also using async for it's Task management, then this async code should also be using the UI thread - so the "callback" of this async operation should also be marshalled back to the UI thread (and so shouldn't be executed during ViewDidLoad itself).
As I said, these factors are puzzling me - I don't have a definitive answer/explanation - sorry! But I'd love to see an example problem and to try to help solve it at a generic level.

How can I track all of my Box2D collisions in a clean, manageable manner?

I am using Box2D for the first time seriously in a medium sized Flash Game that I am working on. My current experience with Box2D is limited to creating a world, bodies and adding those bodies to the world in a functional manner.
I'm finding it easy enough to integrate Box2D into my game environment, maintaining well-written code and have completed a few tutorials that walk through dealing with collisions. The issue that I'm facing now is that my game will have many bodies, each interacting with other bodies in different ways, and I'm finding it hard to write my own b2ContactListener subclass without it getting extremely messy.
Based off a tutorial I used, I have created my own subclass of b2ContactListener and added an override of the BeginContact() method. The argument that BeginContact() receives when it is called will reference an instance of b2Contact, through which I can access two b2Fixture instances (the two instances that have collided). I am then able to access the b2Body instance associated with each of those b2Fixtures.
Problem: Currently I have a roundabout way of finding out what two things collided (i.e. whether they're a wall and a missile, or the player and a tree, etc) which uses GetUserData() and looks like this as an example:
var f1Player:Boolean = contact.GetFixtureA().GetBody().GetUserData() is Player
var f2Player:Boolean = contact.GetFixtureB().GetBody().GetUserData() is Player
var f1Tree:Boolean = contact.GetFixtureA().GetBody().GetUserData() is Tree
var f2Tree:Boolean = contact.GetFixtureB().GetBody().GetUserData() is Tree
// ... continutes with all possible combinations.
// Example of managing a collision:
if(f1Player && f2Tree)
{
// Player (FixtureA) and Tree (FixtureB)
}
if(f2Player && f1Tree)
{
// Player (FixtureB) and Tree (FixtureA)
}
As you can see, this is going to end up extremely long and unmanageable. I also have to write each set of actions to perform twice to cater for a certain element being FixtureA or FixtureB, or vice versa (obviously in the form of a function call with the parameters swapped around rather than literally re-written).
This is clearly not the correct approach, but I haven't been able to locate resources that more thoroughly explain collision detection management.
Does anyone have experience with collision detection management using Box2D that they can share? Also, is using SetUserData( entityThatOwnsTheBody ); the correct way to be using that method?
Yeah, it's a bit of a nuisance indeed. Actually I think the way you have it is quite typical.
fwiw Box2D itself has to deal with a similar problem when testing whether fixtures overlap. There are a bunch of functions such as b2CollideCircles, b2CollidePolygonAndCircle, b2CollidePolygons etc, and when two fixtures come near each other the engine chooses which of these functions should be used.
It does this by putting the function pointers in a 2-dimensional array, then looks up the appropriate function in this array by using the two shape types as index. See the first three functions in b2Contact.cpp for details.
Of course, if you can't pass around function references like this in AS3 then I guess this answer doesn't help much, but I thought I would post anyway as C/C++/JS users might come by.
I've used c++ version of Box2d, but I think the same approach will work in actionscript. I create a class Object, that contain a b2Body *_body pointer and a pointer to graphical representation. _body's UserData was set to point to Object *. class Object had the following methods:
virtual bool acceptsContacts ();
virtual void onContactBegin (const ContactData &data);
virtual void onContactEnded (const ContactData &data);
virtual void onContactPreSolve (const ContactData &data);
virtual void onContactPostSolve (const ContactData &data);
When collision was detected in b2ContactListener subclass, it checked if collided bodies have user data. If so, it casted their user data to Object* and if any of the collided objects accepted contacts - it created ContactData ( a class with all required information about collision) and put it in it's internal list to deliver later.
When b2World::update method returned, ContactListener delivers all contact information to objects to process. Delivery was delayed in order you could create new bodies, joints and so on, right when processing collision (which is not allowed while update is executing)
Also you must notify ContactListener (just put a pointer to it inside ContactData) if one of the collided body was deleted during collision processing, so it can invalidate appropriate contacts and not deliver them
I've come up with something much nicer than the original.
Firstly, I just have my Being class (which owns a b2Body) set itself as its bodies' UserData. This class will also contain an onContact() method and look similar to the below:
public class Being
{
private var _body:b2Body;
public function Being()
{
// Define the body here.
// ...
_body.SetUserData(this);
}
public function onCollision(being:Being = null):void
{
//
}
}
Then in my own b2ContactListener implementation, I simply pass the colliding Being (or null, if there is no Being assigned to the colliding b2Body's UserData) to the opposing Being's onCollision():
override public function BeginContact(contact:b2Contact):void
{
var bodyA:b2Body = contact.GetFixtureA().GetBody();
var bodyB:b2Body = contact.GetFixtureB().GetBody();
var beingA:Being = bodyA.GetUserData() as Being || null;
var beingB:Being = bodyB.GetUserData() as Being || null;
beingA && beingA.onCollision(beingB);
beingB && beingB.onCollision(beingA);
}
And finally in each of my subclasses of Being, I can easily prepare logic appropriate for a collision between other Beings of a certain type:
class Zombie extends Being
{
override public function onCollision(being:Being = null):void
{
if(being && being is Bullet)
{
// Damage this Zombie and remove the bullet.
// ...
}
}
}

Selecting Input TextField throws Security sandbox violation in loaded swf in Adobe AIR

I have a swf, loaded into the non-application sandbox in Adobe AIR 1.5 (the shell is already installed with our users so I can't update to version 2+).
On the stage in the swf are buttons, movieclips, animations etc - all of these work fine.
When we add an input TextField, selecting this TextField causes a Security Sandbox Violation.
Error message (in debug mode) is (I've edited the actual file names):
[trace] *** Security Sandbox Violation ***
[trace] SecurityDomain 'file:///path/to/local/loaded.swf' tried to access incompatible context 'app:/loadingApp-debug.swf'
The user then is unable to enter text into the TextField. The rest of the application is unaffected.
The FDB stacktrace only shows:
this = [Object 57216577, class='flash.utils::Timer'].Timer/tick() at <null>:0
Has anyone got a workaround for this?
I'm guessing it's either the TextField attempting to access the stage, or an event attempting to bubble / access global properties.
I understand the air sandbox restrictions and use them daily - with sandboxBridges from parent to child and child to parent etc - perhaps there is something I need to expose to allow this to work?
Any clues?
Edit:
I've now tracked down the problem to being that the TextField attempts to do
this.stage.focus = this;
or something equivalent when MouseDown happens.
It also appears that there is no access to KeyboardEvents in loaded swfs, so my thought of making the 'field' a button and then controlling input by listening to KeyboardEvents is dead in the water.
Now looking at whether to relay events to callbacks passed through the parent sandbox bridge, or whether minimal comps might save my butt.
Ok, I have an insane workaround, but it's pretty solid. I'm going to post it almost in full here, though I'll probably make it generic and upload it to github at some point.
In my shell, I have a view-with-mediator (I'm using robotlegs) which I'm calling EventRelayer and EventRelayerMediator.
The view's only purpose is to give the mediator access to the stage.
I exposed some functions on the parentSandboxBridge:
public function requestKeyboardEventRelay(eventType:String, callback:Function):void;
public function requestMouseEventRelay(eventType:String, callback:Function):void;
public function cancelKeyboardEventRelay(eventType:String, callback:Function):void;
public function cancelMouseEventRelay(eventType:String, callback:Function):void;
My sandbox bridges always just translate into strong typed events, so these fire events like:
RelayEvent(RelayEvent.START_RELAY_REQUESTED, KeyboardEvent, eventType, callback);
RelayEvent(RelayEvent.CANCEL_RELAY_REQUESTED, MouseEvent, eventType, callback);
These are picked up by the EventRelayerMediator, and translated into handlers in an eventMap:
override public function onRegister():void
{
createRelayHandlerFactories();
eventMap.mapListener(eventDispatcher, RelayEvent.START_RELAY_REQUESTED, startRelay);
}
protected function startRelay(e:RelayEvent):void
{
var handler:Function = createRelayHandler(e.relayEventClass, e.callback);
eventMap.mapListener(view.stage, e.relayEventType, handler, e.relayEventClass);
}
protected function createRelayHandler(relayEventClass:Class, callback:Function):Function
{
var handler:Function = relayHandlerFactoriesByEventClass[relayEventClass](callback);
return handler;
}
protected function createRelayHandlerFactories():void
{
relayHandlerFactoriesByEventClass = new Dictionary();
relayHandlerFactoriesByEventClass[KeyboardEvent] = createKeyboardEventRelayHandler;
relayHandlerFactoriesByEventClass[MouseEvent] = createMouseEventRelayHandler;
}
protected function createKeyboardEventRelayHandler(callback:Function):Function
{
var handler:Function = function(e:KeyboardEvent):void
{
trace("Relaying from shell: " + e.toString());
// passing an object because the sandbox bridge doesn't allow strong typed values, only primitives
var o:Object = {};
o.type = e.type;
o.charCode = e.charCode;
o.keyCode = e.keyCode;
o.altKey = e.altKey;
o.ctrlKey = e.ctrlKey;
o.shiftKey = e.shiftKey;
// no point adding other props as we can't pass them
// to the constructor of the KeyboardEvent
callback(o)
}
return handler;
}
The loaded swf passes a callback which just re-assembles and re-dispatches the events.
My input TextField is now just a dynamic field with a click handler that activates listening for keyboard events on the root of the swf, and then updates the dynamic field accordingly.
At the moment that is super-crude but I'll break it out into a robust, tested class now I know it works.
I've used a dictionary to manage the handlers because I'm sure that memory leakage hell is about to follow and I'm expecting to have to relay the FocusEvents to stop entering text.
I need to test memory leakage, return a binding from the parentSandboxBridge function so that I can make sure I don't add the same handler twice etc etc, but Adobe - you suck for not calling this out and providing a built in relay mechanism.