Creating a Windows Presentation Foundation Slide Show

As described in the previous post on ActionScript, I wanted to create an application for the “Hack Ack” that would dynamically read photos and music from subdirectories and display them in a multimedia slide show. In addition to the Flash/AIR version, I wanted to create a Silverlight version. However, just like Flash in a browser would not have sufficient permissions to read the local hard drive, Silverlight in a browser could not either. So I decided to use the similar Windows Presentation Foundation (WPF). The complete application can be downloaded as part of the Archives for Attendees, but here are the main components.

I wrote the AIR one first and knew it wouldn’t take very long to duplicate the logic. There are few nuances (timers) and different capabilities (Generics), but the logic is largely the same. The design is again to read all the photos into one collection and all the music into another. We randomly grab a music file and start playing it. We handle its “completed” event and launch a new one when the music file is finished. We then start a timer to use with the photos. When the timer fires, we randomly pick a photo, remove it from the collection, and display the photo. Once all the photos are finished, we stop the timer. To store information between timer cycles, we need to declare some variables outside a function block as shown below.

Private photoDelay As Integer = 3 ' seconds
Private photoList As List(Of FileInfo)
Private musicList As List(Of FileInfo)
Private timerId As DispatcherTimer

We use photoDelay in our timer. Note that it is in seconds here rather than milliseconds. The two List(Of FileInfo) variables are Generics that are similar to the ArrayCollection variables used in the AIR example but which allow us to specific the type of object (FileInfo) being stored. I could have used the similar Vector concept in AIR if I wanted. However, it lacks the removeItemAt method that was very helpful. The timerId variable stores the reference to our timer. Notice that this is a DispatcherTimer. I first used a normal Timer and found out that it runs on a background thread in WPF. That was a problem since a background thread cannot update the user interface thread.

When the application starts, we call the configureApp function as shown below:

Private Sub configureApp()
	Dim startingDir As String = AppDomain.CurrentDomain.BaseDirectory
	Dim photoDirectory As New DirectoryInfo(String.Format("{0}\photos", startingDir))
	Dim musicDirectory As New DirectoryInfo(String.Format("{0}\music", startingDir))
	Dim photoFileList As FileInfo() = photoDirectory.GetFiles()
	Dim musicFileList As FileInfo() = musicDirectory.GetFiles()

	photoList = photoFileList.ToList()
	musicList = musicFileList.ToList()
End Sub

We use the DirectoryInfo class to find our two subdirectories (“photos” and “music”) and then get a listing of all the files in them. We store them in our associated photoList and musicList variables.

The code for the “Start” button is shown below:

Private Sub startBtn_Click(ByVal sender As Object, ByVal e As _
    System.Windows.RoutedEventArgs) Handles startBtn.Click

	Dim timerId As New DispatcherTimer() ' use DispatcherTimer rather 
	    ' than Timers.Timer to avoid threading issues

	With timerId
	    .Interval = New TimeSpan(0, 0, 0, photoDelay)
	    AddHandler .Tick, AddressOf timerHandler
	    .Start()
	End With
	playSound()
End Sub

We create our timer and then set its Interval property to be a TimeSpan that we create from our photoDelay variable (3 seconds). The next line is where we tell WPF what handler (timerHandler) to call when the timer fires. We then start the timer and call the playSoundhandler below.

Private Sub playSound()
	If musicList.Count > 0 Then
		Dim rand As New Random()
		Dim indexNum As Integer = rand.Next(0, (musicList.Count - 1))
		Dim musicFileId As FileInfo = musicList(indexNum)

		AddHandler mediaPlayer.MediaEnded, AddressOf soundCompletedHandler
		mediaPlayer.Source = New Uri(musicFileId.FullName, UriKind.Absolute)
		musicList.RemoveAt(indexNum)

		statusLabel.Text = String.Format("Playing {0}. indexNum = {1}. Count = {2}.", _
		    musicFileId.Name, indexNum, musicList.Count)
	End If
