Chapter 16. Interfacing with External Data

CONTENTS
  •  External Scripts
  •  Setting Flash Variables from Within HTML
  •  External Data Files
  •  Server Scripts
  •  XML
  •  Local SharedObjects
  •  The LocalConnection Object
  •  Summary

Flash is not an island. Data from outside sources can be included in a Flash movie. This means that even if the data changes after you upload your finished .swf file, your movie can reflect those changes, too. You can include timely information or make the movie appear customized to the individual user. The implications for such dynamic Flash sites are infinite.

In this chapter, we'll look at the main ways that Flash can interact with outside applications and data. I should note that there are other Macromedia server products that aren't covered in this chapter. These products integrate with Flash and extend what's possible with Flash alone. For example, at the time I wrote this book, Macromedia had only made announcements of very exciting server technology that let the Flash Player 6 do two-way audio and video, as well as real-time data transfer. Although this chapter won't cover such servers, you can bet that the programming syntax and design approach is consistent with what you learn in this book. In this chapter, we will look only at ways to produce .swfs that adapt to outside data.

This chapter includes both detailed information and resources to get you started in the event that you choose to develop your skills elsewhere. Specifically, in this chapter, you will:

  • Create external scripts that contain ActionScript code.

  • Set Flash variables from within HTML.

  • Load data from text files so that you can make changes without having to open Flash.

  • Learn how to load data from or send data to server scripts (such as CGI or ASP).

  • See how you can incorporate XML structured data into Flash.

  • Learn how to use the new Local SharedObject to save data on the user's computer (for later retrieval).

  • Use the new LocalConnection feature to give several .swfs access to each other's variables and properties.

External Scripts

In addition to typing your scripts in the Actions panel, you can store your scripts in external text files that are included when you export a .swf. Although all the other techniques covered in this chapter involve ways to import data while the movie plays, external scripts become locked inside the movie when you export. (It would be nice if changes to an external script could be reflected in a previously exported .swf, but it doesn't work that way.)

External scripts can still make you more efficient. If the same script is used in several places within one movie, and perhaps even used in several different movies, you can store that script in a single "external script" file. If at any time during production you find that you want to adjust or fix that script, you need to do it in only one place. Movies exported from that point forward would see the change in every place where the script was used. It's even more powerful when your site structure is broken into several movies (that get loaded via loadMovie()). That way, a change in a single external script will be reflected in every movie that accesses it once you re-export all your .swfs. Basically, it just takes a bit of planning.

An additional benefit of external scripts is that they can be managed using version control software. Such software tracks every change to an external script file and makes it easy to backtrack and find the cause of a newly occurring bug.

Using external scripts is pretty easy. In place of an actual script (that is, in a keyframe, Button instance, or Movie Clip instance), refer to the filename where you plan to store the actual script. Macromedia suggests that you use a file extension of .as, but you can use .txt if you want it's just a text file. (On my Windows machine, I've associated the .as file type with the Notepad text editor.) So, in the Actions panel, refer to the file after #include, as in:

#include "somefile.as"

Notice that you don't put a semicolon at the end of this line. Also, you should realize that the content of this external script can be just a code segment (like the result of an event), or it can be the entire script (including the event). So, attaching the following two scripts to two different buttons is totally legitimate:

One button:

on (press){     #include "code_segment.as"  }

Another button:

#include "entire_script.as"

The only difference is that the file code_segment.as doesn't contain (and shouldn't contain) any mouse events, such as on (press). The file entire_script.as needs to contain the entire mouse event because any scripts on buttons need to be surrounded by events. Actually, the contents of entire_script.as could include more than one mouse event, as follows:

on (press){     pressing=1;  }  on (release){     pressing=0;  }

Just realize that the entire contents of your external file will be placed in Flash exactly where you refer to them. Imagine that the contents of the external script are pasted in place of the line starting #include.

There's not much more to say except to explain some practical concerns and potential applications. If you use Notepad to create your scripts, realize that you won't get all the enhancements (such as syntax coloring and error checking) that you do in Flash. Some third-party and shareware text editors can be customized to do syntax coloring, and because ActionScript is nearly identical to JavaScript, you can use any editor configured to edit JavaScript. It's still probably going to be easier in Flash. You might consider writing the script in Flash first, and then when you identify that using an external script would be more appropriate, just cut the script from Flash, put the #include statement in its place , and then paste the code into a text file. That way you not only know that the syntax is correct, but that the code works inside Flash.

Before we move on to the really dynamic features, let me suggest a few uses for external scripts. I already mentioned that several different movies within the same site can access the external script. However, you might want to use the same script in several movies in unrelated sites. Of course, it's a slight pain to re-export each .swf every time you make a change to the script, but that's much easier (and less prone to errors) than hunting through the file to find where the script was used. Ultimately, external scripts are good for building a code library. This is also a great way to share scripts. As you saw in Chapter 14, "Extending ActionScript," overriding and extending built-in objects simply requires some code in a keyframe. You can get such "prototype" scripts from other programmers and easily include them in your movies as external scripts. The truth is that storing scripts inside Flash, where you can copy and paste them into new movies, is almost the same thing. Except with external scripts, if you ever find an error in a script you use often, you need only re-export all the movies linked to that script.

I'm sure there are other ways to use the external script feature. In any case, I doubt they'll match the potential power of the other techniques shown in the remainder of this chapter.

Setting Flash Variables from Within HTML

You can easily assign the initial values for variables in your Flash movie by way of HTML. In the following section, "External Data Files," you'll see how to load variables at runtime. That is, you can load variables any time while the movie plays. Setting variables from HTML is different because only the initial values get set (right before the movie starts). It's still a pretty nifty feature because you can place timely data in the HTML file and the Flash movie will have its variables set accordingly.

As a quick example, every morning you could type in a new value for the wordOfTheDay variable and visitors to your site would see the day's word. Even better, if the HTML page is generated dynamically (for example, from a Cold Fusion or ASP page), you could set that variable automatically. Just remember that the technique you're about to see sets only the starting value for a variable.

There are two ways to set a Flash variable from within HTML: by tacking the variables onto the end of the .swf's filename or by placing them in the new FlashVars tag (supported in the Flash Player 6). All you have to do is identify where your HTML specifies the .swf's filename and concatenate (using a question mark) your variable's name and value. For example, the following shows how to set a variable called word through both the OBJECT and EMBED tags (for Internet Explorer and Netscape, respectively):

<OBJECT  <PARAM NAME=movie VALUE="movie.swf?word=skateboarding">  <EMBED src="movie.swf?word=skateboarding"  </EMBED>  </OBJECT>

Notice that this is just an excerpt! Also, you can use any legitimate variable name; I just happened to use a variable named word. Basically, where you'd normally have just seen "myMovie.swf", you instead see "myMovie.swf?word=skateboarding". That's really about it. There are just a few stipulations. If you want to set additional variables, just separate each variable name/value pair with an ampersand (for example, "movie.swf?word=skateboarding&color=red"). If, for some reason, you want to include an ampersand within the value of a variable, you must use the standard URL code %26 instead. Also, you must use a plus sign (+) in place of any spaces (for example, "myMovie.swf?word=teach+yourself"). You can include a literal plus sign by using the code %2b. There are other codes for other special characters (but this is really more of an HTML issue). Notice, however, that the variables' values are always strings. You don't include extra quotation marks around the variables' values they're always strings.

To give you a quick example of where this type of thing might be helpful, I programmed a site to introduce a new chair: www.allsteeloffice.com/number19. The client wanted a total of 7 different entry-paths. That is, some visitors would type in an URL that included one of six different artists' names (and some would use the preceding default address). The visitors who entered by specifying an artist name would default to a part of the site that looked like it was created just for that artist. Instead of making a separate .swf for each artist, I made one .swf that included all the content for all six artists. (Once the visitors arrived, they were able to view other artists, so it wasn't as though they downloaded unnecessary content.) All I needed was the value for the variable defaultartist, and my movie would jump to the correct content. This variable was assigned in the HTML page. Although the HTML was actually generated by another programmer's ASP script, you can imagine that there were 6 nearly identical HTML files just the value for defaultartist varied (for example, movie.swf?defaultartist=2 for the second artist). Once inside my movie, I first converted the variable to a number and then used the value accordingly. Here's a simplified version of the code in my first frame:

defaultartist=Number(defaultartist);  artistClip.gotoAndStop(defaultartist);

graphics/icon02.gif

There's really nothing wrong with the preceding technique. However, a new feature was added for the Flash Player 6 that is slightly more elegant: the FlashVars object/embed tag. It follows most of the same rules from the old technique I just showed. However, instead of tacking the variables onto the end of the .swf's name, there's another section in the HTML. Here are the excerpted parts for both Internet Explorer (OBJECT) and Netscape (EMBED):

<PARAM NAME=FlashVars VALUE="word=snowboarding&color=blue">  <EMBED FlashVars="word=snowboarding&color=blue">

Again, realize that this is an excerpt. (Figure 16.1 shows these changes in the context of the entire HTML.) All the same rules apply regarding spaces (use the plus sign instead), special characters (use URL encoding), multiple values (separate by ampersands), and all values really being strings.

