Introduction Welcome to the EnterPage Newsletter. It hasn't been the best of times for our hometown of Colorado Springs with the recent fires or for our neighboring city to the north (Aurora) with the movie theater shootings last weekend. Our hearts go out to all the victims and families of both events. We greatly appreciate the messages we have received from our friends and customers around the world checking to make sure we and our families are okay. On a happier note, we are pleased to bring you another newsletter with information on Training Studio version 3, blog postings, and Programming for e-Learning Developers tips on ToolBook and JavaScript. |
The Platte Canyon Training StudioŽ Version 3 Allows Programming-Free e-Learning We have completely rewritten this popular authoring tool in HTML and JavaScript, which means that it no longer requires Flash. This allows you to support any browser (IE 7 and later plus Firefox, Safari, etc.) and mobile devices like the iPad. Editing your templates or styles can now be accomplished in your favorite HTML editor or a free product like Visual Web Developer 2010 Express. Here are some of the new features.
Training Studio is priced at $1,995 per development license, which includes all templates (and their HTML/JavaScript source), up to three licenses of the Training Studio Content Editor, one license of the Training Studio Publisher, and the Training Studio Web Service. Additional licenses of the Training Studio Content Editor are $395 each. There is an optional Training Studio Tracker - Administrator, available for $495 per copy, that allows developers to report on and manage comments. Information Example Lesson Example Test Purchase |
Recent Blog Posts
Here are some recent blog posts. Playing a Video in the HTML 5 Era PCI Compliance and Custom Error Pages We hope you will visit our blog often and consider subscribing to the RSS feed. Blog RSS Feed |
Programming for e-Learning Developers Segment This feature is a short segment from Jeff's Programming for e-Learning Developers book. Sending EmailStaying on the web theme for a bit longer, let's look at how we send email from an e-Learning application. The simplest but, unfortunately, least reliable method is to use the same hyperlink approach as in the last chapter but with a "mailto:" hyperlink like this:mailTo:info@plattecanyon.com?subject=This is a Test&body=This is line 1 %0A This is line 2 This is an example of a "query string," where we pass information as part of the URL. You see this frequently on the web when you search Google and perform other actions. It is an example of a GET as opposed to a POST where the information is sent as parameters. We will cover posting below. Notice how the first piece of information comes after a ? and the other pieces are separated by &. Some email programs will accept other attributes such as cc for copy and bcc for blind copy. One that has been pretty much disabled for security reasons is attach for file attachments. Note how you can use %OA to move to a new line. This hyperlink approach is fine for optional emails but not for sending test scores or other important data for these reasons:
The corollary of the above is that any approach that sends email on the client is unreliable. This is mainly due to security settings that try to prevent viruses from sending email without the user's knowledge. These same settings will prevent your e-Learning program from sending email from your user's machine. So what is the answer? What I recommend and do extensively is send the email from a web server. This can be an ASP page, a PHP page, etc. My personal favorite is to use an ASP.NET Web Service. That's what we will focus on in this chapter. ASP.NET Web ServiceI like to think of a web service as a set of functions that you can call over the internet. For example, you might send a "weather" web service your zip code and get back the current temperature, wind speed, and forecast. The information between your application and the web service is sent via XML.Some of you may have used an ASP or PHP page to send email or accomplish another task. In that case, the entire page is normally dedicated to that task. In other words, the page represents a single function . In contrast, you can define numerous functions in a single web service. Another advantage of using web services over asp or even normal ASP.NET pages is that you don't have to worry about HTML tags getting caught up in the data you are returning to your program. So let's get started. Once we have a "web site" in Visual Studio, we add new item and select Web Service. Notice that this type of web service has an .asmx extension. You then get a placeholder HelloWorld function that looks like this: <WebMethod()> Public Function HelloWorld() As String Return "Hello World" End Function Of key importance is that you must mark the methods that you want to call from your e-Learning application as a <WebMethod()> as well as make them Public. We'll change this function to send email instead. Before looking at the code itself, let's think ahead to items that we want the "maintainer" of the web service to be able to change easily. We'll want to put these in the web.config file rather than hard-coding them into the service. I then like to "cache" them in memory to improve performance as described below. The relevant section of web.config is shown below. We store a key (important to make sure unauthorized parties don't use your service to send spam), various mail server and authentication information, and the "reply" email address. <appSettings> <add key="WebServiceAuthorizationKey" value="ProgrammingBook123"/> <add key="SmtpServer" value="smtp.yourdomain.com"/> <add key="EmailCredentialsUsername" value="happy@yourdomain.com"/> <add key="EmailCredentialsPassword" value="password123"/> <add key="EmailCredentialsDomain" value=""/> <add key="EmailReplyAddress" value="support@yourdomain.com"/> </appSettings> Each of these settings is stored in an associated property as shown below. These are marked as ReadOnly properties as they are not Set in code but rather by editing the web.config file. Private ReadOnly Property WebServiceAuthorizationKey() As String Get Dim valueId As String = _ GetPropertyValue("WebServiceAuthorizationKey") Return valueId End Get End Property Private ReadOnly Property SmtpServer() As String Get Dim valueId As String = GetPropertyValue("SmtpServer") Return valueId End Get End Property Private ReadOnly Property EmailCredentialsUsername() As String Get Dim valueId As String = GetPropertyValue("EmailCredentialsUsername") Return valueId End Get End Property Private ReadOnly Property EmailCredentialsPassword() As String Get Dim valueId As String = GetPropertyValue("EmailCredentialsPassword") Return valueId End Get End Property Private ReadOnly Property EmailCredentialsDomain() As String Get Dim valueId As String = GetPropertyValue("EmailCredentialsDomain") Return valueId End Get End Property Private ReadOnly Property EmailReplyAddress() As String Get Dim valueId As String = GetPropertyValue("EmailReplyAddress") Return valueId End Get End Property Private Function GetPropertyValue(ByVal keyId As String) As String Dim valueId As String If Current.Cache(keyId) Is Nothing Then valueId = ConfigurationManager.AppSettings(keyId) Current.Cache(keyId) = valueId Else valueId = Current.Cache(keyId).ToString End If Return valueId End Function For simplicity, we name the property the same as the value in web.config, but that doesn't need to be the case. In each case, we define the valueId variable and call the GetPropertyValue function to figure out what it is. Within this function, we check to see if we have already read the value and put it in the server's Cache, which the server shares across all users. If not, we read it from the web.config file with the ConfigurationManager.AppSettings() method, where we pass in the key that we are looking for. In that case, we add it to the cache so that the next time we access, we'll be able to read it from memory . Let's now look at the code. This listing below shows the web service class and the SendEmail function. Imports System.Web.Services Imports System.Net.Mail Imports System.Web.HttpContext <WebService(Namespace:="http://www.plattecanyon.com/programming/")> _ <WebServiceBinding(ConformsTo:=WsiProfiles.BasicProfile1_1)> _ <Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()> _ Public Class Programming Inherits System.Web.Services.WebService Private Const accessKeyInvalidString As String = _ "Invalid key. Please contact your system administrator." Private Const emailSplitChar As Char = CChar(",") <WebMethod()> Public Function SendEmail(ByVal accessKey As String, _ ByVal emailList As String, ByVal subjectId As String, ByVal bodyId _ As String, ByVal copyAddressList As String) As String Dim returnString As String = String.Format("Sent email successfully to '{0}' with subject '{1}'", emailList, subjectId) If accessKey = Me.WebServiceAuthorizationKey Then If emailList <> "" Then Dim messageID As New MailMessage Dim smtpServerName As String = Me.SmtpServer Dim credUserName As String = Me.EmailCredentialsUsername Dim credPassword As String = Me.EmailCredentialsPassword Dim replyAddress As String = Me.EmailReplyAddress If smtpServerName = "" Then smtpServerName = "localhost" End If Dim smtpObj As New SmtpClient(smtpServerName) Dim emailListArray As String() = emailList.Split(emailSplitChar) Dim emailId As String Dim copyId As String If credUserName <> "" AndAlso credPassword <> "" Then Dim credDomain As String = Me.EmailCredentialsDomain Try Dim credentialId As New _ System.Net.NetworkCredential(credUserName, credPassword) If credDomain <> "" Then credentialId.Domain = credDomain End If smtpObj.Credentials = credentialId smtpObj.UseDefaultCredentials = False Catch ex As Exception smtpObj.UseDefaultCredentials = True End Try Else smtpObj.UseDefaultCredentials = True End If Try With messageID .IsBodyHtml = True For Each emailId In emailListArray .To.Add(emailId.Trim()) Next If copyAddressList <> "" Then Dim copyArray As String() = _ copyAddressList.Split(emailSplitChar) For Each copyId In copyArray .CC.Add(copyId) Next End If .From = New MailAddress(replyAddress) .Subject = subjectId .Body = bodyId End With Try smtpObj.Send(messageID) Catch ex As Exception ' Sending error returnString = ex.Message End Try Catch ex As Exception ' Email Message error returnString = ex.Message End Try Else returnString = "No email address provided." End If Else returnString = accessKeyInvalidString End If Return returnString End Function The first thing to note are the various Imports lines at the top of the listing. The various classes and objects in .NET are organized into namespaces. This makes it easier to find the methods and properties that you need. But as the complete namespace can get long, we use the Imports to keep from having to type the whole thing. For example, the Imports System.Net.Mail line allows us to create a MailMessage rather than a System.Net.Mail.MailMessage. The next lines were all created for us by Visual Studio. The only part we edited was the Namespace of the web service itself. This is an identifier that might be needed if you expose your service to other applications. It is a good idea to put your own URL in there to make it unique. Next up are two constants: accessKeyInvalidString and emailSplitChar. We define them outside the function block to preserve the ability to reuse them in other methods. From there, we get to the SendEmail function itself. Of key importance are the parameters. Not enough and you sacrifice functionality down the road. Too many and your method is needlessly complicated to use. We'll settle on accessKey, emailList (notice that this can be a comma-delimited list of emails if desired), subjectId, bodyId, and copyAddressList (also comma-delimited if desired). We chose to leave off blind copy, reply address (this is a property instead as described above), etc. After checking to make sure that the calling program provided the right key and that there is at least one email address to send to, we get to the good part. We create a MailMessage object and then read our various properties into local variables. I like to use the Me. notation to make it clear that it is a property and not a variable or method, but there is no requirement to do that nor is that necessarily clearer to other programmers. We then do some logic to convert the smtpServerName to localhost if it doesn't have a value. We then use this name to create an SmtpClient object. We'll use this in a few lines to send the email. Next up is the use of our old friend, Split, to take our (potentially) comma-delimited list of email addresses into an array of addresses . We then define some local variables, emailId and copyId, that we'll need below. Our next set of logic has to do with providing proper credentials. It is often not needed (particularly if you can just set SmtpServer to localhost. But if you are trying to test it locally, you'll almost certainly need this part. You basically give it the same information you give your email program to send and receive email. Notice the use of the AndAlso part of the If condition. This is unique to Visual Basic (of the languages we are looking at). It means that the second part of the "AND" is not evaluated unless the first part is true. This makes the code a bit more efficient. If we need to set up our credentials, we recognize the fact that we could encounter an error if the credential information is invalid. So we put the lines in a Try - Catch block. If the program encounters an error within the Try area, it jumps to the Catch block. There can even be a Finally block that gets executed no matter what, though we are not using that here. We attempt to create a new NetworkCredential from the user name and password. If there is a domain, we set that as well. Otherwise, we tell the mail server to use "Default" credentials. Now we are ready to send mail. This is another area that could easily have errors. So we use another Try - Catch block and start setting properties of our messageId object. We use the With messageId syntax that we have seen earlier to keep from having to type messageId. over and over. We first tell it to send in HTML format. We then use our first For - Each loop. This is similar to the "For" or "Step" loops that we have encountered previously. But rather than have a counter and then getting the right element of the array, we can loop through the elements directly. This is quite efficient and the only way to loop through certain types of arrays and objects. Once we have our individual email address, we Trim it to get rid of any spaces or other extraneous characters from the beginning or end and then Add it to the To object of the mail message. After that, we do the same logic for any "copy" addresses. We set the From property to be a special MailAddress object built from our replyAddress variable. Finally, we set the Subject and Body. Before we actually try to Send the message, we do another Try - Catch block. This is so we can distinguish between a problem with the mail server (invalid credentials, no ability to "relay" messages, etc.) and a problem with the mail address (invalid email address format, illegal characters, etc.). In each case, we send a message back as the return value of the function. If there were no problems, then we send back a message that shows the email address(es) and subject. That's probably the most involved code we've written so far. The nice thing is that you can reuse it over and over for all of your applications that need to send email. We use a similar service to send 3000+ emails for our quarterly EnterPage newsletter. In that case, our service takes the subject, the body, the reply address, and the record numbers of the emails to send (e.g., records 1 to 500). It then reads the list of recipients from a query in our database on the server, puts their first name in the right spot and grabs the email address, and then loops through the records specified to send the emails. We've found that sending about 500 at a time avoids timeout issues. This is MUCH easier and faster than doing a mail merge from Outlook! We are now ready to connect to this service from our various environments and use it to send email. Programming for e-Learning Developers Information Order Programming for e-Learning Developers ($45 with free shipping in the U.S.) |
Expert Information from Learning & Mastering ToolBook By Tom Hall, TCC Publishing Multiple Action Triggers on the Same Page Question: Can I use more than one Action Trigger on the same page? I have tried, but the Action Trigger button does not appear to behave like a normal button... I have a Reset Trigger on the same page that is supposed to hide both Action Triggers on load page. It hides the first Action Trigger but not the second one. I have double-checked object names etc., but nothing I do lets me hide the second Action Trigger. Answer: Just name one of the Action Triggers differently from the other, then you can use the Reset Trigger to select each. Also, note that the Action Trigger itself has a property you can set to make it not visible. |
OpenScript Tip from Learning & Mastering ToolBook
By Jeff Rhodes, Platte Canyon Multimedia Software Corporation Adding a Paint Object Programmatically For the ToolBook 11 version of this training, we wanted to add the "Configuring LiveXtensions" button and associated graphic to a training page. However, the page was programmed to show and hide paint objects. Starting with ToolBook 10.5, however, the option to import paint objects was taken out. Instead, inserting a graphic now creates a bitmap resource and image object. We COULD have converted the other paint objects to resources and image objects, but why do that when we can still get a paint object via OpenScript using the code to below. The only slight snag is that the graphic was originally a .png. That wouldn't import but the .bmp version worked fine. importgraphic "C:\Users\Jeff\ToolBookProducts\ICBT110\training\resources\icbt2\update\LiveXtensionsConfiguration.bmp" |
Web Hint from Learning & Mastering ToolBook
By Peter Jackson, Nirvana Bound Pty Ltd Using Microsoft Scripting Agent Helper (MASH) in ToolBook Question: I'd like to be able to use the Microsoft Scripting Agent Helper (MASH) in ToolBook but for DHTML, not Native. I see I can save as a JavaScript HTML file, but does anyone know how to do this in ToolBook to make it work? Answer: If you want to control the MS Agent from your exported book, you will need to make some changes to the JavaScript. First comment the "Window_OnLoad();" line like this: //Window_OnLoad(); Then add: function tbfunction_loadMS_Agent(){ Window_OnLoad(); } In ToolBook, open the book properties dialog and select the Web tab, Import the MASH .js file. Now add the "Execute Script loadMS_Agent()" to a button or other object. Save, export, and test. |
JavaScript Tip
By Jeff Rhodes, Platte Canyon Multimedia Software Corporation JavaScript and jQuery in Training Studio As mentioned earlier in this newsletter, the new version of Training Studio is completely HTML and JavaScript. Rather than using ActionScript to create interactions as in past versions, we now use JavaScript. In particular, we take advantage of the jQuery library to make it even easier than in past versions. Let's look at an example. The script below is the entire JavaScript for the buttonClickLeftShowContentImageMedia template. The interaction is "buttonClick" on hotspots. In response to the click, the template shows content (text), an associated image, and/or plays associated media. I will make some comments/explanations in-line. var lastHotspot = null; var completionArray = []; var numHotspots = 0;Since these variables are declared outside a function block, they are effectively global to the page. They are used to keep track of completion and the "last" hotspot accessed (in order to set its style to HotspotCompleted). $(function () { var pageArrayLocal = parent.pageArray; // associative array var keyId; var keyString; var keyValue; var objectSelector; var objectId; var hotspotNum;The $ refers to jQuery. This function is called once the page completely loads. The reference to parent is to the index.htm page. Since it references TrainingStudioSupport.js, the parent.pageArray means that we are reading the global variable from that file. This pageArrayLocal variable is basically a dictionary that represents the current training page. The key matches up to the node or column in the database. They are title, subtitle, content_0, etc. parent.ShowTransition();As with the pageArray variable, the line above calls the showTransition function in TrainingStudioSupport.js. This just shows the iFrame holding the template. for (keyId in pageArrayLocal) { keyString = keyId.toString(); keyValue = pageArrayLocal[keyId]; // handle unique ones here. Handle the rest in PopulateTemplate (TrainingStudioSupport.js)We loop through each of the keys (content_0, media_0, etc.) and only handle the ones that need special handling by this template. switch (keyString) { // let "media_0" get handled by template case "media_1": case "media_2": case "media_3": case "media_4": case "media_5": case "media_6": case "media_7": case "media_8": case "media_9": case "media_10": // let graphic_0 be handled by template case "graphic_1": case "graphic_2": case "graphic_3": case "graphic_4": case "graphic_5": case "graphic_6": case "graphic_7": case "graphic_8": case "graphic_9": case "graphic_10": break;We let standard PopulateTemplate (see below) method handle media_0 and graphic_0, since we want any initial sound, video, or animation to play and any initial graphic to display. The rest of the media (media_1 - media_10) and graphics (graphic_0 - graphic_10are only played/displayed in response to the hotspot interaction. So we don't do anything except break when we encounter them. case "hotspot_1": case "hotspot_2": case "hotspot_3": case "hotspot_4": case "hotspot_5": case "hotspot_6": case "hotspot_7": case "hotspot_8": case "hotspot_9": case "hotspot_10": var contentId = parent.formatHotspot(keyValue); objectName = "#" + keyString; objectId = $(objectName); hotspotNum = parent.getFieldNum(keyString); numHotspots = Math.max(hotspotNum, numHotspots); objectId.html(contentId); objectId.show(); objectId.click(hotspotClickHandler); break;We first build a reference to the associated span object using jQuery. A jQuery reference to an object with an id of "hotspot_1" looks like this: $("#hotspot_1"); In jQuery terms, the "#hotspot_1" is the selector. We call the formatHotspot() method of the TrainingStudioSupport.js using parent once again. This method looks for special bullet and hyperlink characters and returns the proper HTML. After creating our jQuery object reference, we strip the number (1, 2, 3, etc.) from the name of the object and use it to populate our hotspotNum variable. We keep a running total of the numHotspots as well. We use this to determine when all the hotspots have been selected. Finally, we associate the control's click event with our hotspotClickHandler function. This is what makes something happen when the user clicks on the hotspot. default: objectName = "#" + keyString; objectId = $(objectName); parent.PopulateTemplate(objectId, keyString, keyValue);This line is where all the keys that we didn't specifically handle are sent to PopulateTemplate instead. This avoids duplicate code in every template. } } // bind keyboard events $(document).keydown(parent.ImplementKeyDown);This line associates the keydown event with the ImplementKeyDown function in TrainingStudioSupport.js. This allows us to go forward with the PageDown key and backwards with the PageUp key. It also shows the Comment Editor if the reviewOn variable is true and the user presses Ctrl + Shift + M. }); function hotspotClickHandler(e) { var targetId = $(this);We use the jQuery selector, $(this), to figure out which hotspot (span) the user interacted with. var displayFieldId = $("#displayField");Similarly, we make a reference to our "display field" object. We use this to set its text based on a naming scheme. When the user clicks on hotspot_1, we want to display any text in content_1. var hotspotName = targetId.attr("id"); var hotspotNum = parent.getFieldNum(hotspotName);We grab the id using the jQuery attr() function. We then find the associated number in order to work our naming scheme. hotspot_1 goes with media_1, graphic_1, and content_1 and so on. if (lastHotspot != null) { lastHotspot.attr("class", "HotspotCompleted"); }The first time through, the lastHotspot variable will be null. After that, it will refer to the hotspot previous to this interaction. In that case, we set its class to "HotspotCompleted." This is how we get it to turn blue or otherwise show completion. lastHotspot = targetId;We set the lastHotspot variable so we'll be able to change its class the next time through. targetId.attr("class", "HotspotCurrent");We change the class of this hotspot to "HotspotCurrent" to denote which one we are currently looking at. completionArray[hotspotNum - 1] = true;We set the associated element (subtracting 1 since the array is zero-based) of our completionArray to true. Once all the elements are "true," the page is completed. parent.showTextImageMedia(displayFieldId, hotspotName, $("#graphic_0"), $("#media_0"), true); // include mediaWe call the showTextImageMedia method to display the associated content, play any associated media, and display any associated graphic. Note that we pass the object references to display the content (displayFieldId), show the graphic ($("#graphic_0")), or play the media ($("#media_0")). The parameter at the end determines whether to include media. parent.getHotspotCompleted(completionArray, numHotspots);We pass our completionArray and the numHotspots variable to the getHotspotCompleted method. This will show a "completion" image if all the interactions are completed. } |
The EnterPage is distributed up to four times per year, with occasional special issues. Individuals who have expressed interest in Platte Canyon Multimedia Software Corporation or its products receive The EnterPage. Suggestions for articles or proposals for article submissions are welcome. Send information to ep@plattecanyon.com. Back issues of the EnterPage are available at: http://www.plattecanyon.com/enterpage.aspx |
Platte Canyon Multimedia Software Corporation, 8870 Edgefield Drive, Colorado Springs, CO 80920, (719) 548-1110 |