Cleaning up Adobe Captivate’s SCORM Publishing Template, Part 2: HTML

In part one of this series, we published a simple Captivate course and examined its file structure. In this part, we’ll take an in-depth look at the HTML generated by Captivate (using the SCORM 2004 publishing template) and clean it up as much as we can.

Here’s the default HTML published by Captivate.

I’ll take the high road and avoid the ever-so-easy snarky comments about this code. Suffice to say it needs some serious cleanup; it’s obvious that multiple people with multiple coding styles have worked on this document, and no quality assurance team has run through it to ensure it’s clean and concise. For example, there is an entire block of code commented out at line 141 (commenting it out means it never gets used, and therefore is useless bloat).

There’s also an aborted attempt at using SWFObject from lines 214 to 242 — the object is declared, but never invoked. Never. Did you know this code has been shipping with Captivate for years? I’m amazed no one has bothered to clean it up (and yes, I have reported it to Adobe, but they chose not to act).

As irritated as I get looking at the code, this blog post is not meant to be a rant, so let’s be constructive and get to work cleaning this mess.

Cleanup task #1: Externalize the JavaScript

This document is incredibly hard to read due to all of the <script> elements and JavaScript code. Let’s start by removing the JavaScript in the document’s <head> by cutting it out and pasting it into an external JavaScript file.

View the results of task #1:
The new captivate.js file
The slightly cleaner sample.htm file

This single step makes the HTML file much easier to read.

Cleanup task #2: Fix the doctype

Stepping through the HTML, the very first lines of code (lines 3-4) contain a conflict: the doctype says this is an HTML 4 (transitional) document, but the head element contains an XHTML lang declaration.

We’re in an HTML5 world now, so let’s use the HTML5 doctype. It’s 100% compatible with all browsers (even Internet Explorer 6), and all the cool kids use it. We can retain the lang declaration, too, with a few modifications. While we’re at it, let’s update the charset meta tag to the accepted HTML5 syntax.

View the result of task #2

Cleanup task #3: End the crazy <script> abuse!

There are six <script> blocks in our HTML file, and they are all inconsistent. For example, some have a “type” attribute, some have a “language” attribute, some have both, and some have neither. This is a sure sign of multiple authors working on the document over a long period of time.

Going through the <script> block, we can clean them up as follows:

  • Line 8: Since we’re using the HTML5 doctype, we can simplify it to remove the “type” attribute.
  • Line 9: Leave as-is.
  • Line 10: Remove the “type” and “language” attributes.
  • Lines 17-25: This entire block is never invoked and is completely useless. Delete it.
  • Lines 37-73: The contents of this block need cleanup, but we’ll handle that later one. For now, just remove the “type” and “language” attributes.
  • Lines 74-77: There is no need for a separate <script> block here; merge the contents with the previous <script> block.

View the result of task #3

Cleanup task #4: Remove inline scripts and styles

The <body> element contains two inline attributes: a style attribute, and an onunload handler. It’s an accepted best practice to leave your markup as clean as possible by removing inline styles and scripts when possible, so let’s create a new <style> element to hold the CSS, and move the onunload handler to our captivate.js file (just paste window.onunload = Finish; at the bottom of captivate.js).

View the result of task #4

If you’re not sleeping yet, you’re probably wondering why creating a <style> block is better than leaving the style attribute in the <body> tag. For this project, I know I will be adding more CSS declarations to the <style> block, so we may as well set it up now.

Cleanup task #5: Cure <center>itis

There are two sets of <center> elements on the page that can be replaced by adding one line of CSS to the top of the page.

Delete lines 16, 47, 66 and 72. Add text-align: center; to the body selector in the <style> block.

View the result of task #5

Cleanup task #6: Edit the <noscript> block

For accessibility and usability purposes, it’s usually a good idea to include a <noscript> block; this enables you to present a message or fallback content to visitors who have JavaScript disabled in their browsers.

Captivate uses SWFObject — a JavaScript utility — to embed the Captivate SWF. To hedge their bets, Adobe also attempts to embed the SWF in the <noscript> block using a method that doesn’t require JavaScript. Normally this is a good idea. However, SCORM courses require JavaScript; if JavaScript is disabled, the course won’t be able to communicate with the LMS. Period. End of story.