End Sub

Private Sub soundCompletedHandler(ByVal sender As Object, ByVal e As EventArgs)
	playSound() ' plays next sound
End Sub

We first check to make sure that we have music files to play. If so, we create a Random object. We then call its Next method to give us a number between 0 and the number of file (again -1 to account for starting from 0). We then grab the reference to the associated music file. We associated our MediaPlayer object’s MediaEnded event with our soundCompletedHandler handler. To play the sound, we just set the Source property to a Uri that is basically the URL to the file. Notice how similar this is to the URLReqest that we used in ActionScript. Very importantly, we remove the file from the musicList. This is how we avoid playing the file again and how we know to stop when all the files have been played. Finally, we set the text of our status label to be the information on the name of the file, the index number, and count of the file.

The last piece of the puzzle is displaying the photos. That happens in the timerHandler handler shown below:

Private Sub timerHandler(ByVal sender As Object, ByVal e As EventArgs)
	If photoList.Count > 0 Then
		Dim rand As New Random()
		Dim indexNum As Integer = rand.Next(0, (photoList.Count - 1))
		Dim photoFileId As FileInfo = photoList(indexNum)
		Dim sourceId As New BitmapImage(New Uri(photoFileId.FullName, UriKind.Absolute))

		photoImage.Source = sourceId
		photoList.RemoveAt(indexNum)

		statusLabel.Text = String.Format("Displaying {0}. indexNum = {1}. Count = {2}.", _
		    photoFileId.Name, indexNum, photoList.Count)
	Else
		If timerId IsNot Nothing Then
			timerId.Stop()
		End If
	End If
End Sub

This logic is very similar to that used to play the music files. We again get a random number between 0 and the number of photos still available (-1 to account for the fact that we are starting at 0). We get the complete path (FullName property) and set the source property of our Image control to a Uri built from that path. We remove the file from the collection and update our status label. Once we are done, we stop the timer.

If you would like to see how to implement this application in Adobe AIR and ActionScript, see the previous post.

Creating an Adobe AIR Slide Show

The “Hack Ack” topic for the 2010 e-Learning Authoring Conference was Rock & Roll. I thought it would be fun to create a “Slide Show” application that would randomly grab all photos from a directory and display them. At the same time, it would randomly pick music files out of another directory, starting the next one as soon as the current one is finished. I decided to build my first incarnation in Flash/Flex (see the next post for a Windows Presentation Foundation example). But the security model of normal Flash is such that you cannot read the contents of directories on the user’s hard drive without user interaction. Adobe AIR applications don’t have that restriction, so I decided to tackle my first AIR application. The complete application can be downloaded as part of the Archives for Attendees, but we will look at some of the highlights.

The basic design is to read all the photos into one collection and all the music into another. We randomly grab a music file and start playing it. We handle its “completed” event and launch a new one when the music file is finished. We then start a timer to use with the photos. When the timer fires, we randomly pick a photo, remove it from the collection, and display the photo. To store information between timer cycles, we need to declare some variables outside a function block as shown below.

// configuration constant
private var photoDelay:int = 3000; // milliseconds

// shared variables
private var photoArrayCollection:ArrayCollection;
private var musicArrayCollection:ArrayCollection;
private var timerId:Timer;
private var soundChannelId:SoundChannel;

We use photoDelay in our timer. The two ArrayCollection variables allow us to store our list of photos and music respectively. The timerId variable stores the reference to our timer and the soundChannelId is the SoundChannel that we use to play our music.\

When the application starts, we call the configureAppfunction as shown below:

private function configureApp():void {
	// get list of media and photos
	var currentDirectory:File = File.applicationDirectory;
	var photoDirectory:File = currentDirectory.resolvePath("photos");
	var musicDirectory:File = currentDirectory.resolvePath("music");
	var photoArray:Array = photoDirectory.getDirectoryListing();
	var musicArray:Array = musicDirectory.getDirectoryListing();

	// use ArrayCollection so we can remove items easily after they are used
	photoArrayCollection = new ArrayCollection(photoArray);
	musicArrayCollection = new ArrayCollection(musicArray);
}

