Search code examples
c#wpfbindingdependency-properties

MediaElement not updating from bound property


I'm writing a basic WPF application with an embedded MediaElement control to play audio files. With my app, I want the user to be able to load an MP3 file to listen to, and load a new file as and when they want. The app is storing the last listened-to file in a DAT file in the root folder to have some basic memory to load up the last file you listened to.

When my app initialises the audio file from the saved DAT file, it works. The media element loads the right track, and the visual controls show the correct filename and position in the track.

When I use the "Open new track" functionality I coded into the app, the underlying object gets updated with the new track name and track position, but neither the Media Element or the Labels get triggered to read in the new property.

In essence, the Bindings on the UI elements trigger when the code initialises the default audio track, but they do not trigger when I call identical code from the "Open new track" functionality.

Here is my UI control code with the bindings

<Label Grid.Column="0" Content="{Binding Path=FullFilename}"></Label>
<MediaElement Grid.Column="1" Source="{Binding Path=FullFilename}" Name="mediaPlayer" Width="450"
               Height="250" LoadedBehavior="Manual" UnloadedBehavior="Stop" Stretch="Fill"></MediaElement>
<Label Grid.Column="2" Content="{Binding Path=Position}"></Label>

Here is the On_Loaded method in my main window code-behind, which is called when the UI control loads

