反应式扩展 (Rx) + MVVM =?
-
21-09-2019 - |
题
用于解释响应式扩展 (Rx) 强大功能的主要示例之一是将现有鼠标事件组合成一个新的“事件”,表示鼠标拖动期间的增量:
var mouseMoves = from mm in mainCanvas.GetMouseMove()
let location = mm.EventArgs.GetPosition(mainCanvas)
select new { location.X, location.Y};
var mouseDiffs = mouseMoves
.Skip(1)
.Zip(mouseMoves, (l, r) => new {X1 = l.X, Y1 = l.Y, X2 = r.X, Y2 = r.Y});
var mouseDrag = from _ in mainCanvas.GetMouseLeftButtonDown()
from md in mouseDiffs.Until(
mainCanvas.GetMouseLeftButtonUp())
select md;
来源: Matthew Podwysocki 的反应式框架简介系列.
在 MVVM 中,我通常努力使 .xaml.cs 文件尽可能为空,并且纯粹在标记中使用视图模型中的命令连接视图中的事件的一种方法是使用以下行为:
<Button Content="Click Me">
<Behaviors:Events.Commands>
<Behaviors:EventCommandCollection>
<Behaviors:EventCommand CommandName="MouseEnterCommand" EventName="MouseEnter" />
<Behaviors:EventCommand CommandName="MouseLeaveCommand" EventName="MouseLeave" />
<Behaviors:EventCommand CommandName="ClickCommand" EventName="Click" />
</Behaviors:EventCommandCollection>
</Behaviors:Events.Commands>
</Button>
来源: 布赖恩·杰尼西奥.
响应式框架似乎更适合传统的 MVC 模式,其中控制器知道视图并可以直接引用其事件。
但是,我想鱼与熊掌兼得!
您将如何结合这两种模式?
解决方案
我编写了一个框架来代表我对这个问题的探索,称为 反应式用户界面
它实现了 Observable ICommand,以及通过 IObservable 发出更改信号的 ViewModel 对象,以及将 IObservable“分配”给属性的能力,然后每当 IObservable 发生更改时,该属性就会触发 INotifyPropertyChange。它还封装了许多常见模式,例如让 ICommand 在后台运行任务,然后将结果编组回 UI。
我现在的文档绝对为零,但我将在接下来的几天里努力添加这些信息,以及我编写的示例应用程序
更新: 我现在已经有了很多文档,请查看 http://www.reactiveui.net
其他提示
我的问题的解决方案是创建一个同时实现 ICommand 和 IObservable<T> 的类
ICommand 用于绑定 UI(使用行为),然后可以在视图模型中使用 IObservable 来构造复合事件流。
using System;
using System.Windows.Input;
namespace Jesperll
{
class ObservableCommand<T> : Observable<T>, ICommand where T : EventArgs
{
bool ICommand.CanExecute(object parameter)
{
return true;
}
event EventHandler ICommand.CanExecuteChanged
{
add { }
remove { }
}
void ICommand.Execute(object parameter)
{
try
{
OnNext((T)parameter);
}
catch (InvalidCastException e)
{
OnError(e);
}
}
}
}
其中 Observable<T> 显示在 从头开始实现 IObservable
当我开始思考如何“联姻”MVVM 和 RX 时,我首先想到的是 ObservableCommand:
public class ObservableCommand : ICommand, IObservable<object>
{
private readonly Subject<object> _subj = new Subject<object>();
public void Execute(object parameter)
{
_subj.OnNext(parameter);
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
public IDisposable Subscribe(IObserver<object> observer)
{
return _subj.Subscribe(observer);
}
}
但后来我认为将控件绑定到 ICommand 属性的“标准”MVVM 方式不太符合 RX 风格,它将事件流分解为相当静态的耦合。RX 更多的是关于事件,以及聆听 执行 路由事件似乎是合适的。这是我想出的:
1) 您有一个 CommandRelay 行为,您将其安装在每个用户控件的根目录中,该行为应响应命令:
public class CommandRelay : Behavior<FrameworkElement>
{
private ICommandSink _commandSink;
protected override void OnAttached()
{
base.OnAttached();
CommandManager.AddExecutedHandler(AssociatedObject, DoExecute);
CommandManager.AddCanExecuteHandler(AssociatedObject, GetCanExecute);
AssociatedObject.DataContextChanged
+= AssociatedObject_DataContextChanged;
}
protected override void OnDetaching()
{
base.OnDetaching();
CommandManager.RemoveExecutedHandler(AssociatedObject, DoExecute);
CommandManager.RemoveCanExecuteHandler(AssociatedObject, GetCanExecute);
AssociatedObject.DataContextChanged
-= AssociatedObject_DataContextChanged;
}
private static void GetCanExecute(object sender,
CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
private void DoExecute(object sender, ExecutedRoutedEventArgs e)
{
if (_commandSink != null)
_commandSink.Execute(e);
}
void AssociatedObject_DataContextChanged(
object sender, DependencyPropertyChangedEventArgs e)
{
_commandSink = e.NewValue as ICommandSink;
}
}
public interface ICommandSink
{
void Execute(ExecutedRoutedEventArgs args);
}
2)为用户控件提供服务的ViewModel继承自ReactiveViewModel:
public class ReactiveViewModel : INotifyPropertyChanged, ICommandSink
{
internal readonly Subject<ExecutedRoutedEventArgs> Commands;
public ReactiveViewModel()
{
Commands = new Subject<ExecutedRoutedEventArgs>();
}
...
public void Execute(ExecutedRoutedEventArgs args)
{
args.Handled = true; // to leave chance to handler
// to pass the event up
Commands.OnNext(args);
}
}
3) 您不将控件绑定到 ICommand 属性,而是使用 RoutedCommand 的属性:
public static class MyCommands
{
private static readonly RoutedUICommand _testCommand
= new RoutedUICommand();
public static RoutedUICommand TestCommand
{ get { return _testCommand; } }
}
在 XAML 中:
<Button x:Name="btn" Content="Test" Command="ViewModel:MyCommands.TestCommand"/>
因此,在 ViewModel 上,您可以以非常 RX 的方式收听命令:
public MyVM() : ReactiveViewModel
{
Commands
.Where(p => p.Command == MyCommands.TestCommand)
.Subscribe(DoTestCommand);
Commands
.Where(p => p.Command == MyCommands.ChangeCommand)
.Subscribe(DoChangeCommand);
Commands.Subscribe(a => Console.WriteLine("command logged"));
}
现在,您拥有路由命令的功能(您可以自由选择在层次结构中的任何甚至多个 ViewModel 上处理命令),而且您对所有命令都有一个“单一流程”,这比单独的 IObservable 更适合 RX 。
这也应该可以通过 ReactiveFramework 完全实现。
唯一需要的更改是为此创建一个行为,然后将该行为连接到命令。它看起来像:
<Button Content="Click Me">
<Behaviors:Events.Commands>
<Behaviors:EventCommandCollection>
<Behaviors:ReactiveEventCommand CommandName="MouseEnterCommand" EventName="MouseEnter" />
<Behaviors:ReactiveEventCommand CommandName="MouseLeaveCommand" EventName="MouseLeave" />
<Behaviors:ReactiveEventCommand CommandName="ClickCommand" EventName="Click" />
</Behaviors:EventCommandCollection>
</Behaviors:Events.Commands>
</Button>
只需要意识到,在这种情况下,EventCommand 的工作方式与 ReactiveFramework 的工作方式非常相似。尽管 EventCommand 的实现会得到简化,但您不会真正看到差异。
EventCommand 已经为您提供了推送模型 - 当事件发生时,它会触发您的命令。这是 Rx 的主要使用场景,但它使实现变得简单。
我认为这个想法是创建一个事件“和弦”,在这种情况下可能是拖动操作,这会导致调用命令?这与在代码隐藏中执行此操作的方式几乎相同,但代码在行为中。例如,创建一个 DragBehavior,它使用 Rx 将 MouseDown/MouseMove/MouseUp 事件与调用来处理新“事件”的命令组合起来。