Figure 16.1 The additional FlashVars portion of the HTML is highlighted.
<HTML>  <HEAD>  <meta http-equiv=Content-Type content="text/html; charset=ISO-8859-1">  <TITLE>movie</TITLE>  </HEAD>  <BODY bgcolor="#FFFFFF">  <!-- URL's used in the movie-->  <!-- text used in the movie-->  <OBJECT classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"   codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6, graphics/ccc.gif0,0,0"   WIDTH="550" HEIGHT="400" id="movie" ALIGN="">   <PARAM NAME=movie VALUE="movie.swf">   <PARAM NAME=quality VALUE=high>   <PARAM NAME=bgcolor VALUE=#FFFFFF>   <PARAM NAME=FlashVars VALUE="word=snowboarding&color=blue">   <EMBED src="movie.swf" FlashVars="word=snowboarding&color=blue"    quality=high bgcolor=#FFFFFF WIDTH="550" HEIGHT="400" NAME="movie" ALIGN=""    TYPE="application/x-shockwave-flash"    PLUGINSPAGE="http://www.macromedia.com/go/getflashplayer"></EMBED>  </OBJECT>  </BODY>  </HTML>

External Data Files

You've seen how to store text files containing ActionScript in external files and how to set initial values for variables through HTML. In both cases, however, after the movie starts to play, no data from the outside world is received. (Actually, in the case of external scripts, once you export the movie, nothing comes into your movie). In the next technique, you'll learn how to store data in external files that aren't loaded until you request them at runtime. Specifically, your script can invoke Flash's loadVariables() method at any time to load variables from an external text file (or another data source, as you'll see in the "Server Scripts" section later in this chapter). By loading variables from a file at runtime, a Flash movie reflects the current values found in the file.

There are several ways to load variables. Naturally, I'll recommend the new "Flash MX way" the LoadVars object. However I want to point out the other two ways you can do load variables: as a function and as a method of a Movie Clip. Although this may sound like a case of "see how hard it used to be," I'm mentioning the old ways for two reasons: the LoadVars object only works in the Flash 6 player and perhaps more importantly because all three syntaxes look similar you should be able to recognize the difference.

As a function, the syntax is either:

loadVariables(whatFile,targetClip,optionalMethod);

or:

loadVariablesNum(whatFile,levelNumber,optionalMethod);

The difference between these two forms is simply that the first loads the variables into a named Movie Clip instance and the second loads the variables into a level number. Just replace whatFile with the name of the file containing the variables or with a relative path to that file. Realize that the file is really a URL when the movie's playing on a web server. As such, relative paths use a forward slash (/). Also, although you're allowed to use explicit paths, you're restricted to using paths to files in the same domain where the Flash movie resides (for security reasons). Anyway, it's easiest just to keep the data files in the same directory and simply refer to the filename (which is a relative path). targetClip is replaced with the Movie Clip instance name to where the variables will load. When using loadVariablesNum, you simply specify an integer in place of levelNumber. After the variables load, they will either be part of a clip or a level number. To load variables into the main timeline, use either _root or 0. Finally, we'll look at optionalMethod in the next section.

Consider the following example:

loadVariables("data.txt",_root);

This will load the data in a file called "data.txt" (which is adjacent to the Flash movie) and place all those variables in the root.

There's nothing particularly wrong with the preceding loadVariables function; however, using the following loadVariables() method is better if for no other reason than its syntax is more consistent:

clipInstance.loadVariables(theFile);

To load variables from a file named "data.txt" into a clip named "myClip," use:

myClip.loadVariables("data.txt");

To load this same file into a keyframe in the main timeline, use:

_root.loadVariables("data.txt");

This last example has the same effect as loadVariables("data.txt",_root). And, just so you know, loadVariablesNum doesn't work as a method. (You can use _level0.loadVariables("data.txt") if you want to load into level 0.)

graphics/icon02.gif

Finally, the third (and preferable) way to load variables is by using the new LoadVars object. It takes a few additional steps, but these will look familiar to you and they're certainly worth the extra effort.

To load variables using the LoadVars object, you first need to create an instance of the object, as follows:

myVars=new LoadVars();

This stores an instance of the LoadVars object type into the homemade variable myVars. From this point forward, you can access and use the various properties and methods of the LoadVars object on the myVars variable. For example, here's a load() method that starts the download process into the myVars variable:

myVars.load("data.txt");

In this case, all the data from inside "data.txt" will become named properties of the myVars variable. This is similar to how data loaded into a level number or clip instance (including _root) goes inside the targeted level or clip.

We'll get to the format of this external file soon, but there's a serious consideration when using any of the previous techniques: You must wait for the variables to fully load. Loading variables takes some time to complete because the data is actually traveling from your web server into the Flash movie on the user's machine. The point is that if you start loading variables on, say, the first frame of the movie, you need a way to know when you can start using those variables; otherwise, you'll get erroneous results.

Waiting for Variables to Load

You can take several approaches to determine whether all the variables you've started to load have indeed fully loaded. One way is to repeatedly check whether the last variable in the data file has loaded. This technique is necessary only when you are delivering to Flash 4 (or, when you load variables into the _root timeline in Flash 5). Basically, this "polling" technique is not worth the effort. When loading into a Movie Clip instance, you can take advantage of a second technique namely, writing a script triggered by the data clip event. (Similar to how a load event triggers once when a clip loads, the data event triggers when a clip has fully loaded data.) Additionally, by using the new LoadVars object, you can assign the onLoad callback event to trigger a function that you write. Finally, because all clip events have an equivalent callback function and because _root is really a clip you can use _root.onData the same way onClipEvent(data) works for a clip. Although it might seem like a million choices, it really just depends into what clip you want to load variables. Often, when loading variables into a clip, the first thing I find myself doing (once they've fully loaded) is copying them into the main timeline or into the _global space. Therefore, it's not always convenient to load data into a clip. I'm going to show three techniques: the standard data clip event, the onData() callback function (sort of the same thing), and the new LoadVars object's onLoad callback. Regardless of you do it, you must ensure that the data has fully loaded before you expect to start using the loaded values.

Loading Variables into Clips (Using the data Clip Event)

When you load variables into a clip (rather than a level number or the _root timeline), you can place a script within a onClipEvent(data) event that won't execute until all the data is fully loaded. That is, in a keyframe of the main timeline, start loading data into a target clip (such as myClip.loadVariables("data.txt")), and then include a stop() to keep the timeline from proceeding. Place a script attached to the clip myClip, as follows:

onClipEvent(data){     _root.play();  }

Basically, as soon as all the data is loaded, play() causes the main timeline to proceed. Just realize that the disadvantage is that you must load variables into a clip instance, not a level number or the main _root timeline. That is, once the data has loaded, you'll find all the variables in the myClip instance. For example, if one variable is named age, it will really be _root.myClip.age.

Using the onLoad Callback Function

As you've seen in several places, many objects can have callback functions trigger when particular events occur. In the case of clip instances, all the events you see listed in the Actions panel can be scripted as callbacks. For example, onClipEvent(load) has a matching callback function called onLoad. The syntax here is consistent with all callback functions. Say (like before) you start loading variables into a clip (myClip.loadVariables("data.txt")) and have a stop() in the first frame. You can use the following code (in the first keyframe of the main movie) to cause the main movie to play when the data has fully loaded:

myClip.onLoad=function(){_root.play()};

This is nearly the same thing as you saw earlier:

onClipEvent(data){     _root.play();  }

There are a couple differences, however. First, you don't have to attach any code to the clip instance itself. (You do need to name the instance, of course.) Also, this technique is the only way I can think of to trigger an event when data has fully loaded into the _root timeline. That is, if you begin with _root.loadVariables("data.txt"), you won't find a clip instance onto which you place the onClipEvent(data) code. Even though you can't attach code to it, _root is a clip, and therefore you can do the following:

_root.onData=function(){_root.play()};

Finally, the only warning about using callback functions is that the sequence of code does matter. In this example, you want to declare the callback function first (that is, what's going to happen when it's fully loaded) and then start loading.

Therefore, the complete code would look like this:

_root.stop();  _root.onData=function(){_root.play()};  _root.loadVariables("data.txt");

There's more information about callback functions in Chapter 14; it's just being applied here.

Loading Variables into a LoadVars Object

Using the new LoadVars object is more than just a way to show off to your geeky friends. There are some nice new features that make it really worth learning.

The first step is to create a variable that contains an instance of the LoadVars object:

myVars=new LoadVars();

Then, you can define what you want to occur when data is loaded:

myVars.onLoad=function(){_root.play()};

Then, simply commence to download the data:

myVars.load("data.txt");

