将方法从视图移至视图模型 - WPF MVVM

6

我在代码后台有以下代码:

public partial class MainWindow
{
    private Track _movieSkipSliderTrack;
    private Slider sMovieSkipSlider = null;
    private Label lbTimeTooltip = null;
    private MediaElement Player = null;

    public VideoPlayerViewModel ViewModel
    {
        get { return DataContext as VideoPlayerViewModel; }
    }

    public MainWindow()
    {
        InitializeComponent();
    }

    private void SMovieSkipSlider_OnLoaded(object sender, RoutedEventArgs e)
    {
        _movieSkipSliderTrack = (Track)sMovieSkipSlider.Template.FindName("PART_Track", sMovieSkipSlider);
        _movieSkipSliderTrack.Thumb.DragDelta += Thumb_DragDelta;
        _movieSkipSliderTrack.Thumb.MouseEnter += Thumb_MouseEnter;
    }

    private void Thumb_MouseEnter(object sender, MouseEventArgs e)
    {
        if (e.LeftButton == MouseButtonState.Pressed && e.MouseDevice.Captured == null)
        {
            var args = new MouseButtonEventArgs(e.MouseDevice, e.Timestamp, MouseButton.Left)
            {
                RoutedEvent = MouseLeftButtonDownEvent
            };
            SetPlayerPositionToCursor();
            _movieSkipSliderTrack.Thumb.RaiseEvent(args);
        }
    }

    private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
    {
        SetPlayerPositionToCursor();
    }

    private void SMovieSkipSlider_OnMouseEnter(object sender, MouseEventArgs e)
    {
        lbTimeTooltip.Visibility = Visibility.Visible;
        lbTimeTooltip.SetLeftMargin(Mouse.GetPosition(sMovieSkipSlider).X);
    }

    private void SMovieSkipSlider_OnPreviewMouseMove(object sender, MouseEventArgs e)
    {
        double simulatedPosition = SimulateTrackPosition(e.GetPosition(sMovieSkipSlider), _movieSkipSliderTrack);
        lbTimeTooltip.AddToLeftMargin(Mouse.GetPosition(sMovieSkipSlider).X - lbTimeTooltip.Margin.Left + 35);
        lbTimeTooltip.Content = TimeSpan.FromSeconds(simulatedPosition);
    }

    private void SMovieSkipSlider_OnMouseLeave(object sender, MouseEventArgs e)
    {
        lbTimeTooltip.Visibility = Visibility.Hidden;
    }

    private void SetPlayerPositionToCursor()
    {
        Point mousePosition = new Point(Mouse.GetPosition(sMovieSkipSlider).X, 0);
        double simulatedValue = SimulateTrackPosition(mousePosition, _movieSkipSliderTrack);
        SetNewPlayerPosition(TimeSpan.FromSeconds(simulatedValue));
    }

    private double CalculateTrackDensity(Track track)
    {
        double effectivePoints = Math.Max(0, track.Maximum - track.Minimum);
        double effectiveLength = track.Orientation == Orientation.Horizontal
            ? track.ActualWidth - track.Thumb.DesiredSize.Width
            : track.ActualHeight - track.Thumb.DesiredSize.Height;
        return effectivePoints / effectiveLength;
    }

    private double SimulateTrackPosition(Point point, Track track)
    {
        var simulatedPosition = (point.X - track.Thumb.DesiredSize.Width / 2) * CalculateTrackDensity(track);
        return Math.Min(Math.Max(simulatedPosition, 0), sMovieSkipSlider.Maximum);
    }

    private void SetNewPlayerPosition(TimeSpan newPosition)
    {
        Player.Position = newPosition;
        ViewModel.AlignTimersWithSource(Player.Position, Player);
    }
}

我想遵循MVVM模式,并将这段代码移动到我的ViewModel中,目前我的ViewModel只有几个属性。我在这个主题上阅读了很多答案,不仅仅是StackOverflow上的,还下载了一些Github项目来查看经验丰富的程序员如何处理特定的情况,但没有一个能够消除我的困惑。我想看看如何重构我的情况以遵循MVVM模式。
这些是额外的扩展方法,也是ViewModel本身:
static class Extensions
{
    public static void SetLeftMargin(this FrameworkElement target, double value)
    {
        target.Margin = new Thickness(value, target.Margin.Top, target.Margin.Right, target.Margin.Bottom);
    }

    public static void AddToLeftMargin(this FrameworkElement target, double valueToAdd)
    {
        SetLeftMargin(target, target.Margin.Left + valueToAdd);
    }
}

public class VideoPlayerViewModel : ViewModelBase
{
    private TimeSpan _movieElapsedTime = default(TimeSpan);
    public TimeSpan MovieElapsedTime
    {
        get { return _movieElapsedTime; }
        set
        {
            if (value != _movieElapsedTime)
            {
                _movieElapsedTime = value;
                OnPropertyChanged();
            }
        }
    }

    private TimeSpan _movieLeftTime = default(TimeSpan);
    public TimeSpan MovieLeftTime
    {
        get { return _movieLeftTime; }
        set
        {
            if (value != _movieLeftTime)
            {
                _movieLeftTime = value;
                OnPropertyChanged();
            }
        }
    }

    public void AlignTimersWithSource(TimeSpan currentPosition, MediaElement media)
    {
        MovieLeftTime = media.NaturalDuration.TimeSpan - currentPosition;
        MovieElapsedTime = currentPosition;
    }
}

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    [NotifyPropertyChangedInvocator]
    protected virtual void OnPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
    }
}

