eventsmauimediaelement

calling media element events located on ContentView from ContentPage xaml CS class


I have set up a BindableProperty for Media Elemement that works just fine. But I would like to access the events from the page where I am using the custom class. I have the various bools and string working as well as the methods mostly. I just need to get events working. Here is some sample code.

MediaControl.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<ContentView
    x:Class="NerdNewsNavigator2.Controls.MediaControl"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:item="clr-namespace:NerdNewsNavigator2.Controls"
    xmlns:page="clr-namespace:NerdNewsNavigator2.View"
    xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
    xmlns:vm="clr-namespace:NerdNewsNavigator2.ViewModel"
    Unloaded="ContentView_Unloaded">
    <Grid>

        <Grid.GestureRecognizers>
            <TapGestureRecognizer Command="{Binding Source={RelativeSource AncestorType={x:Type vm:BaseViewModel}}, Path=MovedCommand}" />
            <SwipeGestureRecognizer Direction="Up" Swiped="SwipeGestureRecognizer_Swiped" />
            <SwipeGestureRecognizer Direction="Down" Swiped="SwipeGestureRecognizer_Swiped" />
        </Grid.GestureRecognizers>

        <toolkit:MediaElement
            x:Name="mediaElement"
            ShouldAutoPlay="True"
            ShouldKeepScreenOn="True"
            ShouldShowPlaybackControls="False" />

        <Frame
            BackgroundColor="Black"
            IsEnabled="{Binding Source={RelativeSource AncestorType={x:Type vm:BaseViewModel}}, Path=SetFullScreen}"
            IsVisible="{Binding Source={RelativeSource AncestorType={x:Type vm:BaseViewModel}}, Path=SetFullScreen}"
            Opacity="0.5" />

        <Grid IsEnabled="{Binding Source={RelativeSource AncestorType={x:Type vm:BaseViewModel}}, Path=SetFullScreen}" IsVisible="{Binding Source={RelativeSource AncestorType={x:Type vm:BaseViewModel}}, Path=SetFullScreen}">

            <ImageButton
                x:Name="btnFullScreen"
                Margin="10"
                BackgroundColor="Transparent"
                Clicked="BtnFullScreen_Clicked"
                HeightRequest="40"
                HorizontalOptions="End"
                Source="whitefs.png"
                VerticalOptions="Start"
                WidthRequest="40" />

            <VerticalStackLayout VerticalOptions="End">

                <HorizontalStackLayout HorizontalOptions="Center">

                    <ImageButton
                        x:Name="BtnRewind"
                        Margin="10"
                        BackgroundColor="Transparent"
                        BindingContext="{x:Reference mediaElement}"
                        Clicked="BtnRewind_Clicked"
                        HeightRequest="40"
                        HorizontalOptions="Center"
                        Source="rewind.png"
                        VerticalOptions="End"
                        WidthRequest="40" />

                    <ImageButton
                        x:Name="BtnPLay"
                        Margin="10"
                        BackgroundColor="Transparent"
                        BindingContext="{x:Reference mediaElement}"
                        Clicked="BtnPlay_Clicked"
                        HeightRequest="40"
                        HorizontalOptions="Center"
                        Source="pause.png"
                        VerticalOptions="End"
                        WidthRequest="40" />

                    <ImageButton
                        x:Name="BtnForward"
                        Margin="10"
                        BackgroundColor="Transparent"
                        BindingContext="{x:Reference mediaElement}"
                        Clicked="BtnForward_Clicked"
                        HeightRequest="40"
                        HorizontalOptions="Center"
                        Source="fastforward.png"
                        VerticalOptions="End"
                        WidthRequest="40" />

                    <ImageButton
                        x:Name="ImageButtonMute"
                        Margin="10"
                        BackgroundColor="Transparent"
                        Clicked="OnMuteClicked"
                        HeightRequest="40"
                        Source="muted.png"
                        WidthRequest="40">
                        <ImageButton.Triggers>
                            <DataTrigger
                                Binding="{Binding ShouldMute, Source={x:Reference mediaElement}}"
                                TargetType="ImageButton"
                                Value="True" />
                            <DataTrigger
                                Binding="{Binding ShouldMute, Source={x:Reference mediaElement}}"
                                TargetType="ImageButton"
                                Value="False" />
                        </ImageButton.Triggers>
                    </ImageButton>

                </HorizontalStackLayout>

                <HorizontalStackLayout HorizontalOptions="Start" VerticalOptions="End">

                    <Label
                        Margin="5"
                        FontSize="12"
                        HorizontalOptions="Center"
                        Text="{Binding Source={RelativeSource AncestorType={x:Type item:MediaControl}}, Path=PlayPosition}"
                        TextColor="White" />
                </HorizontalStackLayout>

                <Slider
                    x:Name="PositionSlider"
                    Margin="10"
                    DragCompleted="Slider_DragCompleted"
                    DragStarted="Slider_DragStarted"
                    MaximumTrackColor="LightGray"
                    MinimumTrackColor="Red" />

            </VerticalStackLayout>

        </Grid>
    </Grid>

