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
	End With
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)

		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

		statusLabel.Text = String.Format("Displaying {0}. indexNum = {1}. Count = {2}.", _
		    photoFileId.Name, indexNum, photoList.Count)
		If timerId IsNot Nothing Then
		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.