Okay, here's what should be a simple one for a .NET Maui newbie. I have a MediaElement on my page but hidden because I want to do my own controls. So I also have two buttons. Simple scenario... when user hits the "Play" button, play the mp3 bound to the mediaelement. When the user hits the "Stop" button, stop the mediaelement. I can even do it, just not within MVVM.
Here are the basic View and ViewModel, with no real attempt to actually make the mediaelement play from the ViewModel. First the View, then the ViewModel.
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="LifeTrax.Mobile.MyPage"
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
Title="Main Player Page">
<VerticalStackLayout>
<Button Text="Play Sound" Command="{Binding StartPlayingSoundCommand}"></Button>
<Button Text="Stop Sound" Command="{Binding StopPlayingSoundCommand}"></Button>
<toolkit:MediaElement Grid.Row="0" Grid.Column="0"
ShouldShowPlaybackControls="False" ShouldAutoPlay="False"
ShouldLoopPlayback="False" IsVisible="False" Volume="0.15"
Source="filename.mp3"></toolkit:MediaElement>
</VerticalStackLayout>
</ContentPage>
And here is the ViewModel:
public partial class MyViewModel : ObservableObject
{
[ObservableProperty]
private String _buttonName = "A";
[ObservableProperty]
private String _playerSource = "filename.mp3";
private readonly INavigationService navigationService;
public MyViewModel(INavigationService navigationService)
{
this.navigationService = navigationService;
}
[RelayCommand]
private Task NavigateToEditPage()
=> navigationService.GoToEdit();
[RelayCommand]
private Task NavigateToAboutPage()
=> navigationService.GoToAbout();
[RelayCommand]
private Task StartPlayingSound()
{
// Do whatever
return Task.CompletedTask;
}
[RelayCommand]
private Task StopPlayingSound()
{
// Do whatever
return Task.CompletedTask;
}
}
Pretty basic setup, and I figure this sort of thing has to be simple. I even thought of two ways to accomplish this. The first one is to take the mediaelement off the view and put it in the ViewModel. That way, when the user clicks play, I use the button's command to fire MediaElement.Play() from the ViewModel and everybody's happy. Except by putting a MediaElement in the ViewModel, I violated MVVM.
The second way is to forget the command and the ViewModel and just implement a good, old-fashioned event handler in the code-behind of the view, and again everybody's happy. And again, I violated MVVM by putting business logic in the View.
What I CANNOT figure out is how to do this WITHIN MVVM. It has to be a fairly common sort of thing to do, right? I've looked, and the most "recent" occasion I found this question being asked was way back in 2014, and I figured a lot has changed since then... maybe there's a good way to do this, now?
So, given the setup shown above, how do I do this without breaking MVVM?
EDIT: FOUND A SOLUTION Okay, after some fumbling around, I have arrived at an acceptable solution to this problem while still keeping MVVM intact. All hail to old programming skills. (grin)
It occurred to me that the View and ViewModel communicate using properties. Shame that the MediaElement doesn't have properties to control playing and stopping. Heck, I can fix THAT! And so the EnhancedMediaElement class is born.
I created a class that inherits from MediaElement and added two Boolean Bindable Properties to it: ShouldInitiatePlay and ShouldInitiateStop.
public partial class EnhancedMediaElement : MediaElement
{
#region Should Initiate Play
public static readonly BindableProperty ShouldInitiatePlayProperty =
BindableProperty.Create("ShouldInitiatePlay", typeof(Boolean), typeof(EnhancedMediaElement), false, propertyChanged: OnShouldInitiatePlayChanged);
public Boolean ShouldInitiatePlay
{
get { return (Boolean)GetValue(ShouldInitiatePlayProperty); }
set
{
SetValue(ShouldInitiatePlayProperty, value);
}
}
static void OnShouldInitiatePlayChanged(BindableObject bindable, object oldValue, object newValue)
{
// Property changed implementation goes here
EnhancedMediaElement eme = (EnhancedMediaElement)bindable;
if ((Boolean)newValue == true)
{
eme.Stop();
eme.Play();
eme.ShouldInitiatePlay = false;
}
}
#endregion
#region Should Initiate Stop
public static readonly BindableProperty ShouldInitiateStopProperty =
BindableProperty.Create("ShouldInitiateStop", typeof(Boolean), typeof(EnhancedMediaElement), false, propertyChanged: OnShouldInitiateStopChanged);
public Boolean ShouldInitiateStop
{
get { return (Boolean)GetValue(ShouldInitiateStopProperty); }
set
{
SetValue(ShouldInitiateStopProperty, value);
}
}
static void OnShouldInitiateStopChanged(BindableObject bindable, object oldValue, object newValue)
{
// Property changed implementation goes here
EnhancedMediaElement eme = (EnhancedMediaElement)bindable;
if ((Boolean)newValue == true)
{
eme.Stop();
eme.ShouldInitiateStop = false;
}
}
#endregion
}
Now go to the ViewModel and add two properties to it:
[ObservableProperty]
private Boolean _shouldPlayerInitiatePlay = false;
[ObservableProperty]
private Boolean _shouldPlayerInitiateStop = false;
Now I flipped to the View and modified the MediaElement and Bindings to look like this:
<customcontrols:EnhancedMediaElement Grid.Row="0" Grid.Column="0" ShouldShowPlaybackControls="False" ShouldAutoPlay="False"
ShouldLoopPlayback="False" IsVisible="False" Volume="0.5" x:Name="mediaPlayer"
ShouldInitiatePlay="{Binding ShouldPlayerInitiatePlay, Mode=TwoWay}" Source="{Binding PlayerSource}"
ShouldInitiateStop="{Binding ShouldPlayerInitiateStop, Mode=TwoWay}"></customcontrols:EnhancedMediaElement>
Make sure to add the customControls into the page definition up top.
xmlns:customcontrols="clr-namespace:MySolution.Mobile.Custom_Controls"
And that is that. The customized media element object can now start and stop playback through bindable properties, and MVVM is saved! (grin)
Truly though, I got so bound up looking for some Maui- or MVVM-specific method of dealing with this that I forgot that there's more than one way to skin a cat. This works well, and is expandable in case I need more customized behavior out of the MediaElement class.