How do I throttle a slider's value change event?
Asked Answered
H

2

17

I got a slider that on value change forces a fairly serious computation, so I want to throttle it to fire actual event after for example 50ms pass when user has finished sliding it.

While I learned some various stuff about Rx its unclear how should I approach this using MVVM pattern.

In my current MVVM approach I got slider value bound to my viewModel. I would prefer to add Rx throttle with minimal possible impact on existing code (as a beginning at least).

Ive seen some other threads about MVVM and Rx and I don't think they lead me to some exact direction with my problem. I see various possible approaches and would like not to invent a bycicle.

Haematoma answered 20/9, 2011 at 14:18 Comment(1)
Fair enough, but please trust our tags system. You can get an RSS feed on, say, the [system.reactive] tag, but you can't get a feed of questions that have "system reactive" in the title.Curiosa
M
26

In this case, you should bind to the PropertyChanged event of your ViewModel, something like:

Observable.FromEvent<PropertyChangedEventArgs>(x => this.PropertyChanged +=x, x => this.PropertyChanged -= x)
    .Where(x => x.PropertyName == "SliderName")
    .Select(_ => this.SliderName)
    .Throttle(TimeSpan.FromMilliseconds(50));

Or, if you were using ReactiveUI, it'd look like this:

this.WhenAnyValue(x => x.SliderName)
    .Throttle(TimeSpan.FromMilliseconds(50), RxApp.DeferredScheduler);
Medlock answered 20/9, 2011 at 17:47 Comment(3)
In the ViewModel, the View should be binding to a meaningful property on the ViewModelMedlock
So if I'm understanding this correctly do you then need something to subscribe to this observable?Lancashire
Yeah, you'd maybe do .Subscribe(x => DebouncedSliderValue = x), or maybe actually take the action that you were going to do, like .Subscribe(x => RecalculateTheStuff(x))Medlock
L
-7

Lets just outline the problem. You have a View Model which has some double typed Property. When a value is assigned to this property a fairly expensive calculation takes place. Wouldn't normally be a problem but when the UI binds the value of a Slider to this property the rapid changes generated does create a problem.

First decision to be made is between the view and view-model which is responsible for dealing with this problem. It could be argued both ways the View-Model has "chosen" to make a property assignment an expensice operatione on the other hand the View has "chosen" to assign the property using a Slider.

My choice would be on view side of things because thats a better place to implement this. However rather than fiddle with the View directly I would build a new Control to add the feature. Let's call it the DelaySlider. It will derive from Silder and have two additional dependency properties Delay and DelayedValue. The DelayedValue will match the existing value of Value property but only after Delay milliseconds have elapsed since the last Value changed.

Here is the full code for the control:-

public class DelaySlider : Slider
{
    private DispatcherTimer myTimer;

    private bool myChanging = false;

    #region public double DelayedValue
    public double DelayedValue
    {
        get { return (double)GetValue(DelayedValueProperty); }
        set { SetValue(DelayedValueProperty, value); }
    }

    public static readonly DependencyProperty DelayedValueProperty =
        DependencyProperty.Register(
            "DelayedValue",
            typeof(double),
            typeof(DelaySlider),
            new PropertyMetadata(0.0, OnDelayedValuePropertyChanged));

    private static void OnDelayedValuePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        DelaySlider source = d as DelaySlider;
        if (source != null && !source.myChanging)
        {
            source.Value = (double)e.NewValue;
        }
    }
    #endregion public double DelayedValue

    #region public int Delay

    public int Delay
    {
        get { return (int)GetValue(DelayProperty); }
        set { SetValue(DelayProperty, value); }
    }

    public static readonly DependencyProperty DelayProperty =
        DependencyProperty.Register(
            "Delay",
            typeof(int),
            typeof(DelaySlider),
            new PropertyMetadata(0, OnDelayPropertyChanged));

    private static void OnDelayPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        DelaySlider source = d as DelaySlider;
        if (source != null)
        {
            source.OnDelayPropertyChanged((int)e.OldValue, (int)e.NewValue);
        }
    }

    private void OnDelayPropertyChanged(int oldValue, int newValue)
    {
        if (myTimer != null)
        {
            myTimer.Stop();
            myTimer = null;
        }

        if (newValue > 0)
        {
            myTimer = new DispatcherTimer();
            myTimer.Tick += myTimer_Tick;

            myTimer.Interval = TimeSpan.FromMilliseconds(newValue);
        }
    }

    void myTimer_Tick(object sender, EventArgs e)
    {
        myTimer.Stop();
        myChanging = true;
        SetValue(DelayedValueProperty, Value);
        myChanging = false;
    }
    #endregion public int Delay


    protected override void OnValueChanged(double oldValue, double newValue)
    {
        base.OnValueChanged(oldValue, newValue);
        if (myTimer != null)
        {
            myTimer.Start();
        }

    }
}

Now replace your Silder with DelaySlider and bind your View-Model property to the DelayedValue and specify your millisecond delay value in its Delay property.

You now have a useful re-usable control, you haven't messed about with nasty tricks in the View, you have no additional code in the code-behind of the view, the View-Model is unchanged and undisturbed and you haven't had to do include the Rx stuff at all.

Lancashire answered 20/9, 2011 at 21:43 Comment(10)
Hmm Anthony, I wonder, do you dislike Rx in general? When I look at this and another answer I think another is much more clean. Also imagine that I am using some advanced slider from paid controls library, I migh't be unable to create my own, but with delay so easily. Actually In my case I am using range slider which is fairly complex by itself without a couple of timers on both values, so In my case I think Rx is way better. Adding a reference on RX assembly and rewriting a few lines of code can't be tooo bad.Haematoma
Lol, my solution is 2 lines long. This is a textbook reason as to why you should use Rx.Medlock
@Valentin: Please re-read my first paragraph. Does it describe the scenario that you are facing well enough?Lancashire
@Paul: I'm going to stick to my guns here.Lancashire
Couple issues with the above code: 1) Without Rx it is pretty hard to understand what is going on. 2) It doesn't implement throttling, but rather delays updates until Value stops changing, i.e. if I keep moving slider DelayedValue will not change until I stop. Issues like that make Rx tax worth paying in most cases.Mandibular
@Mandibular re. your point #2, Throttle has the same behavior actually - however, it wouldn't be hard to create the behavior you describeMedlock
@Denis: I also disagree with point 1 and Paul where "short" seems to be equated with "Simple". The vast majority of the code above is simple boiler plate Dependency property code which was created with a snippet. However the really important point is that its done once and then the resulting control can be re-used. Whereas using Rx requires a) a developer to understand Rx (a SL dev really ought to grasp simple DPs but I wouldn't demand they know Rx) and b) its there each time its used in your View model code. Each time a dev reading the code has to stop and ask "Whats that doing?"Lancashire
How reusable is this solution? Can this delay be applied to a textbox? A numeric up-down? Do I need to reimplement every input control just to have delayed values?Cimino
@CameronMacFarland: Yes, yes, yes and yes. How many types of input control are you thinking of using this technique on? Compare that number with how many instances of these types you would need to implement Rx the technique on. My approach here gets implemented per type (and is, as I point out in my comments above, actually quite simple) whereas the "clever" approach needs to be implement per instance. This answer has many down votes and normally I would delete such an answer however I leave it here because a) it gives devs some pause for thought and b) I still think I'm right.Lancashire
> "whereas the "clever" approach needs to be implement per instance" - You'd reuse it the same way you'd reuse any other piece of code, by creating a methodMedlock

© 2022 - 2024 — McMap. All rights reserved.