For this Captivate template, it’s probably best to remove the SWF from the <noscript> block and leave a message for the learner informing them that they can’t take the course unless JavaScript is enabled. Chances are they can’t get to the popup window containing the course anyway!

View the result of task #6

Cleanup task #7: Get rid of the <table> markup

The JavaScript on lines 39-41 and 54-56 generate a <table> whose sole purpose is to make the SWF vertically centered on the page. For our purposes, this is unnecessary because most courses launch in a popup window that has been sized to fit the course.

Ironically, the <table> that is meant to make the embedded SWF look nice on the page actually causes a significant problem — the table has cellpadding and cellspacing set to 7, which means the SWF will never extend to the edge of the window. When you’re trying to make a perfectly-sized popup window for your course, this cellpadding and cellspacing will cause scrollbars to appear, driving you bonkers!

Also, it’s widely accepted that tables shouldn’t be used for layout purposes, anyway, so it’s best to get rid of the <table>.

View the result of task #7

Our HTML file is starting to look pretty sweet!

Cleanup task #8: Externalize more JavaScript where possible

We externalized JavaScript in steps 1 and 4, let’s try and get the rest of this JavaScript externalized. The only remaining inline JavaScript is contained in lines 22 through 51. Our first task is to determine whether it needs to be inline; will moving it break anything?

  • Lines 23 through 36 appear to be safe to move into the document’s <head>, which means it’s safe to move to our captivate.js file. Just cut and paste — place the script at the very bottom of captivate.js.
  • Lines 38 through 47 use SWFObject 1.5 to embed the SWF. We can move this code, but it will need to be wrapped in an onload event or else it will break the page. In a later step, we will upgrade SWFObject to 2.2, which helps us optimize the Captivate template even further.
  • Lines 49 and 50 can be moved, but will also need to be wrapped in an onload event.

