在 WPF 中使用 MediaEement,同时尊重 MVVM

问题描述 投票:0回答:2

我正在 WPF 中制作一个媒体播放器,同时尝试尊重 MVVM 模式。 MediaElement 是一个 XAML 元素,具有一些方法,例如 Play()、Pause() 和 Stop()。如何与这些方法交互,而无需将代码放在 MainWindow.xaml.cs 上?

c# wpf mvvm media-player mediaelement
2个回答
0
投票

控制视频播放是一个通用的、抽象的概念,与任何支持视频的 GUI 平台上的任何应用程序相关。因此,视频何时播放、暂停等的逻辑已经成熟,可以由视图模型处理。但是,WPF 的

MediaElement
并没有为您提供将播放控件和播放状态绑定到视图模型的便捷方法。

这是否意味着我们运气不好?一点也不。我们只需要扩展

MediaElement
。这是一种可能的方法:

public class MvvmMediaElement : MediaElement
{
    public MvvmMediaElement()
    {            
        this.MediaEnded += this.OnMediaEnded;
        this.MediaFailed += this.OnMediaFailed;
        this.LoadedBehavior = System.Windows.Controls.MediaState.Manual;
    }        

    #region PlaybackState PlaybackState dependency property
    public static readonly DependencyProperty PlaybackStateProperty = DependencyProperty.Register(
        nameof(PlaybackState),
        typeof(PlaybackState),
        typeof(MvvmMediaElement),
        new FrameworkPropertyMetadata(
            PlaybackState.Stopped,
            FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
            (obj, args) =>
            {
                ((MvvmMediaElement)obj).OnMediaStateChanged(args);
            }));
    public PlaybackState PlaybackState
    {
        get
        {
            return (PlaybackState)GetValue(PlaybackStateProperty);
        }
        set
        {
            SetValue(PlaybackStateProperty, value);
        }
    }
    private void OnMediaStateChanged(DependencyPropertyChangedEventArgs args)
    {
        if (_suspendUpdateStateHandler)
            // State change comes from the GUI, so don't
            // actually change the underlying element state
            return;            

        if (!(args.NewValue is PlaybackState newState))
            return;

        switch (newState)
        {
            case PlaybackState.Stopped:
                this.Stop();
                break;
            case PlaybackState.Paused:
                this.Pause();
                break;
            case PlaybackState.Playing:
                this.Play();
                break;
        }
    }
    #endregion

    #region string MediaError dependency property
    public static readonly DependencyProperty MediaErrorProperty = DependencyProperty.Register(
        nameof(MediaError),
        typeof(string),
        typeof(MvvmMediaElement),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
    public string MediaError
    {
        get
        {
            return (string)GetValue(MediaErrorProperty);
        }
        set
        {
            SetValue(MediaErrorProperty, value);
        }
    }
    #endregion

    private void OnMediaEnded(object sender, RoutedEventArgs e)
    {
        // reset the time, don't resume playback
        this.Position = default;            
        this.Stop();    
        this.UpdateStateBindingSource(PlaybackState.Stopped);            
    }

    private void OnMediaFailed(object? sender, ExceptionRoutedEventArgs e)
    {
        this.MediaError = e.ErrorException.Message;
        this.UpdateStateBindingSource(PlaybackState.Error);
    }

    // Notifies the binding source of a new the media playback state
    // without actually changing the playback state.
    private void UpdateStateBindingSource(PlaybackState newState)
    {
        _suspendUpdateStateHandler = true;
        try
        {
            this.SetCurrentValue(PlaybackStateProperty, newState);
        }
        catch
        {
            throw;
        }
        finally
        {
            _suspendUpdateStateHandler = false;
        }
    }

    private bool _suspendUpdateStateHandler = false;
}  

现在我们可以使用简单的与 GUI 无关的视图模型和仅 XAML 的视图来控制视频播放:

public enum PlaybackState
{
    Stopped,
    Playing,
    Paused,
    Error
}

public class MediaViewModel : ViewModelBase
{
    #region string Error property
    private string _Error;
    public string Error
    {
        get
        {
            return _Error;
        }
        set
        {
            if (_Error == value)
                return;
            _Error = value;
            OnPropertyChanged();
        }
    }
    #endregion

