ActionScript.org Flash, Flex and ActionScript Resources - http://www.actionscript.org/resources
Understanding Asynchronous Directory Searching in Adobe AIR
http://www.actionscript.org/resources/articles/901/1/Understanding-Asynchronous-Directory-Searching-in-Adobe-AIR-/Page1.html
Dov Goldberg
I like to think of myself as a Flash/Flex specialist. While I am perfectly capable of tackling medium to large projects, I find myself constantly trying to perfect existing code to do more. Currently, I am working on a project to stream personal media to a variety of devices including user computers and ipods. I am open to writing about and discussing anything Flash/Flex in particular or anything else that may be troubling you.  
By Dov Goldberg
Published on July 7, 2009
 

The goal of this tutorial is to get you familiar with the great FileSystem controls included with Adobe AIR.  Furthermore,  I will help explain how to use these controls in an efficient way allowing for a more fluid experience for your users.


Define the Project
My experience with the FileSystem  controls is the result of many hours spent building a media library application.  In this tutorial,  I am going to elaborate on the process I went through to streamline the performance of my application while searching large directories for media files.  The process of searching through directories can be very time consuming and likely to lock up the application for some time.  By the time you are finished reading this tutorial you will have a good understanding of the Events model in flex and know how to use a number of built in flex components asynchronously resulting in better performance and GUI design.

First let me define the task.  I wanted to create a mechanism to allow users to search multiple directories for media files.  Media files include Video,  Picture or Audio files.  Once the files are collected, they are to be added to an XML file so the file meta data is saved for later use.  If XML means nothing to you then head on over to http://www.w3schools.com/xml/ to get a great tutorial.  I personally have learned a lot from that site over the years.  Since we will be working with files located on the local computer it is important to note that the components that will be used are only supported inside AIR applications running on the local computer and will not work in SWF files viewed in a user's browser.

The focus of this tutorial is to gain an understanding of how asynchronous functions work and how program control is handled throughout.  I decided to use directory searching as the task since it is what I did in my last project.  You could easily replace directory searching with any other goal, once you understand how asynchronous function work.  Please download the code so you will have it while reading the tutorial from here

 

Designing the GUI

So, now that we know what we want to do, let's look at how to use the flex components to get the job done.  The first thing a user will need to do in the application is select a directory that will be searched for media files.  I chose the FileSystemTree component since I wanted to create a GUI familiar to the user.  I created a new MXML custom component based on the Panel component and placed a FileSystemTree component inside it.  Below are the MXML code and an image of the final result.

<?xml version="1.0" encoding="utf-8"?>
<mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml"
      layout="absolute" width="400" height="300"
      creationComplete="onCreationComplete()">
      <mx:FileSystemTree id="fstTree" width="100%" height="100%" />
      <mx:ControlBar horizontalAlign="right">
<mx:ButtonBar id="barRootDirs" width="100%" horizontalAlign="left" />
            <mx:Button label="Cancel" width="70" click="onCancel()" />
            <mx:Button label="OK" width="70" click="onOK()" />
      </mx:ControlBar>
</mx:Panel>


File Browser Component

As you can see in the code section above I added a creationComplete event handler and named the function onCreationComplete().  This function is responsible for the following:

 

  1. Setting the extension filter.
  2. Adding event listeners to the FileSystemTree component to track when a directory is opened or closed.
  3. Initialize the button bar component to include buttons to other drives available on the computer.
  4. Sets the initially opened directory to the users Documents folder.

See the function code below:

 

[as]//0 - all 1 - Music 2 - Picture 3 - Video
public var FilterMode:String = "1";

private function onCreationComplete():void{
      this.fstTree.extensions = new Array();
      for each(var filter:int in FilterMode.split(";",3)){
            switch (filter){
                  case 1:
                        for each(var ext:String in SettingsManager.Instance.GetMusicExtensions()){
                              this.fstTree.extensions.push(ext);
                        }
                        break;
                  case 2:
                        for each(var ext1:String in SettingsManager.Instance.GetPictureExtensions()){
                              this.fstTree.extensions.push(ext1);
                        }
                        break;
                  case 3:
                        for each(var ext2:String in SettingsManager.Instance.GetVideoExtensions()){
                              this.fstTree.extensions.push(ext2);
                        }
                        break;
            }
      }
      this.fstTree.addEventListener(FileEvent.DIRECTORY_OPENING, onDirectoryOpening);
      this.fstTree.addEventListener(FileEvent.DIRECTORY_CLOSING, onDirectoryClosing);
      this.initNavBar();
      this.browseToDirectory(File.documentsDirectory.nativePath);
}[/as]
 