While moving the SWFObject code, we can also clean it up a bit:

  • Line 39 is not necessary because quality = high is Flash Player’s default.
  • Line 41 is not necessary because SWFObject is already set to use “Captivate” (the second argument passed on line 38) as the ID for the SWF.
  • Line 42 is not necessary because wmode = window is Flash Player’s default.
  • Line 45 is not necessary because it does nothing, it’s a completely gibberish line of code. I suspect it was a placeholder put there by a developer years ago, and no one has bothered removing it. It has shipped with Captivate for years.
  • Line 46 is useless; the redirect URL is only invoked when two criteria are met: 1) the embed markup is present in the HTML, and 2) the visitor does not have the required version of Flash Player. This Captivate file is using SWFObject to write the embed code. SWFObject has its own check for Flash Player (argument #5 on line 38); if the minimum version specified is not found, SWFObject will not write the embed code into the HTML. This means requirement #1 listed above will never be met, which means the redirect URL will never be invoked.
  • Update: Upon re-reading this post, I noticed even more issues with the SWFObject code: line 40 (‘name’ is not a param, it’s an attribute) and line 43 (bgcolor param isn’t needed, the color should be specified as the 6th argument in the SWFObject declaration). I won’t update the examples because we’re replacing SWFObject 1.5 with 2.2 in the next part of this series, so the entire SWFObject code block will be replaced.

    The updated HTML:

    Nice and simple, huh? We can pare it down more later, but this is a great start.

    View the updated captivate.js file.

    We were able to reduce the sample.htm file from 278 lines of code to 23 lines. Our new captivate.js weighs in at 224 lines of code, but we’ll be taking a sledgehammer to the JavaScript code in part three of this series.

    Continue to part three.

    Advertisements

Cleaning up Adobe Captivate’s SCORM Publishing Template, Part 1: Introduction

Adobe Captivate is an enormously popular tool for e-learning developers. My assumption is that most Captivate users chose Captivate as their development tool because it enables them to publish LMS-compatible courses without requiring any programming skills — no need to know JavaScript, ActionScript, SCORM, etc.

This might explain why no one is up in arms about the astoundingly bad HTML and JavaScript output by Captivate — very few people look under the hood.

In this multi-part series, I will walk through the files Captivate outputs when publishing to SCORM 2004, pointing out the bad parts (hey Adobe: this is constructive criticism, it’s for your own good) and suggesting alternatives when needed. At the end of the series, I will provide a fully-functional SCORM 2004 publishing template you can use with Captivate 5.5.

Getting Started

The first step is to create a very simple Captivate course and publish it using the SCORM 2004 template. This will produce a set of files that we can examine and modify without breaking any templates.

  1. Go to File > New Project > Blank Project
  2. Select a small screen size since we’re just messing around. I selected 640 x 480.
  3. Add something to the stage just so we have something to look at aside from a blank white screen. You can add a text caption, image, etc. — it doesn’t have to be fancy, it just gives us something to look at.
  4. Publish to SCORM 2004
    1. File > Publish Settings
    2. Quiz > Check “Enable reporting for this project”
    3. Under Learning Management System select SCORM.
    4. Click the “Manifest” button. The Manifest dialog will appear; select 2004 from the drop-down menu.
    5. Click OK, then click OK in the Publishing Settings dialog.

Now publish your course: File > Publish. The Publish dialog appears. Nothing to change here, unless you want to select a different location for your files. Click “Publish”.

Attack of the Files

Find the folder containing your published files and take a peek at what Captivate generated for you. It’s quite intimidating, especially with all of those SCORM 2004 XSD files. I suggest sorting the files by kind.

Here’s a quick breakdown of the files:

  • All the DTD and XSD files are a required part of SCORM’s packaging system. The common, extend, unique, and vocab folders all contain SCORM-related XSD files. None of these files or folders should be edited, renamed, moved or deleted. You must keep them, but otherwise it’s best to act as if they don’t exist.
  • The imsmanifest.xml file is required for SCORM 2004. Don’t move or rename it. Some portions of the file may be edited, but you’d better know what you’re doing!
  • The SWF file (in my case sample.swf) is the Captivate course.
  • The HTM file (in my case sample.htm) is the HTML file that contains the SWF.
  • standard.js is a JavaScript file that contains some 3rd-party JavaScript utilities.
  • The SCORM_support folder contains files that Captivate uses to communicate with the LMS when using really old browsers. The files aren’t part of the SCORM standard, they’re just helper files created by Adobe.

Hopefully the files don’t seem so intimidating now that you know what’s what.  Once you ignore the XSD and DTD files, there are actually very few files left to work with.

The next post in this series will cover the HTML file produced by Captivate. Continue to Part 2.

SCORM “Planets” Example Updated

Update 10/13/2011: I added more comments to the ActionScript code to help explain what each line of code does. I also added a Flash CS4 version of the FLA to the ZIP so more people can access the example.

My “Planets” example (How to Add Basic SCORM Code to a Flash Movie) has proven to be one of the most popular items on pipwerks.com. Unfortunately, it was designed as a quick example and had a bunch of flaws and shortcomings. It’s also about 3 years old and starting to show its age. Since people frequently contact me with questions — many of which were due to the flaws in the example — I decided to update the project.

The new Planets example has a bunch of fixes:

  • Full compliance with SCORM XSD requirements (it includes all those pesky XSD files that really don’t do anything but are required anyway).
  • Upgraded the pipwerks JavaScript wrapper to the latest version
  • Upgraded the pipwerks ActionScript 3 wrapper to the latest version
  • Upgraded SWFObject from 1.5 to 2.2
  • Changed the doctype and markup to HTML5
  • Added an unload handler to the JavaScript to ensure course progress is saved if the learner exits early
  • Added support for tracking progress across attempts using cmi.suspend_data
  • Completely rebuilt the FLA file to remove all timeline-based animation and scripts. Now uses a single ActionScript file, loaded on a single frame. All animations (fade transitions) are handled via ActionScript. This makes the file smaller and hopefully less confusing.
  • The SCORM code is clearly separated from the rest of the movie’s ActionScript; it’s in the same file, but not intermingled with other functions.

I haven’t decided if I’ll create a detailed tutorial to explain the updates; I’ve added a ton of comments to the ActionScript file, and hope it’s clear enough for others to follow.

Download the updated Planets example
The ZIP includes two FLA files: one in CS4 format, and the other in CS5.5. The code, movieclips, and graphic elements inside the FLAs are exactly the same.

Abstracting Your Course’s Tracking Code

An abstraction layer is a way of hiding complexities and maintaining cleanliness in your application. For example, if you want to save a file in your word processing application, you simply click “save”, while a whole host of actions is performed for you under-the-hood. In this example, you’re shielded from the complexity of your computer’s I/O operations by a graphical user interface.

When integrating tracking support (SCORM, AICC, etc,) into an an e-learning course, it’s a good idea to abstract as much of the tracking code as possible. For example, if you know your course is going to use SCORM 1.2, you might be tempted to write this:


//Assuming API is the SCORM API

//Display learner's name
var learner_name = API.LMSGetValue("cmi.core.student_name");

//Set course status to incomplete
API.LMSSetValue("cmi.core.lesson_status", "incomplete");

//Tell LMS to save whatever data we've sent
API.LMSCommit("");

This example hard-codes SCORM 1.2 into your course. If you ever decide to use SCORM 2004, you’d need to re-write whole chunks of your course. If you abstract the code by creating functions, you’re making it a little easier to update your code in the future:


//Assuming API is the SCORM API

function getLearnerName(){
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   API.LMSSetValue("cmi.core.lesson_status", "incomplete");
}

function save(){
   API.LMSCommit("");
}

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

Now that your tracking code is encapsulated in functions, it can be easily modified to include SCORM 2004 support:


//Assuming API is the SCORM API
//Checks for SCORM2004 mode, defaults to SCORM 1.2

function getLearnerName(){
   if(tracking_mode === "SCORM2004"){
      return API.GetValue("cmi.learner_name");
   }
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   if(tracking_mode === "SCORM2004"){
      API.SetValue("cmi.completion_status", "incomplete");
   } else {
      API.LMSSetValue("cmi.core.lesson_status", "incomplete");
   }
}

function save(){
   if(tracking_mode === "SCORM2004"){
      API.Commit("");
   } else {
      API.LMSCommit("");
   }
}


//Set tracking mode
var tracking_mode = "SCORM2004";

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

For e-learning courses, abstraction becomes even more powerful if you externalize your tracking-related functions. For example, you’d keep your tracking function invocations in your course code while placing the function definitions in a separate, easy-to-access file. This improves the separation of the tracking mechanism (SCORM, AICC, etc.) from the actual course logic.

Your course code:


//Set tracking mode
var tracking_mode = "SCORM2004";

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

An external JavaScript file containing the function definitions:


//Assuming API is the SCORM API
//Checks for SCORM2004 mode, defaults to SCORM 1.2

function getLearnerName(){
   if(tracking_mode === "SCORM2004"){
      return API.GetValue("cmi.learner_name");
   }
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   if(tracking_mode === "SCORM2004"){
      API.SetValue("cmi.completion_status", "incomplete");
   } else {
      API.LMSSetValue("cmi.core.lesson_status", "incomplete");
   }
}

function save(){
   if(tracking_mode === "SCORM2004"){
      API.Commit("");
   } else {
      API.LMSCommit("");
   }
}

Looking good, except for one thing… do you spot the big problem with this example? The tracking_mode variable is hard-coded in the course. A quick resolution is to dynamically set this variable via a querystring parameter when you load the course:


//Set tracking mode
//This is a quick and dirty example;
//there are better ways to handle querystrings
//Example: mycourse.html?tracking=SCORM2004
var tracking_mode = window.location.href.split("?tracking=")[1];

//Display learner's name
var learner_name = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

At this point, your course code is abstracted enough that it would be (relatively) trivial to add AICC support by modifying your tracking functions.

As you can see, the further we abstract, the cleaner your course’s code becomes. If you’re a Flash developer, use an additional level of abstraction — place your tracking functions in JavaScript and invoke them via ExternalInterface. While it may add a little up-front work, it will enable you to easily update your tracking code without needing to republish your FLAs.

Your course’s JavaScript:


//Assuming API is the SCORM API
//Checks for SCORM2004 mode, defaults to SCORM 1.2

//Set tracking mode
//This is a quick and dirty example;
//there are better ways to handle querystrings
//Example: mycourse.html?tracking=SCORM2004
var tracking_mode = window.location.href.split("?tracking=")[1];

function getLearnerName(){
   if(tracking_mode === "SCORM2004"){
      return API.GetValue("cmi.learner_name");
   }
   return API.LMSGetValue("cmi.core.student_name");
}

function setLessonIncomplete(){
   if(tracking_mode === "SCORM2004"){
      API.SetValue("cmi.completion_status", "incomplete");
   } else {
      API.LMSSetValue("cmi.core.lesson_status", "incomplete");
   }
}

function save(){
   if(tracking_mode === "SCORM2004"){
      API.Commit("");
   } else {
      API.LMSCommit("");
   }
}

Your course’s FLA:


//Display learner's name
var learner_name:String = getLearnerName();

//Set course status to incomplete
setLessonIncomplete();

//Tell LMS to save whatever data we've sent
save();

And here’s the glue: an abstraction layer meant to bridge the course FLA and the JavaScript tracking. Save as an external AS file (or a class/package if you’re feeling adventurous).


import flash.external.*;

function getLearnerName():String {
   return ExternalInterface.call("getLearnerName");
}

function setLessonIncomplete():void {
   ExternalInterface.call("setLessonIncomplete");
}

function save(){
   ExternalInterface.call("save");
}

In this example the tracking mode is set via JavaScript, and all of the tracking functionality is handled via JavaScript. If you ever needed to change from SCORM 1.2 to SCORM 2004 or AICC, you could handle it all via your JavaScript file and leave your Flash untouched. Easy breezy.

These are very simple examples, but hopefully they show the power of abstraction. Notice the last two examples have zero mention of SCORM in the course code itself. Sweet!

Of course, there is a downside to everything. As I mentioned in a thread on the E-Learning Technology and Development Google Group, abstracting like this takes time and can add quite a bit of work to your course development plan. If you know your course is a one-off that will only ever use one tracking method (such as SCORM 1.2), abstraction layers may be more effort than they’re worth. But if you’re not in a hurry, or know that your course will be around for a long time and will run on multiple LMSs, abstraction is a very good idea.

Abstraction and the pipwerks SCORM API Wrappers

What about the pipwerks SCORM API Wrappers? Well, they use some abstraction, but are not as abstracted as these examples. They can certainly help you on your way, but would need a bit of tweaking to reach this level of abstraction. I’m currently working on new SCORM wrappers that aim to support this level of abstraction. Stay tuned.

CaptivateController Updated to Support Adobe Captivate 5

By popular demand, I’ve updated my CaptivateController to work with Adobe Captivate 5 (CP5). Since this is an open-source project, there’s no upgrade fee. (What? “Adobe” and “no upgrade fee” in the same paragraph?!) I kid, I kid… I’m a kidder.

As you may have heard, Adobe Captivate 5 is a complete re-write of the Adobe Captivate application. As such, there are a few significant changes under-the-hood. For example, Adobe Captivate 5 does not support ActionScript 2, and will only publish to ActionScript 3. The Captivate developers eliminated a few of the old system variables while adding a few new ones. Showing and hiding the playbar now works very reliably (yay!). Most notably, the developers added extra ExternalInterface support via the cpEIGetValue and cpEISetValue callback functions while eliminating the cpSetValue callback, which explains why the previous version of the CaptivateController didn’t work with CP5 SWFs.

As for the updated CaptivateController, it works the same as the previous version. Most of the changes were under-the-hood, so you shouldn’t need to edit any of your code, and should be able to drop this new version on top of your old one. No system restart required!

If you encounter any bugs, please let me know by posting in the comments. I’d also be happy to hear any success stories you may have.

Adobe Captivate: What the heck is g_intAPIType?

If you spend any time using Adobe Captivate to create SCORM-conformant courses, you’re bound to have run into an issue or two that caused you to read some Captivate forum posts. Almost without fail, someone will mention that the solution to their problem was changing the value of the magical g_intAPIType JavaScript variable from 1 to 0 or from 0 to 1.

So what the heck is g_intAPIType, and why does changing it make a difference?

The short version is this: Captivate courses, when published for SCORM, typically use one of two communication methods: FSCommand or getURL. FSCommand works really well in some browsers, such as Internet Explorer, but has spotty support in others. getURL works in all browsers, but uses a more complicated communication system when sending data to your Captivate file from the LMS. It also makes a lot of clicking sounds.

Setting g_intAPIType to 0 means you’re forcing Captivate to use FSCommand in all browsers. Setting g_intAPIType to 1 means you’re forcing Captivate to use getURL in all browsers. If you don’t manually specify the value of g_intAPIType, Captivate’s SCORM template includes some browser sniffing that automatically sets g_intAPIType to 0 for Internet Explorer and 1 for all other browsers.

Want to know more about FSCommand versus getURL? Colin Moock wrote a good FSCommand tutorial that covers the basics.

Note: ExternalInterface has replaced both of these methods as the favored Flash-to-JavaScript communication method, but Captivate has not caught up yet.

SCORM Tip: Use an onunload handler

SCORM courses use JavaScript to send data to the LMS. This data then sits in the browser until the LMS writes it to the database (usually via AJAX or form posts). As previously discussed, invoking commit (save) will ensure the LMS actually writes this data to a database.

But what happens if the browser window containing your course is closed by the learner before the course finishes sending data to the LMS? If you’re not careful about how you’ve coded your course, you can lose some of the data. For example, if a learner completes the course and then immediately closes the window, the course might not have had enough time to tell the LMS about the completion, preventing the completion from appearing in the learner’s record.

It’s best to be proactive about this by setting up an event handler for the browser’s unload and onbeforeunload events. Whatever code you place in these events will execute when the browser is closed. In a SCORM course, you should place a commit (save) and terminate (quit) command in these events to ensure the SCORM data is properly persisted in the database and the session is properly terminated.

The code is pretty straightforward (this example uses SCORM 2004):


//Assuming API refers to your SCORM API
//and API_isActive returns a boolean indicating
//whether your API has already been terminated

var unloaded = false;
function unloadHandler(){
   if(!unloaded && API_isActive){
      API.SetValue("cmi.exit", "suspend"); //Set exit to whatever is needed
      API.Commit(""); //save all data that has already been sent
      API.Terminate(""); //close the SCORM API connection properly
      unloaded = true;
   }
}
window.onbeforeunload = unloadHandler;
window.onunload = unloadHandler;

Since some browsers support onbeforeunload and others don’t, we use the unloadHandler on both onbeforeunload and onunload, just to be safe. If a browser supports both of these events, the unloaded boolean ensures the scorm.quit function is not executed more than once.

If you’re using the pipwerks SCORM wrapper, your code will be even simpler, because the pipwerks wrapper automatically checks the API’s availability before performing any action. The wrapper also sets the cmi.exit/cmi.core.exit parameter for you.


var unloaded = false;
function unloadHandler(){
   if(!unloaded){
      scorm.save(); //save all data that has already been sent
      scorm.quit(); //close the SCORM API connection properly
      unloaded = true;
   }
}
window.onbeforeunload = unloadHandler;
window.onunload = unloadHandler;

SCORM Tip: Don’t forget to commit!

A number of people have recently asked me about the scorm.save() function in the pipwerks SCORM wrappers. What is it, and when should it be used?

The pipwerks scorm.save() function is a shortcut for SCORM’s Commit (SCORM 2004) and LMSCommit (SCORM 1.2) methods. Invoking commit in SCORM means you are explicitly instructing the LMS to persist the data you’ve sent. In other words, you’re telling the LMS to save your stuff!

When sending data from your course to the LMS, the data is traveling from the course window to the LMS via JavaScript and is stored in the browser. The SCORM spec gives LMSs the flexibility to decide when to transfer this data to the database. Some LMSs will not immediately write the data to the database because it can clog up the system; they prefer to queue up the data and send it in bunches. This explains why some courses will be successful in sending the data from the course to the LMS, but the LMS doesn’t seem to save the data. You didn’t tell it to! I know, I know, it seems daft, but it happens. So, to be safe, ensure your data gets saved by instructing the LMS to save the data.

Note that some LMSs will automatically commit at the end of a session, usually by detecting if the course window was closed (window.onunload) or if a certain CMI call was invoked, such as setting the completion status. However, you should never take your chances, and should design your SCO to commit regularly. Just be careful not to overdo it.

The rule of thumb is to invoke a commit (save()) if you use the pipwerks wrapper) after a significant chunk of data has been sent to the LMS, but not after each and every call:

