1

I'm registering to an event of a UserControl, this event is thrown every time I move a scrollbar(meaning that we will receive a lot of events). Unfortunately, we have no events to be informed when the user finished to use the scrollbar(== MouseUp).

We decided to implement a mechanism to update our models only once we didn't receive any new notifications from the scrollbar since 300ms.

I can see how to do this with a Timer and reseting the timer everytime the ScrollBar event is comming.

I was wondering if there was a way to do it with Linq and Delay?

private Action _actionOnMoved;

private void OnScrollBarMoved(object sender, EventArgs args){

}

EDIT

I've read the potentially duplicated answer, but Throttling is not the same thing that what I'm asking. In my case, as far I've event coming in less than the specified time, I don't want to do anything(not even the first one), since applying this change will take 3-5 seconds to be retrieved.

So what differs:

  • In the given link, the first call is executed no matter what
  • In my case, if a call is made within the delay, the call should still be executed AFTER the delay

By example, if this event is triggered every 100ms, and that the delay is 300ms, I expect to never have my method called.

J4N
  • 19,480
  • 39
  • 187
  • 340
  • 3
    What is your platform? winforms, wpf or ASP – Kira Apr 04 '16 at 06:36
  • It's Winform, but let's imagine we just receive an event from a third party library. I'm more interessted on the delay problematic than on the how to register to a more specific event. – J4N Apr 04 '16 at 07:03
  • Although this can be solved using a similar approach, I vote to reopen the question anyway because the duplicate deals with a throttling issue, which is different as what is asked here. – Larry Apr 04 '16 at 07:55
  • @Larry Exactly what I was thinking, thank you – J4N Apr 04 '16 at 08:07
  • *"If this event is triggered every 100ms, and that the delay is 300ms, I expect to never have my method called"* - that's sounds wrong. If user is fast enough, then he will be able to bug UI by quickly moving scrollbar by small portions (for less than 300ms). You will never rise event handler then. Throttling seems to be the right way to do it: *react immediately*, but prevent event spamming. If you want a kind of wpf's [`Delay`](https://msdn.microsoft.com/en-us/library/system.windows.data.bindingbase.delay(v=vs.110).aspx), then approach might be slightly but different indeed. – Sinatr Apr 04 '16 at 08:27
  • @Sinatr Yes, but he will eventually stop one day to move, and at this point, we will update. But if he wants to spend his day to move the scrollbar, let him enjoy ;). – J4N Apr 04 '16 at 08:35

3 Answers3

1

You can wrap event handler into following

object _lock = new object();

SomeEvent += (s, e) =>
{
    lock(_lock)
    {
        Monitor.PulseAll(_lock); // pulse any (if any) awaiting events
        if(!Monitor.Wait(_lock, 5000)) // delay
        {
            ... // call event handler
                // we are here if timeout is expired
        }
    }
}

Untested, but here is the idea: when event is received you start waiting for either pulse or timeout. If pulse come (means another event was received), then you simply exit. If timeout occurs (means no another event happens), then you call event handler.

Note: it's not asynchronous, event caller (the call or SomeEvent) will be blocked for duration of delay. You may need to wrap lock into Task.Run:

SomeEvent += (s, e) => Task.Run(() =>
{
    ... // the rest of code
}

but that would rise event handler in some other thread, so you may have to dispatch (invoke) into thread you need.

Sinatr
  • 20,892
  • 15
  • 90
  • 319
  • Every event has to "reset" the time to wait. And at the end of the time to wait, I need to have only one event – J4N Apr 04 '16 at 08:50
  • @J4N, that's exactly what is happening. Need more explanation? What is not clear? How waiting event get discarded? It's discarded because there will be a pulse before timeout is expired and condition `if` is false, so your event handler is not called. – Sinatr Apr 04 '16 at 08:51
  • I didn't read it correctly, I think this should work nicely. I will use the async way and test it, but this seems good – J4N Apr 04 '16 at 08:59
  • @J4N, I don't know how to make it async (`Task.Run` is a workaround). If you do - then post and accept own answer. – Sinatr Apr 04 '16 at 09:00
  • By async I meant not synchronous, didn't meant to use async/await. Your solution seems fine.(Is there a difference between `Task.Run` and `Task.Factory.StartNew` ? – J4N Apr 04 '16 at 09:05
  • @J4N, `Task.Run` is just a [shortcut](http://stackoverflow.com/a/29693430/1997232). – Sinatr Apr 04 '16 at 09:10
  • Do you think It is possible to put the `Task.Run` just before the `Monitor.Wait`? See my own answer to show the difference – J4N Apr 04 '16 at 10:27
  • @J4N, should be possible. Because `lock` + `Pulse` chain is pretty quick you can run it synchronously and only `Wait` has to be in another task. However, you have to synchronize release or lock then, because you want task to reach `Wait` before next event `Pulse`. Or you can make requirement less strict: allow more than one event rise after delay (because without synchronizaton it is possible what while task is still starting - another event occurs and `Pulse` happens before someone is at `Wait`, this is not a prob, but will cause event handler to be possible called twice). – Sinatr Apr 04 '16 at 11:51
0

Use a while loop and ThreadPool to monitor the event.

class MyForm : Form {
    private volatile bool mouseWheelHandling = false;
    private UserControl userControl;

    public void Form_Load(){
        // userControl.Scroll 
        userControl.MouseWheel += (s, a) => {
            if(!mouseWheelHandling) { // capture MouseWheel and Scroll events, only handle 1 times.
                mouseWheelHandling = true;
                ThreadPool.QueueUserWorkItem(_=>{
                    var horizontalScroll = userControl.HorizontalScroll.Value;
                    while(true) {
                        Thread.Sleep(100);
                        if(horizontalScroll == userControl.HorizontalScroll.Value) {
                            // Control.Invoke to handle cross-thread action
                            // do action
                            mouseWheelHandling = false; // reset the flag
                            break;
                        }
                        horizontalScroll = userControl.HorizontalScroll.Value;
                    }
                });
            }
        }
    }
}
ROY
  • 8,800
  • 1
  • 17
  • 13
  • First mouseWheel is not locked. Second, I not a big fan of doing polling, really looks like a bad idea(CPU usage) – J4N Apr 04 '16 at 09:03
0

I did ended by using a modified version of Sinatr answer:

public static EventHandler CreateDelayedEventHandler(EventHandler<EventArgs> handler, TimeSpan delay)
{
    object lockObject = new object();

    return (s, e) =>
    {
        Task.Run(() =>
        {
            lock (lockObject)
            {
                Monitor.PulseAll(lockObject);
                if (!Monitor.Wait(lockObject, delay))
                {
                    handler(s, e);
                }
            }
        });
    };
}

The only doubt I've now is how many task will be created to just potentially do nothing.

J4N
  • 19,480
  • 39
  • 187
  • 340
  • *"how many task will be created to just potentially do nothing"* - for each event arising just one task will be awaiting. It is possible what many tasks will be created and wait before `lock` (or even not start), but the last created task will terminate (by pulsing) any awating (btw, `PulseAll` is not needed, `Pulse` is enough, lock ensure what only single task is able to reach `Wait` and only single task will obtain lock afterwards). I wouldn't worry about this. If you want you can measure specific event behavior to see if it rises too many events and limit maximum number of tasks you allow. – Sinatr Apr 04 '16 at 11:46
  • @Sinatr Yes, but does that cost much? To create Task I mean? I know creating a thread was expensive, I don't know about Tasks. I saw for the `PulseAll`. In my case, I may receive suddenly 200 events in 1sec, so I'm not sure how many resources 200 new tasks will use – J4N Apr 04 '16 at 14:50