Extensions réactives (Rx) + MVVM =?
L'un des principaux exemples utilisés pour expliquer la puissance des Extensions réactives (Rx) est la combinaison d'événements souris existants en un nouvel "événement" représentant des deltas pendant la traînée de la souris:
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: Matthieu Podwysocki de l'Introduction du Réactif Cadre de la série .
dans MVVM Je m'efforce généralement de garder mon .XAML.fichier cs aussi vide que possible et une façon d'accrocher des événements à partir de la vue avec des commandes dans le viewmodel purement dans le markup utilise un comportement:
<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>
Source: Brian Genisio .
le cadre réactif semble être davantage orienté vers le modèle traditionnel MVC où un contrôleur connaît la vue et peut faire référence directement à ses événements.
mais, je veux avoir mon gâteau et le manger!
comment combineriez-vous ces deux motifs?
5 réponses
j'ai écrit un cadre qui représente mes explorations dans cette question appelée Réactiveui
il implémente à la fois un ICommand Observable, ainsi que des objets ViewModel qui signalent des changements via un IObservable, ainsi que la capacité de" assigner " un IObservable à une propriété, qui va ensuite lancer INotifyPropertyChange chaque fois que son IObservable change. Il encapsule également beaucoup de modèles communs, comme avoir un ICommand qui exécute une tâche à l'arrière-plan, il renvoie le résultat à L'interface utilisateur.
je n'ai absolument zéro de la documentation, mais je vais travailler sur l'ajout d'informations dans les prochains jours, ainsi qu'un exemple d'application que j'ai codé
mise à JOUR: j'ai maintenant beaucoup de documentation, découvrez http://www.reactiveui.net
la solution à mon problème s'est avérée être de créer une classe qui implémente à la fois ICommand et IObservable
ICommand est utilisé pour lier L'UI (en utilisant des comportements) et IObservable peut alors être utilisé dans le modèle de vue pour construire des flux d'événements composites.
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);
}
}
}
}
où
quand j'ai commencé à penser à la façon de "marier" MVVM et RX, la première chose à laquelle j'ai pensé était Uncommande observable:
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);
}
}
mais ensuite j'ai pensé que la façon" standard " MVVM de lier les contrôles aux propriétés D'ICommand n'est pas très RX'ish, il casse le flux de l'événement en couplages assez statiques. RX est plus sur les événements, et écouter un exécuté événement routé semble approprié. Voici ce que j'ai trouvé:
1) Vous avez un comportement de CommandRelay que vous installez à la racine de chaque contrôle utilisateur qui devrait répondre aux commandes:
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 servant au contrôle de l'utilisateur est hérité du Réactiveviewmodel:
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) vous ne liez pas les contrôles aux propriétés ICommand, mais utilisez plutôt RoutedCommand:
public static class MyCommands
{
private static readonly RoutedUICommand _testCommand
= new RoutedUICommand();
public static RoutedUICommand TestCommand
{ get { return _testCommand; } }
}
et dans XAML:
<Button x:Name="btn" Content="Test" Command="ViewModel:MyCommands.TestCommand"/>
en conséquence, sur votre ViewModel vous pouvez écouter les commandes d'une manière très 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"));
}
Maintenant, vous avez le pouvoir des commandes routées (vous êtes libre de choisir de gérer la commande sur n'importe quel ou même plusieurs modèles de vue dans la hiérarchie), plus vous avez un "flux unique" pour toutes les commandes qui est plus propre à RX que IObservable séparé.
cela devrait être parfaitement faisable via le cadre de réactivation, ainsi.
le seul changement requis serait de créer un comportement pour cela, puis d'avoir le comportement relié à la commande. Ça ressemblerait à quelque chose comme:
<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>
juste réaliser que EventCommand fonctionne d'une manière très similaire à la façon dont le cadre de réactivation fonctionnerait, dans ce scénario. Vous ne verrez pas vraiment une différence, bien que la mise en œuvre de Event Command serait simplifiée.
EventCommand fournit déjà un modèle push pour vous - quand l'événement se produit, il déclenche votre commande. C'est le scénario d'utilisation principal pour Rx, mais il rend la mise en œuvre simple.
je pense que l'idée était de créer un événement "chord", dans ce cas une opération de drag probablement, qui se traduit par une commande être appelé? Cela se ferait à peu près de la même manière que vous le feriez dans le codebehind, mais avec le code dans un comportement. Par exemple, créez un comportement DragBehavior qui utilise Rx pour combiner les événements MouseDown/MouseMove/MouseUp avec une commande appelée pour gérer le nouvel "événement".