NO (committing too frequently):


scorm.set("cmi.location", "some string indicating location");
scorm.save();
scorm.set("cmi.suspend_data", "your custom suspend_data string");
scorm.save();
scorm.set("cmi.score.raw", 80);
scorm.save();

NO (not committing enough):


scorm.set("cmi.location", "some string indicating location");
scorm.set("cmi.suspend_data", "your custom suspend_data string");
scorm.set("cmi.score.raw", 80);

YES (one commit after a short series of ‘set’ calls):


scorm.set("cmi.location", "some string indicating location");
scorm.set("cmi.suspend_data", "your custom suspend_data string");
scorm.set("cmi.score.raw", 80);
scorm.save();

Customizing SCORM Manifests in Captivate and Articulate Presenter

Someone recently asked me if it was possible to customize Captivate’s SCORM manifest to reduce the need for manual editing after publishing. In her case, the manifest needed to be edited to include SumTotal TotalLMS’s custom SCORM extensions.  The answer is yes. Here’s how.

Find the Captivate files

Captivate’s publishing files are located at:

C:\Program Files\Adobe\Adobe Captivate 4\Templates\Publish

Make your edits

Find the manifest files (manifest.xml for SCORM 1.2 and manifest2004.xml for SCORM 2004), then make your edits.

If you’re adding custom SCORM extensions such as SumTotal’s c2l_cp extensions, don’t forget to include the supporting XSD files. If you’re using SCORM 2004 you can drop the extra XSD file(s) into the “Manifest2004” folder. If you’re publishing to SCORM 1.2, you’ll need to manually add the custom XSD file to your published files.

