Our AudioPlayerPageViewModel
must include our ISoundHandler
interface. We are going to be controlling the audio from this view-model, so our buttons can initiate the required events on the sound handler. Let's begin by making a new file inside the ViewModels
folder called AudioPlayerPageViewModel.cs
, and implementing the private properties to begin with:
public class AudioPlayerPageViewModel : MvxViewModel { #region Private Properties private readonly ISoundHandler _soundHandler; private string _title = "Audio Player"; private string _descriptionMessage = "Moby - The Only Thing"; private MvxCommand _playPauseCommand; private MvxCommand _forwardCommand; private MvxCommand _rewindCommand; private float _audioPosition; private double _currentTime; private double _endTime; private bool _updating; #endregion
Then we must add the public
properties.
public MvxCommand PlayPauseCommand { get { return _playPauseCommand; } set { if (!value.Equals(_playPauseCommand)) { _playPauseCommand = value; RaisePropertyChanged (() => PlayPauseCommand); } } } public MvxCommand RewindCommand { get { return _rewindCommand; } set { if (!value.Equals(_rewindCommand)) { _rewindCommand = value; RaisePropertyChanged(() => RewindCommand); } } }
We also need to add these two public variables, which are going to take both the CurrentTime
and EndTime
double values and create a formatted string from a TimeSpan
value.
Notice how we are also calling RaisePropertyChanged
on the string inside the double setter? Every time we get a new current time value, the formatted string needs to update as well:
public string CurrentTimeStr { get { return TimeSpan.FromSeconds(CurrentTime).ToString("mm\:ss"); } } public double CurrentTime { get { return _currentTime; } set { if (!value.Equals(_currentTime)) { _currentTime = value; RaisePropertyChanged(() => CurrentTime); // everytime we change the current time, the time span values must also update RaisePropertyChanged(() => CurrentTimeStr); } } } public string EndTimeStr { get { return TimeSpan.FromSeconds(EndTime).ToString("mm\:ss"); } } public double EndTime { get { return _endTime; } set { if (!value.Equals(_endTime)) { _endTime = value; RaisePropertyChanged(() => EndTime); RaisePropertyChanged(() => EndTimeStr); } } }
Now for our constructor function:
#region Constructors public AudioPlayerPageViewModel (ISoundHandler soundHandler) { _soundHandler = soundHandler; // load sound file _soundHandler.Load(); EndTime = _soundHandler.Duration(); } #endregion
Here we are pulling out the ISoundHandler
implementation from the IoC container, as we will be registering this view-model inside the IoC container.
Our next step is to add two new functions to the view-model, Load
and Dispose
. These two functions will be called when the AudioPlayerPage
is shown, and when it disappears. They will also be used when the audio stream is started and stopped.
Let's first add the Load
function:
public void Load() { // make sure we only start the loop once if (!_updating) { _updating = true; // we are going to post a regular update to the UI with the current time var context = SynchronizationContext.Current; Task.Run(async () => { while (_updating) { await Task.Delay(1000); context.Post(unused => { var current = _soundHandler.CurrentPosition(); ; if (current > 0) { CurrentTime = current; } }, null); } }); } }
The Load
function will be called when the page is shown, and when the audio stream starts. The function uses the Task
framework to run a repeating loop in the background, so every second we will retrieve the current time of the audio stream from the ISoundHandler
interface. We propagate the updates to the current time label on the AudioPlayerPage
interface.
Notice how we are using the SynchronisationContext.Current
variable?
This is used for threading purposes so we make sure that we set our CurrentTime
variable on the main UI thread. Since this loop is running on a separate thread, if we made changes to this variable on a separate thread, it will break the application because you are trying to make UI changes off the main UI thread.
Now for the Dispose
function; this will be called every time the AudioPlayerPage
disappears and when the audio stream is stopped (we don't need to make updates to the UI when the audio stream is not playing). This ensures we stop the background loop when the page is not visible:
public void Dispose() { _updating = false; _soundHandler.Stop(); }
The private variable _updating
is used to control the status of whether the background loop is running, so we make sure that only one background loop is running at any one time.
Now let's initiate the audio commands:
_playPauseCommand = new MvxCommand(() => { // start/stop UI updates if the audio is not playing if (soundHandler.IsPlaying) { Dispose(); } else { Load(); } _soundHandler.PlayPause(); }); _rewindCommand = new MvxCommand(() => { // set current time to the beginning CurrentTime = 0; _soundHandler.Rewind(); Dispose(); }); _forwardCommand = new MvxCommand(() => { // set current time to the end CurrentTime = _soundHandler.Duration(); _soundHandler.Forward(); Dispose(); });
Looking more closely at these commands, using PlayPauseCommand
we will call Load
or Dispose
based on the playing status of the audio stream, and it will also call PlayPause
on the ISoundHandler
interface, which controls the audio stream. The rewindCommand
property will set the current time to 0, set the current time on the audio stream to 0, and stop the background loop. The forwardCommand
property will set the current time to the end duration of the audio stream (which it will retrieve from the ISoundHandler
interface), set the current time on the audio stream to the end duration, and stop the background loop.
Finally, we have to create a public
function to set the current time of the audio stream. This will be used by our progress slider every time the value changes, this function will be called:
public void UpdateAudioPosition(double value) { _soundHandler.SetPosition(value); }
Now revert back to the AudioPlayerPage
and add the final additions.
Since we declared a local variable before for the view-model that is bound to the view, we want to pull this out of the data context of the UIView
:
_model = (AudioPlayerPageViewModel)DataContext;
Our local variable has the bounded view-model. We need to call some public methods on the view-model from our view. We must add in our event handler for the ValueChanged
event on the progress slider. Add the following under the declaration of the progress slider:
progressSlider.ValueChanged += ProgressSliderValueChanged;
Then create the event handler function:
private void ProgressSliderValueChanged(object sender, EventArgs e) { _model.UpdateAudioPosition(_progressSlider.Value); }
And add the calls to the Load
function when the page appears:
public override void ViewDidAppear(bool animated) { _model.Load(); base.ViewDidAppear(animated); }
Override ViewDidDisappear
to call the Dispose
function:
public override void ViewDidDisappear(bool animated) { _model.Dispose(); base.ViewDidDisappear(animated); }
And create the following bindings in the binding set:
set.Bind(this).For("Title").To(vm => vm.Title); set.Bind(descriptionLabel).To(vm => vm.DescriptionMessage); set.Bind(currentLabel).To(vm => vm.CurrentTime); set.Bind(endLabel).To(vm => vm.EndTime); set.Bind(progressSlider).For(v => v.Value).To(vm => vm.CurrentTime); set.Bind(progressSlider).For(v => v.MaxValue).To(vm => vm.EndTime); set.Bind(playButton).To(vm => vm.PlayPauseCommand); set.Bind(rewindButton).To(vm => vm.RewindCommand); set.Bind(fastForwardButton).To(vm => vm.ForwardCommand);
We have our labels bound to the description, which are hard coded. This is why we must make changes to the CurrentTime
variable on the main UI thread, because it affects what is displayed on the currentLabel
. We also have our MvxCommand
bindings on our audio buttons. Finally, we have our bindings on the Value
property of the progress slider to match the CurrentTime
variable, and the MaxValue
to match the end time of the audio stream, so it matches the percentage playing time of the audio stream.
Excellent! Try running the application and playing around with the play/pause and progress slider functionality.
Let's move on to building the equivalent for the Android version.
3.145.23.123