Adventures in Actionscript 3.0: Building an iTunes-like Browser with Flash and XML

Breakdown of the interface for the browser.swf
Breakdown of the interface for the browser.swf

UPDATED:  Download the project files here: browser.zip

Creative Commons License
XML-Driven iTunes-like Browser in ActionScript 3 by Aaron Silvers is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License.
Based on a work at www.aaronsilvers.com.

So, I’ve scripted me up an application with two listboxes, working off of one set of XML data exported (as previously detailed) from MS Access.

Now for the explanations…

import flash.geom.ColorTransform;

In order to dynamically change the color of a movieclip, you need to bring in the ColorTransform class. I use this to change the background_mc shown in the diagram above — I want to change the color to indicate if the resource being presented is currently available, or available when there’s a big enough pool to demand a course as instructor led training.

var xmlLoader:URLLoader = new URLLoader();
var xmlData:XML = new XML();
xmlLoader.addEventListener(Event.COMPLETE, LoadXML );

xmlLoader.load( new URLRequest( "resources.xml" ) );

With AS3, you can’t just call getURL anymore. It’s deprecated. So, you need to create an instance of the URLLoader class and use its internal method of load to bring it in. The nice thing about these events is that Flash’s Run-Time can detect when you’ve actually completely loaded the URLRequest — which is why the addEventListener above calls on my function/method LoadXML when the loading event is complete.

var MasterArray:Array = [];
var TopLevelArray:Array = [];
var TempSecondLevelArray:Array = [];
var TopIndexLength:Number;
var TempSecondIndexLength:Number;

I’m manipulating the XML in a couple of different ways. I have a hunch that Flash handles its own array data a lot faster than it manipulates XML, even when it’s internalized. That’s the way it used to be, and that’s just what guides my decision making, so I’m going to use MasterArray to do a direct capture of the XML data as an Array inside of Flash, and then use the TopLevelArray and the TempSecondLevelArray to help me maintain just the information I need to populate those two listboxes, respectively.

// Hide the Launch button by default
availability_mc.launch_btn.visible = 0;

function LoadXML( e:Event ):void
{
	xmlData = new XML( e.target.data );
	ParseData( xmlData );
}

The LoadXML method uses the data from the Event object and references it as an XML object, which is then used by my ParseData method, below…

function ParseData( newXML:XML ):void
{
var newList:XMLList = newXML.resources;

// Populate the MasterArray so we don't have to keep manipulating a huge honking XML file
for each (var newItem:XML in newList)
{
	MasterArray.push( {
			 learningRoadmapId:newItem.learningRoadmapId,
			 cardinality:newItem.cardinality,
			 resourceId:newItem.resourceId,
			 availabilityId:newItem.availabilityId,
			 availabilityStatus:newItem.availabilityStatus,
			 tCategories_name:newItem.tCategories_name,
			 tTracks_name:newItem.tTracks_name,
			 tTypes_name:newItem.tTypes_name,
			 tResources_name:newItem.tResources_name,
			 description:newItem.description,
			 link:newItem.link
			 } );
}

…just to interject, I’m creating an Object for each node of the XML data and making each Object an element of the MasterArray. Everywhere above where it says newItem, I’m pulling a node from the xml file (so newItem.resourceId means I’m pulling the resourceId value exported from the MS Access database for the current record. The for each allows me to loop through this series of actions as many times as there are records in the xml file I’m reading. Moving on…

// Now populate an array of just the top-level Learning Map IDs, so that it's easier to sort
for ( var i:Number = 0; i < MasterArray.length; i++ )
{
	TopIndexLength = TopLevelArray.length;
	if( TopIndexLength == 0 ) TopLevelArray.push( {label:MasterArray[i].learningRoadmapId, data:MasterArray[i].learningRoadmapId} );
	if( TopIndexLength >= 1 )
	{
		var now = MasterArray[i];
		var lastIdentified = TopLevelArray[ TopIndexLength - 1 ];
		if ( now.learningRoadmapId != lastIdentified.label ) TopLevelArray.push( {label:MasterArray[i].learningRoadmapId, data:MasterArray[i].learningRoadmapId} );
	}
}

The “Top” Listbox is only to list out all the learning maps in the database. Even though I’ve stored everything as a resource in my previous posts so they are flat in the database, I’m customizing a view to reflect the real-world hierarchy, which I can do because I’ve categorized my information in the database such that I can easily identify what type of resource any one resource is.

//Now populate the top-level listbox
for ( var j:Number = 0; j < TopIndexLength; j++ )
{
	top_lst.addItem( TopLevelArray[j] );
}
}

Bam. That wasn’t so difficult, right? Next up? What do we do when someone selects a LearningMap from the “Top” listbox?

top_lst.addEventListener(Event.CHANGE, topItemChange);

function topItemChange( e:Event ):void
{
	// Remove every item out of the second-level listbox
	middle_lst.removeAll();

	// Reset the length of the second-level array
	TempSecondLevelArray = [];

	// Sort through the MasterArray for any resources that have the selected
	// Learning Map as its parent.
	var selectedLearningMap:String = top_lst.selectedItem.data;

	for ( var i:Number = 0; i < MasterArray.length; i++ )
	{
		if( MasterArray[i].learningRoadmapId == selectedLearningMap )
		{
			TempSecondLevelArray.push( {label:MasterArray[i].resourceId, data:MasterArray[i]} );
			TempSecondIndexLength = TempSecondLevelArray.length;
		}
	}

	// Now populate the middle listbox (currently only the label and description)
	for each ( var ResourceItem in TempSecondLevelArray )
	{
		middle_lst.addItem( ResourceItem );
	}

}

The commments above should be pretty self-explanatory. We need to wipe out anything that’s in that “Middle” listbox and repopulate it based on what is currently selected in the “Top” listbox.

middle_lst.addEventListener(Event.CHANGE, middleItemChange);

function middleItemChange( e:Event ):void
{
	// Populate the textfield with formatted text
	var myText = middle_lst.selectedItem.data.description;
	pane_txt.htmlText = myText;

	// Hide the Launch button by default
	availability_mc.launch_btn.visible = 0;

	// Show the availability of the selected resource
	availability_mc.label_txt.text = middle_lst.selectedItem.data.availabilityStatus;

	// Change the color of the availability status bar based on the status of the resource
	var avStatus = middle_lst.selectedItem.data.availabilityId;

	var newColorTransform:ColorTransform = availability_mc.background_mc.transform.colorTransform;

	// If the resource is "available"
	if ( avStatus == "1" )
	{
		newColorTransform.color = 0xFF0000;
		availability_mc.background_mc.transform.colorTransform = newColorTransform;
	}
	// If the resource is "on demand"
	else if( avStatus == "3" )
	{
		newColorTransform.color = 0xF69200;
		availability_mc.background_mc.transform.colorTransform = newColorTransform;
	}

	// Show a button to launch the resource if a link is provided
	if ( middle_lst.selectedItem.data.link != "0" )
	{
		availability_mc.launch_btn.visible = 1;
		availability_mc.launch_btn.addEventListener( MouseEvent.CLICK, button_function );
		function button_function( evt:MouseEvent )
		{
			var request:URLRequest = new URLRequest( middle_lst.selectedItem.data.link );
			try
			{
				navigateToURL(request, '_blank'); // second argument is target
			}
			catch (e:Error)
			{
  				trace("Error occurred!");
			}
		}
	}
}