Rinse, repeat

You’ll need to repeat the process for every computer in your office that has Captivate installed. Otherwise someone on a different computer may inadvertently publish the Captivate project using the factory publishing templates.

What about Articulate?

You can do the same thing with Articulate Presenter. Their publishing templates are located here:

C:\Program Files\Articulate\Presenter\players\

Best Practices in E-Learning

Someone recently posted a blog entry ranting about the use of the term “best practices” in our industry. I understand the frustration with thoughtless pronouncements about best practices, especially coming from people who may not know any better; it will often sound a lot like how mom used to say “eat this, it’s good for you” without really knowing whether it’s true. However, there is a big difference between best practices in terms of learning theory — something that’s difficult to quantify/prove — and technology.

A friend of mine, upon completing his MA in psychology, joked that his degree is the only one you can get where you can’t prove a thing that you’re taught. Learning theory is a form of psychology, and as such, you are guaranteed to run into a gazillion different opinions on how learning occurs: behaviorism, constructivism, cognitivism, yadda yadda yadda. Likewise, you will hear many opinions on what development models to follow (ADDIE vs agile vs something-or-other), evaluation methodology, and perhaps edge-case debates such as centralized learning structures versus de-centralized learning structures (social media peeps, I’m looking at you).

I guarantee these conversations will involve lots of name-dropping and liberal use of the term “best practice.” In these situations, I agree that there is no single answer to ANY of these issues, and context will be king.

