Использование ReactiveCommand и WhenAny в унаследованных классах

Рассмотрите возможность использования наследования с ReactiveUI. У меня есть базовый класс ViewModel с DoSomethingCommand. CanExecute для этой команды зависит от свойства Prop1.

public class A : ReactiveObject
{
    public int Prop1 { get {...} set {...} }
    public ReactiveCommand DoSomethingCommand { get; private set; }

    public A()
    {
        IObservable<bool> canDoSomething = this.WhenAny(vm => vm.Prop1, p1 => CanDoSomething());
        DoSomethingCommand = new ReactiveCommand(canDoSomething);
        DoSomethingCommand.Subscribe(x => DoSomething());
    }

    protected virtual bool CanDoSomething()
    {
        return ...
    }
}

В унаследованном классе CanExecute для этой команды дополнительно зависит от свойства Prop2.

public class B : A
{
    public int Prop2 { get {...} set {...} }

    public B()
    {
        //Senseless code. For explanation only
        IObservable<bool> canDeleteExecute = this.WhenAny(vm => vm.Prop1, vm => vm.Prop2, (p1, p2) => CanDoSomething());
    }
}

Как лучше всего создать команду и сделать «CanExecute» зависимым от свойств из базовых и унаследованных классов? Конечно, я хочу, чтобы унаследованные классы не менялись, когда «CanExecute» в базовом классе становится дополнительно зависящим от свойства AnotherProp.


person elshev    schedule 30.10.2012    source источник


Ответы (4)


Я бы создал функцию регистрации в своем базовом классе для регистрации наблюдаемых с помощью CombineLatest.

    property IObservable<bool> CanDo;

    public IObservable<bool> RegisterCanDo( IObservable<bool> toRegister ){
            if ( CanDo == null ){
                    CanDo = toRegister;
            }else{
                    Cando = CanDo.CombineLatest(toRegister, (a,b) => a && b);
            }

    }

Итак, теперь, когда у вас есть наблюдаемый объект, который вы хотите сделать частью своей цепочки CanDo, вы просто добавляете его с помощью RegisterCanDo.

    public class B : A
    {
        public int Prop2 { get {...} set {...} }

        public B()
        {
            //Senseless code. For explanation only
            IObservable<bool> canDeleteExecute = this.WhenAny(vm => vm.Prop1, vm => vm.Prop2, (p1, p2) => CanDoSomething());
            RegisterCanDo(canDeleteExecute);
        }
    }
person bradgonesurfing    schedule 30.10.2012
comment
Я понимаю, к чему вы клоните, но нет способа отменить регистрацию CanDo, и поэтому с CombineLatest вы потенциально гарантируете, что CanDo всегда останется ложным. Возможно, RegisterCanDo нужно вернуть IDisposable и вам нужен механизм для управления внутренним состоянием CanDo? - person Enigmativity; 31.10.2012

Это именно то, что вы хотели выше. Однако будьте осторожны с резьбой. Я не сделал его потокобезопасным для добавления и удаления источников сообщений при объединении значений.

Вы можете добавлять и удалять источники сообщений для объекта Концентратора, и вы можете наблюдать за Концентратором на предмет истинного значения.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Subjects;

public class BoolObservableConcentrator : IObservable<bool>
{
    private readonly Dictionary<IObservable<bool>, bool> dict = new Dictionary<IObservable<bool>, bool>();

    public IDisposable Register(IObservable<bool> observable)
    {
        dict.Add(observable, false);
        var d = observable.Subscribe(value =>
        {
            dict[observable] = value;
            Fire();
        });
        return Disposable.Create(() =>
        {
            d.Dispose();
            dict.Remove(observable);
            Fire();
        });
    }

    private readonly Subject<bool> subject = new Subject<bool>();

    private void Fire()
    {
        subject.OnNext(dict.Values.All(x => x));
    }

    public IDisposable Subscribe(IObserver<bool> observer)
    {
        return subject.Subscribe(observer);
    }
}

И протестируйте его:

using System;
using System.Reactive.Subjects;
using Microsoft.VisualStudio.TestTools.UnitTesting;