In order to allow the user access to other drives on the computer I added a ConotrolBar component and used the File.getRootFolders function to create buttons linking to the other drives.  See code below:

 
[as]private function initNavBar():void{
      for each(var dir:File in File.getRootDirectories()){
            var button:Button = new Button();
            button.data = dir.nativePath;
            button.label = dir.nativePath;
            button.addEventListener(MouseEvent.CLICK, onNavigate);
            button.width = 45;
            this.barRootDirs.addChild(button);
      }
}[/as]

Once the user navigates to the desired folder I use the DIRECTORY_OPENING event to set the title property of the Panel to show the selected folder's path.  See code below:


[as]private function onDirectoryOpening(event:FileEvent):void{      
      this.title = this._TitleText + event.file.nativePath;
}
private function onDirectoryClosing(event:FileEvent):void{
      try{            
           this.title = this._TitleText + event.file.parent.nativePath;
      } catch(er:Error){
            this.title = this._TitleText;
      }
}[/as]

 

The user can now click ok and the newly selected directory is placed into a new FileBrowserEvent and dispatched.  The SettingsViewer Custom Component is coded to listen for FileBrowserEvents. When this event is handled the SettingsViewer adds a new row to the TileList component.  The user can now click OK and the currently selected settings are saved to XML. See image below:


Settings Manager Component



Performing the Search
Now that we have learned how to collect the various directories and file types from the user lets start looking at the next step;  Directory Searching.  The user may have selected a folder that contains their entire media library or just a few files.  Either way, we want the next step to run as fast as possible while still allowing the user to use their computer.  Another thing to consider is that there may be subdirectories.  The next step will need to recursively search through all the directories and files and return all the media files.  The obvious solution is to create a function that takes the directory path as a parameter and examines each file in the directory.  If a media file is encountered then add it to an ArrayCollection.  If a directory is found then pass the new directory's path to the same function to process its entries.  Once all the files have been processed the result will be an ArrayCollection of File objects that reference all the media files found.  This will work but can take a long time when processing large directories resulting in our users sitting and waiting and staring at a spinning hour glass.  I knew there had to be a better approach.  I needed a function that would process even the largest of directories but still allow the user access to their computer at the same time.  The solution I was looking for is to call the library building function asynchronously.  Each time a file is found a new LibraryProgressEvent is raised.  Read on to get an understanding of the code used to implement the asynchronous event driven directory search.

Lets look at the code to search the directory.

[as]private function processDir(dir:DirectoryDataItem):Boolean{
      var bReturn:Boolean = true;
      try{
            var viddir:File = new File(dir.DirectoryPath);
            for each(var file:File in viddir.getDirectoryListing()){
                  if(file.isDirectory){
                        var di:DirectoryDataItem = new DirectoryDataItem();
                        di.DirectoryPath = file.nativePath;
                        di.DirectoryType = dir.DirectoryType;
                        processing.addItem(di);
                  } else {
                        for each(var ext:String in SettingsManager.Instance.GetVideoExtensions()){
                              if(file.extension == ext){
                                    addFileToLibrary(file, "Video");
                              }
                        }
                  }
            }
      } catch(er:Error){
            bReturn = false;
      }
      return bReturn;
}[/as]

Each directory is passed to this function.  This function looks at each entry in the directory.  If a sub directory is found it is added to the processing ArrayCollection.  If a file is found then it is passed to a function to be added to the library.

As you can see, if this function was called and blocked the application the user could experience long lag times.  The way to get this to run Asynchronously is to create a public function that will act as a wrapper and get the search process started.  EventListeners will be set up to report the status of the search but the wrapper function will return control to the calling function.  This way the user can continue to interact with the GUI and still be updated as files are found.

