MVVM と RX を「結合」する方法を考え始めたとき、最初に考えたのは 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);
}
}
しかし、その後、コントロールを ICommand のプロパティにバインドする「標準的な」MVVM の方法は、あまり RX 風ではなく、イベント フローをかなり静的な結合に分割すると考えました。RX はイベントに関するものであり、Executedルーティング イベントをリッスンするのが適切と思われます。これが私が思いついたものです:
1) コマンドに応答する各ユーザー コントロールのルートにインストールする CommandRelay 動作があります。
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 は 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) コントロールを ICommand プロパティにバインドせず、代わりに RoutedCommand を使用します。
public static class MyCommands
{
private static readonly RoutedUICommand _testCommand
= new RoutedUICommand();
public static RoutedUICommand TestCommand
{ get { return _testCommand; } }
}
XAML では次のようになります。
<Button x:Name="btn" Content="Test" Command="ViewModel:MyCommands.TestCommand"/>
その結果、ViewModel では非常に 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"));
}
これで、ルーティングされたコマンドの力が得られます (階層内の任意のビューモデルまたは複数のビューモデルでコマンドを処理することを自由に選択できます)。さらに、すべてのコマンドに対して「単一のフロー」があり、個別の IObservable よりも RX に適しています。 .