【问题标题】:Moving methods from view to viewmodel - WPF MVVM将方法从视图移动到视图模型 - WPF MVVM
【发布时间】:2018-04-10 12:49:36
【问题描述】:

我的代码后面有以下代码:

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 中,目前它只有很少的属性。我已经在这里和 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));
    }
}

我已尝试按照 cmets 中的要求准备好代码复制/粘贴,如果您想完全复制它,则视图后面代码中的所有控件都是在 XAML 中创建的。

【问题讨论】:

  • 看起来大多数方法应该被视图模型中的属性替换,并且视图应该绑定到这些属性
  • @ASh 这不会违反 MVVM 模式吗?我直接引用了视图中的一些项目。
  • @ASh 你能否详细说明,也许是一个答案,我对使用属性的解决方案非常感兴趣,我也准备好为这个问题提供赏金。
  • 我想到的是:视图模型类,它使用属性实现 INotifyPropertyChanged,例如:public class Vm:INotifyPropertyChanged { public bool State {get;set;} public string MovieElapsedTimeString {get;set;} }(所有属性都会引发更改事件)。并且视图使用绑定:IsEnabled="{Binding Path=State}"Content="{Binding MovieElapsedTimeString}"。不确定 MediaElement 属性是否可绑定,但有针对这种情况的解决方法。附言未经测试,因为我无法复制您的代码示例并运行,它不会编译

标签: c# wpf mvvm view viewmodel


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

      我推荐使用 Caliburn Micro。 如果您使用该库,您可以像这样绑定事件:

      <Button cal:Message.Attach="Save">

      或者那样

      <Button cal:Message.Attach="[Event MouseEnter] = [Action Save]">

      查看他们的网站了解更高级的可能性:

      https://caliburnmicro.codeplex.com/wikipage?title=Cheat%20Sheet

      【讨论】:

      • Caliburn.MIcro 也有一些不错的约定。例如,如果您的 xaml 中有一个名为 Persons 的 ListView,那么它将尝试将其绑定到您的 ViewModel 中具有此名称的 List。更好的是,如果您的 ViewModel 中有一个名为 SelectedPerson 的属性,那么 ListView 的 SelectedItem 将自动绑定到它。
      【解决方案4】:

      您不能在视图模型中使用事件。因此,您必须创建命令模式类并只创建视图模型类。之后可以使用xml文件或视图文件中的视图模型的名称空间使用“xmlns标签。并为类创建资源并提供含义完整的键名。并在中设置datacontext <Grid datacontext="nameofresource">. 现在进行按键绑定。

      注意:如需进一步说明,请回复

      【讨论】:

      • 请使用问题中的代码提供此类转换的示例
      【解决方案5】:

      我们的想法是在您的虚拟机中为您要更新的 UI 的每个区域或需要处理的事件分别设置一个属性和命令。

      看看你当前的代码,我认为如果你直接挂钩到滑块的 Value 属性并绑定它(双向) 到您的 VM 上的属性。每当用户拖动时,您将能够看到值何时更新并进行相应处理。

      就您的擦洗栏的“隐藏”效果而言,您可能会更轻松地进入滑块的视觉状态。 Here are the styles and visual states.

      编辑:

      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
      

      【讨论】:

      • 非常感谢代码示例:)
      • Thumb 事件怎么样?您还可以详细说明滑块工具提示的视觉状态吗?
      • @Deadzone 每当用户在 UI 上拖动滑块时,“值”都会改变。您的 VM 将接受此更改,您可以根据需要调用方法(在这种情况下,您将调用在 setter 中控制模型数据的方法)。就视觉状态而言,完全解释起来太复杂了。你需要看一个教程。我发现使用 Blend 最简单。要点是每种类型的控件都有“状态”,这些“状态”会在某些事件发生时触发。您将覆盖这些模板化状态,以便在它们被触发时创建动画效果。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-10-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-10-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多