Share via


Exercise

The starting point for this exercise is the solution located in the lab installation folder under the Source\Begin folder.

The solution contains a project with all the user interface (UI) elements we will use in this exercise and the model (Playlist and Track) we will work with. As you progress through the exercise, you will gradually wire up the user interface to the application logic.

Task 1 – Adding the main UI and functionality

In this task, we will work on the application’s main, and only, page and implement the event handlers for Microsoft.Phone.BackgroundAudio.BackgroundAudioPlayer in order to keep playing audio even if the application is not in the foreground.

  1. Open the solution under the Source\Begin folder and examine the project.
  2. Open the MainPage.xaml file.
  3. Set the DataContext property of the page itself by adding the following attribute to the phone:PhoneApplicationPage element:

    XAML

  4. Locate the grid named ContentPanel and add the HorizontalAlignment attribute, setting it to Stretch.

    XAML

  5. Now add the following RowDefinitions and a new TextBlock. You can see that we use the new textblock to display the name of the active playlist. We will add a property to hold the active playlist later in the lab:

    XAML

    <TextBlock Text="{Binding ActivePlaylist.Name}" FontSize="{StaticResource PhoneFontSizeExtraLarge}"/>
    

    </Grid>

  6. Add a new ListBox as seen in the code below. This listbox will display the list of available music tracks.

    XAML

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0" HorizontalAlignment="Stretch"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions>

    <TextBlock Text="{Binding ActivePlaylist.Name}" FontSize="{StaticResource PhoneFontSizeExtraLarge}"/> <ListBox ItemsSource="{Binding ActivePlaylist.Tracks}" Grid.Row="1" x:Name="lstTracks" HorizontalContentAlignment="Stretch" Loaded="lstTracks_Loaded"> <ListBox.ItemTemplate> <DataTemplate> <Grid HorizontalAlignment="Stretch"> <Grid.RowDefinitions> <RowDefinition/> <RowDefinition/> <RowDefinition/> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions>

    <Image Margin="10,0" Source="{Binding Tile, TargetNullValue={StaticResource NoArt}}" Grid.RowSpan="3" Width="60" Stretch="Uniform" VerticalAlignment="Center"/> <TextBlock Text="{Binding Title}" Grid.Column="1" FontSize="{StaticResource PhoneFontSizeLarge}"/> <TextBlock Text="{Binding Artist}" Grid.Row="1" Grid.Column="1"/> <TextBlock Text="{Binding Album}" Grid.Row="2" Grid.Column="1"/> </Grid> </DataTemplate> </ListBox.ItemTemplate> </ListBox> </Grid>

  7. Open the file named MainPage.xaml.cs. This is the code behind file for the main page.
  8. Add an empty method called lstTracks_Loaded as seen below:

    C#

    public partial class MainPage : PhoneApplicationPage { ... private void lstTracks_Loaded(object sender, RoutedEventArgs e) { } ... }

  9. Compile and run the application on the emulator. At this stage, you should see the following screen.

    Figure 1

    The application without an actual playlist.

  10. Let us add the active playlist property we used for binding in the previous steps. Add a Playlist dependency property as seen below and call it ActivePlaylist:

    C#

    public partial class MainPage : PhoneApplicationPage { public Playlist ActivePlaylist { get { return (Playlist)GetValue(ActivePlaylistProperty); } set { SetValue(ActivePlaylistProperty, value); } }

    public static readonly DependencyProperty ActivePlaylistProperty = DependencyProperty.Register("ActivePlaylist", typeof(Playlist), typeof(MainPage), new PropertyMetadata(null)); ... }

  11. Override the OnNavigatedTo method on the page in order to deploy the playlist supplied as one of the application’s resources to isolated storage.

    C#

    public partial class MainPage : PhoneApplicationPage { ... protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e) { Stream playlistStream = Application.GetResourceStream(new Uri("Misc/Playlist.xml", UriKind.Relative)).Stream;

    System.Xml.Serialization.XmlSerializer serializer = new System.Xml.Serialization.XmlSerializer(typeof(Playlist)); ActivePlaylist = (Playlist)serializer.Deserialize(playlistStream);

    using ( IsolatedStorageFile isoStorage = IsolatedStorageFile.GetUserStoreForApplication() ) { using ( IsolatedStorageFileStream file = isoStorage.OpenFile("playlist.xml", FileMode.OpenOrCreate) ) { var writer = new StreamWriter(file);

    serializer.Serialize(writer, ActivePlaylist); } }

    base.OnNavigatedTo(e); } ... }

  12. In the page’s constructor, add the following highlighted code snippet to subscribe to the PlayStateChanged event:

    C#

    public partial class MainPage : PhoneApplicationPage { ... public MainPage() { InitializeComponent();

    BackgroundAudioPlayer.Instance.PlayStateChanged += new EventHandler(Instance_PlayStateChanged); } ... }

  13. Add the event handler function specified in the previous step along with some additional methods. This will update the track number of the currently playing track whenever the background audio player, which we use to play back tracks, changes its playback state:

    C#

    public partial class MainPage : PhoneApplicationPage { ... private void Instance_PlayStateChanged(object sender, EventArgs e) { UpdateSelection(); }

    private void UpdateSelection() { int activeTrackNumber = GetActiveTrackIndex();

    if ( activeTrackNumber != -1 ) { lstTracks.SelectedIndex = activeTrackNumber; } }

    private int GetActiveTrackIndex() { int track = -1; if ( null != BackgroundAudioPlayer.Instance.Track ) { track = int.Parse(BackgroundAudioPlayer.Instance.Track.Tag); }

    return track; } ... }

  14. The background audio player will allow the user to change the actively playing track even while the application is not active. To handle this, update the lstTracks_Loaded method inserting the following code into that method:

    C#

    public partial class MainPage : PhoneApplicationPage { ... private void lstTracks_Loaded(object sender, RoutedEventArgs e) { UpdateSelection(); } ... }

  15. Compile and run the application. At this stage the main page should look like the following:

    Figure 2

    Playlist

  16. Stop the debugging and return to the code.
  17. This step concludes the current task.