We use the AIR File class to find our two subdirectories (“photos” and “music”) and then get a listing of all the files in them. We store them in our associated photoArrayCollection and musicArrayCollection variables.

The code for the “Start” button is shown below:

protected function startBtn_clickHandler(event:MouseEvent):void
{
	// start timer and begin playing sound
	timerId = new Timer(photoDelay, photoArrayCollection.length); // once per photo
	timerId.addEventListener(TimerEvent.TIMER, timerHandler);
	timerId.start();

	playSound();		
}

We create our timer using our photoDelay variable (3000 milliseconds). The second parameter is the number of times that we want the timer to fire. We use the number of photos that we have. This is nice in that we don’t then have to stop the timer. The next line is where we tell Flash/Flex what function (timerHandler) to call when the timer fires. We then start the timer and call the playSoundfunction below.

private function playSound():void {
	if (musicArrayCollection.length > 0) {
		// get a random number between 0 and the length of the photoArrayCollection. 
		// Then show that photo and remove the item from the collection
		var rand:Number = Math.random();
		var indexNum:int = Math.round(rand * (musicArrayCollection.length - 1));
		var musicFileId:File = musicArrayCollection[indexNum] as File;
		var musicPath:String = musicFileId.nativePath;

		var soundId:Sound = new Sound();
		var requestId:URLRequest = new URLRequest(musicPath);

		soundId.load(requestId);
		if (soundChannelId != null) {
			soundChannelId.stop();
		}
		soundChannelId = soundId.play();
		soundChannelId.addEventListener(Event.SOUND_COMPLETE, soundCompletedHandler);	
		musicArrayCollection.removeItemAt(indexNum);				
		status = "Playing " + musicFileId.name + ". indexNum = " + indexNum + 
		    ". length = " + musicArrayCollection.length;
	}
}

private function soundCompletedHandler(e:Event):void {
	playSound();
}

We first check to make sure that we have music files to play. If so, we use the Math.random() function to get a random number between 0 and 1. We then multiply that by the number of sound files (we subtract 1 since we are creating an index that starts from 0). We then round the number so that we don’t have a decimal number. From there, we go to our musicArrayCollection and grab the associated sound file. We read its nativePath property to get its complete path. We create a Sound object and a URLRequest to actually read the file. We load sound and then check to see if a previous sound is playing. If so, we use the SoundChannel to stop it (that’s why we needed to save a reference to soundChannelId). We play the sound, which gives us the SoundChannel. We call the soundCompleted function when the music file finishes. Notice that this function just calls playSound again. Very importantly, we remove the file from the musicArrayCollection. This is how we avoid playing the file again and how we know to stop when all the files have been played. Finally, we set the status property, which causes the information on the name, index, and length to show up on the application’s status bar.
The last piece of the puzzle is displaying the photos. That happens in the timerHandler function shown below:

private function timerHandler(e:TimerEvent):void {
	var rand:Number = Math.random();
	var indexNum:int = Math.round(rand * (photoArrayCollection.length - 1));
	var photoFileId:File = photoArrayCollection[indexNum] as File;
	var photoPath:String = photoFileId.nativePath;

	photoImage.source = photoPath;	
	photoArrayCollection.removeItemAt(indexNum);	
	status = "Displaying " + photoFileId.name + ". indexNum = " + indexNum + 
        ". length = " + photoArrayCollection.length;
}

This logic is very similar to that used to play the music files. We again get a random number between 0 and the number of photos still available (-1 to account for the fact that we are starting at 0). We get the complete path and just set the source property of our Image control to that path. We remove the file from the collection and update our status bar.

If you would like to see how to implement this application in Windows Presentation Foundation and Visual Basic, see the next post.

