Click here to Skip to main content
13,665,306 members
Click here to Skip to main content
Add your own
alternative version

Stats

3.4K views
67 downloads
1 bookmarked
Posted 27 Jan 2018
Licenced CPOL

UWP MediaPlayerAdapter

, 27 Jan 2018
Rate this:
Please Sign up or sign in to vote.
MVVM friendly ways of connecting MediaPlayer to your ViewModel

Introduction

When you are working with media files on UWP platform, it is usually a no brainer. Just add the media player on your page, wire up media controls, set media source and you are done. This works well in simple one page programs but can get a little tricky when dealing with MVVM and dependency injection. More so, if you also need to control the playback from your ViewModel. As usual, I first searched for some solutions already published on web. In this article, I will summarize some solutions I’ve found and present my own take on how to tackle this problem.

Prerequisites

In the sample project, I am using Unity as my DI container, Microsoft.Xaml.Behaviors (UWP port of popular blend behaviors SDK) and Reactive extensions. But you can get away just with Microsoft.Xaml.Behaviors if desired. All of these dependencies should be loaded automatically by NuGet package manager during initial build.

If you haven’t had the chance to come across Reactive extensions, I can recommend you these sites full of resources explaining what they are and how to use them:

Solution 1 – Using MediaPlayer Directly (Not Recommended)

Perhaps the first and most straight forward solution that I’ve found was to declare MediaPlayer property directly on your ViewModel and bind it to a ContentPresenter on View. Although this works, I consider it to be a bad idea for two reasons:

  1. It breaks the View / ViewModel separation
  2. It ties your ViewModel to specific platform

You may say you’re not an MVVM purist and can live with that, but it can bite you very quickly. For example, you need to move your ViewModel to an assembly which doesn’t have access to “Windows.Media.Playback” namespace. Or you want to share ViewModels logic with app on a different platform (like WPF).

One way to enhance this solution would be to define an abstract ViewModel without the knowledge of MediaPlayer and add a couple of virtual methods (Play, Pause, Stop, etc.). Then create a platform specific subclass with MediaPlayer instance and override all virtual calls. But it feels cumbersome and “hacky”.

Solution 2 - MediaService

Hide the MediaPlayer behind an interface and inject its implementation into ViewModel. This is way better, because you break the tight coupling of your ViewModel to the MediaPlayer (view element). In short, you create an interface which defines methods for playback control. Then you implement that interface on your View and pass the View into your ViewModel as IMediaService interface during view initialization. Have a look at this nice implementation and guide by SunnyHoHoHo at StackOverflow:

Most of the time, this is a way to go. It’s simple and achieves our goal of decoupling View from ViewModel. The only downside is, that it can get a little repetitive if you work with media regularly (implementing the interface on every View) and it doesn’t play very nicely with dependency injection (if you favor constructor injection). With a few modifications, you can eliminate those problems but that takes us to our third solution.

Solution 3 - MediaPlayerAdapter

The basic idea over here is to introduce another layer of indirection by encapsulating the MediaPlayer logic into a separate reusable class (Adapter) and exposing its methods and properties to ViewModel via interface (pretty much the same as in Solution 2). But instead of injecting the View to ViewModel, we will inject our Adapter and then adapt the MediaPlayer instance “Inject it into Adapter” once the View is loaded. I know it sounds like extra work, but the code can be placed into shared assembly and then easily reused in all of your other projects.

Let’s start by creating our View with MediaPlayer and its accompanying ViewModel. The ViewModel will expose currently selected file as IStorageFile object which unfortunately cannot be directly bound to Source property of MediaPlayer. So in order to set the source, we will use the SetMediaSourceBehavior which binds to selected file and updates the source on MediaPlayer accordingly. (Note: You could use a converter to accomplish the same task.)

ViewModel with Selected File
public class MainPageViewModel : ViewModel
{
    private IStorageFile selectedMediaFile;

