WPF: Slider with an event that triggers after a user drags

Viewed 78203

I am currently making an MP3 player in WPF, and I want to make a slider that will allow the user to seek to a particular position in an MP3 by sliding the slider to the left or right.

I have tried using the ValueChanged event but that triggers every time it's value is changed, so if you drag it across, the event will fire multiple times, I want the event to only fire when the user has finished dragging the slider and Then get the new value.

How can I achieve this?


[Update]

I have found this post on MSDN which basically discusses the same thing, and they came up with two "solutions"; either subclassing the Slider or invoking a DispatcherTimer in the ValueChanged event that invokes the action after a timespan.

Can you come up with anything better then the two mentioned above?

11 Answers

You can use the thumb's 'DragCompleted' event for this. Unfortunately, this is only fired when dragging, so you'll need to handle other clicks and key presses separately. If you only want it to be draggable, you could disable these means of moving the slider by setting LargeChange to 0 and Focusable to false.

Example:

<Slider Thumb.DragCompleted="MySlider_DragCompleted" />
<Slider PreviewMouseUp="MySlider_DragCompleted" />

works for me.

The value you want is the value after a mousup event, either on clicks on the side or after a drag of the handle.

Since MouseUp doesn't tunnel down (it is handeled before it can), you have to use PreviewMouseUp.

My implementation is based on @Alan's and @SandRock's answer:

public class SliderValueChangeByDragBehavior : Behavior<Slider>
    {
        private bool hasDragStarted;

        /// <summary>
        /// On behavior attached.
        /// </summary>
        protected override void OnAttached()
        {
            AssociatedObject.AddHandler(Thumb.DragStartedEvent, (DragStartedEventHandler)Slider_DragStarted);
            AssociatedObject.AddHandler(Thumb.DragCompletedEvent, (DragCompletedEventHandler)Slider_DragCompleted);
            AssociatedObject.ValueChanged += Slider_ValueChanged;

            base.OnAttached();
        }

        /// <summary>
        /// On behavior detaching.
        /// </summary>
        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.RemoveHandler(Thumb.DragStartedEvent, (DragStartedEventHandler)Slider_DragStarted);
            AssociatedObject.RemoveHandler(Thumb.DragCompletedEvent, (DragCompletedEventHandler)Slider_DragCompleted);
            AssociatedObject.ValueChanged -= Slider_ValueChanged;
        }

        private void updateValueBindingSource()
            => BindingOperations.GetBindingExpression(AssociatedObject, RangeBase.ValueProperty)?.UpdateSource();

        private void Slider_DragStarted(object sender, DragStartedEventArgs e)
            => hasDragStarted = true;

        private void Slider_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
        {
            hasDragStarted = false;
            updateValueBindingSource();
        }

        private void Slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
        {
            if (!hasDragStarted)
                updateValueBindingSource();
        }
    }

You can apply it in that way:

...
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:myWhateverNamespace="clr-namespace:My.Whatever.Namespace;assembly=My.Whatever.Assembly"
...

<Slider
                x:Name="srUserInterfaceScale"
                VerticalAlignment="Center"
                DockPanel.Dock="Bottom"
                IsMoveToPointEnabled="True"
                Maximum="{x:Static localLibraries:Library.MAX_USER_INTERFACE_SCALE}"
                Minimum="{x:Static localLibraries:Library.MIN_USER_INTERFACE_SCALE}"
                Value="{Binding Source={x:Static localProperties:Settings.Default}, Path=UserInterfaceScale, UpdateSourceTrigger=Explicit}">
                <i:Interaction.Behaviors>
                    <myWhateverNamespace:SliderValueChangeByDragBehavior />
                </i:Interaction.Behaviors>
            </Slider>

I've set the UpdateSourceTrigger to explicit, as the behaviour does it. And you are in need of the nuget package Microsoft.Xaml.Behaviors(.Wpf/.Uwp.Managed).

I liked Answer by @sinatr.

My Solution Based on Answer Above: This solution cleans up the code a lot and encapsulates the mechanism.

public class SingleExecuteAction
{
    private readonly object _someValueLock = new object();
    private readonly int TimeOut;
    public SingleExecuteAction(int timeOut = 1000)
    {
        TimeOut = timeOut;
    }

    public void Execute(Action action)
    {
        lock (_someValueLock)
            Monitor.PulseAll(_someValueLock);
        Task.Run(() =>
        {
            lock (_someValueLock)
                if (!Monitor.Wait(_someValueLock, TimeOut))
                {
                    action();
                }
        });
    }
}

Use it in Your class as:

public class YourClass
{
    SingleExecuteAction Action = new SingleExecuteAction(1000);
    private int _someProperty;

    public int SomeProperty
    {
        get => _someProperty;
        set
        {
            _someProperty = value;
            Action.Execute(() => DoSomething());
        }
    }

    public void DoSomething()
    {
        // Only gets executed once after delay of 1000
    }
}
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>

PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));
Related