ActionScript.org Flash, Flex and ActionScript Resources - http://www.actionscript.org/resources
Quiz Application Part 2 - XML
http://www.actionscript.org/resources/articles/898/1/Quiz-Application-Part-2---XML/Page1.html
Jody Hall
My interest in Flash started mostly because of a Jib-Jab cartoon ("This Land") in 2004. I'm the author of a feature I call "Mazoons," which are a combination of mazes and cartoons. In 2002, I even had a book published, "Super Silly Mazes." I'm not a professional programmer, but making my mazes interactive by programming them with Flash became a hobby/obsession of mine, to the point where I have now learned more than I bargained for. Lately I'm working on a new website about Flash and Actionscript 3.0 called The Flash Connection
By Jody Hall
Published on June 27, 2009
 
After I wrote the article about creating a Quiz program, I got comments from people requesting that it be adapted to use an external XML file. In this article, we will modify it so that it does exactly that. So for those of you who requested it, this ought to satisfy both of you :) .

Note: The starter files are available for download on the first page of this article. The finished files are available for download from the bottom of the last page.

Writing the XML File
download Original_Quiz.zip (the starter files)

Let's begin with the XML file. I'm just going to use the same silly questions and answers that I made up previously, except that I added one more so that there are five questions altogether.

Notice that each item in this file has the same basic structure. There is a question, an answer (the digit that represents the correct answer), and any number of choices. This is the same basic structure that we formerly used in the quiz program, so adapting it shouldn't be very much hassle at all.

Make sure this file is in the same folder as your other quiz files. If you don't have the other quiz files, they're available from the link at the top of this page. The XML file is included in the zip. But if you didn't download it from the link above, you can copy and paste it into a text editor and save it as "quiz.xml." And here it is:
<?xml version="1.0" encoding="utf-8" ?>
<data>
<item>
<question>What color is an orange?</question>
<answer>2</answer>
<choice>Orange</choice>
<choice>Blue</choice>
<choice>Purple</choice>
<choice>Brown</choice>
</item>
<item>
<question>What is the shape of planet earth?</question>
<answer>3</answer>
<choice>Flat</choice>
<choice>Cube</choice>
<choice>Round</choice>
<choice>Shabby</choice>
</item>
<item>
<question>Who created SpiderMan?</question>
<answer>2</answer>
<choice>Jack Kirby</choice>
<choice>Stan Lee and Steve Ditko</choice>
<choice>Stan Lee</choice>
<choice>Steve Ditko</choice>
<choice>none of the above</choice>
</item>
<item>
<question>Who created Mad?</question>
<answer>2</answer>
<choice>Al Feldstein</choice>
<choice>William M. Gaines</choice>
<choice>Jack Davis</choice>
<choice>none of the above</choice>
</item>
<item>
<question>Who was our first President?</question>
<answer>1</answer>
<choice>George Washington</choice>
<choice>John Adams</choice>
<choice>Jack Daniels</choice>
<choice>Skeeter Davis</choice>
</item>
</data>

We're only going to work on two files: QuizApp.as and QuizQuestion.as. We're going to make the QuizApp get its data from the XML file, and we're going to make QuizQuestion.as receive an XML object as a parameter instead of the three parameters it gets now. Ready?

P.S. I just realized that the last question I threw in there is kind of US-centric, and this is the Internet. So if you are not in the United States, feel free to change that one to whatever it would be for your country. In fact, change any or all of the questions and make it your quiz!