    public IStorageFile SelectedMediaFile
    {
        get { return selectedMediaFile; }
        private set
        {
            if (selectedMediaFile != value)
            {
                selectedMediaFile = value;

                RaisePropertyChanged();
            }
        }
    }
}
SetMediaSourceBehavior
public class SetMediaSourceBehavior : Behavior<MediaPlayerElement>
{
    public StorageFile SourceFile

    {
        get { return (StorageFile)GetValue(SourceFileProperty); }
        set { SetValue(SourceFileProperty, value); }
    }

    public static readonly DependencyProperty SourceFileProperty =
        DependencyProperty.Register("SourceFile", typeof(StorageFile), 
            typeof(SetMediaSourceBehavior), new PropertyMetadata(null, OnSourceFileChanged));

    private static void OnSourceFileChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (d is SetMediaSourceBehavior setMediaSourceBehavior)
            setMediaSourceBehavior.UpdateSource(e.NewValue as IStorageFile);        
    }

    private void UpdateSource(IStorageFile storageFile)

    {
        AssociatedObject.Source = null;

        if (storageFile != null)
        {
            var mediaSource = MediaSource.CreateFromStorageFile(storageFile);

            AssociatedObject.Source = mediaSource;
        }
    }
}
MediaPlayerElement on view along with XAML namespace declarations
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:behaviors="using:SharedMVVMLibrary.UWP.Behaviors"


<MediaPlayerElement Width="640" Height="480">
    <i:Interaction.Behaviors>
        <behaviors:SetMediaSourceBehavior SourceFile="{Binding SelectedMediaFile}" />
    </i:Interaction.Behaviors>
</MediaPlayerElement>

So far, we have our source setting logic in place without the ViewModel even knowing that there is some playback going on. So the next part is to expose media playback related methods and properties to our ViewModel without letting him know about the MediaPlayer. For this purpose, we will create the mentioned MediaPlayerAdapter class. Our adapter will implement two interfaces - IMediaPlayerAdapter and IMediaPlayerAdapterInjector.

The ViewModel will only have a dependency on IMediaPlayerAdapter interface and have the implementation injected with a help of DI container. This interface will expose methods for playback control, properties to retrieve the current state (Position, PlaybackRate, PlaybackState, etc…) and a mechanism to convey state changes. For that, you can define basic events like PositionChange or you can get a little fancy with IObservable and use the popular Reactive extensions.

public interface IMediaPlayerAdapter
{
    /// <summary>
    /// Gets the information whether a MediaPlayer is injected into adapter
    /// </summary>
    bool MediaPlayerAdapted { get; }

    /// <summary>
    /// Informs ViewModel whenever a MediaPlayer is injected into adapter
    /// </summary>
    event EventHandler MediaPlayerAdaptedChanged;
        

    TimeSpan Position { get; }
        
    IObservable<TimeSpan> WhenPositionChanges { get; }

    double PlaybackRate { get; }

    IObservable<double> WhenPlaybackRateChanges { get; }

    //Using RX - IObservable to notify about state changes
    //Alternative approach would be to use plain events
    //event EventHandler PositionChanged;

    void Play();

    void Pause();        
}

The second interface IMediaPlayerAdapterInjector will be used by the View to inject it’s MediaPlayer into our adapter.

public interface IMediaPlayerAdapterInjector
{
    void Adapt(MediaPlayer mediaPlayer);
}

This injection will be handled by InjectMediaPlayerBehavior.

public class InjectMediaPlayerBehavior : Behavior<MediaPlayerElement>
{
    public IMediaPlayerAdapterInjector MediaPlayerInjector
    {
        get { return (IMediaPlayerAdapterInjector)GetValue(MediaPlayerInjectorProperty); }
        set { SetValue(MediaPlayerInjectorProperty, value); }
    }