Task 2 – Adding Button Controls

This step will explain how to add ApplicationBar buttons, which we will use to control the player. We will add four buttons: play, stop, previous and next track.

These buttons' functions will use services exposed by the BackgroundAudioPlayer class.

  1. Open the MainPage.xaml file, if it is not already opened.
  2. After the LayoutRoot grid ends, instead of the commented use of PhoneApplicationPage.ApplicationBar, add the following code:

    XAML

    <phone:PhoneApplicationPage.ApplicationBar> <shell:ApplicationBar IsVisible="True" IsMenuEnabled="False"> <shell:ApplicationBarIconButton IconUri="/Images/prev.png" Text="prev" Click="appbar_prev"/> <shell:ApplicationBarIconButton IconUri="/Images/play.png" Text="play" Click="appbar_playpause"/> <shell:ApplicationBarIconButton IconUri="/Images/stop.png" Text="stop" Click="appbar_stop"/> <shell:ApplicationBarIconButton IconUri="/Images/next.png" Text="next" Click="appbar_next"/> </shell:ApplicationBar> </phone:PhoneApplicationPage.ApplicationBar>

  3. Open the file MainPage.xaml.cs and add the handlers for the newly inserted buttons.

    C#

    public partial class MainPage : PhoneApplicationPage { ... #region ApplicationBar Buttons Events private void appbar_prev(object sender, EventArgs e) { BackgroundAudioPlayer.Instance.SkipPrevious(); }

    private void appbar_playpause(object sender, EventArgs e) { if ( BackgroundAudioPlayer.Instance.PlayerState == PlayState.Playing ) BackgroundAudioPlayer.Instance.Pause(); else BackgroundAudioPlayer.Instance.Play(); }

    private void appbar_stop(object sender, EventArgs e) { BackgroundAudioPlayer.Instance.Stop(); }

    private void appbar_next(object sender, EventArgs e) { BackgroundAudioPlayer.Instance.SkipNext(); } #endregion ... }

  4. Just as we updated the selected track in the previous task, we need to update the appbar buttons according to the player’s state. To do that, locate the Instance_PlayStateChanged method and add a call to UpdateAppBarStatus method.

    C#

    public partial class MainPage : PhoneApplicationPage { ... private void Instance_PlayStateChanged(object sender, EventArgs e) { UpdateAppBarStatus(); UpdateSelection(); } ... }

  5. Now add the UpdateAppBarStatus method itself:

    C#

    public partial class MainPage : PhoneApplicationPage { ... private void UpdateAppBarStatus() { switch ( BackgroundAudioPlayer.Instance.PlayerState ) { case PlayState.Playing: //Prev Button if ( GetActiveTrackIndex() > 0 ) ( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = true; else ( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = false;

    //Play/Pause Button ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IsEnabled = true; ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).Text = "pause"; ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IconUri = new Uri("/Images/pause.png", UriKind.Relative);

    //Stop Button ( ApplicationBar.Buttons[2] as ApplicationBarIconButton ).IsEnabled = true;

    //Next button if ( GetActiveTrackIndex() < ActivePlaylist.Tracks.Count - 1 ) ( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = true; else ( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = false; break;

    case PlayState.Paused: //Prev Button if ( GetActiveTrackIndex() > 0 ) ( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = true; else ( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = false;

    //Play/Pause Button ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IsEnabled = true; ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).Text = "play"; ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IconUri = new Uri("/Images/play.png", UriKind.Relative);

    //Stop Button ( ApplicationBar.Buttons[2] as ApplicationBarIconButton ).IsEnabled = true;

    //Next button if ( GetActiveTrackIndex() < ActivePlaylist.Tracks.Count - 1 ) ( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = true; else ( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = false; break;

    case PlayState.Stopped: //Prev Button if ( GetActiveTrackIndex() > 0 ) ( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = true; else ( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = false;

    //Play/Pause Button ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IsEnabled = true; ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).Text = "play"; ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IconUri = new Uri("/Images/play.png", UriKind.Relative);

    //Stop Button ( ApplicationBar.Buttons[2] as ApplicationBarIconButton ).IsEnabled = false;

    //Next button if ( GetActiveTrackIndex() < ActivePlaylist.Tracks.Count - 1 ) ( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = true; else ( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = false; break;

    case PlayState.Unknown: //Prev Button ( ApplicationBar.Buttons[0] as ApplicationBarIconButton ).IsEnabled = false;

    //Play/Pause Button ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IsEnabled = true; ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).Text = "play"; ( ApplicationBar.Buttons[1] as ApplicationBarIconButton ).IconUri = new Uri("/Images/play.png", UriKind.Relative);

    //Stop Button ( ApplicationBar.Buttons[2] as ApplicationBarIconButton ).IsEnabled = false;

    //Next button if ( GetActiveTrackIndex() < ActivePlaylist.Tracks.Count - 1 ) ( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = true; else ( ApplicationBar.Buttons[3] as ApplicationBarIconButton ).IsEnabled = false; break;

    default: break; } } ... }

  6. Compile and run the application. At this stage the main page should look like the following:

    Figure 3

    Playlist with playback controls

  7. Stop the debugging and return to the code.

Task 3 – Copying music and image files to Isolated Storage

In order to play the tracks we have in the solution, we need to copy these files to Isolated Storage.

  1. Open the file named App.xaml.cs and add the following using directives:

    C#

    usingSystem.IO.IsolatedStorage; usingSystem.Windows.Resources;

  2. At the constructor for the App class, add the following highlighted code:

    C#

    publicApp() { ... CopyToIsolatedStorage(); }

  3. Now use the following code to add the CopyToIsolatedStorage method itself:

    C#

    private void CopyToIsolatedStorage() { using ( IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication() ) { string[] files = new string[] { "file1.mp3", "file2.mp3", "file3.mp3" };

    foreach ( var _fileName in files ) { if ( !storage.FileExists(_fileName) ) { string _filePath = "Music/" + _fileName; StreamResourceInfo resource = Application.GetResourceStream(new Uri(_filePath, UriKind.Relative));

    using ( IsolatedStorageFileStream file = storage.CreateFile(_fileName) ) { int chunkSize = 4096; byte[] bytes = new byte[chunkSize]; int byteCount;

    while ( ( byteCount = resource.Stream.Read(bytes, 0, chunkSize) ) > 0 ) { file.Write(bytes, 0, byteCount); } } } }

    files = new string[] { "Image1.jpg", "Image3.jpg", "no-art.jpg" };

    foreach ( var _fileName in files ) { string _destFilePath = "Shared/Media/" + _fileName; ; if ( !storage.FileExists(_destFilePath) ) { string _filePath = "Images/" + _fileName; StreamResourceInfo resource = Application.GetResourceStream(new Uri(_filePath, UriKind.Relative));

    using ( IsolatedStorageFileStream file = storage.CreateFile(_destFilePath) ) { int chunkSize = 4096; byte[] bytes = new byte[chunkSize]; int byteCount;

    while ( ( byteCount = resource.Stream.Read(bytes, 0, chunkSize) ) > 0 ) { file.Write(bytes, 0, byteCount); } } } } } }

  4. This step concludes the current task.

Task 4 – Adding and Implementing the Audio Playback Agent

The last thing we need to do is to implement an “Audio Playback Agent”. This is instantiated by the operating system to handle actions requested by the user. AudioPlayerAgent runs in the background and calls into an instance of the BackgroundAudioPlayer, which then calls into the Zune Media Queue to play the audio.

  1. add a new project to the solution using the Windows Phone Audio Playback Agent template located under the Silverlight for Windows Phone category and name it AudioPlaybackAgent.
  2. In this project, we will need to use the model defined in the Models project as well as XML serialization. Add references to both Models and System.Xml.Serialization.
  3. Open the AudioPlayer.cs file created as part of the project and delete the entire contents of the AudioPlayer class.
  4. Add the following using statements to the file:

    C#

    using System.IO.IsolatedStorage; using System.Xml.Serialization; using System.IO; using Models;
  5. Add the following fields to the AudioPlayer class. These will be used to store the current playlist and keep track of the currently playing track:

    C#

    staticintcurrentTrackNumber= 0; staticPlaylistplaylist;
  6. Add the following constructor, which will access the playlist XML file we took care to place in the application’s isolated storage:

    C#

    publicAudioPlayer() : base() {

    //Load fromIsoStore&deserialize using ( IsolatedStorageFile isoStorage = IsolatedStorageFile.GetUserStoreForApplication() ) { using ( IsolatedStorageFileStream file = isoStorage.OpenFile("playlist.xml", FileMode.Open) ) { XmlSerializerserializer= newXmlSerializer(typeof(Playlist)); varreader = newStreamReader(file);

    playlist = (Playlist)serializer.Deserialize(reader); } } }

  7. Add the following OnPlayStateChanged override, which handles changes in the agent’s playback status:

    C#

    protectedoverridevoidOnPlayStateChanged(BackgroundAudioPlayerplayer,AudioTracktrack,PlayStateplayState) { switch(playState) { casePlayState.TrackEnded: PlayNext(player); break; casePlayState.TrackReady: player.Play(); break; default: break; }

    NotifyComplete(); }

    You can see that once a track ends we simply move on to the next track (using a method we will soon introduce) and that once a track is ready we simply use the background audio player to play it. The call to NotifyComplete is required to let the agent know that the state change was handled.

  8. Add the following helper methods, which initiate playback of the next/previous track:

    C#

    privatevoidPlayNext(BackgroundAudioPlayerplayer) { varsongsCount=playlist.Tracks.Count;

    if( ++currentTrackNumber>=songsCount) { currentTrackNumber= 0; } Play(player);

    } privatevoidPlayPrev(BackgroundAudioPlayerplayer) { varsongsCount=playlist.Tracks.Count; if( --currentTrackNumber< 0 ) { currentTrackNumber=songsCount- 1; } Play(player); }

  9. Add the following override for OnUserAction, to handle the various actions a user may perform when interacting with the audio playback agent:

    C#

    protectedoverridevoidOnUserAction(BackgroundAudioPlayerplayer,AudioTracktrack,UserActionaction,objectparam) { switch( action ) { caseUserAction.FastForward: player.FastForward(); break; caseUserAction.Pause: player.Pause(); break; caseUserAction.Play: if(player.PlayerState==PlayState.Paused) { player.Play(); } else { Play(player); } break; caseUserAction.Rewind: player.Rewind(); break; caseUserAction.Seek: player.Position= (TimeSpan)param; break; caseUserAction.SkipNext: PlayNext(player); break; caseUserAction.SkipPrevious: PlayPrev(player); break; caseUserAction.Stop: player.Stop(); break; default: break; }

    NotifyComplete(); }

  10. Add an additional helper method for initiating playback of the current track, as dictated by the currentTrackNumber field we added previously:

    C#

    privatevoidPlay(BackgroundAudioPlayerplayer) { varcurrentTrack=playlist.Tracks[currentTrackNumber]; UritileUri = ( currentTrack.Tile ==null?newUri("Shared/Media/no-art.jpg",UriKind.Relative) : ( currentTrack.Tile.IsAbsoluteUri ?newUri("Shared/Media/no-art.jpg",UriKind.Relative) : newUri(currentTrack.TileString.Replace("/Images","Shared/Media"),UriKind.Relative) ) );

    varaudioTrack=newAudioTrack(currentTrack.Source, currentTrack.Title, currentTrack.Artist, currentTrack.Album, tileUri, currentTrackNumber.ToString(), EnabledPlayerControls.All); player.Track=audioTrack; }

  11. Add a pair of additional overrides for handling errors or cancellations, though we do not have any specific logic for such cases in this lab:

    C#

    protectedoverridevoidOnCancel() { NotifyComplete(); }

    protectedoverridevoidOnError(BackgroundAudioPlayerplayer,AudioTracktrack,Exceptionerror,boolisFatal) { NotifyComplete(); }

  12. The last step is to add a reference to this new project from the MusicPlayer project.
  13. Compile the application and make sure you do not get any errors.
  14. Run the application. At this stage, the main page should be fully functional.
  15. Now it is time to play some music. Click on the play () button and be sure to turn the volume up (press F9 to increase the emulator’s own volume).

    Figure 4

    The fully functional application

  16. Now you can navigate away from the application by pressing the Start Key () and see that the music continues to play.
  17. Pressing F9 to change the volume will reveal the current playback state, and you can control the current playing track just as you would through the application itself.

    Figure 5

    Controlling playback while the application is inactive

  18. Change the currently track and launch the application again from the list of installed applications. The application should display the currently playing track correctly.
  19. Stop the debugger.
  20. This step concludes the current task and the entire lab.