This looks similar to the callback technique shown earlier, but instead of using a Movie Clip instance as the object, you first create an instance of a LoadVars object and use that instead. (We'll look at why LoadVars is more convenient than loading into clips shortly.) It's important to realize that the variables that eventually load into the movie will be named properties of the myVars variable. That is, if the data contains a variable age, it will be this.myVars.age. This should look identical to loading into clips (where the variables become part of the clip).

Finally, the good stuff. Among the handful of methods and properties in the LoadVars object, the most interesting are getBytesLoaded() and getBytesTotal(), which enable you to easily determine how much has downloaded. Even though your external data shouldn't be particularly huge, it still can take a little while to download. While you're forcing the user to wait for all the data to load, you can accurately report how much has downloaded (by using a visual progress bar, for example). Here's a little snippet of code that will display the amount of data loaded into a text field called message:

 1 myVars=new LoadVars();   2   3 function myCheck(){  4   if ( myVars.getBytesLoaded()==0){  5     return;   6   }   7   var percent=(myVars.getBytesLoaded()/myVars.getBytesTotal())*100;   8   percent=Math.floor(percent);   9   message.text="Loaded: "+ percent +"%";  10 }  11 intervalID=setInterval(myCheck,50);  12  13 function done(){ 14   clearInterval(intervalID);  15   if (myVars.loaded){ 16     message.text="Loaded:100%"  17   }else{ 18     message.text="No data file found";  19   }  20 }  21 myVars.onLoad=done;  22  23 myVars.load("http://www.phillipkerman.com/test.txt");

Even though this code doesn't show anything particularly, it does warrant a quick walk through. In line 1, I put an instance of the LoadVars object into myVars. Then, notice in line 11 that I use setInterval() to start triggering the myCheck() function (every 50 milliseconds). (I store it in the variable intervalID so that I can clear it later.) Because the myCheck() function will begin checking as soon as line 11 executes, you can see that I placed a quick escape inside the myCheck() function on line 4. That is, if myVars.getBytesLoaded() is absolutely 0, I exit the function. Line 7 inside myCheck() is pretty simple: I set percent equal to "loaded" divided by "total" multiplied by 100. Then line 8 uses Math.floor() to trim the decimal values from percent. Finally, line 9 sets the text property of the message text instance to a string by combining "Loaded: " with percent and "%". Next, notice line 21, which specifies that the done() function should be triggered as the callback function when the myVars.onLoad event occurs. In line 13, the done function simply clears the interval intervalID (so that it doesn't keep executing). Then the done function checks to see whether the LoadVars object's loaded property is true and makes sure the onscreen text displays either a nice round "100%" (in the event that the last myCheck() execution found that less than 100 percent had downloaded) or a warning message. (The onLoad event triggers even when no data file is found.) Now that everything is built, line 23 initiates the load method on myVars. Notice that I'm downloading a sample text file from an actual server. To really see this script work, it helps to create a moderately large file and store it on a remote site; otherwise, it will load immediately, and you won't see much onscreen information.

