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:
- It breaks the
View
/ ViewModel
separation - 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 ViewModel
s 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
{
bool MediaPlayerAdapted { get; }
event EventHandler MediaPlayerAdaptedChanged;
TimeSpan Position { get; }
IObservable<TimeSpan> WhenPositionChanges { get; }
double PlaybackRate { get; }
IObservable<double> WhenPlaybackRateChanges { get; }
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!