.net maui ContentView custom event

631 views Asked by At

I have a ContentView that wraps my MediaElement to use as audio player. Here is how I implemented it.

namespace Solution.MobileApp.Components;

public partial class AudioPlayer : ContentView
{
    public static readonly BindableProperty SourceProperty = BindableProperty.Create(nameof(Source), typeof(MediaSource), typeof(AudioPlayer), null);

    public MediaSource Source
    {
        get => (MediaSource)GetValue(SourceProperty);
        set => SetValue(SourceProperty, value);
    }

    public AudioPlayer()
    {       
        InitializeComponent();
        BindingContext = this;

        mediaElement.PropertyChanged += OnPropertyChanged;
    }

    private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == MediaElement.DurationProperty.PropertyName)
        {
            positionSlider.Maximum = mediaElement.Duration.TotalSeconds;
        }
    }

    private void OnMediaOpened(object sender, EventArgs e)
    { }

    private void OnStateChanged(object sender, MediaStateChangedEventArgs e)
    { }

    private void OnMediaFailed(object sender, MediaFailedEventArgs e)
    { }

    private void OnMediaEnded(object sender, EventArgs e)
    { 
        eventManager.HandleEvent(this, EventArgs.Empty, "MediaEnded");
        mediaElement.Play();
    }

    private void OnPositionChanged(object sender, MediaPositionChangedEventArgs e)
    {
        positionSlider.Value = e.Position.TotalSeconds;
    }

    private void OnSeekCompleted(object sender, EventArgs e)
    { }
    
    private void OnPlayClicked(object sender, EventArgs e)
    {
        mediaElement.Play();
    }

    private void OnPauseClicked(object sender, EventArgs e)
    {
        mediaElement.Pause();
    }

    private void OnStopClicked(object sender, EventArgs e)
    {
        mediaElement.Stop();
    }

    private void OnMuteClicked(object sender, EventArgs e)
    {
        mediaElement.ShouldMute = !mediaElement.ShouldMute;
    }

    private void OnUnloaded(object sender, EventArgs e)
    {
        // Stop and cleanup MediaElement when we navigate away
        mediaElement.Handler?.DisconnectHandler();
    }

    private void OnSliderDragCompleted(object sender, EventArgs e)
    {
        ArgumentNullException.ThrowIfNull(sender);

        var newValue = ((Slider)sender).Value;
        mediaElement.SeekTo(TimeSpan.FromSeconds(newValue));
        mediaElement.Play();
    }

    private void OnSliderDragStarted(object sender, EventArgs e)
    {
        mediaElement.Pause();
    }
}

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             xmlns:converters="clr-namespace:Solution.MobileApp.Converters"
             x:Class="Solution.MobileApp.Components.AudioPlayer"
             Unloaded="OnUnloaded">
    <ContentView.Resources>
        <toolkit:TimeSpanToSecondsConverter x:Key="TimeSpanConverter" />
        <converters:SecondsToStringConverter x:Key="SecondsToStringConverter" />
    </ContentView.Resources>

    <ScrollView>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="0" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <toolkit:MediaElement
                Grid.Row="0"
                x:Name="mediaElement"
                ShouldAutoPlay="True"
                Source="{Binding Source}"
                MediaEnded="OnMediaEnded"
                MediaFailed="OnMediaFailed"
                MediaOpened="OnMediaOpened"
                PositionChanged="OnPositionChanged"
                StateChanged="OnStateChanged"
                SeekCompleted="OnSeekCompleted"/>

            <HorizontalStackLayout Grid.Row="1" Padding="0,0,0,15">
                <Label HorizontalOptions="Center">
                    <Label.Text>
                        <MultiBinding StringFormat="Current State: {0}">
                            <Binding Path="CurrentState" Source="{x:Reference mediaElement}" />
                        </MultiBinding>
                    </Label.Text>
                </Label>
            </HorizontalStackLayout>

            <Grid Grid.Row="2" Padding="0,10,0,10" ColumnDefinitions="*,*,*,*" ColumnSpacing="5">
                <Button Grid.Column="0" Text="Play" Clicked="OnPlayClicked" />
                <Button Grid.Column="1" Text="Pause" Clicked="OnPauseClicked" />
                <Button Grid.Column="2" Text="Stop" Clicked="OnStopClicked" />
                <Button Grid.Column="3" Text="Mute" Clicked="OnMuteClicked">
                    <Button.Triggers>
                        <DataTrigger TargetType="Button"
                                     Binding="{Binding ShouldMute, Source={x:Reference mediaElement}}"
                                     Value="True">
                            <Setter Property="Text" Value="Unmute" />
                        </DataTrigger>
                        <DataTrigger TargetType="Button"
                                     Binding="{Binding ShouldMute, Source={x:Reference mediaElement}}"
                                     Value="False">
                            <Setter Property="Text" Value="Mute" />
                        </DataTrigger>
                    </Button.Triggers>
                </Button>
            </Grid>

            <VerticalStackLayout Grid.Row="3" Padding="0,10,0,10">
                <Slider x:Name="positionSlider"
                        MinimumTrackColor="Gray"
                        DragStarted="OnSliderDragStarted"
                        DragCompleted="OnSliderDragCompleted"/>

                <HorizontalStackLayout Padding="0,10,0,10">
                    <Label HorizontalOptions="Center">
                        <Label.Text>
                            <MultiBinding StringFormat="{}Position: {0}/{1}">
                                <Binding Path="Position" Source="{x:Reference mediaElement}" Converter="{StaticResource SecondsToStringConverter}" />
                                <Binding Path="Duration" Source="{x:Reference mediaElement}" Converter="{StaticResource SecondsToStringConverter}" />
                            </MultiBinding>
                        </Label.Text>
                    </Label>
                </HorizontalStackLayout>
            </VerticalStackLayout>

            <HorizontalStackLayout Grid.Row="4" Padding="0,10,0,10">
                <Label>
                    <Label.FormattedText>
                        <FormattedString>
                            <Span Text="Volume:" />
                            <Span Text="{Binding Source={x:Reference mediaElement}, Path=Volume, StringFormat='{}{0:P0}'}" />
                        </FormattedString>
                    </Label.FormattedText>
                </Label>
                <Slider Maximum="1.0"
                        Minimum="0.0"
                        MinimumTrackColor="Red"
                        MaximumTrackColor="Gray"
                        Margin="10,0,10,0"
                        WidthRequest="300">
                    <Slider.Value>
                        <Binding Path="Volume" Source="{x:Reference mediaElement}" />
                    </Slider.Value>
                </Slider>
            </HorizontalStackLayout>
        </Grid>
    </ScrollView>