[TestClass]
public class UnitTest
{
    [TestMethod]
    public void TestMethod1()
    {
        var s0 = new BehaviorSubject<bool>(false);
        var s1 = new BehaviorSubject<bool>(false);

        bool l = false;

        var c = new BoolObservableConcetrator();

        var r0 = c.RegisterSource(s0);
        var r1 = c.RegisterSource(s1);

        var s = c.Subscribe(v => l = v);

        Assert.AreEqual(false, l);

        s0.OnNext(true);
        Assert.AreEqual(false, l);

        s1.OnNext(true);
        Assert.AreEqual(true, l);

        s0.OnNext(false);
        Assert.AreEqual(false, l);

        // Removing one of the message sources should update the result
        r0.Dispose();
        Assert.AreEqual(true, l);
    }
}
person bradgonesurfing    schedule 31.10.2012
comment
Исходный пост можно увидеть здесь: stackoverflow.com/revisions/13152784/1. - person Emil; 01.11.2012
comment
@elshev спасибо за переписывание на С#. Я постоянно переключаюсь между vb.net и c# в зависимости от базы кода. Я даже не уверен, почему MS беспокоится о VB.net, поскольку для всех целей он идентичен С#, поэтому я не понял, что id отправил код vb.net, пока id не был отправлен;) - person bradgonesurfing; 01.11.2012
comment
Но вы удалили мое предупреждение о безопасности потоков. Я не проверял последний код пожара, но я думаю, что они делают какую-то блокировку буфера, чего нет в приведенном выше коде. - person bradgonesurfing; 01.11.2012
comment
Большое спасибо @bradgonesurfing. Это хорошее решение, но я попробовал его и обнаружил два недостатка. 1. Каждый наследник должен указать CanExecute в каждом методе Register. 2. Исходное состояние наблюдаемого объекта без срабатывания неизвестно (false в вашем примере). Так что наследник должен беспокоиться об этом. В другом месте dict.Values.All(x => x) будет возвращать false до тех пор, пока не сработают все наблюдаемые. Ниже я написал решение, которое собирает только зависимости свойств. CanExecute указан только один раз в базовом классе. - person ; 01.11.2012
comment
На самом деле они не ложные, но мне пришлось что-то инициировать в полях. Если субъектами являются BehaviourSubjects, они сработают немедленно при подписке с правильным текущим значением. Обратите внимание, что я не подписываюсь на BoolObservableConcentrator, пока все источники не будут зарегистрированы, и к этому времени он не станет стабильным. Я не понимаю, что вы имеете в виду, говоря о том, что каждый наследник указывает CanExecute. Это служебный класс, и он не делает никаких предположений о том, как ваши классы, которые его используют, реализуют наследование или нет. - person bradgonesurfing; 01.11.2012
comment
И ОП также попросил, чтобы источники можно было отменить, иначе WhenAny из ReactiveUI или CombineLatest из основного реактивного справились бы без проблем. - person bradgonesurfing; 01.11.2012

Я написал класс расширения для WhenAny. Это гораздо больше, чтобы сделать его похожим на WhenAny в ReactiveUI, но сейчас мне достаточно. Во-первых, посмотрите на использование:

public class A : ReactiveObject
{
    public A()
    {
        //Using almost like WhenAny from ReactiveUI
        CanExecuteObservable = this.WhenAny(() => AProp, CanExecute);
        Command = new ReactiveCommand(CanExecuteObservable);
        Command.Subscribe(x => Execute());
    }

    protected CanExecuteObservable CanExecuteObservable { get; private set; }
    public ReactiveCommand Command { get; private set; }

    protected virtual bool CanExecute()
    {
        return AProp > 10;
    }

    private int aProp = 10;
    public int AProp { get { return aProp; } set { this.RaiseAndSetIfChanged(x => x.AProp, value); } }
}

public class B : A
{
    public B()
    {
        //Add one more property dependency for CanExecute
        CanExecuteObservable.AddProperties(() => BProp);
    }

    private int bProp = 10;
    public int BProp { get { return bProp; } set { this.RaiseAndSetIfChanged(x => x.BProp, value); } }

