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.
Plate-forme? Silverlight? – AnthonyWJones
Anthony: Est-ce important? –