Moving methods from view to viewmodel - WPF MVVM
Asked Answered
D

5

6

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.

Desai answered 29/10, 2017 at 12:12 Comment(22)
looks like most of methods should be replaced by properties in a view model, and view should have bindings to those propertiesMadgemadhouse
@Madgemadhouse Wouldn't that violate the MVVM pattern? I have direct reference to some items from the view.Desai
#1406239Madgemadhouse
@Madgemadhouse Can you please elaborate more, maybe an answer, I'm really interested in the solution using properties, I'm ready to offer a bounty for the question too.Desai
what I have in mind is: view model class which implements INotifyPropertyChanged with properties e.g.: public class Vm:INotifyPropertyChanged { public bool State {get;set;} public string MovieElapsedTimeString {get;set;} } (all properties raise Change event). And view uses bindings: IsEnabled="{Binding Path=State}", Content="{Binding MovieElapsedTimeString}". Not sure if MediaElement properties are bindable, but there are workarounds for that case. P.S. not tested, because I cannot copy you code sample and run, it won't compileMadgemadhouse
@Madgemadhouse Seems pretty good, I'd prefer using value converter over having another property string MovieElapsedTimeString {get;set;} tho. Thanks a lot!Desai
@Madgemadhouse I'm only uncertain what workarounds are you speaking of?Desai
@Desai is there a specific reason you want to move elements to a ViewModel? For Example: is your Code behind cluttered or bloated? Is there data in the Model you want persisted?. The other question, is to if you can give a description of what the Code you posted is trying to accomplish. It seems to be some sort of video playback for some sort of "Player"? Because it is easy to give a generalized example of how to use Properties and Commands in the Viewmodel, but adapting it to your situation is harder without all the info. If you could show code that we can copy/paste and compile.that helpsTobe
@GingerNinja I appreciate your interest in my question, can you be a bit more specific to what code segments you would need?Desai
What i am getting at is that if you post code that I can copy and paste to my IDE, then compile it. I can lend more assistance. The code you posted has classes and dependencies i can not replicate. OR if you were to post a code example that compiles, that you would like refactored as you asked.Tobe
I have a few ideas how to refactor it, but i am making broad assumptions and am most likely incorrect in said assumptions.Tobe
@GingerNinja please see the updated question.Desai
posting your XAML would help it be fully complete. then its easier to refactor properly. thanks.Tobe
@GingerNinja There is nothing important in the xaml, you can just create the listed controls and it will be fine.Desai
So the issue i have with the code you posted is that i cant really determine if you need to do it in a ViewModel. You dont have a Model or Data that you are trying to maintain (in this view anyways). Which is the main purpose of the VM, to take the inputs from the UI and relay that to the model. But if you dont have a model, i dont see a need to refactor into a VM. I feel like you are just moving UI code (code-behind) to a class which you call a ViewModel, but not necessarily using it AS a Viewmodel.Tobe
@GingerNinja Maybe you can suggest a solution where a Model is used? :)Desai
I also may be a little confused as i am trying to assume what you are trying to accomplish in the overall programTobe
So what is your intention to refactor your program to MVVM? All it looks like to me is a video player app.Tobe
@GingerNinja Yes this is a video player app, since it's built on WPF, I'd like to use the most common used architectural pattern - MVVM.Desai
I guess i am saying that i dont think you need to follow MVVM, unless you have a data layer or some form of "backend" to your app.Tobe
Let us continue this discussion in chat.Desai
This is 100 bounty I have the code to convert it all . Please paste the XAML as wellSly
I
1

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

Immingle answered 7/11, 2017 at 10:45 Comment(1)
Caliburn.MIcro also has some nice conventions. For example, if you have a ListView called Persons in your xaml then it will try to bind it to a List from your ViewModel with this name. Even better, if you have a property in your ViewModel called SelectedPerson then the SelectedItem of the ListView will be automatically binded to it.Clabber
A
2

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
Abadan answered 5/11, 2017 at 20:22 Comment(3)
Code samples would be really appreciated :)Desai
What about the Thumb events? Also can you elaborate more on the visual states for the slider tooltip?Desai
@Desai whenever the user drags the slider on the UI, the "Value" will change. Your VM will pick up on this change and you can invoke a method as needed (in this case, you'd call the method that controls your model's data in your setter). And as far as the visual states go, it'd be far too complicated to explain outright. You'll need to look at a tutorial. I find it easiest to use Blend. The gist is that each type of control has "states" that will trigger when certain events happen. You will override these templated states so you can create animated effects when they are triggered.Abadan
I
1

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

Immingle answered 7/11, 2017 at 10:45 Comment(1)
Caliburn.MIcro also has some nice conventions. For example, if you have a ListView called Persons in your xaml then it will try to bind it to a List from your ViewModel with this name. Even better, if you have a property in your ViewModel called SelectedPerson then the SelectedItem of the ListView will be automatically binded to it.Clabber
C
1

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
Clabber answered 7/11, 2017 at 21:10 Comment(0)
S
0

You should not have mouse handlers in your viewmodel. Those events belong to the UI and hence the view. Instead, move the bloated view code to an attached behavior. From the behavior you can optionally call into your viewmodel through interfaces. E.g.:

var vm = AssociatedObject.DataContext as IPlayerViewModel;
vm?.AlignTimersWithSource(...);
Subterfuge answered 7/11, 2017 at 16:32 Comment(0)
T
-1

you can not use events in viewmodel. So you will have to create command pattern class and just create viewmodel class. After that can use name space of viewmodel in xml file or view file using "xmlns tag. And create resource for the class and provide meaning full key name. And set datacontext in <Grid datacontext="nameofresource">. Now do the keybinding.

Note: If you need more clearification, reply

Threat answered 6/11, 2017 at 10:55 Comment(1)
Please provide examples of such transformation using the code in the questionDesai

© 2022 - 2024 — McMap. All rights reserved.