</ContentView>

Abbreviated code showing the properties and Bindable Properties. MediaControl.xaml.cs:


using Application = Microsoft.Maui.Controls.Application;
using Platform = Microsoft.Maui.ApplicationModel.Platform;

#if ANDROID
using Views = AndroidX.Core.View;
#endif

#if WINDOWS
using Microsoft.UI;
using Microsoft.UI.Windowing;
using WinRT;
using Microsoft.Maui.Controls;
using CommunityToolkit.Maui.Core.Primitives;
#endif

namespace NerdNewsNavigator2.Controls;

public partial class MediaControl : ContentView
{
    #region Properties and Bindable Properties
    /// <summary>
    /// Initilizes a new instance of the <see cref="Position"/> class
    /// </summary>
    private Position Pos { get; set; } = new();
    public string PlayPosition { get; set; }
    public Page CurrentPage { get; set; }

    private bool _fullScreen = false;
#if WINDOWS
    private static MauiWinUIWindow CurrentWindow { get; set; }
#endif

    public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Name), typeof(MediaElement), typeof(MediaControl), propertyChanged: (bindable, oldValue, newValue) =>
        {
            var control = (MediaControl)bindable;
            control.mediaElement.ShouldAutoPlay = (bool)newValue;
            control.mediaElement.ShouldKeepScreenOn = (bool)newValue;
            control.mediaElement.Source = newValue as MediaSource;
            control.mediaElement.ShouldShowPlaybackControls = (bool)newValue;
            control.mediaElement.StateChanged += (System.EventHandler<MediaStateChangedEventArgs>)newValue;
            control.mediaElement.MediaOpened += (System.EventHandler)newValue;
        });

    public static readonly BindableProperty SourceProperty = BindableProperty.Create(nameof(Source), typeof(MediaSource), typeof(MediaControl), propertyChanged: (bindableProperty, oldValue, newValue) =>
    {
        var control = (MediaControl)bindableProperty;
        control.mediaElement.Source = newValue as MediaSource;
    });
    public TimeSpan Position
    {
        get => (TimeSpan)GetValue(TitleProperty);
    }
    public MediaSource Source
    {
        get => GetValue(SourceProperty) as MediaSource;
        set => SetValue(SourceProperty, value);
    }
    public MediaElement Name
    {
        get => GetValue(TitleProperty) as MediaElement;
        set => SetValue(TitleProperty, value);
    }
    public bool ShouldShowPlaybackControls
    {
        get => (bool)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }
    public bool ShouldAutoPlay
    {
        get => (bool)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }
    public bool ShouldKeepScreenOn
    {
        get => (bool)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }
    #endregion
    public MediaControl()
    {
        InitializeComponent();
        PlayPosition = string.Empty;
        mediaElement.PropertyChanged += MediaElement_PropertyChanged;
        mediaElement.PositionChanged += ChangedPosition;
        CurrentPage = Shell.Current.CurrentPage;
    }
    public void SeekTo(TimeSpan position)
    {
        mediaElement.SeekTo(position);
    }
    public void Stop()
    {
        mediaElement.Stop();
    }
    #region Events
#nullable enable
    private void MediaElement_PropertyChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == MediaElement.DurationProperty.PropertyName)
        {
            PositionSlider.Maximum = mediaElement.Duration.TotalSeconds;
        }
    }
    private void OnPositionChanged(object? sender, MediaPositionChangedEventArgs e)
    {
        PositionSlider.Value = e.Position.TotalSeconds;
    }
    private void SwipeGestureRecognizer_Swiped(object sender, SwipedEventArgs e)
    {
#if WINDOWS
        CurrentWindow = BaseViewModel.CurrentWindow;
#endif
        if (e.Direction == SwipeDirection.Up)
        {
            SetFullScreen();
        }
        if (e.Direction == SwipeDirection.Down)
        {
            RestoreScreen();
        }
    }
    private void Slider_DragCompleted(object? sender, EventArgs e)
    {
        ArgumentNullException.ThrowIfNull(sender);

        var newValue = ((Slider)sender).Value;
        mediaElement.SeekTo(TimeSpan.FromSeconds(newValue));
        mediaElement.Play();
    }
#nullable disable
    private void Slider_DragStarted(object sender, EventArgs e)
    {
        mediaElement.Pause();
    }
    private void ChangedPosition(object sender, EventArgs e)
    {
        var playDuration = BaseViewModel.TimeConverter(mediaElement.Duration);
        var position = BaseViewModel.TimeConverter(mediaElement.Position);
        PlayPosition = $"{position}/{playDuration}";
        OnPropertyChanged(nameof(PlayPosition));
    }
    #endregion

    #region Buttons
    private void BtnRewind_Clicked(object sender, EventArgs e)
    {
        var time = mediaElement.Position - TimeSpan.FromSeconds(15);
        mediaElement.Pause();
        mediaElement.SeekTo(time);
        mediaElement.Play();
    }

    private void BtnForward_Clicked(object sender, EventArgs e)
    {
        var time = mediaElement.Position + TimeSpan.FromSeconds(15);
        mediaElement.Pause();
        mediaElement.SeekTo(time);
        mediaElement.Play();
    }

Here is example of event trigger method that I want to call from content page TabletPlayPodcastPage.xaml:

   /// <summary>
    /// Manages IOS seeking for <see cref="mediaElement"/> with <see cref="Pos"/> at start of playback.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    public async void SeekIOS(object? sender, MediaStateChangedEventArgs e)
    {
        if (sender == null)
        {
            return;
        }
        Pos.Title = Preferences.Default.Get("New_Url", string.Empty);
        Pos.SavedPosition = TimeSpan.Zero;
        var positionList = await App.PositionData.GetAllPositions();
        foreach (var item in positionList)
        {
            if (Pos.Title == item.Title)
            {
                Pos.SavedPosition = item.SavedPosition;
                Debug.WriteLine($"Retrieved Saved position from database is: {item.Title} - {item.SavedPosition}");
            }
        }
        if (e.NewState == MediaElementState.Playing)
        {
            mediaElement.SeekTo(Pos.SavedPosition);
            mediaElement.ShouldKeepScreenOn = true;
            Debug.WriteLine("Media playback started. ShouldKeepScreenOn is set to true.");
        }
    }

Here is example calling function I want to use:

   /// <summary>
    /// A method that starts <see cref="MediaElement"/> event for <see cref="TabletPlayPodcastPage"/>
    /// </summary>
    public void Load()
    {
#if WINDOWS || ANDROID
        mediaElement.MediaOpened += Seek;
#endif

#if IOS || MACCATALYST
        mediaElement.StateChanged += SeekIOS;
#endif 
    }

Trying to call mediaElement.StateChanged += SeekIOS gives an error CS1061 Error Message Link

If I use Visual studio quick actions I get this function that gets rid of error but does not let me access the event.

public Action<object, EventArgs> MediaOpened { get; internal set; }

I no longer get an error message but the event does not trigger either. I am looking for a way to call the event from another page that uses MediaControl. I created all of this to reduce code and all the functions and behaviors I want to have working do work and this is about reducing code and making it tidier.


Solution

  • Well It ended up I managed to figure it out myself. I looked at the media element code itself and realized through trial and error this solution

     public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Name), typeof(MediaElement), typeof(MediaControl), propertyChanged: (bindable, oldValue, newValue) =>
            {
                var control = (MediaControl)bindable;
                control.mediaElement.ShouldAutoPlay = (bool)newValue;
                control.mediaElement.ShouldKeepScreenOn = (bool)newValue;
                control.mediaElement.Source = newValue as MediaSource;
                control.mediaElement.ShouldShowPlaybackControls = (bool)newValue;
                control.mediaElement.PositionChanged += (EventHandler<MediaPositionChangedEventArgs>)newValue;
                control.mediaElement.StateChanged += (EventHandler<MediaStateChangedEventArgs>)newValue;
                control.mediaElement.MediaOpened += (EventHandler)newValue;
            });
    
        public static readonly BindableProperty SourceProperty = BindableProperty.Create(nameof(Source), typeof(MediaSource), typeof(MediaControl), propertyChanged: (bindableProperty, oldValue, newValue) =>
        {
            var control = (MediaControl)bindableProperty;
            control.mediaElement.Source = newValue as MediaSource;
        });
        public static readonly BindableProperty StateChangedProperty = BindableProperty.Create(nameof(StateChanged), typeof(EventHandler<MediaStateChangedEventArgs>), typeof(MediaControl), propertyChanged: (bindableProperty, oldValue, newValue) =>
        {
            var control = (MediaControl)bindableProperty;
            control.mediaElement.StateChanged += (EventHandler<MediaStateChangedEventArgs>)newValue;
        });
        public static readonly BindableProperty MediaOpenedProperty = BindableProperty.Create(nameof(MediaOpened), typeof(EventHandler), typeof(MediaControl), propertyChanged: (bindableProperty, oldValue, newValue) =>
        {
            var control = (MediaControl)bindableProperty;
            control.mediaElement.MediaOpened += (EventHandler)newValue;
        });
        public static readonly BindableProperty ShouldKeepScreenOnProperty = BindableProperty.Create(nameof(ShouldKeepScreenOn), typeof(bool), typeof(MediaControl), propertyChanged: (bindableProperty, oldValue, newValue) =>
        {
            var control = (MediaControl)bindableProperty;
            control.mediaElement.ShouldKeepScreenOn = (bool)newValue;
        });
        public static readonly BindableProperty PositionProperty = BindableProperty.Create(nameof(Position), typeof(TimeSpan), typeof(MediaElement), TimeSpan.Zero);
        public static readonly BindableProperty ShouldAutoPlayProperty = BindableProperty.Create(nameof(ShouldAutoPlay), typeof(bool), typeof(MediaControl), propertyChanged: (bindableProperty, oldValue, newValue) =>
        {
            var control = (MediaControl)bindableProperty;
            control.mediaElement.ShouldAutoPlay = (bool)newValue;
        });
        public static readonly BindableProperty ShouldShowPlaybackControlsProperty = BindableProperty.Create(nameof(ShouldShowPlaybackControls), typeof(bool), typeof(MediaControl), propertyChanged: (bindableProperty, oldValue, newValue) =>
        {
            var control = (MediaControl)bindableProperty;
            control.mediaElement.ShouldShowPlaybackControls = (bool)newValue;
        });
        public static readonly BindableProperty PositionChangedProperty = BindableProperty.Create(nameof(PositionChanged), typeof(EventHandler<MediaPositionChangedEventArgs>), typeof(MediaControl), propertyChanged: (bindableProperty, oldValue, newValue) =>
        {
            var control = (MediaControl)bindableProperty;
            control.mediaElement.PositionChanged += (EventHandler<MediaPositionChangedEventArgs>)newValue;
        });
        public EventHandler MediaOpened
        {
            get => GetValue(MediaOpenedProperty) as EventHandler;
            set => SetValue(MediaOpenedProperty, value);
        }
        public EventHandler<MediaStateChangedEventArgs> StateChanged
        {
            get => GetValue(StateChangedProperty) as EventHandler<MediaStateChangedEventArgs>;
            set => SetValue(StateChangedProperty, value);
        }
        public EventHandler<MediaPositionChangedEventArgs> PositionChanged
        {
            get => GetValue(PositionChangedProperty) as EventHandler<MediaPositionChangedEventArgs>;
            set => SetValue(PositionChangedProperty, value);
        }
        public MediaSource Source
        {
            get => GetValue(SourceProperty) as MediaSource;
            set => SetValue(SourceProperty, value);
        }
        public TimeSpan Position => mediaElement.Position;
        public MediaElement Name
        {
            get => GetValue(TitleProperty) as MediaElement;
            set => SetValue(TitleProperty, value);
        }
        public bool ShouldShowPlaybackControls
        {
            get => (bool)GetValue(ShouldShowPlaybackControlsProperty);
            set => SetValue(ShouldShowPlaybackControlsProperty, value);
        }
        public bool ShouldAutoPlay
        {
            get => (bool)GetValue(ShouldAutoPlayProperty);
            set => SetValue(ShouldAutoPlayProperty, value);
        }
        public bool ShouldKeepScreenOn
        {
            get => (bool)GetValue(ShouldKeepScreenOnProperty);
            set => SetValue(ShouldKeepScreenOnProperty, value);
        }
    

    This lets me use all of the types of properties for media element I need to. It is not all of them. But I was able to move all my page specific code back to the actual pages and my ContentView class is no longer cluttered with code for other pages.