Lets take a closer look at how to get all this asynchronous event driven stuff to work.  I always like to start by breaking my goals down into the components that are going to accomplish each step.  We will need the following:

1) Function to recursively search each directory.
2) Mechanism for calling the search function asynchronously.
3) Custom Event object to pass messages from the search function back to the GUI.
4) GUI code that knows how to listen for the custom Event.

For my application I created a class called LibraryManager.  It contains all the methods used to do the asynchronous directory search and raise progress events.  A complete listing of the code can be found in the file src\radshag\medialibrary\LibraryManager.as inside the source zip file at the top of this tutorial.  I will now discuss the key functions and events used for the asynchronous search.

There is one function that is used to start the directory searching process.

   1. public function BuildLibrary():void

This function initializes two ArrayCollections.  The library ArrayCollection is used to store all the files that are found during the search.  The processing ArrayCollection is used to store directories that are to be searched.  As subdirectories are found they are also added to the processing ArrayCollection.  The code for the BuildLibrary function is presented below:

[as]public function BuildLibrary():void{
      library = new ArrayCollection;
      processing = new ArrayCollection;
      // Get a the collection of media directories to search
      var dirs:ArrayCollection = SettingsManager.Instance.GetMediaDirectories();              
      for each(var dir:DirectoryDataItem in dirs){
            // Add directory to collection processing to perform the actual search on each directory
            processing.addItem(dir);
      }
      // each time the ENTER_FRAME event is raised call onEnterFrame to process the next directory
      addEventListener(Event.ENTER_FRAME, onEnterFrame, false, 0, true);
}[/as]

The onEnterFrame function is responsible for determining if there are any directories to search.  Each time the function is called the ArrayCollection processing is checked to see if it contains any item to search.  If it does then the next item is removed from the ArrayCollection and passed to processDir.  The processDir function determines what kind of files are being searched for and calls the correct function.  If a directory is found it is added to the processing ArrayCollection.  If a media file is found then the addFileToLibrary function is called.

The new file is added to the public member library ArrayCollection and a new LibraryProgressEvent called progress is raised so any listening object can handle it.  Once the processing array is empty signaling that all directories have been searched,  the same LibraryProgressEvent is raised but is named complete so that any listening object can access the library once it is completed

At the top of the LibraryManager class file I defined two events as follows:

[as]/**
*     A progress event is dispatched when library item is added.
*     @eventType radshag.medialibrary.events.LibraryProgressEvent
*/
[Event(name="progress",type="radshag.medialibrary.events.LibraryProgressEvent")]
/**
*     A complete event is dispatched when library processing is complete.
*     @eventType radshag.medialibrary.events.LibraryProgressEvent
*/
[Event(name="complete",type="radshag.medialibrary.events.LibraryProgressEvent")][/as]

Each time the search function finds a media file a LibraryProgressEvent named progress is raised.  Once all the directories and files have been searched the same event is raised but is called complete to signal that directory searching has completed.  A complete listing of the LibraryProgressEvent is listed below:

[as]package radshag.medialibrary.events
{
      import flash.events.Event;
      import flash.filesystem.File;
      public class LibraryProgressEvent extends Event
      {
            public static var PROGRESS:String = "progress";
            public static var COMPLETE:String = "complete";
            public var completed:int;
            public var total:int;
            public var file:String;
            public function LibraryProgressEvent(type:String, completed:int, total:int, file:String)
            {
                  super(type);
                  this.completed = completed;
                  this.total = total;
                  this.file = file;
            }
            override public function clone():Event{
                  var event:LibraryProgressEvent = new LibraryProgressEvent(type, completed, total, file);
                  return event;
            }
      }
}[/as]

Summing it All Up
Now let's sum up the main points.  We learned a number of benefits of asynchronous programming.  Namely:

   1. Ability to perform lengthy operations without tying up the program.
   2. Retains fluidity so the user can continue to interact with the GUI.

The concepts I have tried to explain can be used in any project.  Allot of the components you probably already work with use this kind of asynchronous design.  Each time you use an HTTPService object to send a request you are doing so asynchronously.

I hope you found this tutorial informative.  Please download the attachment and play around with it.  Send me any questions and I will be happy to discuss everything with you.  Happy coding….