Most technical issues, on the other hand, most certainly DO have best practices, and for good reason.

For starters, accessibility is a best practice. Why? Well, because it’s the right thing to do. No one should be denied an opportunity to learn simply because their ears or eyes or arms don’t work like yours do. Establishing a baseline level of accessibility is fairly easy to do, regardless of the size of your budget or your time constraints. For example:

  • For the hearing impaired: Text transcriptions and/or closed captioning for videos and Flash animations are as easy to set up as ever. Free/cheap video players like the Flash video component, the JW Player, and Flowplayer all support multiple captioning standards and make it easy to add captioning to a video. Rapid e-learning development tools such as Articulate Presenter and Adobe Captivate allow you to add captions or notes to your SWFs. (Side note: the text transcriptions for TED talks are an excellent example of what can be accomplished with just a little extra effort.)
  • For the visually impaired: If the content of your course is provided in a text format such as HTML, screen readers can read the text to the end user. What does this require of you? Well, if you use standard HTML, not much… just a little extra care in your layout and alternate text. If you embed an image, video, or animation, provide fallback text that describes the image or what happens in the video/animation. SWFObject (a free system for embedding SWF files in HTML documents) makes this easy to do.
    Similarly, Adobe has been working hard to make Flash Player and Adobe Reader more accessible to major screen readers. What do you have to configure to make it work? Nothing so far.
  • For those who can’t use a computer mouse: Thanks to initiatives like WAI-ARIA and companies like Adobe who are actively building keyboard support into their products, many script-based interactions (such as course navigation, quiz questions, and other activities) can be scripted to work without a mouse. Alternate input devices are often mapped to the keyboard input; if your course can be completed using a keyboard, you’re golden.
  • For the color blind: Accessibility can often be improved simply by adding text labels to color-coded objects and not relying on color alone.

