65.9K
CodeProject 正在变化。 阅读更多。
Home

UWP MediaPlayerAdapter

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (6投票s)

2018年1月27日

CPOL

5分钟阅读

viewsIcon

14400

downloadIcon

218

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。虽然这可行,但我认为它是一个坏主意,原因有两个:

  1. 它破坏了 View / ViewModel 的分离
  2. 它将你的 ViewModel 绑定到特定的平台

你可能会说你不是 MVVM 的纯粹主义者,可以接受这一点,但这很快就会让你吃亏。例如,你需要将你的 ViewModel 移动到一个没有访问“Windows.Media.Playback”命名空间的程序集中。或者你想与其他平台的应用程序 (如 WPF) 共享 ViewModel 逻辑。

增强此解决方案的一种方法是定义一个不了解 MediaPlayer 的抽象 ViewModel,并添加几个虚拟方法 (播放、暂停、停止等)。然后创建一个特定于平台的子类,其中包含 MediaPlayer 实例并重写所有虚拟调用。但这感觉很麻烦且“hacky”。

解决方案 2 - MediaService

MediaPlayer 隐藏在一个接口后面,并将它的实现注入到 ViewModel。这要好得多,因为它打破了你的 ViewModelMediaPlayer (视图元素) 的紧密耦合。简而言之,你创建一个定义播放控制方法的接口。然后,你在 View 上实现该接口,并在视图初始化期间将 View 作为 IMediaService 接口传递给你的 ViewModel。看看 StackOverflow 上 SunnyHoHoHo 的这个不错的实现和指南。

大多数情况下,这是可行的。它简单且实现了我们视图与 ViewModel 解耦的目标。唯一的缺点是,如果你经常处理媒体,它可能会变得有些重复 (在每个视图上实现接口),而且它与依赖注入 (如果你偏好构造函数注入) 的配合不太好。通过一些修改,你可以消除这些问题,但这将带我们到第三个解决方案。

解决方案 3 - MediaPlayerAdapter

这里的基本思想是引入另一层间接性,将 MediaPlayer 逻辑封装到一个独立的、可重用的类 (Adapter) 中,并通过接口将它的方法和属性暴露给 ViewModel (与解决方案 2 基本相同)。但不是将 View 注入到 ViewModel,而是注入我们的 Adapter,然后在 View 加载后,将 MediaPlayer 实例“注入到 Adapter”中。我知道这听起来像额外的工作,但代码可以放在共享程序集中,然后轻松地在你的所有其他项目中重用。

让我们首先创建我们的 View,其中包含 MediaPlayer 及其伴随的 ViewModelViewModel 将公开当前选定的文件作为 IStorageFile 对象,不幸的是,它不能直接绑定到 MediaPlayerSource 属性。因此,为了设置源,我们将使用 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 类。我们的适配器将实现两个接口 - IMediaPlayerAdapterIMediaPlayerAdapterInjector

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) 上共享代码。嗯,只需将 IMediaPlayerAdapterMediaPlayerViewModel 放在一个共享程序集 (.NET Standard 1.4) 中,并实现 MediaPlayerAdater 的 WPF 版本即可。

如果你有改进建议或知道更好的方法,请随时发表评论。编码愉快!

© . All rights reserved.