To ease your read, please resume from this chapter where we have set up the MediaElement.
Here we go again for a new chapter!
It’s already episode 13 of this series, so I hope it’s still relatively easy to follow! But you may have questions or comments. If you do, ask me in the comments at the bottom of the article, or e-mail me directly (jeanemmanuel.baillat@gmail.com)!
Today, we will have a look at how to enable the user to download the music that is currently playing. We have been listening to the same song over and over, I’m sure you have been dreaming of being able to download it from the app! π
Adding a new ViewModel
First of all, we need to set up a new ViewModel for the MusicPlayerView. To do this, add a new class named MusicPlayerViewModel to the ViewModels folder, and define it with the following code:
If figuring out this bit of code is difficult for you, then don’t get discouraged and take some time to read again the chapter on MVVM.
Of course, this ViewModel doesn’t do anything at the moment, but it’s ready to be associated with its View. So open the file MusicPlayerView.cs and modify it as follows:
...// This using is mandatory to resolve the definition of MusicPlayerViewModelusingNightClub.ViewModels;namespaceNightClub.Views;publicclassMusicPlayerView:ContentPage{publicMusicPlayerView(){Console.WriteLine("[NightClub] MusicPlayerView - Constructor");// Here is where the association is happeningBindingContext=newMusicPlayerViewModel();NavigationPage.SetHasNavigationBar(this,false);BackgroundColor=Colors.DimGray;...}...}
As with the HomeViewModel associated with the HomeView, here we have modified the MusicPlayerView’s BindingContext to associate it with the new MusicPlayerViewModel.
Well, that was quick. Now let’s see how to structure application data by defining the Model of music tracks!
A new class for music tracks
Each music track played in the application is defined by a panel of information that we’ll group together in a class called MusicTrack. As you’ll have guessed, this new object is part of our application’s Model.
Start by creating a new folder called Models, then add a new class defined by the following code:
For the purposes of this course, 4 string properties are required to contain the following information:
The link for streaming audio (AudioURL),
The link for downloading audio (AudioDownloadURL),
The music track name (Title),
And the name of his author (Author).
Since it is now possible to manipulate music tracks, we will add a MusicTrack property to the MusicPlayerViewModel to define the song currently playing:
Filename:MusicPlayerViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...usingNightClub.Models;// This is required to resolve MusicTrack object!namespaceNightClub.ViewModels;publicpartialclassMusicPlayerViewModel:ObservableObject{#regionProperties [ObservableProperty]MusicTrackcurrentTrack;#endregion...}
This perfectly fits in with the Model-View-ViewModel (MVVM) breakdown, since we’ve declared it as an [ObservableProperty], which is an annotation provided by the MVVM Toolkit library.
This annotation will then generate all the code required to trigger events (generally towards the View) in case of a value change. Indeed, we want to match the information displayed in the View to its associated ViewModel.
β
πβ β Got it! But how are we going to define this song?
For the moment, it’s easy because our application only supports the playback of a single song. So we’ll simply initialize it from the MusicPlayerViewModel constructor, as follows:
All the information is provided by Jamendo, a website for free & independent music.
But since we’ve defined the music track inside the MusicPlayerViewModel, we now need to rework the MusicPlayerView to reconfigure the MusicPlayer.
To do this, we need to modify the InitMusicPlayer() method inside the MusicPlayerView, and apply the Data Binding to the Source property of the MediaElement:
...usingNightClub.Models;// This is required to resolve MusicTrack object!namespaceNightClub.Views;publicclassMusicPlayerView:ContentPage{...#regionMusicPlayerMediaElementMusicPlayer=newMediaElement();// And here's the new definition for that method...voidInitMusicPlayer(){MusicPlayer.ShouldAutoPlay=true;// ... with the binding logic on the MusicPlayer.MusicPlayer.Bind(MediaElement.SourceProperty,nameof(MusicPlayerViewModel.CurrentTrack),convert:(MusicTrackmusicTrack)=>MediaSource.FromUri(musicTrack.AudioURL));}#endregion...}
Remember, the MediaElement’s Source property is used to define the source of the media to play. And now, the MusicPlayer.Source property is dynamically linked to the CurrentTrack property defined inside the MusicPlayerViewModel.
Also in the convert, we must not forget to transform the audio streaming link (musicTrack.AudioURL) with the MediaSource.FromUri() method, to conform to the type of the MediaElement’s Source property.
That’s it! Relaunch the project and check that everything is working as before. I wouldn’t want you to be lost in the middle! π
Is everything OK? Then let’s move on quickly to the most interesting part of this chapter: downloading!
Download a song track
Let’s continue our journey by implementing the download button.
To do this, we’re going to associate an action triggered by clicking on the DownloadButtonβ¦
Filename:MusicPlayerView.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
#regionMediaControlPanel...// The β=>β sign has been replaced with β=βImageButtonDownloadButton=newImageButton{CornerRadius=5,HeightRequest=25,WidthRequest=25,Source="download.png",BackgroundColor=Colors.Black}.BindCommand("DownloadCurrentTrackCommand");// And here's the command to associate#endregion
… and whose behavior will be defined inside MusicPlayerViewModel:
...usingCommunityToolkit.Maui.Alerts;// This "using" is new...usingCommunityToolkit.Maui.Storage;// ... and this one as well !namespaceNightClub.ViewModels;publicpartialclassMusicPlayerViewModel:ObservableObject{...#regionCommands [RelayCommand]asyncTaskDownloadCurrentTrack(CancellationTokencancellationToken){awaitToast.Make($"[TEST] You have successfully downloaded \"{CurrentTrack.Title}-{CurrentTrack.Author}\"!").Show(cancellationToken);}#endregion}
Remember the [RelayCommand] annotation? We already used it in the MVVM chapter. It allows our method DownloadCurrentTrack() to be called from the View!
And let me stop you right there, the Toast() method has nothing to do with your breakfast π
At this stage, we can already test that our button is working properly:
β
πβ β Are you cheating on me? It doesn’t download anything at all! π
I’ll share the final code with you right after… patience! π€
Just before, I’d like to bring your attention to the parameter that is required by our new command, the cancellationToken. This is an object of type CancellationToken that keeps a link with the code that initiated the call to the method DownloadCurrentTrack() in the event of a request to cancel its execution.
This is powerful for operations that take a little longer, for example if our user decides to cancel the download due to bad network. We’re not going to implement this feature today, but it would be a great improvement!
#regionCommands[RelayCommand]asyncTaskDownloadCurrentTrack(CancellationTokencancellationToken){// We raise an exception when cancellation is requestedcancellationToken.ThrowIfCancellationRequested();try{// We need an HTTP client to send our request through the networkHttpClientclient=newHttpClient();client.MaxResponseContentBufferSize=100000000;// We can download up to ~100MB of data per file!// We send an HTTP request to the link for downloading audiousingvarhttpResponse=awaitclient.GetAsync(newUri(CurrentTrack.AudioDownloadURL),cancellationToken);httpResponse.EnsureSuccessStatusCode();vardownloadedImage=awaithttpResponse.Content.ReadAsStreamAsync(cancellationToken);try{stringfileName=$"{CurrentTrack.Title} - {CurrentTrack.Author}.mp3";// The retrieved data is then transferred to a file// Note: we need CommunityToolkit.Maui to be updated to 5.1.0 at leastvarfileSaveResult=awaitFileSaver.SaveAsync(fileName,downloadedImage,cancellationToken);fileSaveResult.EnsureSuccess();awaitToast.Make($"File saved at: {fileSaveResult.FilePath}").Show(cancellationToken);}catch(Exceptionex){awaitToast.Make($"Cannot save file because: {ex.Message}").Show(cancellationToken);}}catch(Exceptionex){awaitToast.Make($"Cannot download file because: {ex.Message}").Show(cancellationToken);}}#endregion
This is a big piece of code, but nothing too complicated!
Let’s walkthrough step by step:
We first define an HTTP client to enable us to make a request to the download link of the currently playing track (CurrentTrack.AudioDownloadURL),
In return, we expect a positive response from the server to provide us with the corresponding data,
And then, if everythingβs fine, we open a read channel to transfer the data to a file and request that it be saved on the device.
As you can see, there’s really no complex logic here. It’s just a bit technical! So, as always, take some time to explore the subject if you need to.
Finally, we’ve coded a few messages to help the user understand what’s going on in the background. After all, we wouldn’t want the user to wait indefinitely because of an error that occurred during the download process! And as these are only informal messages, with no action required, I preferred to use the famous Toast to display ephemeral notifications.
And that’s it. Give it a try!
Congratulations on all your hard work! One last effort and manipulation of media will hold no secrets for you. See you in the next chapter to manage the music playlist!