Question

In WPF, I want to be able to use the mouse events with reactive extensions to create an observable for a UIElement that works like a Click event. There's plenty of examples of using this to create drag/drop behavior, but I can't find anything for just a simple click.

I'm anticipating it'll involve observables on MouseLeftButtonDown, MouseLeftButtonUp, MouseLeave, and MouseEnter. But I'm unsure what combination of Merge, SelectMany, TakeUntil, or TakeWhile I need to use. In trying to wrap it all up in an extension, here is what I have so far:

public static IDisposable GetClick(this UIElement item, Action clickAction)
        {
            var obs1 = Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
                    h => (s, e) => h(s, e),
                    h => item.MouseLeftButtonDown += h,
                    h => item.MouseLeftButtonDown -= h);

            var obs2 = Observable.FromEventPattern<MouseButtonEventHandler, MouseButtonEventArgs>(
                    h => (s, e) => h(s, e),
                    h => item.MouseLeftButtonUp += h,
                    h => item.MouseLeftButtonUp -= h);

            var obs3 = Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>(
                    h => (s, e) => h(s, e),
                    h => item.MouseLeave += h,
                    h => item.MouseLeave -= h);

            var obs4 = Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>(
                    h => (s, e) => h(s, e),
                    h => item.MouseEnter += h,
                    h => item.MouseEnter -= h);

             var finalObs = ???

             return finalObs.Subscribe(x => clickAction.Invoke());

}

Was it helpful?

Solution

The following seems to work, but I suspect that it's possible to do it in a neater way.

var click = mouseEnter
    .SelectMany(_ => mouseDown.TakeUntil(mouseLeave))
    .SelectMany(_ => mouseUp.TakeUntil(mouseLeave).Take(1));

I've renamed finalObs to click, obs1 to mouseDown, obs2 to mouseUp...

EDIT: Added Take(1) to fix the flaw pointed out by Enigmativity

EDIT(2):

Here is another solution that I like more.

You'll need to add a .Select(_ => "U") to the definition of mouseUp, .Select(_ => "D") to mouseDown...

var click = Observable.Merge(mouseDown, mouseUp, mouseLeave, mouseEnter)
    .Scan((s, c) => c == "L" ? "" : s + c)  // Create a string of the events, reset on mouseLeave
    .Where(s => s.Length >= 2 && s.Substring(s.Length - 2) == "DU");

After thinking about it, it's impossible to get exactly correct behavior in the case where the user mouse downs over the item, then moves outside of the item, then moves back and mouse ups. This is because you don't get mouse ups when not over the item, so you can't be sure they didn't mouse up, then mouse down while outside.

OTHER TIPS

The correct way is to use this.CaptureMouse and this.ReleaseMouseCapture which solves some of the problems in the accepted answer to do with detecting the mouse leaving and returning. A full (untested) solution using ReactiveUI to bind the events is.

// Create a factory for capturing the mouse and and releasing it as an 
// IDisposable compatible with Observable.Using
Func<IDisposable> captureDisposable = () => {
        this.CaptureMouse(); 
        return Disposable.Create(()=>this.ReleaseMouseCapture());
};

// Capture the mouse and then release it on mouse up
var up = Observable.Using
    ( captureDisposable
    , capture => this.Events().PreviewMouseUp.Take(1)
    );

// Create the click event
var click = this.Events().PreviewMouseDown.Select(e=>up).Switch();
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top