Modifying the QuizApp Class
Since the data will now be coming from an XML file instead, the first thing to do is to delete the insides of the createQuestions function. But keep the function itself, as we'll just be rewriting it:
[as]private function createQuestions() {
    quizQuestions.push(new QuizQuestion("What color is an orange?",
                                        1,
                                        "Orange",
                                        "Blue",
                                        "Purple",
                                        "Brown"));
    quizQuestions.push(new QuizQuestion("What is the shape of planet earth?",
                                        3,
                                        "Flat",
                                        "Cube",
                                        "Round",
                                        "Shabby"));
    quizQuestions.push(new QuizQuestion("Who created SpiderMan?",
                                        2,
                                        "Jack Kirby",
                                        "Stan Lee and Steve Ditko",
                                        "Stan Lee",
                                        "Steve Ditko",
                                        "none of the above"));
    quizQuestions.push(new QuizQuestion("Who created Mad?",
                                        2,
                                        "Al Feldstein",
                                        "Harvey Kurtzman",
                                        "William M. Gaines",
                                        "Jack Davis",
                                        "none of the above"));
}[/as]
After deleting the insides, the function will look like this:
[as]private function createQuestions() {  

}[/as]
Next, let's turn our attention to the constructor, which right now looks like this:
[as]public function QuizApp() {
    quizQuestions = new Array();
    createQuestions();
    createButtons();
    createStatusBox();
    addAllQuestions();
    hideAllQuestions();
    firstQuestion();
}[/as]

Since everything in the program depends on the XML being loaded first, let's highlight all these function calls in the constructor, and cut it all to the clipboard (right-click "Cut" or CTRL-X). Once again, leave the constructor function itself, just cut the insides of it to the clipboard.

Next, create a new function called xmlLoaded. This is the event listener that will fire when the XML file has completely loaded. This event listener will listen for the Event.COMPLETE event, so give it an event parameter, make it return void, and then paste the block of function calls from the clipboard into this new function. So your constructor (now empty) and this new function should look like this:

[as]public function QuizApp() {
 
}
private function xmlLoaded(event:Event):void {
    quizQuestions = new Array();
    createQuestions();
    createButtons();
    createStatusBox();
    addAllQuestions();
    hideAllQuestions();
    firstQuestion();
}[/as]

Next, inside the constructor, write the code that creates an URLLoader instance, adds the event listener for the Event.COMPLETE event, and gives the command to load the XML file:

[as]public function QuizApp() {
    var urlLoader:URLLoader = new URLLoader();
    urlLoader.addEventListener(Event.COMPLETE, xmlLoaded);
    urlLoader.load(new URLRequest("quiz.xml"));    
}[/as]

One thing to note here is that the urlLoader object has been purposely created as a local variable to this function, and not a class member (or variable). There is only one other place that we'll need to refer to this object ever again, and that will be in the COMPLETE handler that we'll write, and in that place it will be known as event.target. Likewise, the URLRequest object will be needed only this one time, so there is no need to even save it as a named variable. A lot of times it's handier just to create a new object like this and use it "on the fly," so to speak.

And now, since the URLLoader, URLRequest, and Event classes haven't been imported, add these lines to the import block at the top of the code:

[as]import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;[/as]

Next, there needs to be a variable to store the XML data once it loads. Add this line to the variables list:

[as]private var xml:XML;[/as]

The XML class is one of Actionscript's top level classes, so there is no need to add any import statement in this case. Next, insert a couple of lines at the top of the xmlLoaded function, and add these lines of code:

[as]xml = XML(event.target.data);
trace(xml);[/as]
Also, comment out all of the function calls after the trace(xml) line. This prevents the whole rest of the program from executing. That way we can test the program and not get any errors. So your xmlLoaded function should now look like this:
[as]private function xmlLoaded(event:Event):void {
    xml = XML(event.target.data);
    trace(xml);
    //quizQuestions = new Array();
    //createQuestions();
    //createButtons();
    //createStatusBox();
    //addAllQuestions();
    //hideAllQuestions();
    //firstQuestion();
}[/as]

I usually like to take the step of tracing out the XML just to make sure that it loaded correctly. Press CTRL-ENTER to run the program. You should get the contents of the XML file (minus the line at the top of the file that just declares the file as being XML) traced to your output window. Now our little xml variable contains the whole contents of the externally loaded XML file. Notice that it's root node is <data>.

Now I want to tell you a curious little feature or characteristic of working with XML. But maybe it would be best to tell you by showing you. Change the trace command in the above to this version instead:

[as]trace(xml.item[0]);[/as]

You will get this output:

<item>
<question>What color is an orange?</question>
<answer>2</answer>
<choice>Orange</choice>
<choice>Blue</choice>
<choice>Purple</choice>
<choice>Brown</choice>
</item>

What we have done here is to output just the first item. We can treat the <item> node as an array, and use bracket syntax to "get at" just one of the items. But what happened to the <data> node? Notice that we didn't say:

[as]trace(xml.data.item[0]);[/as]

That would have been incorrect. The "curious feature" I mentioned is this: Whenever you set an XML variable equal to a chunk of XML data, it's as though the variable name "becomes" the root node. So it's as if the <data> node got "absorbed" by the xml variable. Now the xml variable name takes the place of the root node whenever we put together a path with dot syntax. I hope that's clear.

Speaking of dot syntax, let's further modify this trace statement to this:

[as]trace(xml.item[0].question);[/as]

You'll get this output:

What color is an orange?

Change the trace statement to this:

[as]trace(xml.item[0].answer);[/as]

You'll get this output:

2

Change the trace statement to this:

[as]trace(xml.item[0].choice[0]);[/as]

You'll get this output:

Orange

So, you can see that using dot syntax and brackets (whenever there are items that repeat), you can "get at" whatever piece of data you want. Using XML data this way is fairly easy once you get used to it. The trace command is your friend, because before using a piece of data, you can always trace out its value first to make sure it's what you want.

Next, go ahead and uncomment all the function calls you formerly commented out. Now we'll work on the createQuestions function. Add these lines inside:

[as]private function createQuestions() {
    for each(var i:XML in xml.item) {
        quizQuestions.push(new QuizQuestion(i));
     }
}[/as]

Here I am using a "for each" loop instead of my usual for loop. Why? Well, we might as well acquire a new skill here (me included!). The "for each" loop works really well for XML data. In this case, the "i" variable is typed as XML. Each time through the loop, "i" will equal the next item in the xml.item list. Each QuizQuestion instance, as it is created, will be sent a little piece of XML data to its constructor that looks like this:

<item>
<question>What color is an orange?</question>
<answer>2</answer>
<choice>Orange</choice>
<choice>Blue</choice>
<choice>Purple</choice>
<choice>Brown</choice>
</item>

Obviously, we're going to have to edit the QuizQuestion.as file next. We're going to make it accept one parameter of an XML type, instead of the several parameters it takes now. Then it will get its information from the XML snippet that it's now being sent. We'll do that on the next page. For now, here's the completed QuizApp file, with all the changes outlined above:

[as]package {
 
    import flash.display.Sprite;
    import fl.controls.Button;
    import flash.events.MouseEvent;
    import flash.text.TextField;
    import flash.text.TextFieldAutoSize;
    import flash.events.Event;
    import flash.net.URLLoader;
    import flash.net.URLRequest;
 
    public class QuizApp extends Sprite {
        //for managing questions:
        private var quizQuestions:Array;
        private var currentQuestion:QuizQuestion;
        private var currentIndex:int = 0;
        //the buttons:
        private var prevButton:Button;
        private var nextButton:Button;
        private var finishButton:Button;
        //scoring and messages:
        private var score:int = 0;
        private var status:TextField;
     private var xml:XML;
 
        public function QuizApp() {
            var urlLoader:URLLoader = new URLLoader();
         urlLoader.addEventListener(Event.COMPLETE, xmlLoaded);
         urlLoader.load(new URLRequest("quiz.xml"));  
        }
     private function xmlLoaded(event:Event):void {
         xml = XML(event.target.data);
         quizQuestions = new Array();
            createQuestions();
            createButtons();
            createStatusBox();
            addAllQuestions();
            hideAllQuestions();
            firstQuestion();
     }
        private function createQuestions() {
            for each(var i:XML in xml.item) {
        quizQuestions.push(new QuizQuestion(i));
         }
        }
        private function createButtons() {
            var yPosition:Number = stage.stageHeight - 40;
 
            prevButton = new Button();
            prevButton.label = "Previous";
            prevButton.x = 30;
            prevButton.y = yPosition;
            prevButton.addEventListener(MouseEvent.CLICK, prevHandler);
            addChild(prevButton);
 
            nextButton = new Button();
            nextButton.label = "Next";
            nextButton.x = prevButton.x + prevButton.width + 40;
            nextButton.y = yPosition;
            nextButton.addEventListener(MouseEvent.CLICK, nextHandler);
            addChild(nextButton);
 
            finishButton = new Button();
            finishButton.label = "Finish";
            finishButton.x = nextButton.x + nextButton.width + 40;
            finishButton.y = yPosition;
            finishButton.addEventListener(MouseEvent.CLICK, finishHandler);
            addChild(finishButton);
        }
        private function createStatusBox() {
            status = new TextField();
            status.autoSize = TextFieldAutoSize.LEFT;
            status.y = stage.stageHeight - 80;
 
            addChild(status);
        }
        private function showMessage(theMessage:String) {
            status.text = theMessage;
            status.x = (stage.stageWidth / 2) - (status.width / 2);
        }
        private function addAllQuestions() {
            for(var i:int = 0; i < quizQuestions.length; i++) {
                addChild(quizQuestions[i]);
            }
        }
        private function hideAllQuestions() {
            for(var i:int = 0; i < quizQuestions.length; i++) {
                quizQuestions[i].visible = false;
            }
        }
        private function firstQuestion() {
            currentQuestion = quizQuestions[0];
            currentQuestion.visible = true;
        }
        private function prevHandler(event:MouseEvent) {
            showMessage("");
            if(currentIndex > 0) {
                currentQuestion.visible = false;
                currentIndex--;
                currentQuestion = quizQuestions[currentIndex];
                currentQuestion.visible = true;
            } else {
                showMessage("This is the first question, there are no previous ones");
            }
        }
        private function nextHandler(event:MouseEvent) {
            showMessage("");
            if(currentQuestion.userAnswer == 0) {
                showMessage("Please answer the current question before continuing");
                return;
            }
            if(currentIndex < (quizQuestions.length - 1)) {
                currentQuestion.visible = false;
                currentIndex++;
                currentQuestion = quizQuestions[currentIndex];
                currentQuestion.visible = true;
            } else {
                showMessage("That's all the questions! Click Finish to Score, or Previous to go back");
            }
        }
        private function finishHandler(event:MouseEvent) {
            showMessage("");
            var finished:Boolean = true;
            for(var i:int = 0; i < quizQuestions.length; i++) {
                if(quizQuestions[i].userAnswer == 0) {
                    finished = false;
                    break;
                }
            }
            if(finished) {
                prevButton.visible = false;
                nextButton.visible = false;
                finishButton.visible = false;
                hideAllQuestions();
                computeScore();
            } else {
                showMessage("You haven't answered all of the questions");
            }
        }
        private function computeScore() {
            for(var i:int = 0; i < quizQuestions.length; i++) {
                if(quizQuestions[i].userAnswer == quizQuestions[i].correctAnswer) {
                    score++;
                }
            }
            showMessage("You answered " + score + " correct out of " + quizQuestions.length + " questions.");
        }
    }
}[/as]



Adapting the QuizQuestion Class

Next, let's turn our attention to the QuizQuestion.as file. Our goal here is to modify the parameters in the constructor so that it uses XML instead. We have changed the QuizApp class so that it creates a series of new QuizQuestion instances in a loop. Each time through the loop, it sends an item of XML data to the constructor of the QuizQuestion class. Finally (as you may recall), QuizApp stores each QuizQuestion instance in an array.

So all we have to do is modify the constructor of the QuizQuestion class so that it just takes just one parameter, of the XML type. Then we'll modify the first few lines of this function, where the data is set. Everything else in the whole class will work exactly the same way. All we will have done is change out the source of the data. Here's the constructor function as it stands now: 

[as]public function QuizQuestion(theQuestion:String, theAnswer:int, ...answers) {
    //store the supplied arguments in the private variables:
    question = theQuestion;
    theCorrectAnswer = theAnswer;
    choices = answers;
    //create and position the textfield (question):
    questionField = new TextField();
    questionField.text = question;
    questionField.autoSize = TextFieldAutoSize.LEFT;
    questionField.x = questionX;
    questionField.y = questionY;
    addChild(questionField);
    //create and position the radio buttons (answers):
    var myGroup:RadioButtonGroup = new RadioButtonGroup("group1");
    myGroup.addEventListener(Event.CHANGE, changeHandler);
    for(var i:int = 0; i < choices.length; i++) {
        var rb:RadioButton = new RadioButton();
        rb.textField.autoSize = TextFieldAutoSize.LEFT;
        rb.label = choices[i];
        rb.group = myGroup;
        rb.value = i + 1;
        rb.x = answerX;
        rb.y = answerY + (i * spacing);
        addChild(rb);
    }
}[/as]

