2009-11-19 9 views
27

L'un des principaux exemples étant utilisés pour expliquer la puissance de Reactive Extensions (Rx) est la combinaison d'événements de souris existants en un nouveau représentant « événement » deltas lors de glisser la souris:Extensions réactives (Rx) + MVVM =?

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.

En MVVM je cherche généralement à garder mes .xaml.cs fichier comme vide que possible et un moyen d'accrochage des événements de la vue avec des commandes dans le viewmodel purement dans le balisage 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 plus orienté vers le modèle MVC traditionnel où un contrôleur connaît la vue et peut directement référencer ses événements. Mais, je veux à la fois avoir mon gâteau et le manger!

Comment combiner ces deux modèles?

+0

Plate-forme? Silverlight? – AnthonyWJones

+5

Anthony: Est-ce important? –

Répondre

35

J'ai écrit un cadre qui représente mes explorations dans cette question appelée ReactiveUI

Il met en œuvre à la fois un ICommand Observable, ainsi que des objets ViewModel qui signalent des changements via un IObservable, ainsi que la capacité à « attribuer "un IObservable à une propriété, qui va ensuite déclencher 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 en arrière-plan, puis ramène le résultat à l'interface utilisateur.

J'ai absolument aucune documentation en ce moment, mais je vais travailler sur l'ajout de cette information au cours des prochains jours, ainsi qu'un exemple d'application que j'ai codé en

MISE À JOUR: J'ai maintenant beaucoup de documentation jusqu'à, consultez http://www.reactiveui.net

+0

Votre projet semble intéressant, avec impatience les docs et l'application exemple! –

+0

http://blog.paulbetts.org/index.php/2010/06/22/reactivexaml-series-reactivecommand/ est un post sur l'une des classes principales, une ICommand réactive –

+0

En tant que dev de WPF, je peux dire les idées derrière l'interface utilisateur réactive sont très bonnes, recommandées! – Xcalibur

3

Ceci devrait également être parfaitement réalisable via le ReactiveFramework.

Le seul changement requis serait de créer un comportement pour cela, puis d'avoir le comportement de connexion à la commande. Il 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> 

Il faut juste réaliser que EventCommand travaille d'une manière très similaire à la façon dont le ReactiveFramework fonctionnerait, dans ce scénario. Vous ne verrez pas vraiment de différence, bien que l'implémentation d'EventCommand soit simplifiée. EventCommand vous fournit déjà un modèle push - lorsque l'événement se produit, il déclenche votre commande. C'est le scénario d'utilisation principal pour Rx, mais cela simplifie l'implémentation.

+0

Je ne cherche pas seulement un modèle push - je sais que c'est ce que la commande fournit. Je cherche un moyen de combiner des événements existants dans de nouveaux événements dans mon ViewModel plutôt que dans le code-behind –

0

Je pense que l'idée était de créer un événement "accord", dans ce cas, une opération de glisser probablement, ce qui entraîne l'appel d'une commande? Cela serait fait à peu près de la même façon que dans le codebehind, mais avec le code dans un comportement. Par exemple, créez un DragBehavior qui utilise Rx pour combiner les événements MouseDown/MouseMove/MouseUp avec une commande appelée pour gérer le nouvel "événement".

+0

C'était mon idée initiale et ça pourrait être la peine d'envelopper dans un nouveau comportement si les nouveaux événements que vous créez sont réutilisables «assez». Mais je suis vraiment à la recherche d'un moyen plus flexible de mixer un événement unique. –

7

La solution à mon problème avéré être de créer une classe qui implémente les deux ICommand et IObservable <T>

ICommand est utilisé pour lier l'interface utilisateur (à l'aide des comportements) et IObservable peuvent ensuite être utilisés dans la vue modèle 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ù Observable <T> est montré dans Implementing IObservable from scratch

6

Quand j'ai commencé à penser à la façon de "marier" MVVM et RX, la première chose que je pensais était 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); 
    } 
} 

Mais alors je pensais que la façon MVVM "standard" de lier les contrôles aux propriétés ICommand n'est pas très RX'ish, il casse le flux d'événements en couplages assez statiques.RX est plus sur les événements, et l'écoute d'un événement routé Executed semble approprié. Voici ce que je suis venu avec:

1) Vous avez un comportement CommandRelay que vous installez à la racine de chaque contrôle utilisateur qui doit 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 au service du contrôle de l'utilisateur est héritée de la 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) Vous ne lient pas les contrôles aux propriétés ICommand, mais utilisez RoutedCommand de la place:

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

Et en 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 de commandes routés (vous êtes libre de choisir de manipuler la commande sur n'importe quel ViewModels ou même plusieurs dans la hiérarchie), plus vous avez un "flux unique" pour toutes les commandes, ce qui est plus agréable à RX que de séparer IObservable.

Questions connexes