How to handle the Slider.ValueChanged event in a view model?
Asked Answered
A

3

5

I have a PlayerV.xaml View with a Slider inside:

<Slider Value="{Binding CurrentProgress}"/>

and have a button:

<Button Content="next song" Command="{Binding playNext}"/>

Button works correct. Button's playNext command is contained in PlayerVM.cs

I want slider's ValueChanged to call a function which is stored in PlayerVM.cs:

[1]:<Slider Value="{Binding CurrentProgress}" ValueChanged="{Binding playNext}"/>

I know [1] has an incorrect syntax, I used it for sake of clarity of explanation.

====Additional Explanation====

I know I can write:

<Slider ValueChanged="Slider_ValueChanged" Value="{Binding CurrentProgress}" />

And in PlayerV.xaml.cs there will be

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }
    private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        //some logic (actions) 
    }    
}

But I don't want any logic in there. I want it to be in PlayerVM.cs (like button's command handler functions). How to do that? Also in my App.xaml.cs startup function is:

private void OnStartup(object sender, StartupEventArgs e)
{
    MainWindow _mainWindow = new MainWindow();
    PlayerVM playerVM = new PlayerVM();
    _mainWindow.DataContext = playerVM;
    _mainWindow.Show();
 }
Argali answered 5/8, 2014 at 12:20 Comment(4)
As you bound value of slider to CurrentProgress prop of your VM, than each time you slide using your slider it changes too. So you just have call the methode of command yuou need from the setter of CurrentProgress prop. Make sure you have TwoWay binding mode <Slider Value="{Binding CurrentProgress, Mode=TwoWay}" />Butlery
I think I am doing something wrong. In PlayerVM I have a thread (timer tick) which sets CurrentProgress value according to mediaPlayer.Position. But I wanted to when the user changes the position of the slider -> the position of the player should be passed in accordance. How do I know that SLiders value was changed by user, not by timer_ticks function?Argali
the most trivial way - your ticker has fixed intrval that changes progress, in your setter just compare old and new value, and depending on the result make a choiceButlery
I had an idea to use that trivial way. But, as it turned out, my previous formula was incorrect and I thought that it is not a solution. Now I improved formula and it works! THanks, @Den.Argali
I
17

You have two options. First, despite what you said about not wanting to use code behind, one solution is for you to do just that. In the ValueChanged event handler, you can simply call your view model method when ever the value is changed:

private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
    Slider slider = sender as Slider;
    PlayerVM viewModel = (PlayerVM)DataContext;
    viewModel.YourMethod(slider.Value);
} 

I've offered this solution because I suspect that you're new to MVVM and still think that you're not allowed to use the code behind. In fact, that is not the case at all and for purely UI matters such as this, it's a good place for it.

Another option is just to data bind a property directly to the Slider.Value. As the Slider value is changed, so will the property be. Therefore, you can simply call your method from the data bound property setter:

public double CurrentProgress
{
    get { return currentProgress; }
    set
    {
        currentProgress = value;
        NotifyPropertyChanged("CurrentProgress");
        YourMethod(value);
    }
}

One further option involves handling the ValueChanged event in a custom Attached Property. There is a bit more to this solution than the others, so I'd prefer to direct you to some answers that I have already written for other questions, rather than re-writing it all again. Please see my answers to the How to set Focus to a WPF Control using MVVM? and WPF C# - navigate WebBrowser on mouseclick through Binding questions for explanations and code examples of this method.

Idaho answered 5/8, 2014 at 14:16 Comment(0)
A
3

By using the event to commend logic you can bind events to your view model, but you need help. You need to use functions from the System.Windows.Interactivity Namespace and include a MVVM Light (there might be other MVVM libraries that have that feature but i use MVVM Light).

refer this: Is it possible to bind a WPF Event to MVVM ViewModel command?

Antimagnetic answered 5/8, 2014 at 13:48 Comment(0)
D
0

There is one more option. You can make your own version of Slider control, that can execute command when user interacts with it.

An imperfect but working code might look like this:

using System.ComponentModel;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;

public class CommandSlider : Slider, ICommandSource
{
    public static readonly DependencyProperty CommandParameterProperty = ButtonBase.CommandParameterProperty.AddOwner(typeof(CommandSlider));
    public static readonly DependencyProperty CommandProperty = ButtonBase.CommandProperty.AddOwner(typeof(CommandSlider));
    public static readonly DependencyProperty CommandTargetProperty = ButtonBase.CommandTargetProperty.AddOwner(typeof(CommandSlider));
    private volatile int _commandArmed = 0;

