The AdditionExample Applet

From WeBWorK_wiki
Jump to navigation Jump to search

Introduction

In this example, it is shown how to set up a simple applet for use in a WeBWork problem. The complete code for the applet, together with the corresponding PG file and debugging tools, can be downloaded from (add download site). The applet, shown below, asks for the sum of two integers. Notice that there are no buttons for checking the answer in the applet, since this will be handled by WeBWork.

AdditionExampleStart.jpg

Initial Setup

The initial code for the applet can be downloaded from (add download site), or it can be created by the following steps.

  1. Create a new applet in Flash and draw the controls as shown in the picture above. The first two text fields are of type Dynamic Text, and the rightmost one is of type Input Text
  2. Name the text fields, respectively, txtFirstNumber, txtSecondNumber and txtResult.
  3. Click anywhere on the stage outside of the controls and, in the properties pane, set the Class field to AdditionExample.
  4. Create a new ActionScript 3.0 class file and add the code below and save the file as AdditionExample.as.
package {
        import flash.display.MovieClip;

 	public class AdditionExample extends MovieClip {
 		public function AdditionExample() {
 			txtFirstNumber.text = "9";
 			txtSecondNumber.text = "7";
 		}
 	}
 }

At this point, the movie can be tested in Flash. It should display the applet with the numbers "9" and "7" in the first two text fields.

Debugging Tools

The interface between the applet and WeBWork cannot be tested inside the Flash environment. To simplify debugging, included in the downloaded package are two files:

  • DebugBox.as, an ActionScript class that displays a text area where debug messages can be sent.
  • ww_minimal.html, which contains JavaScript code that simulates the interaction between JavaScript and the applet without having to upload it to WeBWork. This makes it much easier to test the interface functions.

Copy these two files to the directory where the applet is located.

Important notice: in order to test the applet interface, the Flash player must be authorized to access local content. To do this, open ww_minimal.html in your browser. Right-click on the applet and select Global settings.... Click the Advanced tab and the Trusted Location Settings... button. Add the folder where your applet is located to the trusted sites.

Add the following two variables to the AdditionExample class:

private var debugLevel:uint = 0;
private var dbBox:DebugBox;

Change the constructor of the class as follows:

public function AdditionExample(debugLevel:uint=2) {
	setDebug(debugLevel);
}

Finally, add the following function to the class:

public function setDebug(debugLevel:uint):void {
	if (debugLevel > 0 && dbBox == null) {
		dbBox = new DebugBox(400, 100, 0, 0, 0.8);
		addChild(dbBox);
	}
	if (debugLevel == 0 && dbBox != null) {
		removeChild(dbBox);
		dbBox = null;
	}
	this.debugLevel = debugLevel;
	if (this.debugLevel > 0) {
		dbBox.add("setDebug called. debugLevel set to " + this.debugLevel.toString());
	}
}

Save the file and run it in Flash. The applet should now also display a text area with debug messages. Close the applet and open ww_minimal.html in your browser. The modified version of the applet should appear (if it does not, try clearing the cache in the browser).

Click on the applet and press <Ctrl>-<Shift>-D. The debug box should become invisible. Pressing the same key combination again should make the box appear again.

Initializing the Interface

Before we go on, add the following imports under the package declaration:

import flash.events.MouseEvent;
import flash.events.Event;
import flash.display.Shape;
import flash.external.ExternalInterface;
import flash.utils.Timer;
import flash.events.TimerEvent;

Now add the following declarations to the AdditionExample class:

private const appletName:String = "AdditionExample";
private const DELAY:uint = 100;
private var maxTrials:uint;
private var interfaceTimer:Timer;
private var configString:String = "<xml> </xml>";

and modify the constructor as follows:

public function AdditionExample(maxTrials:uint=5, debugLevel:uint=2) {
	this.maxTrials = maxTrials;
	setDebug(debugLevel);
	addEventListener(Event.ADDED_TO_STAGE, addedToStage);
}

Add the following code to the class:

private function setUpInterface():void {
	if (debugLevel == 2) {
		dbBox.add("setUpInterface called.");
	}
	ExternalInterface.addCallback("debug", setDebug);
	ExternalInterface.call("applet_loaded", appletName, 1);
}