Visual Basic in Reports

I’ve been spending a great deal of time building reports in Visual Studio for our new Tracker Reports product. While SQL Server Reporting Services has a number of great features, one of my favorites is the fact that you can embed Visual Basic code in the report itself. This can be extremely helpful with complex logic. For example, the completion status of a lesson or course can be expired, critical, or alert when it has an expiration date. Here is what the function looks like:

Public Function DetermineCompletionStatus(ByVal completionStatusObj As Object, _
    ByVal expirationDateObj As Object, ByVal currentDays_CriticalObj As Object, _
    ByVal currentDays_AlertObj As Object, Optional ByVal isCourse as Boolean = True) As String

    Dim completionStatus As String
    Dim expirationDateId, currencyCriticalDate, currencyAlertDate As DateTime
    Dim currencyCriticalDays, currencyAlertDays As Integer
    Dim checkExpiration As Boolean = isCourse
    Dim defaultStatus as String = "Not Started"

    If isCourse = False Then
    	defaultStatus = "not attempted"
    End If

    If IsDBNull(completionStatusObj) = True Then
        completionStatus = defaultStatus
    Else
        completionStatus = CStr(completionStatusObj)
        If completionStatus = ""
        	completionStatus = defaultStatus
        End If
    End If

    Dim returnString As String = completionStatus

    If IsDBNull(expirationDateObj) = True OrElse expirationDateObj Is String.Empty OrElse _
        IsNothing(expirationDateObj) = True OrElse expirationDateObj.ToString = "" Then

        expirationDateId = DateTime.MinValue
    Else
        Try
            expirationDateId = Convert.ToDateTime(expirationDateObj)
        Catch ex As Exception
            expirationDateId = DateTime.MinValue
        End Try

        If expirationDateId > DateTime.MinValue Then
            If IsDBNull(currentDays_CriticalObj) = True OrElse IsNothing(currentDays_CriticalObj) = True OrElse _
                currentDays_CriticalObj.ToString = "" OrElse currentDays_CriticalObj.ToString = "-10" Then

                currencyCriticalDays = 0
                currencyCriticalDate = expirationDateId
            Else
                currencyCriticalDays = CInt(currentDays_CriticalObj)
                currencyCriticalDate = expirationDateId.AddDays(-(currencyCriticalDays))
            End If

            If IsDBNull(currentDays_AlertObj) = True OrElse IsNothing(currentDays_AlertObj) = True OrElse _
                currentDays_AlertObj.ToString = "" OrElse currentDays_AlertObj.ToString = "0" OrElse _
                currentDays_AlertObj.ToString = "-10" Then

                currencyAlertDays = 0
                currencyAlertDate = expirationDateId
            Else
                currencyAlertDays = CInt(currentDays_AlertObj)
                currencyAlertDate = expirationDateId.AddDays(-(currencyAlertDays))
            End If
        End If
    End If

    Select Case completionStatus.ToLower
    Case "completed", "passed"
	' expiration Date

	checkExpiration = True
    End Select

    If checkExpiration = True Then
        If expirationDateId <> DateTime.MinValue Then
            If (expirationDateId <= Now) Then
                Dim expiredString As String = "Expired"

                returnString = expiredString
            ElseIf (currencyCriticalDate <= Now) Then
                returnString = "Critical"
            ElseIf (currencyAlertDate <= Now) Then
                returnString = "Alert"
            End If
        End If
    End If

	returnString = StrConv(returnString, VbStrConv.ProperCase)
    Return returnString
End Function

Within the report itself, we can use an expression for the value for the “status” rather than linking to just a single column from our query. For example, here is an expression for the course status:

=Code.DetermineCompletionStatus(Fields!CompletionStatus.Value, Fields!ExpirationDate.Value, _
    Fields!CurrencyDaysFlag_Critical.Value, Fields!CurrencyDaysFlag_Alert.Value)

