mvvm system.reactive reactive-programming

Extensiones reactivas(Rx)+MVVM=?



system.reactive reactive-programming (5)

Creo que la idea era crear un evento "acorde", en este caso una operación de arrastre probablemente, lo que resulta en un comando que se llama? Esto se haría más o menos de la misma manera que lo harías en el código subyacente, pero con el código en un comportamiento. Por ejemplo, cree un DragBehavior que use Rx para combinar los eventos MouseDown / MouseMove / MouseUp con un comando llamado para manejar el nuevo "evento".

Uno de los ejemplos principales que se utilizan para explicar el poder de las Extensiones reactivas (Rx) es combinar eventos de mouse existentes en un nuevo ''evento'' que representa deltas durante la operación de arrastrar del mouse:

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;

Fuente: Serie de Introducción a la Estructura del Marco Reactivo de Matthew Podwysocki .

En MVVM generalmente me esfuerzo por mantener mi archivo .xaml.cs lo más vacío posible y una forma de vincular eventos desde la vista con comandos en el modelo de vista puramente en marcado utiliza un comportamiento:

<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>

Fuente: Brian Genisio .

El Marco Reactivo parece estar más orientado hacia el patrón MVC tradicional donde un controlador conoce la vista y puede referenciar sus eventos directamente.

Pero, ¡quiero tener mi pastel y comérselo!

¿Cómo combinarías estos dos patrones?


Cuando comencé a pensar en cómo "casarme" con MVVM y RX, lo primero que pensé fue en un 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); } }

Pero luego pensé que la forma "estándar" de MVVM de unir controles a las propiedades de ICommand no es muy RX''ish, rompe el flujo del evento en acoplamientos bastante estáticos. RX trata más sobre eventos, y escuchar un evento enrutado Executed parece apropiado. Aquí es lo que se me ocurrió:

1) Tiene un comportamiento CommandRelay que instala en la raíz de cada control de usuario que debe responder a los comandos:

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 que sirve el control del usuario se hereda de 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) No enlaza los controles a las propiedades de ICommand, sino que usa RoutedCommand en su lugar:

public static class MyCommands { private static readonly RoutedUICommand _testCommand = new RoutedUICommand(); public static RoutedUICommand TestCommand { get { return _testCommand; } } }

Y en XAML:

<Button x:Name="btn" Content="Test" Command="ViewModel:MyCommands.TestCommand"/>

Como resultado, en su ViewModel puede escuchar los comandos de una manera muy 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")); }

Ahora, tiene la potencia de los comandos enrutados (puede elegir manejar el comando en cualquiera o incluso múltiples modelos de vista en la jerarquía), además tiene un "flujo único" para todos los comandos que es más agradable para RX que para los de IObservable. .


Esto también debería ser perfectamente factible a través de ReactiveFramework.

El único cambio requerido sería crear un comportamiento para esto, luego hacer que el comportamiento se conecte al Comando. Se vería algo así como:

<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>

Solo tenga en cuenta que EventCommand está trabajando de forma muy similar a cómo funcionaría ReactiveFramework en este escenario. Realmente no verá la diferencia, aunque la implementación de EventCommand se simplificaría.

EventCommand ya le proporciona un modelo push: cuando ocurre un evento, desencadena su comando. Ese es el escenario de uso principal para Rx, pero simplifica la implementación.


He escrito un marco que representa mis exploraciones en esta pregunta llamada ReactiveUI

Implementa un Observable ICommand, así como objetos ViewModel que señalan cambios a través de un IObservable, así como la capacidad de "asignar" un IObservable a una propiedad, quien luego activará INotifyPropertyChange cada vez que se cambie IObservable. También encapsula muchos patrones comunes, como tener un ICommand que ejecuta una Tarea en segundo plano, luego ordena el resultado a la IU.

No tengo absolutamente ninguna documentación en este momento, pero trabajaré para agregar esa información en los próximos días, así como también una aplicación de muestra que he codificado

ACTUALIZACIÓN: ahora tengo un montón de documentación, echa un vistazo a http://www.reactiveui.net


La solución a mi problema fue crear una clase que implementa tanto ICommand como IObservable <T>

ICommand se usa para vincular la IU (uso de comportamientos) y IObservable se puede usar dentro del modelo de vista para construir secuencias de eventos compuestas.

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); } } } }

Donde Observable <T> se muestra en Implementación IObservable desde cero