在WPF中,我希望能够使用带有响应式扩展的鼠标事件来为一个像Click事件一样工作的UIElement创建一个可观察对象。有很多使用它来创建拖放行为的例子,但我找不到任何用于简单单击的例子。
我预计它将涉及到MouseLeftButtonDown, MouseLeftButtonUp, MouseLeave和MouseEnter上的可观察对象。但我不确定合并,SelectMany, TakeUntil或TakeWhile我需要使用什么组合。在尝试将其全部包装在一个扩展中,以下是我到目前为止所做的:
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());
}
下面的代码似乎可以工作,但是我怀疑是否可以用一种更简洁的方式来完成。
var click = mouseEnter
.SelectMany(_ => mouseDown.TakeUntil(mouseLeave))
.SelectMany(_ => mouseUp.TakeUntil(mouseLeave).Take(1));
我已经将finalObs
重命名为click
, obs1
重命名为mouseDown
, obs2
重命名为mouseUp
…
编辑:添加Take(1)来修复Enigmativity指出的缺陷
编辑(2):这是另一个我更喜欢的解决方案。
你需要添加一个.Select(_ => "U")
到mouseUp的定义,.Select(_ => "D")
到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");
仔细想想,在用户将鼠标放在项目上,然后移到项目外,然后移回并抬起鼠标的情况下,不可能获得完全正确的行为。这是因为当你不在项目上方时,你不会让鼠标向上,所以你不能确定他们没有鼠标向上,然后鼠标向下,而在外面。
正确的方法是使用this.CaptureMouse
和this.ReleaseMouseCapture
,它们解决了在检测鼠标离开和返回的公认答案中的一些问题。使用ReactiveUI绑定事件的完整(未经测试的)解决方案如下:
// 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();