リアクティブ拡張機能 (Rx) + MVVM =?
-
21-09-2019 - |
質問
Reactive Extensions (Rx) の能力を説明するために使用されている主な例の 1 つは、既存のマウス イベントを、マウス ドラッグ中のデルタを表す新しい「イベント」に結合することです。
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;
ソース: Matthew Podwysocki の Reactive Framework 入門シリーズ.
MVVM では、一般的に .xaml.cs ファイルを可能な限り空に保つよう努めており、純粋にマークアップ内のビューモデル内のコマンドを使用してビューからのイベントをフックする 1 つの方法は、次のビヘイビアーを使用することです。
<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>
ソース: ブライアン・ジェニシオ.
Reactive Framework は、コントローラーがビューを認識し、そのイベントを直接参照できる従来の MVC パターンをより重視しているようです。
でも、ケーキも食べたい!
この 2 つのパターンをどのように組み合わせますか?
解決
私はこの質問での私の探求を表すフレームワークを書きました。 リアクティブUI
これは、Observable ICommand と、IObservable を介して変更を通知する ViewModel オブジェクトの両方を実装し、IObservable をプロパティに「割り当てる」機能も実装しており、IObservable が変更されるたびに INotifyPropertyChange を起動します。また、バックグラウンドでタスクを実行し、その結果を UI にマーシャリングする ICommand など、多くの一般的なパターンもカプセル化します。
現時点ではドキュメントはまったくありませんが、コードを作成したサンプル アプリケーションと同様に、数日以内にその情報を追加することに取り組む予定です。
アップデート: かなり多くのドキュメントが作成されましたので、チェックしてください http://www.reactiveui.net
他のヒント
私の問題を解決するには、ICommandのとIObservable
次に、複合イベント・ストリームを構築するビューモデル内で使用することができる。
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);
}
}
}
}
ここで観測
私は「結婚」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'ishではありません、それはイベントがかなり静的なカップリングに流入壊れると思いました。 RXは、イベントの詳細で、かつ実行さを聴いのルーティングイベントは、適切と思われます。ここに私が思い付いたものです。
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);
}
ユーザーコントロールがReactiveViewModelから継承されるサービング2)ビューモデル
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"));
}
さて、あなたは、ルーティングされたコマンドのパワーを持っている(あなたは、階層内の任意の、あるいは複数のviewmodels上でコマンドを処理するために自由に選択できます)、プラスあなたはRXにnicierあるすべてのコマンドについては、「単一のフロー」を持っています別のIObservableのよります。
これは完全にReactiveFramework経由なんとか、同様である必要があります。
必要な唯一の変更は、コマンドまでの行動フックを持って、その後、このための動作を作成することです。それはようになります:
<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>
ただ、EventCommandがReactiveFrameworkこのシナリオでは、どのように動作するかに非常によく似た方法で動作していることを実感。 EventCommandの実装が簡素化されるだろうが、あなたは本当に、違いは見られません。
EventCommandはすでにあなたのためのプッシュモデルを提供している - イベントが発生したとき、それはあなたのコマンドを起動します。それはRxのための主要な使用シナリオだが、それは実装が簡単になります。
アイデアはイベント「コード」を作成することであったと思います。この場合はおそらくドラッグ操作で、結果としてコマンドが呼び出されます。これは分離コードで行うのとほぼ同じ方法で行われますが、ビヘイビア内のコードを使用します。たとえば、Rx を使用して MouseDown/MouseMove/MouseUp イベントと、新しい「イベント」を処理するために呼び出されるコマンドを組み合わせる DragBehavior を作成します。