Notice how we begin the expression with Code to denote that we are using our own code rather than built-in operators.

Converting an ActionScript Value to Boolean

In a typical week, I’ll work with ActionScript, Visual Basic, OpenScript, JavaScript, and perhaps even InstallScript. One of the possible traps of moving between languages is making assumptions that similar functions in different languages will in fact operate the same way. One recent example that bit me was in working with a Boolean (true/false) value in Flex and ActionScript. The issue as in a simple confirmation as to whether it was OK to exit the lesson. The design was to call this JavaScript function in order to get a standard “confirmation” dialog from the browser:

confirmClose = function (message) {
    return window.confirm(message);
}

Here is how we called it from ActionScript:

var returnString:String = ExternalInterface.call("confirmClose", exitMessage);

So far, so good. But how do we convert the string returnString into a Boolean value? In other words, returnString will be either “true” or “false.” My mistake was to use the syntax:

var okToExit:Boolean = Boolean(returnString);

You would think this would work since the ActionScript Number() function will take a numeric string and convert it to a number. Similarly, the Visual Basic CBool() function will take “true” or “True” and convert it to the Boolean True. However, this “casting” function returns true for both “true” and “false!” So the user exited even if she clicked the “Cancel” button in the dialog. This is NOT what we wanted. If I had read the Flex documentation, this behavior is clearly stated:

Casting to Boolean from a String value returns false if the string is either null or an empty string (“”). Otherwise, it returns true.

So what was the fix? Easiest for me was to create a ConvertToBoolean function that operated like I expected. The new syntax plus the function is shown below.

var okToExit:Boolean = ConvertToBoolean(returnString);

public function ConvertToBoolean(inputVal:String):Boolean {
    var returnVal:Boolean;

    if (inputVal == "true") { // need to check string since Boolean() looks for 1 or 0
        returnVal = true;
    }
    else {
        returnVal = false; 
    }

    return returnVal;			
}

Aligning Objects within Groups

I was doing a demo of the Plug-In Pro awhile back and we got into a discussion of how it is a hassle if objects within a group (such as a multiple choice question) have gotten moved around slightly. I came up with the script to the left to deal with that. You figure out the desired left coordinate (4403 page units in this example), select the group, and then press Enter in the Command Window. The getObjectListfunction grabs all the objects within the selected object (group). We then pop each item from the list into a separate variable and set the x coordinate (item 1 of position) to the desired value. Note that passing FALSE to getObjectList means that all matching objects are included, regardless of whether they have a script.

leftVal = 4403

objList = getObjectList(selection, "", FALSE)

while objList <> null
	pop objList into objId
	item 1 of position of objId = leftVal
end while

Using Resources of Another Book

Question: I want to use a system book as a source of my images. Is this possible?

Answer: This is definitely possible. I have included some code from our Plug-In Pro below that should help. The main idea is that you want to check to see if the resource is already there. If so, either use it or call replace resource. If not, you want to use import bitmap resource. In the code below, callingBookId would refer to your system book. You could get your hands on that by using sysBooks. For example:

callingBookId = book (item 1 of sysBooks)

Here is the code I mentioned.

shortName = chars max(1, maxChars - 31) to maxChars of fullImageFile 
tempRef = "bitmap" && quote & shortName & quote && "of" && callingBookId 
oldImageRef = normalGraphic of placeholderId 
if isObject (tempRef)
        replace resource tempRef with imageFile
        normalGraphic of placeholderId = tempRef
        errString = ASYM_RestoreSuspend(blkSus) 
else
        import bitmap resource imageFile as shortName
        normalGraphic of placeholderId = bitmap shortName
        errString = ASYM_RestoreSuspend(blkSus) 
end if

Dictionaries in .NET

In the previous post, we looked at a question related to the Dictionary object in ActionScript. Here is some continued information on the equivalent object in .NET.

