【问题标题】:Observable Subscription Not Firing可观察到的订阅未触发
【发布时间】:2018-07-08 12:10:44
【问题描述】:

我开始研究响应式扩展以及如何将它们应用于常见场景以实现更易于管理和可读的代码。

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

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。

这也忽略了 Check 方法的评估结果可能会根据 Value 属性的更改而更改的事实。我可以看到 Check 方法可能还需要在这里返回一个 observable。

为什么在这种情况下 IsValid 没有更新,设置这种行为的最小实现是什么样的?

【问题讨论】:

  • 如果您提供minimal reproducible example 那就太好了,因为我无法运行您的代码来查看它的实际效果。
  • 但是现在作为一个快速回答 - 您提供的代码中的 Rules 集合是空的,因此它无法产生值。
  • 啊,我想我明白你在问题中的意思了。 .ToObservable() 运算符是该时间点集合的一次性快照。任何未来添加到集合中的内容都不会通过 observable。
  • 我明白了,我的印象是 ToObservable() 也会观察变化。在这种情况下,更好的设置是什么?规则应该改为主题,这将允许我添加项目并观察更改吗?我读了很多书,认为我看到了避免创建主题,而是依赖扩展。
  • 是的,一般来说避免使用Subject 是对的,但有些地方完全可以。使用它们的地方是当您想要创建一个临时的可观察序列时。我现在正在整理答案。

标签: c# reactive-programming system.reactive


【解决方案1】:

.ToObservable() 运算符是该时间点集合的一次性快照。任何未来添加到集合中的内容都不会通过 observable。

所以你需要使用一些可以让你观察到变化的东西。

为了让您最直接地了解您当前使用的内容,我建议将 ICollection&lt;IValidationRule&lt;TValue&gt;&gt; 更改为 System.Collections.ObjectModel.ObservableCollection&lt;IValidationRule&lt;TValue&gt;&gt;。请注意,这不是您应该这样做的方式,但我想为您提供一些您可以立即看到的与您问题中的代码相关的内容。

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

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);
}

我明白了:

真的 真的 错误的

这是意料之中的。

但是,您的代码结构不适合使用 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);

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

真的 真的 真的

基本上,最终的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&lt;bool&gt;,它摆脱了竞争条件。

(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`
}

我得到了预期值:

真的 错误的 真的

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

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);
    }
}

【讨论】:

  • 我喜欢这个社区。很棒的答案,谢谢。有很多东西要我消化。我对我的问题进行了编辑,基本上我什至还没有考虑过 Check 方法可能会根据 Value 的状态返回不同结果的事实。不要泄露它,但如果我要进入兔子洞,我认为 Check 应该返回一个 IObservable 是否走在正确的轨道上?
  • @MaxHampton - Check 可能不需要返回 IObservable&lt;bool&gt;。如果您正在观察值的变化,那么您的IsValid observable 只有第二个触发器。
  • @MaxHampton - 我已将代码添加到我的答案末尾以观察对象本身。
  • 这很有意义。再次感谢,帮了我很大的忙。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-07-23
  • 1970-01-01
  • 2017-06-21
  • 1970-01-01
  • 1970-01-01
  • 2018-08-31
  • 1970-01-01
相关资源
最近更新 更多