</ContentView>

In my page I am using it like this:

<?xml version="1.0" encoding="utf-8" ?>
<pages:BasePage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:pages="clr-namespace:Solution.MobileApp.Pages"
             xmlns:views="clr-namespace:Solution.MobileApp.Pages.Tabs"
             xmlns:component="clr-namespace:Solution.MobileApp.Components"
             xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
             xmlns:viewModels="clr-namespace:Solution.MobileApp.ViewModels"
             x:TypeArguments="viewModels:MusicPageViewModel"
             x:DataType="viewModels:MusicPageViewModel"
             x:Class="Solution.MobileApp.Pages.Tabs.MusicPage"
             Title="MediaElement">
    <VerticalStackLayout>
        <Label Text="Welcome to PLAYLIST PAGE!"
               VerticalOptions="Center" 
               HorizontalOptions="Center" />
        <component:AudioPlayer x:Name="audioPlayer" />
    </VerticalStackLayout>
</pages:BasePage>

public partial class MusicPageViewModel : BaseViewModel
{
    [ObservableProperty]
    private MediaSource source;

    private List<string> songs = new List<string>
    {
        "Mostantol.mp3",
        "Vihar.mp3"
    };

    public MusicPageViewModel()
    {

        source = MediaSource.FromResource(songs[0]);
    }

    public string NextSong() => songs[1];
}

public partial class MusicPage : BasePage<MusicPageViewModel>
{
    private readonly ILogger logger;

    public MusicPage(MusicPageViewModel viewModel, ILogger<MusicPage> logger) : base(viewModel)
    {
        this.logger = logger;

        InitializeComponent();
        audioPlayer.Source = viewModel.Source;
    }

    public void OnMediaEnded(object sender, EventArgs e)
    {
        audioPlayer.Source = BindingContext.NextSong();
    }
}

Now my idea is that I would like to define an property (probably later more) on the AudioPlayer component, to have a property of event type (ex. OnMediaEndedEvent) that I can trigger in the MediaElement on OnMediaEnded event?

What I can see in debug that the event is triggered, the AudioPlayer component BindingContext is got a new value (Vihar.mp3), but the time song not started, the player state is stopped.

thnx

1

There are 1 answers

0
Wasyster On

I made a mistake in my ViewModel:

public string NextSong() => songs[1];

should be

public MediaSource NextSong() => MediaSource.FromResource(songs[1]);