We are looking at a Silverlight version of Training Studio 2 right now (note: this was true when written. But we have since decided to have the next version be HTML and JavaScript). One advantage in .NET is that we can use Generics to specify the type of objects within the Dictionaries or ArrayCollections. For example, here is how the same objects are defined in ActionScript and Silverlight:

ActionScript

public static var masterContentArray:ArrayCollection = new ArrayCollection();
public static var pageArray:Dictionary;

Silverlight

Friend masterContentDictionary As New Dictionary(Of Integer, Dictionary(Of String, String))
Friend pageArray As Dictionary(Of String, String)

Note that we renamed masterContentArray to masterContentDictionaryand made the “page number” an explicit key. The direct equivalent to the ArrayCollection would be:

Friend masterContentArray As New List(Dictionary(Of String, String))

The advantage of Generics is that we can specify exactly what kind of objects are stored in the Dictionary or List objects. If we try to put an object of the wrong type in it, Visual Studio will tell us. When we pull objects out of the Dictionary or List, we can use them as that type right away, rather than having to use the Dictionary() syntax.

ActionScript Dictionaries

Question: In the context of Training Studio 2.0 templates, I am unclear on the full meaning of “dictionary.” Specifically, I am curious what this statement in the documentation means: The pageArrayLocal variable is a Dictionary that represents the current training page.

Answer: A Dictionary is actually a Flex/ActionScript object. It is similar to a Hashtable in Visual Basic or an associative array in JavaScript. The Dictionary has a key and a value. When we read the content.xml file in Training Studio, we put the content of each page into its own Dictionary.
The keys are column names and the values are the text that the subject matter put in. For example, here is a small section of the XML for a page:

<title>Training Studio TBCON Faculty Sample</title> 
<content_0>Welcome to the TBCON Faculty sample created in the Platte Canyon  Training Studio. To learn more about Training Studio, please visit ~external=http://www.trainingstudio.net~www.trainingstudio.net~.</content_0>        <
<graphic_0>trainingstudio.gif</graphic_0>


title, content_0, and graphic_0 are keys. “Training Studio TBCON Faculty Sample”, “Welcome…”, and “trainingstudio.gif” are values.
When Training Studio navigates to a page, it sets the pageArrayLocal variable to be the Dictionary associated with that page. We then look for the “templateType” key to figure out which template to use. Within the template, the loadData method loops through the keys and sets the appropriate content, reads in the media, load graphics, etc.

A related data type that you’ll run into is the ArrayCollection. It is similar to a Dictionary but does not have keys. Instead you refer to objects by their numeric position. In Training Studio, the TSCommon.masterContentArray is an ArrayCollection of all the Dictionaries (one per page) in the training.

See the next post for the equivalent objects in .NET.

Determining if an Object is Within a Group

I’m putting some recent (2010 and later) segments from The EnterPage Newsletter into my blog. That is what is behind this flurry of activity:).

Question: I want to find out through OpenScript whether an object is part of a certain group.

Answer: Since objects and groups can be within other groups, I think the easiest way is to loop through the parents of the object as in the code below. If you know that there is only a single group, though, a quicker solution would be to use the objectContainer function like this:

groupId = objectContainer(button "button 1", "group") 
if groupId = group "group 2"
	request "IsInGroup = true"
else
	request "IsInGroup = false"
end if

Here is a more general solution that will work with groups inside groups.

to handle buttonClick
	local logical isInGroup

	isInGroup = findIfInGroup(button "button 1", group "group 2")
	request "isInGroup = " & isInGroup
end buttonClick

to get findIfInGroup object startingObject, object groupObject
	local logical inGroup
	local object parentId
	local string parentType

	parentId = startingObject
	parentType = object of startingObject

	inGroup = FALSE
	while (inGroup = FALSE AND (parentType <> "page" AND parentType <> "background"))
		parentId = parent of parentId
		if parentId is groupObject
			inGroup = TRUE
		else
			parentType = object of parentId
		end if
	end while

	return inGroup
end findIfInGroup