Although the most critical issue is that you wait for data to fully load before using that data, it's also important to account for the possibility that the data never loads. Perhaps the user lost his Internet connection or your server can't find the file (or you've specified a data file with the wrong name). In such situations, it's nice to tell the user what has happened.

Building a timeout feature is really a matter of strategy it's not like there's a timeout feature. The general approach is always the same: As you're waiting for data to load, pay attention to how much time has elapsed. If the download has not completed after a reasonable amount of time has elapsed, you can tell the user. To adjust the preceding example, just add this line of code before line 23 (where the load begins):

shouldBeDone=getTimer()+4000.

That is, four seconds from now the download should be complete. (You can adjust 4000 to whatever you think is reasonable.) Then change the entire myCheck() function to read as follows:

1  function myCheck(){ 2    if ( myVars.getBytesLoaded()==0){ 3      return;  4    }  5    var percent=(myVars.getBytesLoaded()/myVars.getBytesTotal())*100;  6    percent=Math.floor(percent);  7    message.text="Loaded: "+ percent +"%";  8    if (percent<100 && getTimer()>shouldBeDone){ 9      clearInterval(intervalID);  10     message.text="Download is not progressing";  11     myVars.onLoad=null;  12   }  13 }

First, notice that only the new stuff appears in lines 8 12. The idea is that every time the myCheck() function executes, I check to see whether the conditions that both percents are less than 100 and the current time is greater than the shouldBeDone variable are met. If so, we've waited too long without the percent reaching 100. In such a case, we clear the interval function, display a message, and set the onLoad callback to null. You can put different code in this area. For example, you might want to give the user an option to continue waiting, or maybe you don't want to clear out the onLoad callback. (If you don't, it will still trigger once the data eventually loads). Anyway, you can vary this basic strategy however you want but some kind of timeout is a nice feature for the user experience.

Data File Format

At last we're going to look at the contents of the files that get loaded via loadVariables() or myLoadVarsObj.load(). Although the contents are nothing more than a series of variable assignments, the format is not like ActionScript as you might suspect. That is, the following is not a legitimate file format:

age=37;  name=Phillip;

Instead, the format of the data file must be formatted in what's called URL-encoded text the same format required when you use HTML to initialize Flash variables. This is a standard format about which I'm sure that you can easily find more information. I'll give you the basics here. In URL-encoded text, the preceding data looks like this:

age=37&name=Phillip

Rather than spaces or returns between variables, an ampersand is used. There are other restrictions on certain characters that cannot be used without first being converted to legitimate URL encoding. It's not so much that returns are not allowed, but consider the following string:

age=37  &name=Phillip

In this case, age's value is "37/r/n" that is, 37, a return, and a line feed. (Actually, if I used a Macintosh to create this file, I might find that only the return and no line feed appears after 37.) You'll see how you can use the preceding format in a later example. The point I want to make is that age's value isn't wrong, it just includes extra garbage that we don't really want (although we can remove it). If you're using a script to generate the text file automatically, it's probably no big deal to use the standard URL-encoded format (with no spaces). If you're creating the data file by hand, however, it's much more legible if you can include returns. Consider the following two examples.

Example of standard encoding:

total=3&page_1=First page&page_2=Page two&page_3=Last page

Example with extra lines:

total=3  &page_1=First page  &page_2=Page two  &page_3=Last page

Obviously, the second example is much more legible.

Another thing to always remember is that (just like the FlashVars object/embed tag) all values are strings. You don't need quotation marks, and even if you type a number (such as 37), it is read in as "37". You can easily convert any variable (after it has fully loaded) to a number version if you want. For example, age=Number(age) will reassign the value of age to a number version of age.

Examples of Using External Data Files

First, let's look at a simple example, and then we can explore ways to make it more sophisticated (and, unfortunately, more complicated).

Let's say you want to make a movie that displays text that's imported from an external text file. Maybe you plan to have several different famous quotations for the user to read and you want to be able to update and add quotes without reopening the source .fla file. External data files are perfect for this.

Create a text file called "quotes.txt" and type the following text:

quote_1=A penny saved is a penny earned  &quote_2=Haste makes waste  &quote_3=It takes two to tango

(Notice the ampersand before each variable in the second and third lines.)

Create a new Flash movie and save it in the same directory as the "quotes.txt" file. Into a Static Text field, type "Loading", select the block of text with the Arrow tool, and convert it to a Movie Clip Symbol. Double-click to edit its contents, and then insert a blank keyframe. In this new frame (2), create a Dynamic Text field, set the Line Type to Multiline, and use the margin handle to make the width and height large enough to accommodate a lot of text. Return to the main timeline and attach the following script on the instance of the Movie Clip you just made:

onClipEvent (load) {   this.loadVariables("quotes.txt")    this.stop ();  }  onClipEvent (data) {   this.gotoAndStop(2);    this.allQuotes.text=quote_1 + quote_2 + quote_3;  }

It should work great when you test it (see Figure 16.2).

Figure 16.2. Loading variables from text files enables you to display their values inside Flash.

graphics/16fig02.gif

Although this exercise should give you some ideas, there are a few things we need to analyze. Notice that we first go to frame 2 and then set the text property of the allQuotes text field. We don't want to change the order (that is, try to set a text property before we're at a frame with the text present). Also, how did we get away with having returns in the text file? Well, when you see the quotations on the Stage, you actually see the words plus an extra return but that's not a problem because it's at the end of the line. It just so happens that the extra returns don't pose any problems in this situation. Nonetheless, I'll show you how to remove the extra garbage at the end of the lines.

You can also spice up this example slightly by using some skills you acquired earlier. For example, you could include an additional variable in the text file that specifies how many quotes were in the file, as follows:

totalQuotes=3&quote_1=A penny saved is a penny earned  &quote_2=Haste makes waste  &quote_3=It takes two to tango

Notice that the value for the first variable (totalQuotes) doesn't have the extra return we would otherwise have to deal with. Now you can change the clip to contain just one Dynamic Text field associated with the variable currentQuote.

Then, change the instance script to read:

onClipEvent (load) {   this.stop ();  }  onClipEvent (data) {   this.gotoAndStop(2);    this.allQuotes.text=quote_1;    this.currentQuoteNum=1;  }

Finally, you can use the clip's totalQuotes variable in conjunction with "Next Quote" and "Previous Quote" buttons. For example, here's a script for the "Next Quote" button:

1 on (release) { 2   if (currentQuoteNum<totalQuotes){ 3     currentQuoteNum++;  4     currentQuote=this["quote_"+currentQuoteNum];  5   }  6 }

Basically, knowing the value of totalQuotes enables us to make sure that the user doesn't go too far when pressing the "Next Quote" button. Also, line 4 uses the standard bracket reference technique to address a clip or variable with a dynamic string (path["string"]). (Chapter 7, "The Movie Clip Object," covered this dynamic referencing technique.)

The quotations example shows the potential of external data files. I have another sample script that does several additional maneuvers, which are not only useful but are also a good way to learn the concepts. In this example, I wanted the text file to include returns after each line to make it easier to read; however, I didn't want those extra returns (and line feeds) to be part of the loaded variable. So, I needed to strip off any returns or line feeds at the end of the line. Here's the contents of the "notes.txt" file that gets loaded:

totalNotes=4&note_1=The note for page one is here  &note_2=Here is the note for page two  &note_3=On page three this note appears  &note_4=The fourth page has this for a note

I use totalNotes so that my Flash movie knows how many note_ variables to load. Also, instead of remembering to strip off excess returns after the totalNotes variable, I figured it was still pretty legible to define note_1 without first placing a return. The last thing to notice is that all my variables' names (except totalNotes) follow the same convention that is, "note_" followed by a number.

Here's the script that I attached to the clip into, in which the variables were loaded:

 1 onClipEvent (load) {  2   this.loadVariables("notes.txt");   3 }   4 onClipEvent (data) {  5   totalNotes=Number(totalNotes);   6   for (var i=0; i<totalNotes; i++ ) {  7     var thisOne=this["note_"+(i+1)];   8     var lastChar=thisOne.charCodeAt(thisOne.length-1);   9     while (lastChar==10 || lastChar==13 ) { 10       thisOne=thisOne.substr(0,thisOne.length-2);  11       lastChar=thisOne.charCodeAt(thisOne.length-1);  12     }  13     this["note_"+(i+1)]=thisOne;  14   }  15   //done  16 }

Let me explain each line. Because the bulk of the code is in a data clip event, it won't execute until all the variables are loaded. On line 5, I convert the totalNotes variable to a number because all variables loaded will be strings. Next, I start a for loop in line 6 (which encloses everything except the very last line). The for loop sets i to 0 and keeps incrementing i while i is less than the totalNotes variable. Notice the use of var in the for loop statement; I want this variable to be discarded automatically when the function finishes. Inside the loop (and every time the loop iterates), on line 7, I set the temporary variable thisOne to the value of this["note_"+(i+1)] (translated: the first time that I find the value of note_1 because i starts at 0). Because the string note_1 is in brackets preceded by an address (this), it returns the value of note_1. Now, with thisOne containing the value of note_1, I can start stripping return characters or line feeds from it. In line 8, I put the last character into the variable lastChar and begin a while loop on line 9 that will continue "while" lastChar's charCode is either 10 or 13 (the codes for linefeed and return, respectively). Notice that the while loop strips the last character for every iteration (and resets lastChar to the new last character). Line 13 in the for loop re-assigns a value to the current note variable to thisOne (the string that has had its end characters trimmed). Finally, after every "note" variable has gone through the process of stripping excess characters and moving into the root timeline, I have a comment in line 15 where I would put any code I want to execute now that the variables are all fixed. Perhaps this.play() would be appropriate. You can test this script with the Debugger open to verify that it works. It's really cool if you put a breakpoint on line 10 and then step into the code with the Locals tab selected so that you want watch the string get fixed (see Figure 16.3). Try adding a few extra returns into the source notes.txt file to have even more to watch while stepping the code.

Figure 16.3. Adding a breakpoint and stepping into the code is a great way to watch the loop do its work.

graphics/16fig03.gif

Server Scripts

It might seem as though we went through a lot of effort just to load text files that ultimately have to be edited by hand. They're not really that dynamic if you have to open the darn text file to make a change. Don't forget, it is indeed dynamic because you don't have to reopen the Flash movie but we can do even more. For example, many scripting languages (such as CGI, ASP, and Perl) in effect create URL-encoded strings based on dynamic information. Such scripting languages can easily interface with databases to produce different results, depending on the current data in the database. The URL-encoded strings these languages produce can get into Flash in several ways. Practicing with creating your own text files as we did in the previous section is not a waste of time because it mimics the process involved when you make Flash interface with server scripts.

I know just enough about this subject to work productively with others who are experts on the subject. My goal in this section is to give you the tools so that you can make your Flash movies talk to (and listen to) server scripts that someone else produces. Obviously, if you know a server script language, you'll be that far ahead; otherwise, you'll either need to study that subject further or work with someone who has. One big difference between loading variables from text files and loading variables from server scripts is that you can additionally send data from your Flash movie to the server script. We'll look at how shortly; for now, let me give you a quick overview of how server scripts work.

Here's my quick explanation. When a user requests an HTML file from your server, the server receives the request and then decides to send the HTML file as well as any embedded images and .swf files to which the HTML refers to the user's machine. Some web pages, however, are produced dynamically. The user requests a web page, and, somehow, the server decides to send the user an HTML file that is created the instant that the user asks for it. Basically, it's as though the user says, "Give me a web page," and the server says, "Hold on one second; let me make a fresh one for you." Often, the server will look at data in a database and deliver a customized HTML file based on timely information. Other times, the web page the user sees is not only customized with timely information, but it reflects the user's personal interests. How does the server know? One way the server knows what the user wants is based on information sent when the user makes a request. For example, if you visit a search engine, type something in the search field, and then press "Search," the text you typed is sent to the server, which processes it and configures a web page just for you.

The two ways (that apply to this section) of getting data to and from the server are called GET and POST. Both have their respective advantages and disadvantages. You might have noticed a telltale sign of the GET technique because it will actually show the variables in the URL's address (for example, http://www.example.com/searchengine.html?searchfor=flashbooks). Check it out: The part after the actual HTML file looks like URL-encoded text which is exactly what it is! In the case of POST, the data is still sent back to the server, but it's done stealthily; you won't see it in the URL address. Some servers are built around special applications that receive data from the users in the server's own proprietary way. Often you'll see your URL address end in something other than an HTML file (such as search.dll or search.cfm). In those cases, the server is still configuring a web page especially for you, but doing so might involve several steps. For example, before sending the user a web page, the server might store information for statistical purposes about what was requested and then send a page back. I'm not going to (nor am I able to) explain all these technologies, but just that realize data is sent from the user to a server, which responds, eventually, with a web page.

Provided that the server has been configured to send URL-encoded text back to Flash, you can easily have Flash ask the server for such a dynamic string of data. Flash taps into the standard GET and POST mechanisms to send data to a server and, optionally, get data back.

Consider the following possibilities:

  • Flash can send data to a server.

  • Flash can ask a server for data.

  • Flash can send data to a server and get data back.

  • Flash can jump to another web page at the same time that it sends data to a server.

  • A server can send data to an HTML file, which in turn gives all the data to the Flash movie when it initially displays.

Instead of providing examples for each scenario, I can summarize them quite simply. The two gateways to GET and POST are through getURL() and loadVariables(). When you execute a getURL() hyperlink, you can optionally send all the variables currently in the _root main timeline by specifying either "GET" or "POST" as a third parameter. For example, getURL ("other.html", "", "GET") uses the GET technique. Notice that because the second parameter is for an optional window parameter, if you don't plan to use this parameter, you need to provide at least an empty string in its place. Keep in mind that most likely you'll actually specify a server application file (rather than "other.html"), which will not only accept the variables you're sending but, in turn, send an HTML file to the user for example, getURL("someapp.php","","GET"). If you have only one custom variable in your main timeline (say, username and its value is currently "phillip"), this will perform the same function as typing the following into the URL address: http://www.example.com/someapp.php?username=phillip. A good way to test whether the server script is responding correctly is to type the preceding URL into the browser's address bar. (Additional variables are, as with all URL-encoded text, separated with question marks.)

The only catch to notice in preceding example is that only the variables in the main timeline are sent in a URL-encoded string.

The way to load variables from a server into Flash is so simple that you're going to flip. You use the same loadVariables() technique explained earlier, but instead of pointing to a text file, you simply point to a server script. For example, myClip.loadVariables("someapp.cgi") acts the same as loading from a text file but waits for the server to send URL-encoded variables into Flash. Keep in mind that this technique simply asks the server for variables. If you want to ask the server for variables by first sending all of what you have in Flash's main timeline, you simply provide either "GET" or "POST" as an additional parameter, as in myClip.loadVariables("someapp.cgi","GET").

Without explaining how to write server scripts, let me say that they can be very easy. The most effort you'll invest is in deciding which variables will be sent from Flash (that is, what variables the server script will need) and then which variables need to be sent back to Flash (that is, which variables the server script will produce). I highly recommend using either loadVariables() in conjunction with the data clip event, or the LoadVars object along with its onLoad callback function (as we did in the preceding section).

Obviously, if you were expecting an extensive explanation of server scripting, you've been disappointed. However, there are countless resources for topics such as CGI and the PHP scripting languages. It turns out that macromedia just introduced a new set of protocols called"Flash Remoting" to improve the way Flash "talks" to servers. In any project, you'll spend a lot of time working in Flash and at the same time the server scripting person will spend a lot of time in her databases and scripts. But simply getting the data to travel between Flash and a server is pretty nominal after you learn the proper protocols.

XML

Even though using loadVariables() to import data (and getURL() to send data to a server) is quite powerful, the limiting factor is how you must format the data. Only name-value pairs are supported, which means you can only import or export variables and their values. Sometimes that is enough, but some types of data are more complex than that (for example, an object with its properties). In addition, the techniques we've seen in this chapter require that you carefully plan the structure of such data and stick to it. After all, Flash needs to know which variables are coming in, and a server script needs to know which variables are being sent.

As a format, XML is not very exciting. It's not supposed to be. It's only supposed to be standardized so that data structured as XML in one program can be interpreted by another. In Flash, XML is interesting for two reasons. First, because it's extensible; even after you design a data structure, you can add levels of information without breaking what's already built. Second, it's a standard that many applications support. For example, any database program worth its salt can export a database in XML format. The exciting part is that through its extensibility, you can design your XML data to be as complex as necessary. Flash can import XML-structured data, make sense of it (that is, parse out the elements it needs), and also modify or create XML data that can be sent to server applications.

Just like the other technologies in this chapter, the difficult part isn't exchanging data (in this case, as XML data), but rather designing an application to fill a particular need. I use an imaginary application to explain the foundation tools available for exchanging XML data. Again, the challenge for you will be to apply this knowledge to your own projects as appropriate.

First, let's look at the way some simple XML data is formatted. In my example, we'll load in data from a text file that contains XML-structured data. (It would be just as easy to load the same data from a server application that provides it in XML format.)

<ROSTER>  <STUDENT>    <NAME>Phillip</NAME>    <SEX>MALE</SEX>    <GPA>4.0</GPA >    <DEGREE>Photography</DEGREE>  </STUDENT>  </ROSTER>

XML separates the structure from the data (not unlike the code-data separation concept discussed in Chapter 3, "The Programmer's Approach"). Tags or, to use an XML term, nodes such as <ROSTER>, <STUDENT>, and <GPA> serve to provide arbitrary names for the data they enclose. For example, the value of the <GPA> node is "4.0". You can have nodes nested inside of nodes; for example, <NAME>, <SEX>, <GPA>, and <DEGREE> are all "children" nodes of the <STUDENT> node (which, in turn, is a child node of <ROSTER>).

In this example, notice that enclosed in the ROSTER node, is a STUDENT. Inside the student node, there are four nodes (NAME, SEX, GPA, and DEGREE), each of which encloses a value. Depending on the level of the hierarchy, you can look at this data in different ways. For example, there's only one student in the roster. The name for the first (and only) student is Phillip, his GPA is 4.0, and so on. You can extend this basic format to include more students provided that they are enclosed between the tags <STUDENT> and </STUDENT>. If they are going to be elements of the roster as well, additional students must appear before the closing </ROSTER>. Consider the following example:

<ROSTER>  <STUDENT>    <NAME>Phillip</NAME>    <SEX>male</SEX>    <GPA>4.0</GPA >    <DEGREE>Photography</DEGREE>  </STUDENT>  <STUDENT>    <NAME>Sally</NAME>    <SEX>female</SEX>    <GPA>3.81</GPA >    <DEGREE>Painting</DEGREE>  </STUDENT>  </ROSTER>

This is a very basic XML structure to which you can add additional elements at will. Not only can you add more children to the ROSTER node (that is more STUDENTs), but also you can add children to the STUDENT node perhaps a GRADUATION_DATE tag. Also, it's possible to add attributes to any node. Think of attributes like named properties of objects. Attributes are a good solution when you know how much data is expected. For example, because you don't know the total number of students, regular nodes would be best. However, if you know that each student will have an ID number, you can create an ID attribute. Compare nodes to array items of which you can have any number. Named properties are like attributes because it's easiest if you know their names. To add an attribute in this case a student ID number for each student node you can change <STUDENT> to <STUDENT ID="123">. You can add additional attributes within the tag, for example _<STUDENT ID="123" YEAR="Sophomore" EXPECTED_GRAD="1989">. Later, you'll see how easy it is to extract such attribute values.

So far, we've just been formatting the data. There's at least one more detail that you need to add to an XML data file: a declaration at the top (<?xml version="1.0"?>). Besides being a requirement, the declaration ultimately means that the first node we care about is really the first child. Notice that I include only the version property. There are other properties that you can include in the declaration that generally let you explain the overall structure and mode of operation but this one is the minimum necessary.

Here is a complete XML data source used for the following examples (that I'm saving in a file called "my_data.xml"):

<?xml version="1.0"?>  <CATALOG>  <SONG duration="2:50">    <TITLE>Alec Eiffel</TITLE>    <ALBUM>Trompe Le Monde</ALBUM>    <LABEL>4AD</LABEL>    <ARTIST>Pixies</ARTIST>  </SONG>  <SONG duration="5:16">    <TITLE>Optimistic</TITLE>    <ALBUM>Kid A</ALBUM>    <LABEL>Capitol</LABEL>    <ARTIST>Radiohead</ARTIST>  </SONG>  <SONG duration="2:37">    <TITLE>Brass Monkey</TITLE>    <ALBUM>Licensed to Ill</ALBUM>    <LABEL>Columbia</LABEL>    <ARTIST>Beastie Boys</ARTIST>  </SONG>  <SONG duration="2:43">    <TITLE>I don't want to grow up</TITLE>    <ALBUM>Adios Amigos</ALBUM>    <LABEL>Radioactive Records</LABEL>    <ARTIST>Ramones</ARTIST>    <WRITER>Tom Waits</WRITER>  </SONG>  </CATALOG>

Note

Before we jump into Flash to learn how to load in all this data, let me point out a couple things. First, XML data needs to be "valid." For example, every < must be balanced with a corresponding >. There are other rules to make valid XML files. If you use a plain text editor, it's easy to make a small typo that will invalidate the XML. When your XML data grows you should probably consider getting a dedicated XML editor. On the simple (and free) end of the spectrum is Microsoft's XML notepad (http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnxml/html/xmlpaddownload.asp). On the advanced end of the spectrum, you'll find XML Spy (http://www.xmlspy.com/), which will help manage many XML files. I've used both of these Windows-based programs, and they both help create valid XML.

Notice that spaces and returns (like in my XML data) make it more legible. (I'm talking about the spaces between the tags.) While this is totally valid, XML it will pose a problem if you're viewing the Flash movie in a Flash Player prior to version 5.41. (In Workshop Chapter 1, "Ensuring That Users Have the Flash Player 6," you'll learn how to check exactly which version the user has.) In the upcoming scripts, I am careful to take advantage of an XML Object property called ignoreWhite (which effectively resolves an issue for earlier Flash Players). Basically, if users have the old Flash Player, blank spaces can be interpreted as nodes; therefore, your first node may be a space character! Because the Flash Player 6 has vastly improved the speed performance of XML parsing, it seems reasonable to require users to have the new Player. Of course, it's possible that you don't want to force the users to download the most-recent Player (although I think you'd be doing them a favor). If that's the case, you can send all the loaded XML data through a recursive function that searches for illegitimate characters and removes erroneous nodes. Basically, clean it up before you try to use it. Flash guru Colin Moock has a nice piece of code that does just that, available at: www.moock.org/asdg/codedepot (under "strip xml whitespace").

To load this data into Flash, we need to use the XML object. The general procedure is to instantiate the object, load data from an XML source, and then parse the data into a form that we can use inside Flash. Just like loadVariables, you want to make sure that the data is fully loaded before you start trying to use it and naturally, there's a way to ensure that it's loaded. Here's a starter script that you could put in the first frame:

my_xml=new XML();  my_xml.load("data.xml");

This script creates an instance of the XML object, places it in the variable my_xml, and then uses the load() method to start the import process. By the way, "data.xml" could just as easily be a URL address or a server application that returns XML structured data. It's really best that you adjust the preceding code to appear as follows:

my_xml=new XML();  my_xml.ignoreWhite=true;  my_xml.onLoad=parse;  my_xml.load("data.xml");

The second line sets the ignoreWhite property to true so that blank space between nodes is ignored (as explained earlier). The third line sets the onLoad callback function to trigger the homemade parse() function (which we'll write in a minute) as soon as the data is fully loaded. Notice that we do all of this before we start loading in the fourth line. If you want to advance to the next frame when importing is complete, you could change the line to read:

my_xml.onLoad=_root.nextFrame();

(Notice that I refer to the _root time absolutely, not by using this example that would address the my_xml object, which can't go to the next frame.)

So, that's really it! The first line made an object. Next we set the ignoreWhite property, and then specified a function name that we want to execute when it's finished. Then after it was fashioned like we need, we can start loading. Next we can write the parse() function, which will go through all the XML data and extract parts that we can use in Flash. This is a common approach: Structure your data as you want it, load it in, and use only the parts you want. For example, you could make an array full of the XML data's values. You could also load the XML data and extract portions (from the my_xml variable), as needed. Finally, you could send the XML data back to a server (say, after your Flash movie has made adjustments the user has invoked). In the following example, we'll just load in all the data and then parse it into an array full of generic objects in addition to fashioning a presentable string (to appear onscreen).

Here is the final script that steps through all the XML data, populates an array, and builds a string containing both the node names and the node values:

 1 function parse(success){  2   if (!success){  3     trace("problem");   4     return;   5   }   6   myCatalog=new Array();   7   var songs=this.childNodes[0].childNodes;   8   for (var s=0; s<songs.length; s++){  9     var thisData= new Object();  10     thisData.duration=songs[s].attributes.duration;  11     for (var t=0; t<songs[s].childNodes.length;t++){ 12       thisData[songs[s].childNodes[t].nodeName] = songs[s].childNodes[t].firstChild. graphics/ccc.gifnodeValue;  13     }  14     myCatalog[s]=thisData;  15   }  16   doString();  17 }

Let me first explain this function's objective and how it works in pseudo-code; then we can walk through each line. In the end, I want an array (myCatalog) that contains a slot for each song. Each song will be in the form of a generic object with named properties (one for duration, for TITLE, and so on). I want to use a generic object for each song because named properties are easier to access. For example, the duration for the first song in my array is returned with the expression myCatalog[0].duration. If I had made an array for each song, I'd have to remember the index for each value (for example, duration could be in slot 0 but I'd have to remember). However, named properties are slightly more difficult to handle when you don't know how many each song will contain. You can see that it gets sort of ugly in the for loop in lines 11 13. (I'll explain them shortly.) If I had known that each song had a value for duration plus four nodes (TITLE, ALBUM, LABEL, and ARTIST), it would have been slightly easier. However, just to mix things up, I added another node for WRITER in the last song. This meant I couldn't do something such as myObject.title=title and myObject.album=album because I would be locked into the same structure for each song. (Naturally, the italic text would need to be replaced with expressions that resulted in values from the current song's nodes.) So the objective is to make an array of songs, with each song being a generic object with named properties.

As a quick pseudo-code explanation: My function steps through each child node of "catalog." For each song, it first grabs the value for the duration attribute and then steps through each child-node of the song. For each node inside the song, I grab both the name of the node and its value. Sounds easy enough, eh? (I'll explain some techniques to make it fairly easy after the code walkthrough.)

Now for the walkthrough: You should notice in the first few lines that my parse() function expects a parameter (success). The funny part is that I didn't say anything about parameters when I identified the callback function (my_xml.onLoad = parse). Well, the parameter is just a built-in feature of the onLoad event. Because onLoad triggers even when there's a failure, the value for success (either true or false) tells you whether onLoad is triggering because it's really finished loading or because there was a problem. (By the way, as an exercise, you might try to re-work some of the preceding scripts using the LoadVars object's onLoad event; the same "success" parameter works there, too.) Anyway, if there was a problem, I just return out of the function (line 4).

The meat starts on line 6, where I initialize the main array (myCatalog). Line 7 creates a local variable (songs) that contains an array full of all the songs. That is, "this" is the whole XML node and this.childNodes is an array of all the children nodes of the whole XML (really, just the CATALOG node). The first node of the children (this.childNodes[0]) is the first CATALOG. In this case, there's only one catalog; however, to get an array of the songs within the first catalog, we grab all the child nodes of that catalog: this.childNodes[0].childNodes. In line 8, I define my main loop (looping through all the songs in the catalog). The songs variable is already proving useful, as I use its length to calculate the number of times to loop. (My variable s will go from 0 to 3 for four songs.) Line 9 initializes a generic object (thisData) into which I will store data about the current song. Notice that line 9 executes for each song, so I create a fresh one each time, and then (on line 14) I stuff it into a slot of myCatalog (before looping again). Line 10 simply assigns the duration property of my thisData variable to the value of the duration attribute of this song. Because songs is an array of all songs, songs[s] is the first song (if s is 0). That song's attributes property (songs[s].attributes) contains all the attributes, but since we want only the duration attribute, we use songs[s].attributes.duration. Then, in order to loop through each node in this song (songs[s]), the for loop in line 11 sets a variable t to loop through all the childNodes (of this song) or songs[s].childNodes.length. Line 12 looks sort of ugly but it's really just long. The pseudo-code is thisData.currentNodeName=currentNode's value. That is, I want to set a property in thisData with the same name as the node. The value for that property will be that node's value. As you recall, to access or set properties for which you don't know their actual names, you use object[string]. So, the part on the left side of line 12 is simply setting a property name called songs[s].childNodes[t].nodeName (that is, the nodeName property of this song's current child if the loop variable t is 0, then the first child). The funky thing (I think) is that you don't just say songs[s].childNodes[t].nodeValue to get the node's value, but rather songs[s].childNodes[t].firstChild.nodeName. If songs[0].childNodes[0].nodeName is "TITLE", then songs[0].childNodes[0].firstChild.nodeValue is the TITLE tag's value. Think of it this way: The text that appears within the node tag is the value of that node's first child. If you understood all that, you can skip the next paragraph (where I show some tricks to deduce these expressions). Anyway, right outside the loop in line 14, I stuff into myCatalog (into slot s) all this data (the value for each node as well as the duration attribute). Then, in the last line, I trigger another function: doString() (which is discussed later).

If you think I just typed in those long expressions and got it right on the first try, you don't know me very well. I tried a few variations in a trace statement. I did it in pieces, always diving down from the top. For example, I initially wanted to know how to address the main catalog. My parse() function contained only one line: trace(this.childNodes[0].nodeName). Even that expression could take some time to work out, but once I had a handle on it, I could comment that line (to save it for later), and then move on to trying to express the first song. The point is that once I did such gymnastics, I had a library of expressions that I could grab later. If I needed to address a song, I could grab all or part of one of the trace statements I had tested. Such warm-up exercises are a great way to start. Once you know how to access any item in the XML data, you can concentrate on writing the function looping and populating the array. Occasionally, I'd have to pause and consider how to address another node (or child or whatever), but I could check my thinking quickly with a trace statement.

I should probably mention that there's no requirement that you move your XML data into an array. I usually do it this way because I'm just more comfortable accessing data in arrays and generic objects than nodes and children of XML. Here's a function nearly identical to the parse() function. It's what I call in line 17 in the preceding example. The idea simply is to extract data from the XML object and present it onscreen (in a text field called my_txt:

function doString(){   var songs=my_xml.childNodes[0].childNodes;    var theString= "My " + my_xml.childNodes[0].nodeName +                   " has " +songs.length + " "+                   my_xml.childNodes[0].firstChild.nodeName +                   "s \r __________" + "\r";    for (var s=0; s<songs.length; s++){     theString += "#"+s+" "+      my_xml.childNodes[0].firstChild.nodeName + "====" +"\r"+      "Duration: "+songs[s].attributes.duration + "\r";      for (var t=0; t<songs[s].childNodes.length;t++){       theString += songs[s].childNodes[t].nodeName + " = " +        songs[s].childNodes[t].firstChild.nodeValue +"\r";      }    }    my_txt.text=theString;  }

It might look different, but it's same nested loop structure from the parse() function. In this case, though, I'm fashioning a text string. You can see how the finished output looks with content below. Notice that I'm taking advantage of the fact that Flash ignores line breaks. I did it just for legibility. Also, notice that I can't refer to the main XML node using this (as I did in the parse() function). The reason it worked before is that parse was identified as the onLoad callback function for the my_xml instance. Therefore, the "this" was my_xml. The doString() function was invoked in the last line of parse(). There is no "this" for doString(), except the function itself.

Here's the onscreen output from the doString() function:

My CATALOG has 4 SONGs  .  #0 SONG====  Duration: 2:50  TITLE = Alec Eiffel  ALBUM = Trompe Le Monde  LABEL = 4AD  ARTIST = Pixies  #1 SONG====  Duration: 5:16  TITLE = Optimistic  ALBUM = Kid A  LABEL = Capitol  ARTIST = Radiohead  #2 SONG====  Duration: 2:37  TITLE = Brass Monkey  ALBUM = Licensed to Ill  LABEL = Columbia  ARTIST = Beastie Boys  #3 SONG====  Duration: 2:43  TITLE = I Don't Want To Grow Up  ALBUM = Adios Amigos  LABEL = Radioactive Records  ARTIST = Ramones  WRITER = Tom Waits

To really study all the code in this section, it's best to see it in Flash with syntax coloring. You can download code for this chapter from www.phillipkerman.com/actionscripting. Figure 16.4 shows a visualization of the data from this example.

Figure 16.4. A hierarchical view of the XML data (in the file on the right) shows that values are ascertained only for nodes three levels deep.

graphics/16fig04.gif

Hopefully you can see some of the potential for XML-structured data. It's easy for me to say that most of the work really isn't in Flash, but rather in the way you design the XML solutions. It's true. You'll find your time is spent deciding how the data will be structured and then parsing out just the elements (node values) that you can use. It doesn't hurt, however, to remember the following key points:

  • Right before you initiate the load() command, you should identify a callback function to execute when it's finished loading that is my_xml.onLoad=someFunction. This way you'll know for sure when the data has loaded.

  • Extracting nodes, their values, and their properties utilizes one of the following methods: firstChild, which returns an XML object; childNodes, which returns an array of all the nodes nested within a node; or nodeName and nodeValue, which both return strings.

Local SharedObjects

graphics/icon02.gif

It's hard to pick a favorite new feature in Flash MX, but local SharedObjects has got to be one. Basically, local SharedObjects are like HTML cookies but much more. They enable you to store data (securely on users' hard drives) and then read that data later when they revisit your site. For example, if they've typed in their names the first time they visited your site, you can present them with a "Welcome back..." message when they return. The process is not only simple, but it requires no server technology. Naturally, like most things, coming up with a good strategy on how to use local SharedObjects takes a little more work.

The idea is that you store data in a generic object and then save that object in a special file on the users' hard drives. Even the data types of the values in your generic object are retained. This is a big deal. Numbers are stored as numbers, arrays as arrays, and so on. Compared to all the other techniques that send and receive string-only data (such as loadVariables()), this is a major convenience. (In the "Advanced Local SharedObject Examples" section, you'll see how easily different data types are stored .)

The data for your local SharedObject is stored in a text file located in a subfolder of the "Flash Player" folder in a designated location (depending on the user's operating system). The subfolder's name matches your web site's domain, but within that folder, you have some control over where the file is saved.

Example: Never See the Intro Again

Here's a simple example that jumps a user who has visited this site ahead to a frame labeled "post_intro":

1 my_so= SharedObject.getLocal("myCookie");  2 if (my_so.data.seenIntro){ 3   gotoAndPlay("post_intro");  4 }  5 my_so.data.seenIntro=true;  6 my_so.flush();

Let me explain each line. Line 1 effectively instantiates a generic object and places it in the homemade variable my_so. It's nearly the same as saying my_so=New Object(), except that the object we create above automatically includes a named property called data, and it's value is in the form of a generic object. As if you had another line of code (my_so.data=new Object()), it's just automatic when you use the getLocal() method of the local SharedObject. Line 1 also attempts to determine whether there is already a local SharedObject file on this user's machine (in this case, a file called "myCookie.sol"). Finally, line 1 will create such a file if none exists. Anyway, the very first time this runs, the condition for the if statement (line 2) is false. That is, my_so.data.seenIntro is undefined, so it doesn't evaluate to true and they skip down to line 5. At that point, the seenIntro property (of the data property in the my_so object) is set to true. Finally, in line 6, the flush() method forces all new values in the my_so variable to be written to disk. If you type this code into frame 1 of your movie (and make a frame, say, out at frame 10 labeled "post_intro"), the second time you test the movie, it will jump ahead to frame 10.

Details of Local SharedObject Files

There are just a couple of details to know about before I show you a more advanced example. The getLocal() method first looks for a file, and then creates one if none is found. The name of the file matches the parameter plus ".sol" (for, SharedObject Library). The exact location of that file is within the Flash Player folder. And the Flash Player folder is in a different location on every operating system. (You can find details about this location in the readme file in the Configuration folder adjacent to your installed version of Flash.) Within the Flash Player folder, .sol files are always saved under a folder name that matches your domain (such as phillipkerman.com), or if you're running the movie locally, inside a folder called localhost. Within your domain's folder, additional sub-folders are created to match exactly which part of your web site the user visited. For example, if you're only using local SharedObjects in an .swf that resides in a folder called "flashsite" that sits in the root directory of your domain (say, phillipkerman.com), then .sol files generated by .swfs in the flashsite folder will appear in the folder Flash Player\phillipkerman.com\flashsite\.

This automatic sub-folder creation means that from different parts of your site you could create .sol files with the same name, and you'll have no conflicts. In practice, though, I think this might be complicated and, besides, it's easy to come up with unique names for the .sol files (it's the first parameter in the getLocal() method). However, if you want to share data from .sol files throughout different parts of your site, you can override the default behavior of sub-folder creation. Specifically, an optional second parameter in the getLocal() method enables you to specify a path. For example, getLocal("myCookie", "/") will access the myCookie.sol file in the root directory and getLocal("myCookie","/datafolder") will access myCookie.sol in a subdirectory called datafolder. By the way, .sol files cannot include spaces or any of the following characters: ~ % & \ ; : " ' , < > ? #.

Disk Space Limits

There are a few more miscellaneous details. The flush() method immediately tries to write data to disk. If you never bother to invoke the flush() command, it should do so automatically when the user leaves your site. It still seems safer to invoke it at logical points in time (namely, when the user makes some kind of significant change that you want to save). Notice that I said flush() attempts to write the data to disk. In fact, the amount of data that you're able to store is limited by the user's settings, which are accessible from the Flash Player when they right-click on a Flash movie playing in the browser (or control-click, for Mac users) see Figure 16.5.

Figure 16.5. The user will be presented with the Macromedia Flash Player Settings dialog box as soon as a movie attempts to save large amounts of data.

graphics/16fig05.gif

Actually, the user will see the Macromedia Flash Player Settings dialog box appear any time you attempt to save more data than they have set. (The default is 100KB.) Whatever setting the user makes is permanent. The whole strategy works very well in its automatic form (presenting the dialog box only when necessary, for example). However, you can monitor which setting the user has granted to you. Any time you invoke flush() and your .sol file will exceed the user's current settings, she will see the Macromedia Flash Player Settings dialog box. To trigger a function when she finally answers the dialog box, you can use the onStatus event. Here's an example that you can place after the getLocal() step but before the first flush() occurrence. In this example the text property of a field called message will update according to how the user has answered the dialog box:

my_so.onStatus= function(info){   if (info.code=="SharedObject.Flush.Success"){     message.text="It worked!";    }    if (info.code=="SharedObject.Flush.Failed"){     message.text="It failed."    }  }

Basically, we're just assigning a function as the callback for the onStatus event of our my_so object. Notice that the built-in parameter (which I'm calling info). It includes a property called code containing a string that reports what issue occurred (good or bad). Keep in mind that the above function will never execute nor will the user automatically see the settings in the dialog box unless your flush() method is about to pose a problem (go over the quota). To test code like that above, you can include the optional parameter for the flush() method that is, a "minimum disk space." For example, flush(100) will see if it's okay to save 100KB. You might request more space than you really need in order to get the user's approval early instead of finding out later that you can't save all the data you've gathered. You can also force the settings dialog to appear anytime with teh code: system.showSettings(). By the way, you can ascertain the current size of your SharedObject instance by using the getSize() method, as in my_so.getSize().

One more detail and then you'll see an example that brings it all together. The flush() command will actually return information as to the success of the operation. That is, you can say my_so.flush(), but you can also say result=my_so.flush() and the value of result will turn into either true, false, or (the string) "pending". You'll get true when it was a success, false when the user has already said "deny" or there was some kind of error, and "pending" if the Settings dialog box was forced to appear (and therefore you're waiting for the user's response). Regardless of how fast the user clicks "allow" or "deny," the value returned from flush() will stay "pending." A practical situation might be that you want the movie to pause if the Settings dialog box appears. You'll need to stop the movie only if flush() returns "pending." To get the movie rolling again, you'll want to use the onStatus event (and respond according to the code property discussed earlier that is, according to how the user answered).

Advanced Local SharedObject Examples

It turns out that 100KB is really a fair bit of data. In this next example, I give users the ability to create blocks and then drag them to any location onscreen. When they revisit the site, everything appears as they left it. You'll see that just saving and restoring the local SharedObject data is fairly simple; the majority of the work involves gathering the data while the users interact as well as re-dis-playing the clips when they return. My basic strategy was to keep count of each new clip the users created. That way, I could name each instance this["clip_"+count] and place it in a level number equal to count. Then, when it came time to save data, I would loop through 1, through the value of count, and gather information about each clip: its location and its color. (Each clip is an instance of the same clip, just colored differently.) Inside the first keyframe of the "dragclip" is the following code:

this.onPress=function(){   dragging=true;  }  this.onRelease=function(){   _root.updateRecords();    dragging=false;  }  this.onReleaseOutside=function(){   _root.updateRecords();    dragging=false;  }  this.onMouseMove=function(){   if(dragging){     this._x=_parent._xmouse;      this._y=_parent._ymouse;      updateAfterEvent();    }  }

This code has little to do with local SharedObjects. Notice, however, that whenever the user lets go, I call the custom function updateRecords() (which you'll see in a minute). I suppose it might look a bit fancy the way no code is placed on any clips but since this clip will be spawn by way of attachMovie, I couldn't put any code on the clip. Anyway, this clip has a linkage identifier of "dragclip".

Next I had three button instances (named red, green, and blue) on the Stage. In addition, I placed the following code in the first keyframe:

 1 red.onPress=function(){pick(1)}   2 green.onPress=function(){pick(2)}   3 blue.onPress=function(){pick(3)}   4   5 colors=[0xFF0000, 0x00FF00, 0x0000FF];   6 function pick(which){  7   count++;   8   this.attachMovie("dragclip","clip_"+count,count);   9   this["clip_"+count].c=new Color(this["clip_"+count]);  10   this["clip_"+count].c.setRGB(colors[which-1]);  11   updateRecords();  12 }

The first three lines cause the three buttons to trigger the pick() function but with a different value for a parameter each time. Line 5 contains an array full of color values (red, green, and blue). The pick() function (line 6) first increments count (so that the first time it will be 1, then 2, and so on). Then it creates a new instance of the "dragclip" symbol with an instance name clip_1 (or, clip_2 based on count) and places it in a level number matched to count. The next two lines (9 and 10) set the newly created clip's color. Notice that this["clip_"+count] appears in a few places this is just a dynamic reference to the newly created clip. The first line stores a Color object instance into the variable c (that is part of the clip at hand). The second line sets the RGB of that Color object instance to a color from the colors array, based on the value for the parameter received (which). Finally, the homemade function updateRecords()is called which you're about to see.

My final code has more stuff above the code I just showed you, but it's easiest to look at the code in this order. Anyway, the following code is placed above what you just saw, even though I'm going to explain the updateRecords() function next (despite the fact that it's at the bottom of the following code first):

 1 my_so= SharedObject.getLocal("locations","/");   2   3 if (my_so.data.count!=undefined){  4   count=my_so.data.count;   5   for(var i=1; i<count+1;i++){  6     this.attachMovie("dragclip","clip_"+i,i);   7     var thisOne=this["clip_"+i];   8     thisOne.c=new Color(thisOne);   9     thisOne.c.setRGB(my_so.data.clipLocs[i-1].c);  10     thisOne._x=my_so.data.clipLocs[i-1].x;  11     thisOne._y=my_so.data.clipLocs[i-1].y;  12   }  13 }  14 function updateRecords(){ 15   my_so.data.count=count;  16   my_so.data.clipLocs=new Array();  17   for(var i=1; i<count+1;i++){ 18     var thisOne=this["clip_"+i];  19     var thisData=new Object();  20     thisData.x=thisOne._x;  21     thisData.y=thisOne._y;  22     thisData.c=thisOne.c.getRGB();  23     my_so.data.clipLocs.push(thisData)  24   }  25   my_so.flush();  26 }

In line 1, you can that see I'm storing into the variable my_so either a new instance of the local SharedObject (if there wasn't one already) or a copy of the old one. Line 3 begins the restoration process automatically, but let's jump down to line 14 where the updateRecords() is defined. After all, the if statement's condition (my_so.data.count!=undefined) will most certainly be false the first time through (count is our variable), so the if statement is skipped. Inside the updateRecords() function, we start stuffing properties onto the my_so.data property. Line 15 assigns a property count equal to the movie's count variable, and then line 16 creates a new array (into a property called clipLocs). The for loop in line 17 goes through each one-by-one (1 through count). Line 18 creates a local variable that points to the current clip (so that I can repeatedly type thisOne rather than this["clip_"+i] in the following lines). Then I make a generic object (thisData) onto which I'm about to set three named property values (x, y, and c). The clipLocs array will become an array full of generic objects (one for each clip), and those objects will each have an x, y, and c property. Lines 20 22 assign values for those three properties, based on the values for the current clip's _x and _y properties plus the homemade variable c (which contains a Color object instance). Then line 23 stuffs the generic object onto the end of the array (using the push() method). Finally, the current value for my_so is written to disk in line 25.

Now that we know how data was saved, we can look through the restoration code (lines 4 12). Realize lines 4 12 will be executed in the first keyframe, provided that the condition in line 3 is true that is, the my_so's data has a count value. So, if we get to line 4, we know they're returning and we need to restore the variables and display. Line 4 shows the only variable we really need from the local SharedObject file: count. That is, we need to make sure that count starts where they left off. The rest involves creating instances, putting them in the right location, and coloring them all according to values in the clipLocs array (which is a property of the data property of the my_so instance). Line 6 creates a new clip, and line 7 stores a local variable to save typing in the next few lines. Line 8 makes a new instance of the Color object into a variable c (of the current clip). Line 9 sets RGB (based on the c value found in the current position of the clipLocs array). Notice that the portion my_so.data.clipLocs[i-1] appears in several places. This simply returns the value in a particular slot of the clipLocs array (namely, the i-1 slot because i goes from 1 to count and arrays count starting at 0). The value in that slot of the clipLocs is itself a generic object (with three named properties). Looking at line 10 and 11 is somewhat intimidating unless you break it down or read it backwards: We're setting this one's _x and _y equal to the x property of the generic object in the current slot of the myLocs array (which itself is a property of the data property of the my_so instance). Believe it or not, I think it was easier to write than it is to read. If the details of this code are fuzzy, try to first understand the overall objectives and sequence. You can also download this file (from www.phillipkerman.com/actionscripting) or try to write it from scratch yourself. Hopefully, it will give you some ideas, too.

This chapter is already long, so adding one more paragraph can't hurt. After building the preceding file, I realized it was tiresome for me to keep deleting the .sol file while testing. I thought the user might want to clear everything. Well, I just added a button (instance name clear) and placed the following code in my first keyframe (see if you can find the bug):

1 clear.onPress=function(){ 2   for(var i=1; i<count+1;i++){ 3     this["clip_"+i].removeMovieClip()  4   }  5   count=0;  6   updateRecords();  7 }

Although this code is totally logical (and not very far off), line 3 has a bug. The problem is that this is the button instance itself. You can fix this bug by changing this to _root. While outside of the onPress callback function, this["clip_"+i] would indeed refer to the clip called clip_1 (if i were 1), but it doesn't work here because this is the button! (If you like finding bugs, you'll love Workshop Chapter 18, "Fixing Broken Scripts.")

The LocalConnection Object

graphics/icon02.gif

I guess all the new Flash MX features are my favorite because this one is pretty cool too! The Local Connection object enables two separate Flash movies to communicate with each other. Think of it this way: This feature lets one movie trigger functions that are written in another movie. The applications for this are really quite vast. You can have different .swfs in HTML table cells or different frames triggering events in each other. Perhaps, one .swf that contains code and music is placed in a hidden frame, and then when the visitor navigates to different parts of your web site, the music continues to play. Scripts in various other movies can trigger sound effects to play in that hidden movie. Not only does the audio download just once, you won't hear any breaks in the music as different pages load. This is just one example; I can also see doing something where a pop-up HTML window containing an .swf could trigger scripts present in other windows perhaps a master controller to play or stop other movies. Again, the hard part is coming up with an idea; the code for LocalConnection objects is really quite simple.

To work through the upcoming code, you need to consider a few things. We're going to have two movies: receiver.swf and transmitter.swf. We'll write a function inside receiver.swf and then let transmitter.swf trigger that code. Inside receiver.fla, make a text field with the instance name message, and then put the following code in the first frame:

lc = new LocalConnection();  lc.onMessageTime=function(param){   message.text=param;  }  lc.connect("receiver");

The first line stores an instance of the LocalConnection object into the variable lc. Then, you need to define all the functions to which the other movie will be given access in this case, just one function called onMessageTime. When the other movie invokes onMessageTime(), it will also send a single parameter (param) and we'll be able to see its value in the message text field. Finally, the last line invokes the LocalConnection object's connect() method. The name I provided ("receiver") is just an arbitrary name I came up with; the other movie will need to refer to this same name.

In the other movie (transmitter.fla), create a button with the following code on it:

on (press){   lc = new LocalConnection();    lc.send("receiver", "onMessageTime", getTimer());    lc.close();    delete lc;  }

This code first makes an instance of the LocalConnection object. (Because this code is in another file, however, it's not as though lc is the same variable.) Next, the LocalConnection object's send() method is invoked. The syntax is send(theName, theFunction, parameterValue), where theName matches the name provided in the other movie's connect() method; theFunction is the function we plan to trigger; and parameterValue is the parameter being sent. It's almost as though this code were onMessageTime(getTimer()) but it doesn't work this way because onMessageTime is really a callback event attached to the other movie's lc instance; in addition, it's in another movie, so instead we address the name that the other movie connected to. The only thing I find difficult is that the syntax doesn't follow other addressing techniques we've seen. Finally, the lc variable (in the transmitter movie) is deleted, as we're done sending messages to the other movie and no longer need to be connected. When you export and launch both movies, you will be able to click the button in transmitter.swf and see a number change in the receiver.swf file (see Figure 16.6).

Figure 16.6. Two separate .swfs can send messages via the LocalConnection object.

graphics/16fig06.gif

I'm not going to work through an advanced example here; you will see one in Workshop Chapter 17, "Using the LocalConnection Object for Inter-Movie Communication." I will, however, mention a few more details about the LocalConnection object. First, it's easy to send additional parameters to your receiver movie just separate them with commas. For example, you could trigger the onMultiFunction event and send three parameters (p1, p2,and p3) by using the following code:

lc.send("receiver", "onMultiFunction", p1, p2, p3);

Also, as with any function, you can pass any sort of data type that you want. It might be confusing because since the send() method's second parameter is always a string version of the callback name, the parameters' data type is retained. For example, you could pass an array or an object in addition to a number or a string.

One thing that I didn't really mention earlier is that the order of the code in the receiver movie is important. Notice that the first bit of code had three steps: instantiate the LocalConnection object, declare the callbacks, and then connect.

lc = new LocalConnection();  lc.onMessageTime=function(param){   message.text=param;  }  lc.connect("receiver");

The point is, you want to be sure to define all the callbacks (in this case, only onMessageTime) before you connect. You can have many more callbacks, just be sure to put them before connect().

Finally, the LocalConnection object has only three methods: (open(), send(), close() (plus the System Object's allowDomain() method). They actually return a value, so you could use result=lc.open("somename"). Although they always return either true or false, the result can be different than you might expect. You might think that true means the connection was successful and that false means it was a failure. However, open() returns true if the named connection isn't already open, and both send() and close() return true if the named connection is open. It kind of makes sense: The result just tells you whether everything seems to be in order. However, additional information can be useful; namely, whether the send() method was successful. You need to define a callback function for the onStatus event. Unfortunately, I can't seem to make it work for an individual LocalConnection instance (such as lc.onStatus=function(){}). However, you can overwrite one super onStatus callback for the LocalConnection object itself. For example, you can place the following code inside the transmitter movie, and it will display either "error" or "status" (the two possible values for the LocalConnection's onStatus event) into a text field called message:

LocalConnection.prototype.onStatus = function(info) {   message.text=this.info.level;  }

This is just like the way we overwrote events of the Movie Clip and Array objects in Chapter 14. It's also similar to the onStatus callback function we wrote for the local SharedObject. In this case, however, the parameter received has the interesting data hidden inside the property called level (rather than the code property). By the way, if you want a single onStatus callback function like this, but for the local SharedObject, you can change my_so.onStatus to read SharedObject.prototype.onStatus in the definition earlier.

Summary

What can I say? This was a long chapter! It's the last one in the Foundation section, and we're about to move on to some fun workshop exercises. Hopefully, you are beginning to see things synthesize. For example, adding callbacks to the topics in this chapter was relatively easy because you already understand callbacks generally. At this point, you should feel comfortable in any programming language; you haven't just been learning Flash, you've been learning programming. (Congratulations, you're now a programming geek!)

Seriously, this chapter showed you many of the ways in which Flash can interact with the world around it. We started by including linked scripts when exporting a movie. Then we saw how Flash can read in data from text files. The same format was used when we learned how Flash can read data sent to it via a server script. The advantage to server scripts is that Flash can also send data (that is, all its variables) to the server. Although the standard technique (loadVariables() and getURL()) used the URL-encoded format, we also took a dive into XML-structured data. We rounded out the chapter with a good look at two new Flash MX features: local SharedObjects (to save data between sessions) and the Local Connection Object (to let multiple .swfs talk to each other). Although you might not have an immediate need for all the material in this chapter, you should have a clear idea of what's possible.

CONTENTS


ActionScripting in MacromediaR FlashT MX
ActionScripting in MacromediaR FlashT MX
ISBN: N/A
EAN: N/A
Year: 2001
Pages: 41

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net