我正在 WPF 中制作一个媒体播放器,同时尝试尊重 MVVM 模式。 MediaElement 是一个 XAML 元素,具有一些方法,例如 Play()、Pause() 和 Stop()。如何与这些方法交互,而无需将代码放在 MainWindow.xaml.cs 上?
控制视频播放是一个通用的、抽象的概念,与任何支持视频的 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 中。
只需将代码放入 MainWindow.xaml.cs 中即可。 MVVM 是一种架构模式,其目标是将视图与业务逻辑(模型)解耦。代码隐藏并不违反此模式,而且有时是不可避免的。