    [Bindable(true), Category("Action")]
    [Localizability(LocalizationCategory.NeverLocalize)]
    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    [Bindable(true), Category("Action")]
    [Localizability(LocalizationCategory.NeverLocalize)]
    public object CommandParameter
    {
        get { return GetValue(CommandParameterProperty); }
        set { SetValue(CommandParameterProperty, value); }
    }

    [Bindable(true), Category("Action")]
    public IInputElement CommandTarget
    {
        get { return (IInputElement)GetValue(CommandTargetProperty); }
        set { SetValue(CommandTargetProperty, value); }
    }

    protected override void OnDecreaseLarge()
    {
        try
        {
            Interlocked.Increment(ref _commandArmed);

            base.OnDecreaseLarge();
        }
        finally
        {
            Interlocked.Decrement(ref _commandArmed);
        }
    }

    protected override void OnDecreaseSmall()
    {
        try
        {
            Interlocked.Increment(ref _commandArmed);

            base.OnDecreaseSmall();
        }
        finally
        {
            Interlocked.Decrement(ref _commandArmed);
        }
    }

    protected override void OnIncreaseLarge()
    {
        try
        {
            Interlocked.Increment(ref _commandArmed);

            base.OnIncreaseLarge();
        }
        finally
        {
            Interlocked.Decrement(ref _commandArmed);
        }
    }

    protected override void OnIncreaseSmall()
    {
        try
        {
            Interlocked.Increment(ref _commandArmed);

            base.OnIncreaseSmall();
        }
        finally
        {
            Interlocked.Decrement(ref _commandArmed);
        }
    }

    protected override void OnMaximizeValue()
    {
        try
        {
            Interlocked.Increment(ref _commandArmed);

            base.OnMaximizeValue();
        }
        finally
        {
            Interlocked.Decrement(ref _commandArmed);
        }
    }

    protected override void OnMinimizeValue()
    {
        try
        {
            Interlocked.Increment(ref _commandArmed);

            base.OnMinimizeValue();
        }
        finally
        {
            Interlocked.Decrement(ref _commandArmed);
        }
    }

    protected override void OnThumbDragCompleted(DragCompletedEventArgs e)
    {
        try
        {
            Interlocked.Increment(ref _commandArmed);

            base.OnThumbDragCompleted(e);
        }
        finally
        {
            Interlocked.Decrement(ref _commandArmed);
        }
    }

    protected override void OnThumbDragDelta(DragDeltaEventArgs e)
    {
        try
        {
            Interlocked.Increment(ref _commandArmed);

            base.OnThumbDragDelta(e);
        }
        finally
        {
            Interlocked.Decrement(ref _commandArmed);
        }
    }

    protected override void OnThumbDragStarted(DragStartedEventArgs e)
    {
        try
        {
            Interlocked.Increment(ref _commandArmed);

            base.OnThumbDragStarted(e);
        }
        finally
        {
            Interlocked.Decrement(ref _commandArmed);
        }
    }

    protected override void OnValueChanged(double oldValue, double newValue)
    {
        base.OnValueChanged(oldValue, newValue);

        if (_commandArmed > 0 && IsEnabled && oldValue != newValue)
        {
            ExecuteCommand();
        }
    }

    private void ExecuteCommand()
    {
        var command = Command;
        if (command == null)
            return;

        var parameter = CommandParameter;
        if (command is RoutedCommand routedCommand)
        {
            var target = CommandTarget ?? this;
            if (routedCommand.CanExecute(parameter, target))
            {
                routedCommand.Execute(parameter, target);
            }
        }
        else if (command.CanExecute(parameter))
        {
            command.Execute(parameter);
        }
    }
}

Then you can use this like that:

<local:CommandSlider Value="{Binding CurrentProgress, Mode=OneWay}"
    Command="{Binding ChangeCurrentProgress, Mode=OneWay}"
    CommandParameter="{Binding Value, RelativeSource={RelativeSource Self}}" />

I would also suggest to add some debounce, because during drag of slider's handle, the command will be executed dozens of times.

Donato answered 16/10, 2023 at 8:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.