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:

        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"));
        }

After deleting the insides, the function will look like this:

        private function createQuestions() {
 
        }

Next, let's turn our attention to the constructor, which right now looks like this:

        public function QuizApp() {
            quizQuestions = new Array();
            createQuestions();
            createButtons();
            createStatusBox();
            addAllQuestions();
            hideAllQuestions();
            firstQuestion();
        }

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:

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

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:

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

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:

import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;

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

private var xml:XML;

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:

xml = XML(event.target.data);
trace(xml);

 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:

	    private function xmlLoaded(event:Event):void {
		xml = XML(event.target.data);
		trace(xml);
		//quizQuestions = new Array();
                //createQuestions();
                //createButtons();
                //createStatusBox();
                //addAllQuestions();
                //hideAllQuestions();
                //firstQuestion();
	    }

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:

trace(xml.item[0]);

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:

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

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:

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

You'll get this output:

What color is an orange?

Change the trace statement to this:

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

You'll get this output:

2

Change the trace statement to this:

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

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:

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

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:

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.");
        }
    }
}