Реактивные расширения (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;

Источник: Серия статей Мэтью Подвизоцкого в серию статей о реактивном фреймворке.

В MVVM я обычно стараюсь, чтобы мой файл .xaml.cs был как можно более пустым, и один из способов подключения событий из представления с командами в модели представления исключительно в разметке - это использование поведения:

<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, где контроллер знает представление и может напрямую ссылаться на его события.

Но я хочу и свой торт, и съесть его!

Как бы вы совместили эти два паттерна?


person Jesper Larsen-Ledet    schedule 19.11.2009    source источник
comment
Энтони: Это имеет значение?   -  person Reed Copsey    schedule 19.11.2009


Ответы (5)


Я написал структуру, которая представляет мои исследования по этому вопросу, под названием ReactiveUI.

Он реализует как Observable ICommand, так и объекты ViewModel, которые сигнализируют об изменениях через IObservable, а также возможность «назначать» IObservable свойству, которое затем запускает INotifyPropertyChange всякий раз, когда его IObservable изменяется. Он также инкапсулирует множество общих шаблонов, таких как наличие ICommand, который запускает задачу в фоновом режиме, а затем маршалирует результат обратно в пользовательский интерфейс.

У меня сейчас абсолютно нулевая документация, но я буду работать над добавлением этой информации в ближайшие дни, а также над примером приложения, которое я закодировал.

ОБНОВЛЕНИЕ: у меня сейчас довольно много документации, посмотрите http://www.reactiveui.net

person Ana Betts    schedule 15.06.2010
comment
Ваш проект выглядит интересным, с нетерпением жду документации и примера приложения! - person Markus Johnsson; 24.06.2010
comment
blog.paulbetts.org/index.php/2010 / 06/22 / - это сообщение об одном из основных классов, Reactive ICommand. - person Ana Betts; 24.06.2010
comment
Как опытный разработчик WPF, я могу сказать, что идеи, лежащие в основе Reactive UI, очень хороши, рекомендую! - person Xcalibur; 05.05.2011
comment
некоторая незапрошенная рекомендация, которая хорошо сочетается с ReactiveUI: Расчетные свойства Nito [github.com/StephenCleary/CalculatedProperties] . Он почти не имеет накладных расходов и может быть подключен к существующему проекту в любое время, я использую его для всех простых уведомлений о вычисленных свойствах и прибегаю к Rx / ReactiveUI для более сложных наблюдаемых. - person KolA; 21.08.2019

Решением моей проблемы оказалось создание класса, реализующего как 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);
            }
        }
    }
}

Где Observable ‹T› показано в Реализация IObservable с нуля

person Jesper Larsen-Ledet    schedule 25.11.2009

Когда я начал думать о том, как «поженить» 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 больше касается событий и прослушивания Выполнено перенаправленное событие кажется подходящим. Вот что я придумал:

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. .

person Sergey Aldoukhov    schedule 15.02.2010

Это также должно быть идеально выполнено с помощью 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, но он упрощает реализацию.

person Reed Copsey    schedule 19.11.2009
comment
Я не просто ищу модель push - я знаю, что предоставляет команда. Я ищу способ объединить существующие события в новые события в моей ViewModel, а не в коде программной части. - person Jesper Larsen-Ledet; 20.11.2009

Я думаю, идея заключалась в том, чтобы создать событие «аккорд», в данном случае, вероятно, операцию перетаскивания, которая приводит к вызову команды? Это будет сделано примерно так же, как в коде, но с кодом в поведении. Например, создайте DragBehavior, который использует Rx для объединения событий MouseDown / MouseMove / MouseUp с командой, вызываемой для обработки нового «события».

person wekempf    schedule 19.11.2009
comment
Это была моя первоначальная идея, и, возможно, стоит потрудиться обернуть ее в новое поведение, если новые события, которые вы создаете, можно повторно использовать «достаточно». Но я действительно ищу более гибкий способ смешивания разовых событий. - person Jesper Larsen-Ledet; 20.11.2009