Using Rx over events on a WPF UserControl, why doe

2020-07-27 04:19发布

问题:

This one's got me a bit baffled.

I've written some extension methods on UIElement to provide Observables on some of the mouse events. Here are the relevant ones:

public static IObservable<EventPattern<MouseEventArgs>> ObserveMouseLeftButtonDown(this UIElement element)
{
    return Observable.FromEventPattern<MouseEventArgs>(element, "MouseLeftButtonDown");
}

public static IObservable<EventPattern<MouseEventArgs>> ObserveMouseMove(this UIElement element)
{
    return Observable.FromEventPattern<MouseEventArgs>(element, "MouseMove");
}

So far, so mind-numbingly simple. Then I create a composite event to detect when the user starts dragging on the UIElement (this is all used by a custom control of mine, the exact nature of which isn't particularly relevant). First here's a little helper function to see if the user's dragged the minimum drag distance:

private static bool MinimumDragSeen(Point start, Point end)
{
    return Math.Abs(end.X - start.X) >= SystemParameters.MinimumHorizontalDragDistance
        || Math.Abs(end.Y - start.Y) >= SystemParameters.MinimumVerticalDragDistance;
}

And then the composite observable itself:

public static IObservable<EventPattern<MouseEventArgs>> ObserveMouseDrag(this UIElement element)
{
    return Observable.CombineLatest(
        element.ObserveMouseLeftButtonDown().Select(ep => ep.EventArgs.GetPosition(element)),
        element.ObserveMouseMove().Where(ep => ep.EventArgs.LeftButton == MouseButtonState.Pressed),
        (md, mm) => new { Down = md, Move = mm })
        .Where(i => MinimumDragSeen(i.Down, i.Move.EventArgs.GetPosition(element)))
        .Select(i => i.Move);
}

This all works nicely, after much gnashing of teeth and annoyance that there aren't any decent examples for Rx-based drag and drop with WPF controls (most of them are naively simplistic duplicates of dragging an image around in a canvas).

The problem comes when the window is maximized, specifically by double-clicking on the title bar. If, in the maximized layout, one of my controls which is subscribed to its own ObserveMouseDrag() ends up under the mouse cursor it receives a MouseLeftButtonDown and a MouseMove, one of them apparently before the maximize and the other one afterwards. The net result is that it starts a drag event, which then immediately stops as the mouse button has been released, and then tends to cause the control to drop onto itself, which in some situations in this app actually does something.

This is extremely odd, because I do not see why I should be receiving MouseDown and MouseMove caused by a double-click maximize. Surely all the mouse events involved in that should be handled by Windows, because I'm not using custom window borders or anything like that.

So, does anybody have any ideas?

The following day...

Fixed it! (With some help from Lee's answer below, and this question: What is the proper way to determine the end of a mouse drag using Rx?)

The code now looks like this:

public static IObservable<EventPattern<MouseEventArgs>> ObserveMouseDrag(this UIElement element)
{
    var mouseDown = element.ObserveMouseLeftButtonDown().Select(ep => ep.EventArgs.GetPosition(element));
    var mouseMove = element.ObserveMouseMove();

    var stop = Observable.Merge(
        element.ObserveMouseUp(),
        element.ObserveMouseLeave().Where(ep => ep.EventArgs.LeftButton == MouseButtonState.Pressed)
    );

    return mouseDown.SelectMany(
        md => mouseMove
                .Where(ep => ep.EventArgs.LeftButton == MouseButtonState.Pressed)
                .Where(ep => MinimumDragSeen(md, ep.EventArgs.GetPosition(element)))
                .TakeUntil(stop)
        );
    }

And handles minimum drag distance, mouse up events and all kinds of things necessary for drag to work entirely properly. The maximize bug has gone away as well (although I still don't entirely understand that one, I suspect the mouse up handling might have something to do with it).

The key thing here is using SelectMany to handle multiple streams of events from mouseMove until either mouseUp in the control, or the mouse pointer leaving it with the mouse button down.

回答1:

Sounds like it could be a bug in that I am not sure why a mouse move first when you maximise the window.

I would use selectMany + takeuntil instead of combine latest. ie:

var mouseDown = element.ObserveMouseLeftButtonDown().Select(ep => ep.EventArgs.GetPosition(element));
var mouseUp = element.ObserveMouseLeftButtonUp();
var mouseMove = element.ObserveMouseMove();
var from md in mouseDown  
    from mm in mouseMove.TakeUntil(mouseUp)
    where MinimumDragSeen(md, mm.EventArgs.GetPosition(element)))   
    select mm;

This will now only kick off a move when the mouse is down, and kill it as soon as the mouse is up. I dont think you have the feature of MouseUp in your code.

I would also consider trying to avoid storing the element and using it as state as I think this is available via the event Args. I would also consider using Zip on the MouseMove so you can get just the delta of the last mouse move change (if appropriate).

I would be really keen to hear about what you are doing. I am in the middle of writting a book on Rx and at work building I just finished buidling some WPF controls that use Drag Drop.

Hope that helps

Lee