private void LibraryViewControl_Loaded(object sender, RoutedEventArgs e)
{
    var viewModel = new LibraryViewModel(message, confirm);
    viewModel.LoadDefault();     // If I move this line below the next one, it does not work
    LibraryViewControl.DataContext = viewModel.LoadedItem;

and here is the class LoadedItem, which I wanted to create the properties on to which the UI control binds

public class LoadedItem : INotifyPropertyChanged
{
    private string? _fullFileName;
    private TimeSpan _position;

    public LoadedItem(string fullFileName, long millisecondsSinceStart = 0)
    {
        _fullFileName = fullFileName;
        Position = TimeSpan.FromMilliseconds(millisecondsSinceStart);
    }

    public string? FullFilename
    {
        get => _fullFileName;
        set
        {
            if (_fullFileName == value) return;
            _fullFileName = value;
            RaisePropertyChanged("FullFilename");
        }
    }

    public TimeSpan Position
    {
        get => _position;
        set
        {
            if (_position == value) return;
            _position = value;
            RaisePropertyChanged("Position");
        }
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    private void RaisePropertyChanged(string property)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }
}

If I move the viewModel.LoadDefault to after the line where I initialise the DataContext for the UI control, it does not work.

So something about the way I have coded the LoadedItem class is missing in order to propogate updates on the Position and FullFilename properties up to the UI control which binds to those properties.

Is it something basic I've missed?


Solution

  • I'll show you an example of a more or less working implementation:

        public class LoadedItem : BaseInpc
        {
            private static readonly Uri empty = new Uri("about:empty");
            private Uri? _fullUri = empty;
            private TimeSpan _position;
    
            public LoadedItem()
            {
                LoadFile = new RelayCommand<string>(LoadFileExecute);
                GotoTime = new RelayCommand<long>(ms => GotoTimeAction?.Invoke(ms));
            }
    
            public LoadedItem(string fullFileName)
                : this()
            {
                _fullUri = new Uri(fullFileName, UriKind.Absolute);
            }
    
            public Uri? FileUri
            {
                get => _fullUri;
                private set => Set(ref _fullUri, value ?? empty);
            }
    
            public TimeSpan Position
            {
                get => _position;
                set => Set(ref _position, value);
            }
    
            public RelayCommand LoadFile { get; }
            private void LoadFileExecute(string fileName) => FileUri = new Uri(fileName, UriKind.Absolute);
            public RelayCommand GotoTime { get; }
    
            public Action<long>? GotoTimeAction;
        }
    

    The BaseInpс class is my simple implementation of the INotifyPropertyChanged interface.
    You can replace it with any convenient one for you or take the code for this class from here: https://stackoverflow.com/a/68748914/13349759

        public class MediaWindowHelper
        {
            public static readonly RoutedUICommand OpenBrowser = new RoutedUICommand("Getting file name from Windows Browser", nameof(OpenBrowser), typeof(MediaWindowHelper));
            public static readonly RoutedUICommand Play = new RoutedUICommand("Play MediaElement ", nameof(Play), typeof(MediaWindowHelper));
            static MediaWindowHelper()
            {
                CommandManager.RegisterClassCommandBinding(
                    typeof(MediaWindow),
                    new CommandBinding(OpenBrowser, (sender, e) =>
                    {
                        TextBox textBox = (TextBox)e.Source;
                        OpenFileDialog dialog = new OpenFileDialog();
                        if (dialog.ShowDialog() == true)
                        {
                            textBox.Text = dialog.FileName;
                        }
                    }));
    
                CommandManager.RegisterClassCommandBinding(
                    typeof(MediaWindow),
                    new CommandBinding(Play, (sender, e) =>
                    {
                        MediaElement media = (MediaElement)e.Source;
                        media.Play();
                    }));
            }
    
            public long Milliseconds { get; set; }
        }
    
    <Window x:Class="Core2024.SO.NZJames.question78884480.MediaWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:Core2024.SO.NZJames.question78884480"
            mc:Ignorable="d"
            Title="MediaWindow" Height="450" Width="800"
            DataContext="{DynamicResource vm}">
        <Window.Resources>
            <local:LoadedItem x:Key="vm"/>
            <local:MediaWindowHelper x:Key="helper"/>
        </Window.Resources>
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="Auto"/>
                </Grid.ColumnDefinitions>
                <TextBox x:Name="textBox"/>
                <Button Grid.Column="1" Content="..." Margin="5" Padding="5"
                        CommandTarget="{Binding ElementName=textBox, Mode=OneWay}"
                        Command="{x:Static local:MediaWindowHelper.OpenBrowser}"
                        ToolTip="{Binding Command.Text, RelativeSource={RelativeSource Self}}"/>
                <Button Grid.Column="2" Content="Open File" Margin="5" Padding="15 5"
                        CommandParameter="{Binding Text, ElementName=textBox}"
                        Command="{Binding LoadFile, Mode=OneWay}"/>
                <TextBox Grid.Column="3" MinWidth="50" HorizontalContentAlignment="Center" VerticalContentAlignment="Center"
                         Text="{Binding Milliseconds, Source={StaticResource helper}}"/>
                <Button Grid.Column="4" Content="Go to" Margin="5" Padding="15 5"
                        CommandParameter="{Binding Milliseconds, Source={StaticResource helper}}"
                        Command="{Binding GotoTime, Mode=OneWay}"/>
            </Grid>
            <Grid Grid.Row="1">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>
                    <RowDefinition/>
                    <RowDefinition Height="Auto"/>
                </Grid.RowDefinitions>
                <TextBlock Grid.Row="0" Text="{Binding Source, ElementName=mediaPlayer}"/>
                <MediaElement x:Name="mediaPlayer" Grid.Row="1" Source="{Binding FileUri}"
                              Width="450" Height="250"
                              LoadedBehavior="Manual" UnloadedBehavior="Stop" Stretch="Fill"
                              Loaded="OnMediaElementLoaded"/>
                <UniformGrid Grid.Row="2" Rows="1">
                    <TextBlock Text="{Binding Position}"/>
                    <Button Content="Play" Margin="5" Padding="5"
                            CommandTarget="{Binding ElementName=mediaPlayer, Mode=OneWay}"
                            Command="{x:Static local:MediaWindowHelper.Play}"
                            ToolTip="{Binding Command.Text, RelativeSource={RelativeSource Self}}"/>
                </UniformGrid>
            </Grid>
        </Grid>
    </Window>
    

    The MediaPlayer.Position property has no change notification.
    Therefore, the only way to track its value is to periodically read it using a timer. In this example, this is implemented in window Code Behind, but you can also do this in an Attached Property or Behavior.

        public partial class MediaWindow : Window
        {
            public MediaWindow()
            {
                InitializeComponent();
                LoadedItem loadedItem = (LoadedItem)mediaPlayer.DataContext;
                loadedItem.GotoTimeAction = ms => mediaPlayer.Position = TimeSpan.FromMilliseconds(ms);
            }
    
            private readonly DispatcherTimer timer = new DispatcherTimer();
            private void OnMediaElementLoaded(object sender, RoutedEventArgs e)
            {
                timer.Interval = TimeSpan.FromMilliseconds(10);
                timer.Tick += OnMediaElementTick;
                timer.Start();
            }
    
            private void OnMediaElementTick(object? sender, EventArgs e)
            {
                LoadedItem loadedItem = (LoadedItem)mediaPlayer.DataContext;
                loadedItem.Position = mediaPlayer.Position;
            }
        }