none
Create Holding event from MouseDown that is not followed by MouseUp within x ms RRS feed

  • Question

  • How do I create a sequence that would issue a single event when MouseDown is NOT followed by MouseUp within certain time?
    Wednesday, September 24, 2014 12:34 AM

Answers

  • Hi Niall,

    That was the first thing I tried too, but unfortunately it didn't work.  IIRC, Throttle continued to push out the "down" events anyway, for the lifetime of the outer observable's subscription, but I don't recall why.

    > I guess even just selecting a new { MouseDown = true } would make it a little clearer - power of T again and all.

    Agreed.  It's unfortunate that we even have to resort to it in this case.  I think this particular problem makes yet another case for the definition of additional Throttle (and Sample) operators with slight differences in behavior.  I've been looking into this for Rxx actually and may include it in a subsequent release.

    - Dave


    http://davesexton.com/blog

    Thursday, September 25, 2014 4:44 AM

All replies

  • I think you can write it as follows. It would issue a single event when the button is held down for one second. LUp and LDown are observables created from your mouse down and up event.

                var LHold = LDown
                    .Take(1)
                    .SelectMany(x => Observable.Timer(TimeSpan.FromSeconds(1.0)))
                    .TakeUntil(LUp)
                    ;
    

    Wednesday, September 24, 2014 4:16 AM
  • Use Throttle?  It is designed for this purpose.

    Wednesday, September 24, 2014 5:30 AM
  • Hi,

    One way to think about this kind of problem is in terms of windows.  A window captures the idea of correlating events, represented as a duration.

    Start by getting observables that represent the constituent parts of the query.

    var downs = GetMouseDowns();
    var ups = GetMouseUps();

    Next, define the correlation by writing a window query.  This can be done in several ways, but I'll show the most declarative solution here, the Window operator:

    var downDurations = downs.Window(() => ups);

    Note that you'll probably want to use some kind of mouse capture to ensure that these events are raised in pairs.

    Now we have an observable of windows that describes the durations of mouse click events.  downDurations is an IObservable<IObservable<YourEventType>>

    Finally, we can project your logic into a Throttle query (though due to a small technicality this isn't entirely correct, so keep reading):

    var slowClicks =
        from duration in downDurations
        from downWithoutUp in duration.Throttle(threshold).Take(1)
        select downWithoutUp;

    Notice how the final query is very close to the specification.  Look what happens when I annotate this query with your spec:

    var slowClicks =
        from duration in downDurations  // MouseDown followed by MouseUp
        from downWithoutUp in duration.Throttle(threshold).Take(1)  // is NOT within certain time
        select downWithoutUp;  // single event

    Take(1) is necessary in case the mouse is released after the threshold has elapsed; otherwise, the query would generate another value for the up event.

    Unfortunately, the behavior of Throttle is to push the last buffered notification when the source completes, even if the threshold hasn't elapsed; i.e., when the duration completes due to a mouse up event before the threshold, you'll still get a downWithoutUp notification anyway!  Since we can't eliminate the completion notification for a few reasons, and we can't change the behavior of Throttle, and you probably don't want to define your own complex throttling operator, the next best solution is to use the power of T.

    We must filter out the notification from Throttle if it's caused by an OnCompleted notification in the source, so we can use T to encapsulate our logic.  If you need to keep the event arguments around you can use a tuple, for example, though I'm just going to eliminate the original event arguments entirely for the sake of simplicity.

    (Note that Throttle introduces concurrency, so if you must observe notifications on the UI thread, then you'll probably want to pass DispatcherScheduler as the second argument to this overload of Throttle as shown below; alternatively, you could add ObserveOnDispatcher before calling Subscribe, as perhaps most people do, though it may be less efficient.)

    var slowClicks =
        from duration in downDurations  // MouseDown followed by MouseUp
        from downWithoutUp in duration
            .Select(_ => true)  // Down event
            .Concat(Observable.Return(false))  // Up event
            .Throttle(threshold, DispatcherScheduler.Current).Take(1)  // is NOT within certain time
        where downWithoutUp
        select downWithoutUp;  // single event

    - Dave


    http://davesexton.com/blog

    • Edited by Dave Sexton Wednesday, September 24, 2014 7:22 AM
    Wednesday, September 24, 2014 7:20 AM
  • Thank you all, particularly Dave for such a thorough explanation. I have learned a lot! Thanks
    Wednesday, September 24, 2014 2:49 PM
  • There seems to be a problem though: I only get an event on every 2nd long hold. I am not sure how you debug the statement though to see where it is eating up events after the first down-delay-up sequence. Any ideas?
    Wednesday, September 24, 2014 3:30 PM
  • Hi,

    That's strange.  I tested it before posting and it seemed to work for me.

    To debug Rx queries the Do operator is your best friend.  Just lace your query with it, before and after operators, and call Debug.WriteLine.  That should be a good start.

    - Dave


    http://davesexton.com/blog

    Wednesday, September 24, 2014 3:53 PM
  • You are right - works fine. I was testing it by doing MessageBox.Show in my Subscribe, which changed window focus so my next mouse down was used up by OS to re-select my original window. Thanks again.
    Wednesday, September 24, 2014 4:41 PM
  • I haven't tested this out, but I'm thinking another approach could be to concat the Throttled observable with a .Never so that Throttle will not see an OnComplete.

    From your original solution:

    var slowClicks =
        from duration in downDurations  // MouseDown followed by MouseUp
        from downWithoutUp in duration.Concat(Observable.Never<T>()). Throttle(threshold).Take(1)  // is NOT within certain time
        select downWithoutUp;  // single event

    I'm a fan of the Power of T approaches to building event models, but sometimes with examples like this it can make it a little hard to quickly grasp what the query is doing. True or false what? You have to go find a where clause to work out what the true or false means.

    I guess even just selecting a new { MouseDown = true } would make it a little clearer - power of T again and all.

    Wednesday, September 24, 2014 10:46 PM
  • Hi Niall,

    That was the first thing I tried too, but unfortunately it didn't work.  IIRC, Throttle continued to push out the "down" events anyway, for the lifetime of the outer observable's subscription, but I don't recall why.

    > I guess even just selecting a new { MouseDown = true } would make it a little clearer - power of T again and all.

    Agreed.  It's unfortunate that we even have to resort to it in this case.  I think this particular problem makes yet another case for the definition of additional Throttle (and Sample) operators with slight differences in behavior.  I've been looking into this for Rxx actually and may include it in a subsequent release.

    - Dave


    http://davesexton.com/blog

    Thursday, September 25, 2014 4:44 AM