views:

309

answers:

3

I've been working with AS3 a lot over the last weeks and I've run into a situation that google hasn't been able to help with. The crux of the problem is that before I run a function I need to know that certain conditions have been met, for example loading my config.xml and putting it into a class variable. Where I run into trouble is that URLLoader is async so I can't put code after the load, the only thing I can do is put it in a lambda on the listener.

var conf:Object = new Object();

var ldr:URLLoader = new URLLoader();
ldr.addEventListener(Event.COMPLETE, geoLoader, false, 0, true);
ldr.load(new URLRequest("http://ipinfodb.com/ip_query.php"));

function geoLoader (e:Event):void
{
    var data = new XML(e.target.data);

    conf.zip    = data.ZipPostalCode.toString();
    conf.city   = data.City.toString();
    conf.state  = data.RegionName.toString();
}

Or slightly more concisely and I believe more understandable:

var conf:Object = new Object();

var ldr:URLLoader = new URLLoader();
ldr.addEventListener(Event.COMPLETE, function (e:Event){
    var data = new XML(e.target.data);

    conf.zip    = data.ZipPostalCode.toString();
    conf.city   = data.City.toString();
    conf.state  = data.RegionName.toString();
}, false, 0, true);
ldr.load(new URLRequest("http://ipinfodb.com/ip_query.php"));

This worked great for short bits of code, but I've got a bunch of functions that are relying on huge amounts of preconditions (this pollutes the namespace if I use the first method) or functions that rely on one bit of the stack (which means that I can't use the second method even though it's slightly more obvious to me).

So, my question is: How do flash professionals handle preconditions? I'm trying to find a solution that allows tons of preconditions as well as a solution that allows different orders of preconditions or just a few preconditions from a group.

Edit: I'm not sure I made it clear originally but the problem is that I'll have 5 preconditions which means that I end up with 5 of the above blocks. As you can imagine having 5 nested listeners makes for terrible spaghetti code. An example, one of my functions needs to know: when the config.xml is loaded, when the location is updated, when the current weather conditions have been loaded, and when the images for the current weather have been loaded. However there is another function that needs to know only when the config.xml is loaded and the location is updated. So it's a really complicated set but I'm sure that I'm not the only one out there that's had to deal with a complex set of events.

Also worth noting, I have looked at Senocular's Sequence.as but to the best of my understanding that's based on returns rather than events. But I could be wrong about this.

Thanks for any help you can offer!

+1  A: 

What I usually do is load as much as I can at the start, just because I'm lazy and I want to make sure I've got everything I need ready before I actually do anything.

you can make use of 2 things:

  1. The Event.COMPLETE handler, that lets you know when data is ready
  2. your conf object.

I would actually declar conf, but I wouldn't initialize it there. I would initialize it when the data is ready. It wouldn't help beforehand anyway. This would allow me though to check if the data is there...If conf is not null, then there might some data for me to use, otherwise if the data wasn't loaded, trigger the load, etc.

HTH, George

George Profenza
That makes sense to leave the conf empty. But the main problem is that I'll end up with like six of these preconditions and the code looks awful. Or the conditions may be much more complex than just conf being empty, it may be something like conf is full, location object has been built with x properties, and three events have been dispatched at some point in the past. So that would be 5 nested listeners, but the listeners may have already fired in the past so I need storage too. It's hairy.
Chuck Vose
Can you not load all the data first then initialize all your other elements ? Something like BulkLoader could come in handy
George Profenza
+1  A: 

Maybe this is a situation for polling? Create "Conditionals" that have an enterframe listener that calls their ovverridden isSatisfied() method, where you put whatever logic will tell you if it's time to execute()

public function onEnterFrame(event:Event):void
{
   if(isSatisfied())
   {
     execute();
     removeEventListener(Event.ENTER_FRAME,onEnterFrame);
   } 
}
justkevin
I'm going to have to research this a little bit more but this seems closer to the mark. I like being able to have that if statement.
Chuck Vose
+3  A: 

I use a combination of open-source libraries and custom classes to handle this exact type of situation.

For loading, I mostly use BulkLoader as it will handle anything you want to load and has all of the events ready to listen for.

The main thing I strive to do is keep my implementation decoupled as much as possible. For this reason, any function that relies on data is handled via a subscription, notification setup. I use a custom class which I call DataController. The DataController is a singleton class that has a few core functions including:

subscribe, notify

Let's say I have the situation where I want to load a bunch of images, but only when my XML containing the image paths has been fully loaded I would do that like this:

Inside my document class, or some other controller type class I would use BulkLoader to load some XML. In the complete handler, I would grab the XML, say imageXML and run a command like this:

DataController.instance.notify("ImageXMLLoaded", imageXML);

In my ImageHolder class I would have the following:

public function ImageHolder() {
    DataController.instance.subscribe("ImageXMLLoaded", _populate);
}

private function _populate($imageXML:XML):void {
    // do whaterver with your XML
}

The DataController has a few instance variables like these:

private var _subscribers:Dictionary = new Dictionary(true);
private var _processed:Dictionary = new Dictionary(true);
private var _requests:Dictionary = new Dictionary(true);

The subscribe function looks something like this:

public function subscribe($eventName:String, $subscriber:Function): void {
    var _funcArray:Array;
    var _func:Function;
    if (_processed[$eventName]) {
        $subscriber(_processed[$eventName]);
        return;
     }
     if (! _subscribers[$eventName]) {
        _subscribers[$eventName] = new Array();
     }
     _subscribers[$eventName].push($subscriber);
}        

And the notify function looks like this:

public function notify($eventName:String, $data:*, $args:Object = null): void {
    var _cnt:Number;
    var _subscriberArray:Array;
    var _func:Function;

   _processed[$eventName] = $data;

   if (_subscribers[$eventName] != undefined) {
       _subscriberArray = _subscribers[$eventName].slice();
       _cnt = 0;
       while (_cnt < _subscriberArray.length) {
           _func = _subscriberArray[_cnt] as Function;

           if ($args) {
               _func($feedXML, $args);
           } else {
               _func($feedXML);
           }
           _cnt++;
        }
    }
    if (_requests[$eventName]) {
        _subscriberArray = _requests[$eventName].slice();
        delete _requests[$eventName];
        _cnt = 0;

        while (_cnt < _subscriberArray.length) {
        if (_subscriberArray[_cnt] != null) {
                _subscriberArray[_cnt]($data);
            }
            _cnt++;
        }
    }
}

And my request function:

public function request($eventName:String, $requestFunction:Function): void {
    var _requestArray:Array;
    var _request:Function;

    if (! _feeds[$eventName]) {
        if (! _requests[$eventName]) {
            _requests[$eventName] = new Array();
        }
        _requests[$eventName].push($feedFunction);
    } else {
        $requestFunction(_feeds[$eventName]);
    }
}

Basically, when you call subscribe it will first check to see if the $eventName data has been already set in _processed. If it has, it will pass that data to its subscribing function (_populate in this case.) If it hasn't been loaded, it will add the subscribing function to the _subscribers Dictionary and do nothing else. When the data is sent via notify, it will be set in the _processed Dictionary first, then DataController will loop over and subscribers for the $eventName and pass the data to each of the subscribing functions.

There are also methods I omitted that let you do the same thing, but without actually passing any data. These are useful for animation events, or chaining any other type of events together.

I also have methods to delete subscribers and remove data from _processed Dictionary.

I hope this all makes sense.

EDIT

Based off of your edit, you could do something like this:

Code somewhere, maybe in your Document Class.

public function startMyLoading():void {
    loadConfig(onConfigLoaded);
}

public function onConfigLoaded($xml:XML):void {
    DataController.instance.notify("configLoaded", $xml);
    loadWeatherContiditons(onWeatherConditionsLoaded);
}

public function onWeatherConditionsLoaded($xml:XML):void {
    DataController.instance.notify("weatherXMLLoaded", $xml);
}

Code somewhere, maybe some weather display view:

public function WeatherDisplayView():void {
    DataController.instance.subscribe("weatherXMLLoaded", _populateWeatherPanel);
}

private function _populateWeatherPanel($xml:XML):void {
    // for each (var weatherNode:XML in $xml.children()) ...
    // use BulkLoader to load, and add Event.COMPLETE function for when it is finished.
}

private function _onBulkLoaderComplete($evt:Event):void {
    DataController.instance.broadcast("ImagesComplete"); // the broadcast function is one that I did not include above, but basically works the same as subscribe, notify, but without any data being passed.
}

Code in something only requiring config.xml to be loaded:

public function SomeClass():void {
    DataController.instance.subscribe("configLoaded", _handleConfigLoaded);
}

private function _handleConfigLoaded($xml:XML):void {
    // do something with config
}

Using this method, your requests are actually synchronous. After it loads the config.xml it loads the weather.xml. After that it loads something else... etc, etc.

You can also load your 5 things up with BulkLoader and listen for each one to complete. Firing off notifications and broadcasts as needed.

sberry2A
Hmm, I'd never heard of BulkLoader before... it seems I've created something similar (though a bit less complex and not quite as flexible)!
Cameron
BulkLoader, FTW! I have even abstracted BulkLoader and LoadingItem to have BulkLoaderGroup and BulkLoaderEntry. This allows me to control individual LoadingItem's COMPLETE handlers.
sberry2A
This is awesome, thanks for the input, it's exactly what I was looking for!
Chuck Vose