可观察的订阅未触发

时间:2018-07-08 02:44:51

标签: c# reactive-programming system.reactive

我开始研究Reactive Extensions,以及如何将它们应用于常见方案以启用更易管理和可读的代码。

我现在正在玩基本概念,并建立了一个简单的类:

public class ValidatableObject<TValue>
{
    public bool IsValid { get; private set; } = true;
    public TValue Value { get; }
    public ICollection<IValidationRule<TValue>> Rules { get; }

    public ValidatableObject(TValue value)
    {
        Value = value;
        Rules = new List<IValidationRule<TValue>>();
        Rules.ToObservable()
             .All(rule => rule.Check(Value))
             .Subscribe(b => IsValid = b);
    }
}

public interface IValidationRule<T>
{
    bool Check(T value);
}

public class FailingValidationRule<T> : IValidationRule<T>
{
    public bool Check(T value) => false;
}

public static void main()
{
    var theObject = new object();
    var v = new ValidatableObject<object>(theObject);

    // v.IsValid should be true

    v.Rules.Add(new FailingValidationRule<object>());

    // v.IsValid should be false
}

对该类的简单单元测试表明,它的运行不符合我的预期,并且我可能缺少了基本内容。我期望的是,当我向“规则”中添加一个项目时,“规则”中的每个规则都将被评估,结果将存储在IsValid中。但是,在添加旨在失败的规则集合的规则后,我没有看到IsValid被更新为false。

这也忽略了以下事实:基于Value属性的更改,Check方法的评估结果可能会更改。我可以看到Check方法可能还需要在此处返回一个observable。

在这种情况下,为什么IsValid不更新,而最小实现看起来如何设置这种行为?

1 个答案:

答案 0 :(得分:2)

.ToObservable()运算符是该时间点的集合的一次性快照。以后对集合的任何添加都不会通过可观察的过程。

因此,您需要使用一些可以观察到变化的东西。

为了给您与您当前正在使用的内容最直接的相似之处,建议您从ICollection<IValidationRule<TValue>>更改为System.Collections.ObjectModel.ObservableCollection<IValidationRule<TValue>>。请注意,这不是您应该采取的方式,但是我想给您一些可以立即看到的与问题代码相关的信息。

然后您可以像这样编写代码:

public class ValidatableObject<TValue>
{
    public bool IsValid { get; private set; } = true;
    public TValue Value { get; }
    public System.Collections.ObjectModel.ObservableCollection<IValidationRule<TValue>> Rules { get; }

    public ValidatableObject(TValue value)
    {
        Value = value;
        Rules = new ObservableCollection<IValidationRule<TValue>>();

        Observable
            .FromEventPattern<
                    System.Collections.Specialized.NotifyCollectionChangedEventHandler,
                    System.Collections.Specialized.NotifyCollectionChangedEventArgs>(
                h => Rules.CollectionChanged += h,
                h => Rules.CollectionChanged -= h)
            .Select(ep => Rules.All(rule => rule.Check(Value)))
            .Subscribe(b => IsValid = b);
    }
}

请注意,我已经使用FromEventPatternCollectionChanged事件构建了一个可观察对象,但是我仍在使用LINQ to Objects来评估规则。在代码中执行此操作的方式只能将IsValid设置为true。我完成操作的方式将根据规则将其设置为truefalse

我创建了一些测试规则:

public class ValidationRuleTrue<T> : IValidationRule<T>
{
    public bool Check(T value) => true;
}

public class ValidationRuleFalse<T> : IValidationRule<T>
{
    public bool Check(T value) => false;
}

然后我运行了此

void Main()
{
    var theObject = new object();
    var v = new ValidatableObject<object>(theObject);
    Console.WriteLine(v.IsValid);
    v.Rules.Add(new ValidationRuleTrue<object>());
    Console.WriteLine(v.IsValid);
    v.Rules.Add(new ValidationRuleFalse<object>());
    Console.WriteLine(v.IsValid);
}

我知道了

True
True
False

这是意料之中的。

但是,您的代码使用Rx的结构并不完善。您的查询可能很容易进行结构化,以将可观察的管道推送到其他线程。如果发生这种情况,代码将无法产生正确的结果。

举一个例子,尝试这个简单的改变:

        Observable
            .FromEventPattern<
                    System.Collections.Specialized.NotifyCollectionChangedEventHandler,
                    System.Collections.Specialized.NotifyCollectionChangedEventArgs>(
                h => Rules.CollectionChanged += h,
                h => Rules.CollectionChanged -= h)
            .Select(ep => Rules.All(rule => rule.Check(Value)))
            .ObserveOn(Scheduler.Default)
            .Subscribe(b => IsValid = b);

现在,当我运行相同的测试代码时,我得到了:

True
True
True

基本上,最后的Console.WriteLine(v.IsValid);Scheduler.Default上运行的可观察的更新IsValid之前运行。您有比赛条件。

如果您打算开始更多地使用Rx,则可以轻松创建一个查询,该查询使用在不同线程上运行的调度程序。因此,您需要以一种对Rx有意义的方式编写类。

像这样尝试:

public class ValidatableObject<TValue>
{
    public IObservable<bool> IsValid { get; private set; }
    public TValue Value { get; }

    public IEnumerable<IValidationRule<TValue>> Rules { get; }
    private ObservableCollection<IValidationRule<TValue>> _rules;

    public IDisposable AddRule(IValidationRule<TValue> rule)
    {
        _rules.Add(rule);
        return Disposable.Create(() => _rules.Remove(rule));
    }

    public ValidatableObject(TValue value)
    {
        Value = value;

        _rules = new ObservableCollection<IValidationRule<TValue>>();

        this.IsValid =
            Observable
                .FromEventPattern<
                        NotifyCollectionChangedEventHandler,
                        NotifyCollectionChangedEventArgs>(
                    h => _rules.CollectionChanged += h,
                    h => _rules.CollectionChanged -= h)
                .Select(ep => _rules.All(rule => rule.Check(Value)))
                .ObserveOn(Scheduler.Default);
    }
}

注意两件事。

(1)IsValid现在是IObservable<bool>,它摆脱了竞争条件。

(2)规则没有作为ObservableCollection公开,以供整个外部世界操纵,而是有一个单独的AddRule方法向其中添加规则和IDisposable删除规则。这意味着只有添加规则的代码才能删除它。它有更好的控制。您的代码不需要100%就能正常工作-您当然可以公开ObservableCollection-但这是思考Rx世界中操作的好方法。

现在我可以这样编写测试代码:

void Main()
{
    var theObject = new object();
    var v = new ValidatableObject<object>(theObject);

    var subscription = v.IsValid.Subscribe(isValid => Console.WriteLine(isValid));

    var rule1 = v.AddRule(new ValidationRuleTrue<object>());
    var rule2 = v.AddRule(new ValidationRuleFalse<object>());
    rule2.Dispose(); //remove `rule2`
}

我得出以下期望值:

True
False
True

这是我观察实现值本身的基本方法:

public interface IValidationRule<T> where T : INotifyPropertyChanged
{
    bool Check(T value);
}

public class ValidatableObject<TValue> where TValue : INotifyPropertyChanged
{
    public IObservable<bool> IsValid { get; private set; }
    public TValue Value { get; }

    public IEnumerable<IValidationRule<TValue>> Rules { get; }
    private ObservableCollection<IValidationRule<TValue>> _rules;

    public IDisposable AddRule(IValidationRule<TValue> rule)
    {
        _rules.Add(rule);
        return Disposable.Create(() => _rules.Remove(rule));
    }

    public ValidatableObject(TValue value)
    {
        Value = value;

        _rules = new ObservableCollection<IValidationRule<TValue>>();

        var rulesChanged = 
            Observable
                .FromEventPattern<
                        NotifyCollectionChangedEventHandler,
                        NotifyCollectionChangedEventArgs>(
                    h => _rules.CollectionChanged += h,
                    h => _rules.CollectionChanged -= h)
                .Select(ep => Unit.Default);

        var valueChanged =
            Observable
                .FromEventPattern<
                        PropertyChangedEventHandler,
                        PropertyChangedEventArgs>(
                    h => value.PropertyChanged += h,
                    h => value.PropertyChanged -= h)
                .Select(ep => Unit.Default);

        this.IsValid =
            Observable
                .Merge(rulesChanged, valueChanged)
                .Select(ep => _rules.All(rule => rule.Check(Value)))
                .ObserveOn(Scheduler.Default);
    }
}