private function addedToStage(evt:Event):void {
	if (debugLevel == 2) {
		dbBox.add("Event ADDED_TO_STAGE received, will try to set" + 
				  "up communication with JavaScript");
	}
	//Set up communication with JavaScript
	if (ExternalInterface.available) {
		try {
			setUpInterface();
		} catch (err:Error) {
			if (debugLevel > 0) {
				dbBox.add("Exception caught calling applet_loaded." +
					"Setting up timer for further calls.", 
                                        DebugBox.severityWarning);
				interfaceTimer = new Timer(DELAY, maxTrials);
				interfaceTimer.addEventListener(TimerEvent.TIMER, timerListener);
				interfaceTimer.addEventListener(TimerEvent.TIMER_COMPLETE, 
                                                                timerCompleteListener);
				interfaceTimer.start();
				return;
			}
		}
		if (debugLevel == 2) {
			dbBox.add("Interface with JavaScript apparently established.");
		}
	} else {
		if (debugLevel > 0) {
			dbBox.add("External interface not available", DebugBox.severityError);
		}
	}
}
		
private function timerListener(evt:TimerEvent):void {
	if (debugLevel == 2) {
		dbBox.add("Retrying to set up communication with Javascript. " +
			  "Number of trials: " + interfaceTimer.repeatCount.toString());
	}
	try {
		setUpInterface();
	} catch (err:Error) {
		if (debugLevel > 0) {
			dbBox.add("Exception caught in timerListener. Will try again in " +
				   DELAY.toString() + " milliseconds (unless maximum number " +
				   "of trials has been exceeded).");
		}
	}
	if (debugLevel == 2) {
		dbBox.add("Interface with JavaScript apparently established.");
	}
	interfaceTimer.stop()
	interfaceTimer = null;
}
		
private function timerCompleteListener(evt:TimerEvent):void {
	if (debugLevel > 0) {
		dbBox.add("Unable to establish interface with JavaScript after " +
				  maxTrials.toString() + " attempts",
				  DebugBox.severityCritical);
	}
	interfaceTimer = null;
}

The important point to note in this code are the refences to ExternalInterface in the function setUpInterface(). These are the calls that will establish the interface with JavaScript in the WeBWork page. Right now, there are two references to ExternalInterface:

  • ExternalInterface.addCallback("debug", setDebug); This registers the applet function setDebug to be called by the JavaScript function debug. JavaScript calls this function to set the debug level in the applet.
  • ExternalInterface.call("applet_loaded", appletName, 1); This calls the JavaScript function applet_loaded, with arguments appletName and 1. This signals to JavaScript that the applet has loaded.

The rest of the code attempts to call setUpInterface() repeatedly, with a delay between each call. In practice, most of the times the applet will take longer to load than the other items in the WeBWork problem, so the call to setUpInterface() will succeed in the first time. However, if there are more than one applet being loaded in the page, it might take longer for the page to respond.

To test the code, first test the movie in Flash to generate a new .swf file, and then open (or reload) ww_minimal.html. The applet will load, and in the debug box you should see the message:

Interface with JavaScript apparently established.

Also check the Log text area at the bottom of the page. There will be a lot of messages saying that the interface functions are not found (we still did not define them). At a certain point in the Log, however, the following will be seen:

>applet_loaded
Setting reportsLoaded to 1
<applet_loaded

This indicates that the JavaScript function applet_loaded has been called, so that the applet is able to communicate with the page.

Defining the Interface Functions

isActive

Add the following code to the applet:

public function isActive():uint {
	if (debugLevel == 2) {
		dbBox.add("isActive called, returning true");
	}
	return 1;
}

Also add the following line to the setupInterface:

ExternalInterface.addCallback("isActive", isActive);

Run the applet in Flash, then reload ww_minimal.html. Now, the following message will be displayed in the debug box:

isActive called, returning true

Also, in the Log are you will see the message:

Applet loaded, will now try to configure it

This means that JavaScript considers the applet as being loaded, since it passed two tests: the JavaScript function applet_loaded has been called by the applet, and JavaScript was able to call the applet function isActive, being returned a true value. This indicates that two-way communications between the applet and JavaScript has been established.

setConfig

Add the following code the to applet:

public function setConfig(xmlString:String):uint {
	var firstNumber:uint = 1;
	var secondNumber:uint = 1;
	try {
		var xmlData:XML = new XML(xmlString);
		firstNumber = uint(xmlData.first_number);
		secondNumber = uint(xmlData.second_number);
	} catch (error:TypeError) {
		if (debugLevel > 0) {
			dbBox.add("Exception calling setConfig, probabaly due to a " + 
					   "malformed XML string:" + xmlString,
					   DebugBox.severityError);					
		}
		return 0;
	}
	txtFirstNumber.text = firstNumber.toString();
	txtSecondNumber.text = secondNumber.toString();
	if (debugLevel == 2) {
		dbBox.add("setConfig called, configuration set to: " +
				  "firstNumber = " + firstNumber.toString() + 
				  " secondNumber = " + secondNumber.toString()); 
	}
	return 1;
}

Although not defined by the interface, it is recommended that the function getConfig is added for debugging purposes:

public function getConfig():String {
	return configString;
}