    protected override bool CanExecute()
    {
        return base.CanExecute() && BProp > 100;
    }
}

Реализация:

public static class WhenAnyExtensions
{
    public static CanExecuteObservable WhenAny(this IReactiveNotifyPropertyChanged obj,
        IEnumerable<Expression<Func<object>>> expressions, Func<bool> func)
    {
        return new CanExecuteObservable(obj, expressions, func);
    }

    public static CanExecuteObservable WhenAny(this IReactiveNotifyPropertyChanged obj, Expression<Func<object>> property1, Func<bool> func)
    {
        return obj.WhenAny(new[] { property1 }, func);
    }

    public static CanExecuteObservable WhenAny(this IReactiveNotifyPropertyChanged obj, Expression<Func<object>> property1, Expression<Func<object>> property2, Func<bool> func)
    {
        return obj.WhenAny(new[] { property1, property2 }, func);
    }

    //etc...
}

public class CanExecuteObservable : IObservable<bool>
{
    internal CanExecuteObservable(IReactiveNotifyPropertyChanged obj,
        IEnumerable<Expression<Func<object>>> expressions, Func<bool> func)
    {
        this.func = func;
        AddProperties(expressions);
        obj
            .Changed
            .Where(oc => propertyNames.Any(propertyName => propertyName == oc.PropertyName))
            .Subscribe(oc => Fire());
    }

    private readonly List<string> propertyNames = new List<string>();
    private readonly Func<bool> func;

    public void AddProperties(IEnumerable<Expression<Func<object>>> expressions)
    {
        foreach (var expression in expressions)
        {
            string propertyName = ReflectionHelper.GetPropertyNameFromExpression(expression);
            propertyNames.Add(propertyName);
        }
    }

    public void AddProperties(Expression<Func<object>> property1) { AddProperties(new[] { property1 }); }
    public void AddProperties(Expression<Func<object>> property1, Expression<Func<object>> property2) { AddProperties(new[] { property1, property2 }); }
    //etc...

    public void Clear()
    {
        propertyNames.Clear();
    }

    private readonly Subject<bool> subject = new Subject<bool>();

    private void Fire()
    {
        subject.OnNext(func());
    }

    public IDisposable Subscribe(IObserver<bool> observer)
    {
        return subject.Subscribe(observer);
    }
}

И неинтересный в этом контексте вспомогательный класс для получения имени свойства из выражения:

public class ReflectionHelper
{
    public static string GetPropertyNameFromExpression<T>(Expression<Func<T>> property) 
    {
        var lambda = (LambdaExpression)property;
        MemberExpression memberExpression;

        if (lambda.Body is UnaryExpression) 
        {
            var unaryExpression = (UnaryExpression)lambda.Body;
            memberExpression = (MemberExpression)unaryExpression.Operand;
        } 
        else 
        {
            memberExpression = (MemberExpression)lambda.Body;
        }
        return memberExpression.Member.Name;
    }
}
person elshev    schedule 01.11.2012

Почему бы просто не заменить его в производном конструкторе:

Обновление: это версия, учитывающая оригинал.

public class A
{
    public ReactiveCommand SomeCommand { get; protected set; }

    public A()
    {
        SomeCommand = new ReactiveCommand(this.WhenAny(x => x.SomeProp, ...));
    }
}


public class B : A
{
    public A()
    {
        var newWhenAny = this.WhenAny(x => x.SomeOtherProp, ...);

        var canExecute = SomeCommand == null ?
            newWhenAny :
            SomeCommand.CanExecuteObservable.CombineLatest(newWhenAny,(oldCommand, whenAny) => oldCommand && whenAny);

        SomeCommand = new ReactiveCommand(canExecute);
    }
}
person Ana Betts    schedule 31.10.2012
comment
Потому что я хочу: 1. SomeCommand зависит от SomeProp и SomeOtherProp одновременно. 2. Класс B не будет беспокоиться, если SomeCommand станет зависеть от SomeProp2, SomeProp3 и т.д. в классе A - person ; 01.11.2012