UWP MediaPlayerAdapter






4.88/5 (6投票s)
MVVM 友好地将 MediaPlayer 连接到 ViewModel 的方法
引言
当你在 UWP 平台上处理媒体文件时,通常很简单。只需在页面上添加媒体播放器,连接媒体控件,设置媒体源,就完成了。这在简单的单页面程序中效果很好,但在处理 MVVM 和依赖注入时可能会有点棘手。更不用说,如果你还需要从 ViewModel
控制播放。一如既往,我首先在网上搜索了一些已发布的解决方案。在这篇文章中,我将总结一些我找到的解决方案,并提出我自己的方法来解决这个问题。
必备组件
在示例项目中,我将 Unity
作为我的 DI 容器,Microsoft.Xaml.Behaviors
(流行的 blend behaviors SDK 的 UWP 版本) 和 Reactive extensions
。但如果需要,你也可以只使用 Microsoft.Xaml.Behaviors
。所有这些依赖项都应该由 NuGet 包管理器在初始构建期间自动加载。
如果你还没有机会接触 Reactive extensions,我推荐你访问这些网站,它们提供了大量解释它们是什么以及如何使用它们的资源。
解决方案 1 – 直接使用 MediaPlayer (不推荐)
我找到的第一个也是最直接的解决方案可能是直接在你的 ViewModel
上声明 MediaPlayer
属性,并将其绑定到 View
上的 ContentPresenter
。虽然这可行,但我认为它是一个坏主意,原因有两个:
- 它破坏了
View
/ViewModel
的分离 - 它将你的
ViewModel
绑定到特定的平台
你可能会说你不是 MVVM 的纯粹主义者,可以接受这一点,但这很快就会让你吃亏。例如,你需要将你的 ViewModel
移动到一个没有访问“Windows.Media.Playback
”命名空间的程序集中。或者你想与其他平台的应用程序 (如 WPF) 共享 ViewModel
逻辑。
增强此解决方案的一种方法是定义一个不了解 MediaPlayer
的抽象 ViewModel
,并添加几个虚拟方法 (播放、暂停、停止等)。然后创建一个特定于平台的子类,其中包含 MediaPlayer
实例并重写所有虚拟调用。但这感觉很麻烦且“hacky”。
解决方案 2 - MediaService
将 MediaPlayer
隐藏在一个接口后面,并将它的实现注入到 ViewModel
。这要好得多,因为它打破了你的 ViewModel
与 MediaPlayer
(视图元素) 的紧密耦合。简而言之,你创建一个定义播放控制方法的接口。然后,你在 View
上实现该接口,并在视图初始化期间将 View
作为 IMediaService
接口传递给你的 ViewModel
。看看 StackOverflow 上 SunnyHoHoHo 的这个不错的实现和指南。
大多数情况下,这是可行的。它简单且实现了我们视图与 ViewModel 解耦的目标。唯一的缺点是,如果你经常处理媒体,它可能会变得有些重复 (在每个视图上实现接口),而且它与依赖注入 (如果你偏好构造函数注入) 的配合不太好。通过一些修改,你可以消除这些问题,但这将带我们到第三个解决方案。
解决方案 3 - MediaPlayerAdapter
这里的基本思想是引入另一层间接性,将 MediaPlayer
逻辑封装到一个独立的、可重用的类 (Adapter
) 中,并通过接口将它的方法和属性暴露给 ViewModel
(与解决方案 2 基本相同)。但不是将 View
注入到 ViewModel
,而是注入我们的 Adapter
,然后在 View
加载后,将 MediaPlayer
实例“注入到 Adapter”中。我知道这听起来像额外的工作,但代码可以放在共享程序集中,然后轻松地在你的所有其他项目中重用。
让我们首先创建我们的 View
,其中包含 MediaPlayer
及其伴随的 ViewModel
。ViewModel
将公开当前选定的文件作为 IStorageFile
对象,不幸的是,它不能直接绑定到 MediaPlayer
的 Source
属性。因此,为了设置源,我们将使用 SetMediaSourceBehavior
,它绑定到选定的文件并相应地更新 MediaPlayer
上的源。(注意:你可以使用转换器来完成相同的任务。)
带有选定文件的 ViewModel
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 以及 XAML 命名空间声明
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>
到目前为止,我们已经实现了源设置逻辑,而 ViewModel
甚至不知道有任何播放正在进行。所以下一部分是将媒体播放相关的方法和属性暴露给我们的 ViewModel
,而不让他知道 MediaPlayer
的存在。为此,我们将创建前面提到的 MediaPlayerAdapter
类。我们的适配器将实现两个接口 - IMediaPlayerAdapter
和 IMediaPlayerAdapterInjector
。
ViewModel
将只依赖于 IMediaPlayerAdapter
接口,并通过 DI 容器注入实现。该接口将公开用于播放控制的方法,用于检索当前状态的属性 (Position
, PlaybackRate
, PlaybackState
等),以及一种传达状态变化的方法。为此,你可以定义像 PositionChange
这样的基本事件,或者使用 IObservable
并利用流行的 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();
}
第二个接口 IMediaPlayerAdapterInjector
将被 View
用于将它的 MediaPlayer
注入到我们的适配器中。
public interface IMediaPlayerAdapterInjector
{
void Adapt(MediaPlayer mediaPlayer);
}
此注入将由 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);
}
}
现在我们需要将 IMediaPlayerAdapter
注册到 Unity 容器,并将创建的行为附加到 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>
最后,我们可以将 IMediaPlayerAdapter
依赖项添加到我们的 ViewModel
中,并公开一个属性供我们的注入行为绑定。
public class MediaPlayerViewModel : ViewModel
{
private readonly IMediaPlayerAdapter mediaPlayerAdapter;
public MediaPlayerViewModel(IMediaPlayerAdapter mediaPlayerAdapter)
{
this.mediaPlayerAdapter = mediaPlayerAdapter;
}
public IMediaPlayerAdapter MediaPlayerAdapter
{
get { return mediaPlayerAdapter; }
}
}
你现在可以将你的命令和所需的逻辑添加到 MediaPlayerViewModel
。在文章开头,我曾谈到在不同平台 (UWP 和 WPF) 上共享代码。嗯,只需将 IMediaPlayerAdapter
和 MediaPlayerViewModel
放在一个共享程序集 (.NET Standard 1.4) 中,并实现 MediaPlayerAdater
的 WPF 版本即可。
如果你有改进建议或知道更好的方法,请随时发表评论。编码愉快!