I am attempting to build a generic XMLLoader class with a static LOAD
method, with the intent to use it as follows...
private var _data:XML = XMLLoader.LOAD("path/to/xml_file.xml");
Then I could use it in any client and go-to-town with e4x.
The general problem I am having is with the URLLoader's COMPLETE
event, which necessarily calls a different function to set the XML data.
This is preventing me from being able to return the XML from the LOAD
method, since the data is being set outside that function. Obviously I need to wait for the COMPLETE
event in order to make sure the data is available.
I though, perhaps, I could create and return a _waitForData
function, which recursively calls itself until the _data is set, then returns the data. But it seems redundant (since Event.COMPLETE
is doing that anyway), and the way I tried it generates a stack overflow error.
Heres what I tried:
public class XMLLoader {
private static var _url:String = "";
private static var _data:XML = null;
public static function LOAD(url:String):XML {
_url = url;
var _xmlLoader:URLLoader = new URLLoader();
_xmlLoader.addEventListener(Event.COMPLETE, _setData);
_xmlLoader.load(new URLRequest(_url));
return _waitForData();
}
static function _setData(e:Event):void {
XML.ignoreWhitespace = true;
_data = new XML(e.target.data);
}
static function _waitForData():XML {
if( _data == null ) {
_waitForData();
}
return _data;
}
public function LoadXML() {}
}
I pretty new to the concept of recursive functions, so I have a feeling I implemented my _waitForData function incorrectly (if its even a good idea at all).
I would appreciate any pointers on using static methods in this way, and whether what I am trying to do is possible...seems like a good idea I think.
You don't want to go the all static route. It's a mess, complicates things without need, is hard to mantain and what if you want to load two xmls at the same time? (hint: you can't).
You are better off creating a regular class that does what you want (load an xml and dispatch an event or call a function with the results). Then, you may add a simpler interface for your calling code; if that's a static function to be used as a one-liner, so be it. This function could be seen as a kind of factory that initiates creates an instance of the class and also initiates the load operation sets the result callback, all in one step (from the caller perspective)
I'm thinking something along these lines (just wrote it and compiles fine; haven't tested it thoroughly, but should be enough to give the idea).
package
{
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.IOErrorEvent;
import flash.events.SecurityErrorEvent;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.utils.Dictionary;
public class XMLLoader extends EventDispatcher
{
private var _loader:URLLoader;
private var _data:XML;
private var _callback:Function;
private static var _map:Dictionary = new Dictionary();
public function XMLLoader(callback:Function = null)
{
_data = null;
_callback = callback;
_loader = new URLLoader();
_loader.addEventListener(IOErrorEvent.IO_ERROR,handleError);
_loader.addEventListener(SecurityErrorEvent.SECURITY_ERROR,handleError);
_loader.addEventListener(Event.COMPLETE,handleComplete);
}
public function load(request:URLRequest):void {
_loader.load(request);
}
private function dispatchError():void {
cleanUp();
dispatchEvent(new Event(Event.COMPLETE));
if(_callback is Function) {
_callback(null);
}
}
private function dispatchSuccess():void {
cleanUp();
dispatchEvent(new Event(Event.COMPLETE));
if(_callback is Function) {
_callback(_data);
}
}
private function handleError(e:Event):void {
dispatchError();
}
private function handleComplete(e:Event):void {
var success:Boolean = false;
try {
_data = new XML(e.target.data);
success = true;
} catch(err:TypeError) {
success = false;
} finally {
if(success) {
dispatchSuccess();
} else {
dispatchError();
}
}
}
public function get data():XML {
return _data;
}
private function cleanUp():void {
if(_loader) {
_loader.removeEventListener(IOErrorEvent.IO_ERROR,handleError);
_loader.removeEventListener(SecurityErrorEvent.SECURITY_ERROR,handleError);
_loader.removeEventListener(Event.COMPLETE,handleComplete);
}
if(_map[this]) {
delete _map[this];
}
}
public static function loadXml(url:String,callback:Function):XMLLoader {
var loader:XMLLoader = new XMLLoader(callback);
loader.load(new URLRequest(url));
// pin down the instance just to be safe; I've seen loaders being collected...
_map[loader] = true;
return loader;
}
}
}
Use:
private function handleResult(data:XML):void {
if(data) {
trace(data);
} else {
trace("failed");
}
}
private function test():void {
XMLLoader.loadXml("your.xml",handleResult);
}
Alternatively, you could switch to use the instance methods, if you need more flexibility:
private function test():void {
var loader:XMLLoader = new XMLLoader(handleResult);
loader.load(new URLRequest("your.xml"));
}
This sample implementation could be used with callback or events. For simplicity I'm just defining one callback function; you could tell if the loading operation succeded by checking the data you receive; null means it failed. Also, I'm dispatching an Event.COMPLETE event to signal the loader finished loading (even if it failed); again, you could know if it failed or succeded by checking the data property. You could use custom events if you want and/or define a fault callback, but I think you want a simpler as possible interface, so I think this should be enough.
You're asking too much of the system, flash wasn't designed for this. You can't (or shouldn't) hang the whole system just in order to wait for one result. Writing in flex/flash necessitates using completion handlers because in some cases, like this, there's just no way around it. If you did things this way, the frames would never move forwards, and everything in the program would come to a screeching halt until the thing returned, making a very unhappy user experience that would feel like "hanging" or "crashing".
On the technical side, if you absolutely, positively insisted on doing this, then you would (1) have to find a way to put a delay in that if-null loop, and (2) change it to a while( _data != null ){ /* wait a while */ }
but I really don't recommend going down that path. More specifically, you need to be careful when calling a function inside itself. In your case, _waitForData
is asking _waitForData
to ask _waitForData
to ask _waitForData
to... etc, until it hits an arbitrary limit and halts. It's called infinite recursion.
Solution: You need to structure your program so that it stops doing what its doing when it needs to load something, and picks up doing what it needs to when a completion handler gets called.
Update:
Solution 2: Write a class that does handles all the XML loading you need. You can have it pre-populate things at the beginning of the program if you know what might be asked-for. Then, as you need more of them, ask the class for more. Then, if it already has it loaded, it can just hand it back immediately. If it doesn't have it, it'll return null
, fetch it in the background and call a handler when it's done. That way, the code calling it can at least try to do things with the XML immediately, but in the end, it will always have to handle the case for when it's not loaded.