我已经尝试按照评论中的要求使代码可以复制/粘贴,如果您想完全复制它,则视图代码后台中的所有控件均在XAML中创建。


1
看起来大多数方法应该被视图模型中的属性所取代,而视图应该与这些属性进行绑定。 - ASh
1
我所想的是:视图模型类实现了INotifyPropertyChanged接口,具有属性例如:public class Vm:INotifyPropertyChanged { public bool State {get;set;} public string MovieElapsedTimeString {get;set;} }(所有属性都会引发更改事件)。视图使用绑定:IsEnabled="{Binding Path=State}"Content="{Binding MovieElapsedTimeString}"。不确定MediaElement属性是否可绑定,但对于这种情况有解决方法。附注:未经测试,因为我无法复制您的代码示例并运行它,它无法编译。 - ASh
@Deadzone,你想将元素移动到ViewModel中的具体原因是什么?例如:你的代码后台是否混乱或臃肿?模型中是否有需要持久化的数据?另一个问题是,你能否描述一下你发布的代码试图实现什么目标。它似乎是某种视频播放器的视频播放?虽然可以给出如何在Viewmodel中使用属性和命令的概括示例,但如果没有所有信息,就很难将其适应到你的情况中。如果你能展示我们可以复制/粘贴并编译的代码,那会很有帮助。 - Ginger Ninja
@GingerNinja,感谢您对我的问题的关注,您能具体说明一下您需要哪些代码段吗? - Deadzone
让我们在聊天中继续这个讨论 - Deadzone
显示剩余17条评论
5个回答

2
这段话的意思是:在你的VM中,为你想要更新的UI区域和需要处理的事件分别设置属性和命令。看一下你当前的代码,如果直接连接到滑块的Value属性并将其绑定(双向)到VM上的一个属性,你将会更容易地完成任务(还能够删除一些事件处理程序)。每当用户拖动时,你可以看到数值何时更新,并相应地进行处理。至于你的滚动条“隐藏”效果,如果直接连接到滑块的可视状态,你可能会更容易地完成任务。这里是样式和可视状态。
public class VideoPlayerViewModel : ViewModelBase
{
    // your existing properties here, if you decide that you still need them

    // this could also be long/double, if you'd like to use it with your underlying type (DateTime.TotalTicks, TimeSpan.TotalSeconds, etc.)
    private uint _elapsedTime = 0; //or default(uint), whichever you prefer
    public uint ElapsedTime
    {
        get { return _elapsedTime; }
        set
        {
            if (_elapsedTime != value)
            {
                _elapsedTime = value;
                //additional "time changed" logic here, if needed
                //if you want to skip programmatically, all you need to do is set this property!
                OnPropertyChanged();
            }
        }
    }

    private double _maxTime = 0;
    public double MaxTime
    {
        // you get the idea, you'll be binding to the media's end time in whatever unit you're using (i.e. if I have a 120 second clip, this value would be 120 and my elapsed time would be hooked into an underlying TimeSpan.TotalSeconds)
    }
}

在你的滑块上:
Value={Binding ElapsedTime, Mode=TwoWay}
Maximum={Binding MaxTime, Mode=OneWay} //could also be OneTime, depending on the lifecycle of the control

非常感谢提供代码示例 :) - Deadzone
拇指事件怎么样?您能详细说明滑块工具提示的可视状态吗? - Deadzone
每当用户在UI上拖动滑块时,“Value”将会改变。您的VM将会捕捉到这个变化,然后您可以根据需要调用一个方法(在这种情况下,您将会调用控制模型数据的方法在您的setter中)。至于视觉状态方面,直接解释会过于复杂。您需要查看教程。我发现使用Blend最容易。要点是每种类型的控件都有“状态”,当某些事件发生时,它们将会被触发。您将会覆盖这些模板状态,以便在触发时创建动画效果。 - ellison

1

Caliburn.Micro还有一些不错的约定。例如,如果您在xaml中有一个名为Persons的ListView,则它将尝试将其绑定到ViewModel中具有此名称的List。更好的是,如果您的ViewModel中有一个名为SelectedPerson的属性,则ListView的SelectedItem将自动绑定到它。 - bogdanbujdea

1

我有一些在XAML应用程序中遵循的简单规则:

  1. ViewModel不应知道View,因此ViewModel中永远不会找到任何与UI相关的代码。
  2. 所有与UI相关的代码都在代码后台(xaml.cs)中。
  3. 用户控件和依赖属性是您最好的朋友,所以请使用它们。视图应由用户控件组成,每个用户控件都有自己的ViewModel。
  4. 通过构造函数注入来注入依赖项,以便在编写单元测试时可以模拟它们。

0

你的视图模型中不应该有鼠标处理程序。这些事件属于 UI,因此属于视图。相反,将臃肿的视图代码移动到附加行为中。从行为中,你可以通过接口可选地调用视图模型。例如:

var vm = AssociatedObject.DataContext as IPlayerViewModel;
vm?.AlignTimersWithSource(...);

-1

你不能在视图模型中使用事件。所以你需要创建一个命令模式类,并且只需创建视图模型类。之后可以在xml文件或者视图文件中使用视图模型的命名空间,使用“xmlns”标签。并为该类创建资源,并提供有意义的键名。然后在<Grid datacontext="nameofresource">中设置数据上下文。现在进行按键绑定。

注意:如果你需要更清楚的解释,请回复。


请提供使用问题中的代码示例进行此类转换的例子。 - Deadzone

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接