Реактивные расширения (Rx) + MVVM =?
Один из основных примеров, используемых для объяснения мощности Reactive Extensions (Rx), объединяет существующие события мыши в новое "событие", представляющее дельта во время перетаскивания мыши:
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;
Источник: Мэтью Подвайски Введение в серию Reactive Framework.
В MVVM я обычно стараюсь, чтобы мой .xaml.cs файл был как можно более пустым, и один из способов подключения событий из представления с командами в модели view чисто в разметке использует поведение:
<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, где контроллер знает представление и может напрямую ссылаться на его события.
Но я хочу, чтобы у меня был торт и съел его!
Как бы вы объединили эти два шаблона?
Ответы
Ответ 1
Я написал структуру, которая представляет мои исследования в этом вопросе под названием ReactiveUI
Он реализует как Observable ICommand, так и объекты ViewModel, которые сигнализируют об изменениях через IObservable, а также возможность "назначать" свойство IObservable для свойства, которое затем запускает INotifyPropertyChange, когда изменяется его IObservable. Он также инкапсулирует множество общих шаблонов, например, имеет ICommand, который запускает задачу в фоновом режиме, а затем возвращает результат обратно в пользовательский интерфейс.
У меня есть абсолютно нулевая документация прямо сейчас, но я буду работать над добавлением этой информации в ближайшие дни, а также пример приложения, которое я закодировал
ОБНОВЛЕНИЕ: Теперь у меня довольно много документации, проверьте http://www.reactiveui.net
Ответ 2
Решение моей проблемы оказалось для создания класса, который реализует как ICommand, так и IObservable <T>
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);
}
}
}
}
Где наблюдаемый <T> показано в Внедрение IObservable с нуля
Ответ 3
Когда я начал думать о том, как "жениться" на 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);
}
}
Но потом я подумал, что "стандартный" способ MVVM привязки элементов управления к свойствам ICommand не очень RX'ish, он разбивает поток событий на довольно статические связи. 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"));
}
Теперь у вас есть сила маршрутизируемых команд (вы можете выбрать, чтобы обрабатывать команду на любом или даже нескольких ViewModels в иерархии), плюс у вас есть "единственный поток" для всех команд, которые более удобны для RX чем отдельный IObservable.
Ответ 4
Это также должно выполняться с помощью 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 уже предоставляет вам модель push - когда событие происходит, оно запускает вашу команду. Это основной сценарий использования для Rx, но он делает реализацию простой.
Ответ 5
Я думаю, идея заключалась в том, чтобы создать событие "аккорд", в данном случае, возможно, операция перетаскивания, которая приводит к вызову команды? Это будет сделано почти так же, как вы делали бы это в коде, но с кодом в поведении. Например, создайте DragBehavior, которое использует Rx для объединения событий MouseDown/MouseMove/MouseUp с помощью команды, вызываемой для обработки нового "события".