First, change the insides of the parameter list to this version: 

[as]public function QuizQuestion(snippet:XML) {[/as]

We get to just make up the parameter name, any meaningful name will do. I have chosen to call it "snippet," because it makes sense to me, it makes it sound like a fragment of the original XML, which is what it is. Each set of XML data coming to this class will look like this (I'll use the first one as an example):

<item>
<question>What color is an orange?</question>
<answer>2</answer>
<choice>Orange</choice>
<choice>Blue</choice>
<choice>Purple</choice>
<choice>Brown</choice>
</item>

Now, the root node of this little bit of XML is <item>. Like I said previously, the curious little feature of working with XML is that the variable you set "becomes" the root node. So "snippet" takes the place of <item> when we make a path with dot syntax. So, for example, snippet.question is our way of gettting the question, snippet.answer is the answer, and snippet.choice[0] is the first choice.

That being the case, the first couple of lines of the constructor can be changed to:

[as]question = snippet.question;
theCorrectAnswer = snippet.answer;[/as]

For the choices array, we're going to have to write another loop. But first, since we are no longer sending an array and then setting the choices array equal to that, the choices array never gets a value. So we need to instantiate the choices array as a new array before we can add anything to it (so add this line):

[as]choices = new Array();[/as]

Now all we need to do is write another "for each" loop, so that the information in the XML can be pushed into the choices array:

[as]for each(var string:String in snippet.choice) {
    choices.push(string);
}[/as]

Here's the completed QuizQuestion file:

[as]package {
    import flash.display.Sprite;
    import flash.text.TextField;
    import flash.text.TextFieldAutoSize;
    import flash.events.Event;
    import fl.controls.RadioButton;
    import fl.controls.RadioButtonGroup;
 
    public class QuizQuestion extends Sprite {
        private var question:String;
        private var questionField:TextField;
        private var choices:Array;
        private var theCorrectAnswer:int;
        private var theUserAnswer:int;
 
        //variables for positioning:
        private var questionX:int = 25;
        private var questionY:int = 25;
        private var answerX:int = 60;
        private var answerY:int = 55;
        private var spacing:int = 25;
 
        public function QuizQuestion(snippet:XML) {
            //store the supplied arguments in the private variables:
            question = snippet.question;
            theCorrectAnswer = snippet.answer;
            choices = new Array();
        for each(var string:String in snippet.choice) {
        choices.push(string);
            }
            //create and position the textfield (question):
            questionField = new TextField();
            questionField.text = question;
            questionField.autoSize = TextFieldAutoSize.LEFT;
            questionField.x = questionX;
            questionField.y = questionY;
            addChild(questionField);
            //create and position the radio buttons (answers):
            var myGroup:RadioButtonGroup = new RadioButtonGroup("group1");
            myGroup.addEventListener(Event.CHANGE, changeHandler);
            for(var i:int = 0; i < choices.length; i++) {
                var rb:RadioButton = new RadioButton();
                rb.textField.autoSize = TextFieldAutoSize.LEFT;
                rb.label = choices[i];
                rb.group = myGroup;
                rb.value = i + 1;
                rb.x = answerX;
                rb.y = answerY + (i * spacing);
                addChild(rb);
            }
        }
 
        private function changeHandler(event:Event) {
            theUserAnswer = event.target.selectedData;
        }
        public function get correctAnswer():int {
            return theCorrectAnswer;
        }
        public function get userAnswer():int {
            return theUserAnswer;
        }
    }
}[/as]

Save the file, then test the application. It should behave exactly as the original did, but now it is getting its information from an external XML file instead!

In case you didn't know it already, the huge advantage of using XML as the data source is that it makes the application open-ended! In other words, you can add to (or delete from)  the questions that the application uses just by editing and saving (and maybe uploading) the XML file. The next time the application (the swf) runs, it will load the XML file again, and automatically use the new revised list of questions.

I've attached to this article the ZIP file containing the finished files