    public static readonly DependencyProperty MediaPlayerInjectorProperty =
        DependencyProperty.Register("MediaPlayerInjector", typeof(IMediaPlayerAdapterInjector), 
        typeof(InjectMediaPlayerBehavior), new PropertyMetadata(null, OnMediaPlayerInjectorChanged));

    private static void OnMediaPlayerInjectorChanged(DependencyObject d, 
                                      DependencyPropertyChangedEventArgs e)
    {
        var injectMediaPlayerBehavior = d as InjectMediaPlayerBehavior;

        if (injectMediaPlayerBehavior != null)
            injectMediaPlayerBehavior.TryToInjectMediaPlayer();
    }


    public MediaPlayer MediaPlayer
    {
        get { return (MediaPlayer)GetValue(MediaPlayerProperty); }
        set { SetValue(MediaPlayerProperty, value); }
    }

    public static readonly DependencyProperty MediaPlayerProperty =
        DependencyProperty.Register("MediaPlayer", typeof(MediaPlayer), 
          typeof(InjectMediaPlayerBehavior), new PropertyMetadata(null, OnMediaPlayerChanged));

    private static void OnMediaPlayerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var injectMediaPlayerBehavior = d as InjectMediaPlayerBehavior;

        if (injectMediaPlayerBehavior != null)
            injectMediaPlayerBehavior.TryToInjectMediaPlayer();
    }    

    protected override void OnAttached()
    {
        base.OnAttached();

        if (ReadLocalValue(MediaPlayerProperty) == DependencyProperty.UnsetValue)
            SetBindingOnLocalMediaPlayer();
    }

    private void SetBindingOnLocalMediaPlayer()
    {
        Binding mediaPlayerBinding = new Binding
        {
            Source = AssociatedObject,
            Mode = BindingMode.OneWay,
            Path = new PropertyPath(nameof(AssociatedObject.MediaPlayer))
        };

        BindingOperations.SetBinding(this, InjectMediaPlayerBehavior.MediaPlayerProperty, 
                mediaPlayerBinding);
    }

    private void TryToInjectMediaPlayer()
    {
        MediaPlayerInjector?.Adapt(MediaPlayer);
    }
}

Now we need to register our IMediaPlayerAdapter to Unity container and attach the created behavior on MediaPlayer:

container.RegisterType<IMediaPlayerAdapter, MediaPlayerAdapter>();
<MediaPlayerElement Width="640" Height="480">
    <i:Interaction.Behaviors>
        <behaviors:SetMediaSourceBehavior SourceFile="{Binding SelectedMediaFile}" />
        <behaviors:InjectMediaPlayerBehavior MediaPlayerInjector="{Binding MediaPlayerAdapter}"/>
    </i:Interaction.Behaviors>
</MediaPlayerElement>

And finally, we can add our IMediaPlayerAdapter dependency to our ViewModel and expose a property for our injection behavior to bind to.

public class MediaPlayerViewModel : ViewModel
{
    private readonly IMediaPlayerAdapter mediaPlayerAdapter;

    public MediaPlayerViewModel(IMediaPlayerAdapter mediaPlayerAdapter)
    {
        this.mediaPlayerAdapter = mediaPlayerAdapter;
    }

    public IMediaPlayerAdapter MediaPlayerAdapter
    {
        get { return mediaPlayerAdapter; }
    }
}

You can now add your commands and desired logic to MediaPlayerViewModel. At the beginning of the article, I was talking about sharing code on different platforms (UWP and WPF). Well, all it takes is to place the IMediaPlayerAdapter and MediaPlayerViewModel in a shared assembly (.NET Standard 1.4) and implement a WPF version of MediaPlayerAdater.

If you have a tip for improvement or know a better approach, feel free to leave a comment. Happy coding!

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Matej Pavlů
Software Developer
Slovakia Slovakia
No Biography provided

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web01-2016 | 2.8.180810.1 | Last Updated 27 Jan 2018
Article Copyright 2018 by Matej Pavlů
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid