63

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?

Andreas Grech
  • 105,982
  • 98
  • 297
  • 360

13 Answers13

85

Besides using the Thumb.DragCompleted event you can also use both ValueChanged and Thumb.DragStarted, this way you don’t lose functionality when the user modifies the value by pressing the arrow keys or by clicking on the slider bar.

Xaml:

<Slider ValueChanged="Slider_ValueChanged"
    Thumb.DragStarted="Slider_DragStarted"
    Thumb.DragCompleted="Slider_DragCompleted"/>

Code behind:

private bool dragStarted = false;

private void Slider_DragCompleted(object sender, DragCompletedEventArgs e)
{
    DoWork(((Slider)sender).Value);
    this.dragStarted = false;
}

private void Slider_DragStarted(object sender, DragStartedEventArgs e)
{
    this.dragStarted = true;
}

private void Slider_ValueChanged(
    object sender,
    RoutedPropertyChangedEventArgs<double> e)
{
    if (!dragStarted)
        DoWork(e.NewValue);
}
aloisdg
  • 22,270
  • 6
  • 85
  • 105
Alan
  • 1,301
  • 9
  • 12
  • How can you find the DragStarted and DragCompleted events programatically? – CodyF Jul 08 '15 at 19:05
  • @CodyF: You may be able to override the protected OnThumbDragCompleted and OnThumbDragStarted methods. – ravuya Mar 13 '17 at 21:28
  • Thanks for that; my Visual Studio didn't detect that Thumb.DragStarted was even a possibility! – Ruud van Gaal Apr 25 '18 at 13:55
  • I had an issue with the ValueChanged event being invoked before the page variables (a TextBox) had been initialized. So don't forget your null checking... – tgraupmann Jun 21 '19 at 16:38
  • 1
    It gives me the error `... does not contain a definition for Slider_DragCompleted and no accessible extension method Slider_DragCompleted accepting first argument of type 'MainWindow'` – mrid Jul 13 '19 at 14:10
61

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" />
YotaXP
  • 3,844
  • 1
  • 22
  • 24
21
<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.

Peter
  • 47,963
  • 46
  • 132
  • 181
  • @ minusvoter : cool, 10 years later a minus 1, and the only one, without even leaving a comment :) – Peter Mar 16 '19 at 19:05
  • I like Peter's solution! – BoiseBaked Aug 07 '19 at 19:10
  • Still not sure why you have to use PreviewMouseUp (a tunnel propagating event that is raised first) rather than MouseUp (a bubble propagating event) but that's for another day to understand. – BoiseBaked Aug 08 '19 at 15:20
  • If set `IsMoveToPointEnabled="True"` and `mySlider.AddHandler(Slider.PreviewMouseDownEvent, new MouseButtonEventHandler(mySlider_PreviewMouseDown), true);` , then when I click on the slider (not the thumb), the thumb jumps to the mouse pointer position, then I can't drag the thumb, if I need to drag the thumb, I have to release the mouse and then operate the drag separately. – CodingNinja Feb 09 '22 at 02:16
9

Another MVVM-friendly solution (I was not happy with answers)

View:

<Slider Maximum="100" Value="{Binding SomeValue}"/>

ViewModel:

public class SomeViewModel : INotifyPropertyChanged
{
    private readonly object _someValueLock = new object();
    private int _someValue;
    public int SomeValue
    {
        get { return _someValue; }
        set
        {
            _someValue = value;
            OnPropertyChanged();
            lock (_someValueLock)
                Monitor.PulseAll(_someValueLock);
            Task.Run(() =>
            {
                lock (_someValueLock)
                    if (!Monitor.Wait(_someValueLock, 1000))
                    {
                        // do something here
                    }
            });
        }
    }
}

It's delayed (by 1000 ms in given example) operation. New task is created for every change done by slider (either by mouse or keyboard). Before starting task it signals (by using Monitor.PulseAll, perhaps even Monitor.Pulse would be enough) to running already tasks (if any) to stop. Do something part will only occurs when Monitor.Wait don't get signal within timeout.

Why this solution? I don't like spawning behavior or having unnecessary event handling in the View. All code is in one place, no extra events needed, ViewModel has choice to either react on each value change or at the end of user operation (which adds tons of flexibility, especially when using binding).

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • Reading it with details, apparently is a really good approach, the only issue is that it creates a new thread in each Task.Run... so it can create 100 threads in a little time, does it matter? – Juan Pablo Garcia Coello Feb 24 '16 at 07:14
  • If task become running and reach `Wait` when the property is changed, then pulsing will simply finish it. It's like always waiting for timeout and do *something* only if timeout happens, counting from last change. You are right, there is no synchronization to ensure what task is awaiting, so theoretically several tasks (not 100) may await until timeout. But only single one will be able to obtain lock and perform something at a time. In my case it's not a problem to have **several** of *something* happens one after another one second after user finished updating slider. – Sinatr Feb 24 '16 at 08:15
  • 1
    There is [`Delay`](https://msdn.microsoft.com/en-us/library/system.windows.data.bindingbase.delay(v=vs.110).aspx) property of binding, using some small value (100 ms?) will make this case (when multiple tasks are waiting) very unlikely to happens. Perhaps this property can be used alone as solution. – Sinatr Feb 24 '16 at 08:20
  • Ok, so only few threads will be running because the PulseAll will 'dispose' the Task.Run thread because the Wait was 'canceled' thanks, even cooler – Juan Pablo Garcia Coello Feb 24 '16 at 09:01
  • I really like this, I tried it in a project I'm working on right now and it appears to work perfectly in a UWP app. To be honest I don't think I've used the Monitor class before, so I'm going to look into it in more detail. Very useful – Dan Harris Jan 26 '17 at 23:13
  • What would be the right way to "do something" on the UI thread? I have tried "await Dispatcher.RunAsync()" but then I get the error: "cannot await in the body of lock statement". – Captain Sensible Jul 29 '20 at 10:41
  • @CaptainSensible, consider to ask question if you have issue. You can't use `await` inside `lock`, see [this topic](https://stackoverflow.com/q/7612602/1997232), either do synchronously or use synchronization without thread-affinity (e.g. `SemaphoreSlim`). – Sinatr Jul 29 '20 at 11:14
5

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

Teneko
  • 1,417
  • 9
  • 16
1

Here is a behavior that handles this problem plus the same thing with the keyboard. https://gist.github.com/4326429

It exposes a Command and Value properties. The value is passed as the parameter of the command. You can databind to the value property (and use it in the viewmodel). You may add an event handler for a code-behind approach.

<Slider>
  <i:Interaction.Behaviors>
    <b:SliderValueChangedBehavior Command="{Binding ValueChangedCommand}"
                                  Value="{Binding MyValue}" />
  </i:Interaction.Behaviors>
</Slider>
SandRock
  • 5,276
  • 3
  • 30
  • 49
  • I've downvoted your answer for several reasons: The implementation you provided is pretty unreliable and incorrect. The variable applyKeyUpValue isn't used, keysDown enters in some conditions an invalid state, the ValueProperty is not necessary, as we have access to AssociatedObject.Value and I am not that good in WPF, but I am quite sure that the binding of Value(Property) does not get updated, when you assign NewValue to Value(Property). I've added an alternative behaviour solution in the answers. – Teneko Sep 17 '19 at 06:46
0

My solution is basically Santo's solution with a few more flags. For me, the slider is being updated from either reading the stream or the user manipulation (either from a mouse drag or using the arrow keys etc)

First, I had wrote the code to update the slider value from reading the stream:

    delegate void UpdateSliderPositionDelegate();
    void UpdateSliderPosition()
    {
        if (Thread.CurrentThread != Dispatcher.Thread)
        {
            UpdateSliderPositionDelegate function = new UpdateSliderPositionDelegate(UpdateSliderPosition);
            Dispatcher.Invoke(function, new object[] { });
        }
        else
        {
            double percentage = 0;  //calculate percentage
            percentage *= 100;

            slider.Value = percentage;  //this triggers the slider.ValueChanged event
        }
    }

I then added my code that captured when the user was manipulating the slider with a mouse drag:

<Slider Name="slider"
        Maximum="100" TickFrequency="10"
        ValueChanged="slider_ValueChanged"
        Thumb.DragStarted="slider_DragStarted"
        Thumb.DragCompleted="slider_DragCompleted">
</Slider>

And added the code behind:

/// <summary>
/// True when the user is dragging the slider with the mouse
/// </summary>
bool sliderThumbDragging = false;

private void slider_DragStarted(object sender, System.Windows.Controls.Primitives.DragStartedEventArgs e)
{
    sliderThumbDragging = true;
}

private void slider_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
{
    sliderThumbDragging = false;
}

When the user updates the slider's value with a mouse drag, the value will still change due to the stream being read and calling UpdateSliderPosition(). To prevent conflicts, UpdateSliderPosition() had to be changed:

delegate void UpdateSliderPositionDelegate();
void UpdateSliderPosition()
{
    if (Thread.CurrentThread != Dispatcher.Thread)
    {
        UpdateSliderPositionDelegate function = new UpdateSliderPositionDelegate(UpdateSliderPosition);
        Dispatcher.Invoke(function, new object[] { });
    }
    else
    {
        if (sliderThumbDragging == false) //ensure user isn't updating the slider
        {
            double percentage = 0;  //calculate percentage
            percentage *= 100;

            slider.Value = percentage;  //this triggers the slider.ValueChanged event
        }
    }
}

While this will prevent conflicts, we are still unable to determine whether the value is being updated by the user or by a call to UpdateSliderPosition(). This is fixed by yet another flag, this time set from within UpdateSliderPosition().

    /// <summary>
    /// A value of true indicates that the slider value is being updated due to the stream being read (not by user manipulation).
    /// </summary>
    bool updatingSliderPosition = false;
    delegate void UpdateSliderPositionDelegate();
    void UpdateSliderPosition()
    {
        if (Thread.CurrentThread != Dispatcher.Thread)
        {
            UpdateSliderPositionDelegate function = new UpdateSliderPositionDelegate(UpdateSliderPosition);
            Dispatcher.Invoke(function, new object[] { });
        }
        else
        {
            if (sliderThumbDragging == false) //ensure user isn't updating the slider
            {
                updatingSliderPosition = true;
                double percentage = 0;  //calculate percentage
                percentage *= 100;

                slider.Value = percentage;  //this triggers the slider.ValueChanged event

                updatingSliderPosition = false;
            }
        }
    }

Finally, we're able to detect whether the slider is being updated by the user or by the call to UpdateSliderPosition():

    private void slider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
    {
        if (updatingSliderPosition == false)
        {
            //user is manipulating the slider value (either by keyboard or mouse)
        }
        else
        {
            //slider value is being updated by a call to UpdateSliderPosition()
        }
    }

Hope that helps someone!

beefsupreme
  • 125
  • 1
  • 1
  • 9
0

If you want to get the manipulation ended information even if the user is not using the thumb to change the value (ie clicking somewhere in the track bar), you can attach an event handler to your slider for the pointer pressed and capture lost events. You can do the same thing for the keyboard events

var pointerPressedHandler   = new PointerEventHandler(OnSliderPointerPressed);
slider.AddHandler(Control.PointerPressedEvent, pointerPressedHandler, true);

var pointerCaptureLostHandler   = new PointerEventHandler(OnSliderCaptureLost);
slider.AddHandler(Control.PointerCaptureLostEvent, pointerCaptureLostHandler, true);

var keyDownEventHandler = new KeyEventHandler(OnSliderKeyDown);
slider.AddHandler(Control.KeyDownEvent, keyDownEventHandler, true);

var keyUpEventHandler   = new KeyEventHandler(OnSliderKeyUp);
slider.AddHandler(Control.KeyUpEvent, keyUpEventHandler, true);

The "magic" here is the AddHandler with the true parameter at the end which allows us to get the slider "internal" events. The event handlers :

private void OnKeyDown(object sender, KeyRoutedEventArgs args)
{
    m_bIsPressed = true;
}
private void OnKeyUp(object sender, KeyRoutedEventArgs args)
{
    Debug.WriteLine("VALUE AFTER KEY CHANGE {0}", slider.Value);
    m_bIsPressed = false;
}

private void OnSliderCaptureLost(object sender, PointerRoutedEventArgs e)
{
    Debug.WriteLine("VALUE AFTER CHANGE {0}", slider.Value);
    m_bIsPressed = false;
}
private void OnSliderPointerPressed(object sender, PointerRoutedEventArgs e)
{
    m_bIsPressed = true;
}

The m_bIsPressed member will be true when the user is currently manipulating the slider (click, drag or keyboard). It will be reset to false once done .

private void OnValueChanged(object sender, object e)
{
    if(!m_bIsPressed) { // do something }
}
Vincent
  • 3,656
  • 1
  • 23
  • 32
0

This subclassed version of the Slider wokrs as you want:

public class NonRealtimeSlider : Slider
{
    static NonRealtimeSlider()
    {
        var defaultMetadata = ValueProperty.GetMetadata(typeof(TextBox));

        ValueProperty.OverrideMetadata(typeof(NonRealtimeSlider), new FrameworkPropertyMetadata(
        defaultMetadata.DefaultValue,
        FrameworkPropertyMetadataOptions.Journal | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
        defaultMetadata.PropertyChangedCallback,
        defaultMetadata.CoerceValueCallback,
        true,
        UpdateSourceTrigger.Explicit));
    }

    protected override void OnThumbDragCompleted(DragCompletedEventArgs e)
    {
        base.OnThumbDragCompleted(e);
        GetBindingExpression(ValueProperty)?.UpdateSource();
    }
}
Dominik Palo
  • 2,873
  • 4
  • 29
  • 52
0

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
    }
}
soan saini
  • 230
  • 3
  • 9
0

My solution for the WinUI3 v1.2.2 follows here:

Xaml file:

                <Slider Margin="10, 0" MinWidth="200" LargeChange="0.5"
                                TickPlacement="BottomRight" TickFrequency="10" 
                                SnapsTo="StepValues" StepFrequency="5"
                                Maximum="719" 
                                Value="{x:Bind Path=XamlViewModel.XamlSliderToDateInt, Mode=TwoWay}">
                </Slider>

To-Date slider property:

    private int _sliderToDateInt;
    public int XamlSliderToDateInt
    {
        get { return _sliderToDateInt; }
        set
        {
            SetProperty(ref _sliderToDateInt, value);
            _myDebounceTimer.Debounce(() =>
            {
                this.XamlSelectedTimeChangedTo = TimeSpan.FromMinutes(value);

                // time-expensive methods:
                this.XamlLCModel = _myOxyPlotModel.UpdatePlotModel(_myLCPowerRecList, XamlSliderFromDateInt, XamlSliderToDateInt, _myOxyPlotPageOptions);
                this.XamlTRModel = _myOxyPlotModel.UpdatePlotModel(_myTRPowerRecList, XamlSliderFromDateInt, XamlSliderToDateInt, _myOxyPlotPageOptions);
            },
            TimeSpan.FromSeconds(0.6));
        }
    }

Timer declaration:

        private DispatcherQueueTimer _myDebounceTimer;

Timer initialization in constructor:

        _myDebounceTimer = _dispatcherQueue.CreateTimer();

The method _myOxyPlotModel.UpdatePlotModel() will be called not faster than every 0.6sec, even when the XamlSliderToDateInt property is updated much faster by dragging the slider.

It feels like drag to a position then stop dragging with/without releasing the mouse button and just after the stop the timer counts to 0.6sec and calls my oxyplot-methods.

The Debounce() method belongs to the namespace CommunityToolkit.WinUI.UI.

0

This works for me and covering every angle as well. Based on Peter's answer, you can use PreviewMouseUp for handling when the user completes the dragging. Then you can use PreviewKeyUp for handling the value changes based on arrow keys.

<Slider PreviewMouseUp="MySlider_DragCompleted" PreviewKeyUp="MySlider_KeyCompleted" />

If you use this for command in Interactivity (now XAML Behavior) library:

<behaviors:Interaction.Triggers>
      <behaviors:EventTrigger EventName="PreviewKeyUp">
            <behaviors:InvokeCommandAction Command="{Binding yourcommand}"/>
      </behaviors:EventTrigger>
      <behaviors:EventTrigger EventName="PreviewMouseUp">
           <behaviors:InvokeCommandAction Command="{Binding yourcommand}"/>
     </behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
-1
<Slider x:Name="PositionSlider" Minimum="0" Maximum="100"></Slider>

PositionSlider.LostMouseCapture += new MouseEventHandler(Position_LostMouseCapture);
PositionSlider.AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(Position_DragCompleted));
Andreas Grech
  • 105,982
  • 98
  • 297
  • 360
ggarber
  • 8,300
  • 5
  • 27
  • 32
  • I would be nice to describe your solution. Throwing code like that does not help. – SandRock Apr 03 '19 at 15:40
  • I'll add a description. ggarber is the same as @YotaXP's solution but in code rather than in XAML. No difference in solutions. – BoiseBaked Aug 08 '19 at 15:16
  • Oh and this type of event declaration is called an Attached Event meaning you can attach an event to an element that natively doesn't support the event. – BoiseBaked Aug 08 '19 at 16:07