    #region Uri Source property
    private Uri _Source;
    public Uri Source
    {
        get
        {
            return _Source;
        }
        set
        {
            if (_Source == value)
                return;
            _Source = value;
            OnPropertyChanged();
        }
    }
    #endregion

    #region MediaState State property
    private PlaybackState _State;
    public PlaybackState State
    {
        get
        {
            return _State;
        }
        set
        {
            if (_State == value)
                return;
            _State = value;
            OnPropertyChanged();
        }
    }
    #endregion

    #region ICommand Play Command
    private Command _PlayCommand;
    public ICommand PlayCommand
    {
        get
        {
            return _PlayCommand ?? (_PlayCommand = new Command(
                () =>
                {
                    this.State = PlaybackState.Playing;
                }));
        }
    }
    #endregion

    #region ICommand Pause Command
    private Command _PauseCommand;
    public ICommand PauseCommand
    {
        get
        {
            return _PauseCommand ?? (_PauseCommand = new Command(
                () =>
                {
                    this.State = PlaybackState.Paused;
                }));
        }
    }
    #endregion

    #region ICommand Stop Command
    private Command _StopCommand;
    public ICommand StopCommand
    {
        get
        {
            return _StopCommand ?? (_StopCommand = new Command(
                () =>
                {
                    this.State = PlaybackState.Stopped;
                }));
        }
    }
    #endregion
}

查看:

<Window.DataContext>
    <local:MediaViewModel />
</Window.DataContext>
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="*" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <local:MvvmMediaElement Source="{Binding Source}"
                            PlaybackState="{Binding State}"
                            MediaError="{Binding Error, Mode=OneWayToSource}" />
    <TextBlock Grid.Row="1"
               Text="{Binding Error}" />
    <TextBlock Grid.Row="2"
               Text="{Binding State, Mode=OneWay}" />
    <StackPanel Grid.Row="3"
                Orientation="Horizontal">
        <Button Command="{Binding PlayCommand}">Play</Button>
        <Button Command="{Binding PauseCommand}">Pause</Button>
        <Button Command="{Binding StopCommand}">Stop</Button>
    </StackPanel>
</Grid>

我非常不同意“只需将代码放入 MainWindow.xaml.cs 中”的评论,除非您将来无意扩展或移植您的应用程序。该评论正确地指出“[MVVM 的目标是]将视图与业务逻辑(模型)解耦”,但这绝不是故事的全部。

该模式的另一个基本目标,也是“无代码隐藏”偏好的根源,是将通用的、抽象的 UI 逻辑(属于视图模型)与实现细节(属于视图和控件)分开。视图消耗。精心设计的视图模型 + 仅 XAML 视图允许您锯掉控件,甚至可能锯掉整个 UI 平台,而无需更改任何命令式(即 C#)代码。

相比之下,代码隐藏——这是“很少”“不可避免的”——将抽象 UI 逻辑束缚到单个 GUI 平台,通常是一组控件。这就像原力的黑暗面——更快、更容易、更诱人。但它很少会扩展到使用它的单个实例之外,并且测试、调试或重构可能是一场噩梦。如果您决定更改 GUI 平台,甚至决定使用一组不同的控件,这将不可避免地成为技术债务。

当我们完成(稍微)困难的工作时,MVVM 的真正威力就会显现出来。在这里,我们制作了一个可重复使用的扩展 MediaElement

,其用途远远超出了一个应用程序。这使其成为一项有价值的长期投资。另外,我们在扩展

MediaElement

 中所做的大部分工作无论如何都会进入代码隐藏解决方案,或者只是样板 
DependencyProperty
 定义。
“无代码隐藏”不仅仅是一种审美偏好。它迫使您提高架构的效率和可扩展性,并且不应在无法开箱即用的符合 MVVM 的解决方案时就放弃它。您寻找 MVVM 友好的方法来解决这个问题是正确的,我希望您能继续为其他人这样做。

我正在寻找一个不必要的解决方案。如果需要,可以将代码放入 MainWindow.xaml.cs 中。

-1
投票
@BionicCode 说:

只需将代码放入 MainWindow.xaml.cs 中即可。 MVVM 是一种架构模式,其目标是将视图与业务逻辑(模型)解耦。代码隐藏并不违反此模式,而且有时是不可避免的。

© www.soinside.com 2019 - 2024. All rights reserved.