Controling MediaElement and its source in MVVM pattern

2.2k views Asked by At

I have DataGrid with list of video clips and MediaElement which should play a clip selected in that DataGrid. I've managed to achieve this by doing this:

MainWindow.xaml

<DataGrid x:Name="dataGrid_Video"
...
SelectedItem="{Binding fosaModel.SelectedVideo}"
                  SelectionUnit="FullRow"
                  SelectionMode="Single">
...
<MediaElement x:Name="PlayerWindow" Grid.Column="1" HorizontalAlignment="Left" Height="236" Grid.Row="1" VerticalAlignment="Top" Width="431" Margin="202,0,0,0" Source="{Binding fosaModel.SelectedVideo.fPath}"/>

But then I have no control over playback. So I searched a little bit, and I found this solution, and implemented Play button:

MainWindow.xaml

...
<ContentControl Content="{Binding Player}" Grid.Column="1" HorizontalAlignment="Left" Height="236" Grid.Row="1" VerticalAlignment="Top" Width="431" Margin="202,0,0,0"/>
...
<Button x:Name="playButton" Content="Odtwórz" Grid.Column="1" HorizontalAlignment="Left" Margin="202,273,0,0" Grid.Row="1" VerticalAlignment="Top" Width="75" IsDefault="True" Command="{Binding PlayVideoCommand}"/>
...

MainViewModel.cs

    public class MainViewModel : ViewModelBase
    {
        public MainViewModel()
        {
          ...
            Player = new MediaElement()
            {
                LoadedBehavior = MediaState.Manual,
            };
            PlayVideoCommand = new RelayCommand(() => { _playVideoCommand(); });
    ...
        public MediaElement Player{ get; set; }
        private void _playVideoCommand()
        {
            Player.Source = new System.Uri(fosaModel.SelectedVideo.fPath);
            Player.Play();
        }

fosaModel.cs

public class fosaModel : INotifyPropertyChanged

{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public ObservableCollection<fosaVideo> ListOfVideos { get; set; } = new ObservableCollection<fosaVideo>();
    public ObservableCollection<fosaAudio> ListOfAudios { get; set; } = new ObservableCollection<fosaAudio>();
    public fosaVideo SelectedVideo { get; set; }
}

So now selected video is played when I press play button. Changing selection doesn't change the source of MediaElement, like it did earlier. What I want is to have control over playback of video, but also to have selecting other video in DataGrid change the source of MediaElement. Is there anyway to do so without breaking the MVVM pattern? I'm new to WPF, and I'm feeling like I'm missing something basic. I use MVVM Light and Fody.PropertyChanged.

EDIT:

I went with Peter Moore's answer and Play/Stop feature works like a charm. But now I would like to have control over position of playback of a video. I've made another attached property:

public class MediaElementAttached : DependencyObject
{
   ...
   #region VideoProgress Property

    public static DependencyProperty VideoProgressProperty =
        DependencyProperty.RegisterAttached(
            "VideoProgress", typeof(TimeSpan), typeof(MediaElementAttached),
            new PropertyMetadata(new TimeSpan(0,0,0), OnVideoProgressChanged));
    public static TimeSpan GetVideoProgress(DependencyObject d)
    {
        return (TimeSpan)d.GetValue(VideoProgressProperty);
    }
    public static void SetVideoProgress(DependencyObject d, TimeSpan value)
    {
        d.SetValue(VideoProgressProperty, value);
    }
    private static void OnVideoProgressChanged(
        DependencyObject obj,
        DependencyPropertyChangedEventArgs args)
    {
        MediaElement me = obj as MediaElement;
        me.LoadedBehavior = MediaState.Manual;
        if (me == null)
            return;
        TimeSpan videoProgress = (TimeSpan)args.NewValue;
        me.Position = videoProgress;
    }

    #endregion
}

MainWindow.xaml

<MediaElement 
    ... 
    local:MediaElementAttached.VideoProgress="{Binding fosaModel.videoProgress, Mode=TwoWay}" x:Name="playerWindow" Grid.Column="1" HorizontalAlignment="Left" Height="236" Grid.Row="1" VerticalAlignment="Top" Width="431" Margin="202,0,0,0" Source="{Binding fosaModel.SelectedVideo.fPath}" LoadedBehavior="Manual" />

I can set value of it, when I change videoProgress property, but I can't get the position of video playback, that is, videoProgress isn't updating as video is played. What should I do?

1

There are 1 answers

9
Emperor Eto On BEST ANSWER

The short answer is that you're going to have to employ a hack no matter what, because MediaElement is not MVVM friendly by default.

Attached properties can be very useful in situations like this though and you can do what you want to do without really breaking the pattern (or at least, not in a way that would upset anyone). You could create an attached DependencyProperty for MediaElement called IsPlaying. In the property change handler, you could either Play or Stop the MediaElement depending on the new value of the property. Something like this might work:

    public class MediaElementAttached : DependencyObject
    {
        #region IsPlaying Property

        public static DependencyProperty IsPlayingProperty =
            DependencyProperty.RegisterAttached(
                "IsPlaying", typeof(bool), typeof(MediaElementAttached ),
                new PropertyMetadata(false, OnIsPlayingChanged));
        public static bool GetIsPlaying(DependencyObject d)
        {
            return (bool)d.GetValue(IsPlayingProperty);
        }
        public static void SetIsPlaying(DependencyObject d, bool value)
        {
            d.SetValue(IsPlayingProperty, value);
        }
        private static void OnIsPlayingChanged(
            DependencyObject obj,
            DependencyPropertyChangedEventArgs args)
        {
            MediaElement me = obj as MediaElement;
            if (me == null)
                return;
            bool isPlaying = (bool)args.NewValue;
            if (isPlaying)
                me.Play();
            else
                me.Stop();
        }

        #endregion
    }

Then make an IsPlaying property in your view model, and bind it to the attached property MediaElementAttached.IsPlaying on the MediaElement.

Just keep in mind the binding will only be one-way: you'll be able to control the MediaElement, but you won't be able to use your view model to learn the value of IsPlaying if the user changes the playback state with the transport controls. But you can't really do that with MediaElement anyway, so you're not giving up anything.

As with most things there are multiple ways to skin this cat, but this is my personal preference and keeps you closest to the pattern I think. This way you can at least get that ugly reference to MediaElement out of your view model code.

Also as far as the video not changing with the selection, the way you have it now, the MediaElement's Source property isn't bound to anything because you're creating it in code, so it's not going to change just because SelectedVideo changes. I would go back to the way you had it before, and try my attached property solution.

Edit - As far as the Position issue goes, it's the same as what I mentioned with IsPlaying, i.e., you can only use these attached properties as one-way setters; you can't use them to obtain the value of anything on the MediaElement. But you have another problem too when it comes to Position which is that it isn't a DependencyProperty, so there's no way to get a notification as to when it actually changes and thus no way to update your attached property with a new value (probably because the updates are so fast and frequent they would bog down the system quite a bit). The only solution I can think of is to poll the MediaElement at some interval (something like 100ms shouldn't be too bad) and then set your attached property with every poll. I would do that in your View's code-behind. Make sure to employ a thread lock and some kind of "suspend update" flag too so you can make sure the MediaElement's Position is not updated during a poll event. Hope this helps.