Reactive Extensions (Rx) + MVVM =?
One of the main examples being used to explain the power of Reactive Extensions (Rx) is combining existing mouse events into a new 'event' representing deltas during mouse drag:
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;
Source: Matthew Podwysocki's Introduction to the Reactive Framework series.
In MVVM I generally strive to keep my .xaml.cs file as empty as possible and one way of hooking up events from the view with commands in the viewmodel purely in markup is using a behavior:
<Button Content="Click Me">
<Behaviors:Events.Commands>
<Behaviors:EventCommandCollection>
<Behaviors:EventCommand CommandName="MouseEnterCommand" EventName="MouseEnter" />
<Behaviors:Event开发者_如何学GoCommand CommandName="MouseLeaveCommand" EventName="MouseLeave" />
<Behaviors:EventCommand CommandName="ClickCommand" EventName="Click" />
</Behaviors:EventCommandCollection>
</Behaviors:Events.Commands>
</Button>
Source: Brian Genisio.
The Reactive Framework seems to be more geared towards the traditional MVC pattern where a controller knows the view and can reference its events directly.
But, I want to both have my cake and eat it!
How would you combine these two patterns?
I've written a framework that represents my explorations in this question called ReactiveUI
It implements both an Observable ICommand, as well as ViewModel objects who signal changes via an IObservable, as well as the ability to "assign" an IObservable to a property, who will then fire INotifyPropertyChange whenever its IObservable changes. It also encapsulates a lot of common patterns, like having an ICommand who runs a Task in the background, then marshalls the result back to the UI.
I have absolutely zero documentation up right now, but I'll be working on adding that information over the coming days, as well as a sample application I've coded up
UPDATE: I now have quite a lot of documentation up, check out http://www.reactiveui.net
The solution to my problem turned out to be to create a class that implements both ICommand and IObservable<T>
ICommand is used to bind the UI (using behaviors) and IObservable can then be used within the view model to construct composite event streams.
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);
}
}
}
}
Where Observable<T> is shown in Implementing IObservable from scratch
When I started to think of how to "marry" MVVM and RX, the first thing I thought of was an 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);
}
}
But then I thought that the "standard" MVVM way of binding controls to ICommand's properties is not very RX'ish, it breaks the event flow into fairly static couplings. RX is more about events, and listening to an Executed routed event seems appropriate. Here is what I came up with:
1) You have a CommandRelay behavior which you install at the root of each user control which should respond to commands:
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 serving the user control is inherited from the 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) You do not bind controls to ICommand properties, but use RoutedCommand's instead:
public static class MyCommands
{
private static readonly RoutedUICommand _testCommand
= new RoutedUICommand();
public static RoutedUICommand TestCommand
{ get { return _testCommand; } }
}
And in XAML:
<Button x:Name="btn" Content="Test" Command="ViewModel:MyCommands.TestCommand"/>
As a result, on your ViewModel you can listen to the commands in a very RX way:
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"));
}
Now, you have the power of routed commands (you are free to choose to handle the command on any or even multiple ViewModels in the hierarchy), plus you have a "single flow" for all the commands which is nicier to RX than separate IObservable's.
This should be perfectly doable via the ReactiveFramework, as well.
The only change required would be to create a behavior for this, then have the behavior hook up to the Command. It would look something like:
<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>
Just realize that EventCommand is working in a very similar way to how the ReactiveFramework would work, in this scenario. You won't really see a difference, although the implementation of EventCommand would be simplified.
EventCommand already is providing a push model for you - when the event happens, it fires your command. That's the main usage scenario for Rx, but it makes the implementation simple.
I think the idea was to create an event "chord", in this case a drag operation probably, which results in a command being called? This would be done pretty much the same way you'd do it in the codebehind, but with the code in a behavior. For example, create a DragBehavior that uses Rx to combine the MouseDown/MouseMove/MouseUp events with a command called to handle the new "event".
精彩评论