Finally, add the following lines to setUpInterface():

ExternalInterface.addCallback("setConfig", setConfig);
ExternalInterface.addCallback("getConfig", getConfig);

Then test the applet: run it in Flash, then reload ww_minimal.html. Notice now that the debug box will have a message stating that the configuration has been set. Hide the debug box by clicking on the applet and pressing <Ctrl>-<Shift>-D. You should see the numbers 5 and 3 in the first two text fields in the applet.

Now click the button Call getConfig. If the interface is correctly defined, the following will appear in the Output text area:

<XML> <first_number>5</first_number> <second_number>3</second_number> </XML>

This is the string that was passed by JavaScript to configure the applet. In WeBWork, this string is defined in the PG file. Let's now test setConfig. Copy the string from the Output box to the Input box and change it to:

<XML> <first_number>4</first_number> <second_number>7</second_number> </XML></nowiki>

and press Call setConfig. This will cause the numbers in the applet to change to 4 and 7.

As a last test, let's see what happens in case there are errors in the XML string. Change the string in the Input area to:

<XML> <first_number>3</first_number> <second_number>2 </XML>

This XML string is malformed, since the <second_number> is never closed. When you press Call setConfig, an error message will be displayed in the debug box. (If the debug box is not visible, click on the applet and press <Ctrl>-<Shift>-D. It is extremely important that this kind of error is caught in the debug phase, since it may make the applet mysteriously fail in WeBWork. Notice that, when there is an error, the configuration of the applet does not change. The result of that is that either the applet will not have meaningful problem data, or that all versions of the problem will be the same.

Were the configuration string came from?

In WeBWork, the configuration string is defined in the PG file. The test page ww_minimal.html simulates passing a configuration string, which is hard-coded in the page. Click View source on the browser, and scroll down the code until you see the following lines:

<script>
	theApplet = new ww_applet("AdditionExample");
	theApplet.configuration             =
Base64.decode("PFhNTD4gPGZpcnN0X251bWJlcj41PC9maXJzdF9udW1iZXI+IDxzZWNvbmRfbnVtYmVyPjM8L3NlY29uZF9udW1iZXI+IDwvWE1MPg==");
	theApplet.getStateAlias             = "getXML";
	theApplet.setStateAlias             = "setXML";
	theApplet.setConfigAlias            = "setConfig";
	theApplet.getConfigAlias            = "getConfig";
	theApplet.getAnswerAlias            = "getAnswer";
	theApplet.isActiveAlias             = "isActive";
	theApplet.maxInitializationAttempts = 5;
</script>

This is similar to the code that WeBWork inserts in the problem page. The configuration string is contained in the theApplet.configuration string. This string is encoded in Base64, simply to be faithful to actual WeBWork page. There are several tools in the Web to encode/decode Base64 strings. If one of these is used to decode the string above, it will be seen that it contains the same XML code from the previous session.

When testing your own strings, you can put an initial configuration string in ww_minimal.html, or use the Call setConfig and Call getConfig on the page for testing. In this case, set theApplet.configuration to any valid XML string, such as <XML> </XML>.

setXML

Recall that the state of the applet reflects the result of student interaction. In this example, the only possible interaction is to type in the result of the operation, so the state has a single element, the number typed in the answer box. So, setXML reads this value and returns a XML representation of this information. Here is the code:

public function setXML(xmlString:String):uint {
	try {
		var xmlData:XML = new XML(xmlString);
		txtResult.text = xmlData.answer;
	} catch (error:TypeError) {
		if (debugLevel > 0) {
			dbBox.add("Exception calling setXML, probabaly due to a " +
				  "malformed XML string:" + xmlString,
			          DebugBox.severityError);		
		}
		return 0;
	}
	if (debugLevel == 2) {
		dbBox.add("setXML called, setting state to: answer = " + txtResult.text
		          + " hintState = " + hintState.toString());
	}
	return 1;
}

Also add to setUpInterface():

ExternalInterface.addCallback("setXML", setXML);

Now, to test the function, proceed as usual and enter the following in the Input field in ww_minimal.html:

<xml><answer>8</answer></xml>

and click Call setXML. This will set the answer field in the applet to the number 8;

getXML

This is the function that JavaScript calls when it needs to save the state of the applet. This function returns an XML string encoding the current contents of the answer text field. Add the following code:

public function getXML():String {
	var xmlData:XML = <XML> </XML>;
	xmlData.answer = txtResult.text;
	if (debugLevel == 2) {
		dbBox.add("getXML called, returning" + xmlData.toString());
	}
	return xmlData.toString();
}

and the usual:

ExternalInterface.addCallback("getXML", getXML);

to setUpInterface After making the changes and reloading www_minimal.html