I could go on for a while, but the point is that accessibility is definitely a best practice. It isn’t hard, and it certainly isn’t expensive to make a course accessible. It’s also the law if you receive any Federal funding.

There are definitely other technical best practices for e-learning:

  • SCORM: Technically not a standard but rather a collection of standards, SCORM is a best practice because it ensures your course will work on pretty much every major LMS (if you don’t like SCORM, AICC is equally valid). How can I say with confidence that SCORM is a best practice? Because in the bad old days before SCORM, developers had to spend weeks re-coding courses to work with each LMS’s proprietary code base. Once SCORM was widely adopted, the issue largely went away. No one wants to go back to the bad old days.
  • Valid HTML and CSS: If you write HTML and CSS, ensuring they validate means you know your pages will work in every major browser. We learned this lesson in the Netscape/Internet Explorer wars. Best practices on the web are still evolving; for example, sometimes it’s ok to write CSS that won’t validate if you know the repercussions and your code fails gracefully in older browsers. The best practice is simply that your pages work in most, if not all, browsers.
  • Don’t use proprietary code: See above. If your course uses ActiveX, which is only supported in Internet Explorer, your course won’t work in any other browsers. Almost anything implemented with ActiveX can be implemented using other non-proprietary methods. Again, the best practice is to ensure your pages work in most, if not all, browsers.
  • Follow sensible coding conventions: Well-written code that follows documented — and very well-reasoned — code conventions means your code will likely contain less errors, will be easier to update if the need arises, and will be more future-proof, avoiding expensive bugs like the Y2K bug, which could have been prevented with a bit of foresight. A great example of this type of code convention is Douglas Crockford’s JavaScript: The Good Parts.

There are definitely times when people throw around the phrase “best practice” and are simply talking out of their butts. “Never use yellow.” “Never use clip art.” “Never hire a penguin.” “Never let the learner do X.” “Always make the user do Y.” “Always use ___ format.” “Always use ___ pedagogy.”

Whatever.

Just remember that best practices DO exist, but not in every circumstance. And unless you want the evil eye from me and my compadres, remember to never use the phrase “best practice” unless you can back it up with evidence and sound reasoning.

Post script: I’ve noticed this bandying of best practices usually occurs when someone is trying to establish their expertise or exert control on a project, frequently in front of management-types. This is the same sort of thing Machiavelli did when he wrote The Prince, so why not treat them as Machiavelli would and … well, I guess another best practice is to know when to shut up, so I’ll stop here.