Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Moving methods from view to viewmodel - WPF MVVM

I have the following code in my code behind:

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);
    }
}

I would like to follow the MVVM pattern and have this code moved to my ViewModel which at the moment has only few properties. I have read a lot of answer here and outside of StackOverflow on the topic, I've downloaded some github projects to check out how experienced programmers handle specific situations, but none of that seem to clear out the confusion for me. I'd like to see how can my case be refactored to follow the MVVM pattern.

Those are the extra extension methods and also the ViewModel itself:

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));
    }
}

I have tried to make the code copy/paste ready as requested in the comments, all of the Controls in the View's code behind are created in the XAML, if you want to fully replicate it.

like image 414
Deadzone Avatar asked Oct 29 '17 12:10

Deadzone


3 Answers

The idea is to have a property and command in your VM for every area of the UI that you'd like to update or event that needs to be handled, respectively.

Just glancing at your current code, I think you will have a much easier time (you'll be able to remove a few of your event handlers) if you hook directly into your slider's Value property and bind it (two-way) to a property on your VM. Whenever the user drags, you will be able to see when the value updates and you can handle accordingly.

As far as the "hidden" effect of your scrub bar goes, you may have a much easier time just hooking into the visual state of your slider. Here are the styles and visual states.

EDIT:

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)
    }
}

and on your slider:

Value={Binding ElapsedTime, Mode=TwoWay}
Maximum={Binding MaxTime, Mode=OneWay} //could also be OneTime, depending on the lifecycle of the control
like image 65
ellison Avatar answered Oct 23 '22 02:10

ellison


I recommend using Caliburn Micro. If you use that library you can bind events like this:

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

or like that

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

Check out their website for more advanced possibilities:

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

like image 38
horotab Avatar answered Oct 23 '22 02:10

horotab


I have some simple rules that I follow in XAML apps:

  1. The ViewModel should not know about the View, so no UI related code will ever be found in the ViewModel
  2. All UI related code is in the code behind(xaml.cs)
  3. User controls and dependency properties are your best friends, so use them. The view should be made up of user controls, each with its own ViewModel.
  4. Inject your dependencies through constructor injection so they can be mocked when you write unit tests
like image 22
bogdanbujdea Avatar answered Oct 23 '22 01:10

bogdanbujdea