仅订阅属性链中的尾部属性

Posted

技术标签:

【中文标题】仅订阅属性链中的尾部属性【英文标题】:Subscribe only to tail property in a chain of properties 【发布时间】:2022-01-12 00:19:07 【问题描述】:

我有一个具有属性 A 的视图模型。属性 A 属于具有属性 B 的类型。现在我想在我的视图模型的构造函数中订阅属性 B 的任何直接更改。我的意思是“直接”我只想在属性 A 的当前值的属性 B 发生变化时运行我的订阅,而不是在属性 A 的值发生变化时运行。

现在我有这样的东西:

this.WhenAnyValue(x => x.A.B)
    .Subscribe(b => DoSomethingWithB(b));

但是,如果属性 A 的值发生变化,这当然也会执行DoSomethingWithB。我已经尝试过是否可以使用 WhenAnyObservableSwitch 扩展方法,但到目前为止我无法弄清楚它的外观。

编辑:

由于我不知道我最初的问题是否足够清楚,我现在添加了一个工作示例,涵盖了我需要考虑的所有情况。为简单起见,属性 B 的类型为 int,我在 TypeA 中添加了一个 ID 属性以便能够区分它们。

using ReactiveUI;
using System;
using System.ComponentModel;

namespace ObservePropertyTail

    class Program
    
        static void Main(string[] args)
        
            ViewModel vm = new ViewModel();

            // Pings but should not because A was changed.
            vm.A = new TypeA(1)  B = 1 ;

            // Pings which is the desired behavior.
            vm.A.B = 2;

            // Does not ping (by chance because value of B remains the
            // same although A is changed) which is the desired behavior.
            vm.A = new TypeA(2)  B = 2 ;

            // Pings but should not because A was changed.
            vm.A = new TypeA(3)  B = 3 ;

            // Should not ping and does not.
            vm.A = null;

            // Should not ping but does.
            vm.A = new TypeA(4)  B = 4 ;

            // Should ping and does.
            vm.A.B = 3;
        
    

    class ViewModel : INotifyPropertyChanged
    
        private TypeA a;

        public ViewModel()
        
            this.WhenAnyValue(x => x.A.B)
                .Subscribe(b => Console.WriteLine($"Ping: A = A.ID, b = b"));
        

        public TypeA A
        
            get => a;
            set
            
                if (a != value)
                
                    a = value;
                    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(A)));
                
            
        

        public event PropertyChangedEventHandler PropertyChanged;
    

    class TypeA : INotifyPropertyChanged
    
        private int b;

        public TypeA(int id) => ID = id;

        public int ID  get; 

        public int B
        
            get => b;
            set
            
                if (b != value)
                
                    b = value;
                    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(B)));
                
            
        

        public event PropertyChangedEventHandler PropertyChanged;
    

输出:

Ping: A = 1, b = 1
Ping: A = 1, b = 2
Ping: A = 3, b = 3
Ping: A = 4, b = 4
Ping: A = 4, b = 3

【问题讨论】:

可以选择使用DistinctUntilChanged()吗? “但是,如果属性 A 的值发生变化,这当然也会执行 DoSomethingWithB” - 不,它不应该。 我现在在我的问题中添加了一个工作示例,以证明它的行为不符合预期。明确期望的行为是什么。 在您的 working 示例中。为什么不应该在第一种情况下不进行 ping 操作? B 从 0 更改为 1。第二个 ping 也是,因为它从 1 更改为 2。第三个不 ping,因为它没有更改 B(仍然是 2),在最后一种情况下,您同时更改了 A 和 B (B 从 2 到 3)。所以恕我直言,它按预期工作。每当 B 更改(无论 A 是什么)时,都会调用您的订阅。 嗯,看来我还是误会了:我知道我的演示应用程序的行为是 Reactive Extensions 和 ReactiveUI 的开发人员所期望的,所以并不是我认为我发现了一个错误.我的实际问题是是否有过滤器运算符或不同的方法来实现我的演示代码中描述的行为。 DistinceUntilChanged 朝着正确的方向前进,我尝试使用接受 IEqualityComparer 的自定义实现但没有成功的重载。 【参考方案1】:

您可以使用ObservableForProperty() 为您拥有的属性构建IObservable<T>,这不会触发它们拥有的初始值。这结合Switch() 允许您为属性B 构建IObservable<T>,它只会触发对属性B 的更改,但不会在属性A 更改时触发。代码可能如下所示:

ViewModel vm = new ViewModel();
vm.ObservableForProperty(it => it.A)
    .Select(it => it.Value)
    .Select(it => it.ObservableForProperty(it2 => it2.B))
    .Switch()
    .Select(it => it.Value)
    .Subscribe(it => 
        Console.WriteLine("B is: "+it);
    );
    
// Pings but should not because A was changed.
vm.A = new TypeA(1)  B = 1 ;

// Pings which is the desired behavior.
vm.A.B = 2;

// Does not ping (by chance because value of B remains the
// same although A is changed) which is the desired behavior.
vm.A = new TypeA(2)  B = 2 ;

// Pings but should not because A was changed.
vm.A = new TypeA(3)  B = 3 ;

// Should not ping and does not.
vm.A = null;

// Should not ping but does.
vm.A = new TypeA(4)  B = 4 ;

// Should ping and does.
vm.A.B = 3;

这将生成以下输出:

B is: 2
B is: 3

如您所见,它只会在属性B 更改时触发,但不会在A 更改时触发。此外,您在属性 B 上有 一个 可观察的,并且不会注意到 A 的内部值由于 Switch() 调用而发生更改。

【讨论】:

非常感谢您的回答!我曾期望 switch 方法可以以某种方式用于此目的,但无法弄清楚如何。顺便说一句:与此同时,我提出了自己的解决方案,但看起来很像过头了。我将在第二个答案中发布。 一个补充:我在第一个Select 之后添加了一个Where 以过滤掉那些A 设置为null 的情况。这个案例不是我原始演示代码的一部分,但可能发生在我的应用程序中。【参考方案2】:

虽然我更喜欢@Progman 的回答并认为它是正确的,但我想发布我自己的问题解决方案。它基于WhenAnyDynamic,但将一个主题放入观察者管道中,该管道拦截通知并将尾部属性的父对象的当前值与前一个实例进行比较。然后它只转发那些尾巴的父母没有改变的通知。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using ReactiveUI;

namespace Bruker.Spr.Analyzer.ViewModels.PreProcessing.ReportPoints

    public static class WhenAnyCustomExtensions
    
        public static IObservable<TRet> WhenAnyTailValue<TSender, TRet>(
            this TSender sender,
            Expression<Func<TSender, TRet>> expression)
        
            var tailObserver = new TailObservable<TSender, TRet>(sender, expression);

            return tailObserver;
        

        private class TailObservable<TSender, TRet> : ISubject<TRet>
        
            private readonly TSender sender;
            private readonly Func<TSender, object> tailParentFunc;
            private readonly List<IObserver<TRet>> observers = new List<IObserver<TRet>>();

            private object previousTailParent;

            public TailObservable(TSender sender, Expression<Func<TSender, TRet>> expression)
            
                this.sender = sender;

                var chainElementsWithoutTail = expression.Body
                    .GetExpressionChain()
                    .SkipLast(1);

                if (chainElementsWithoutTail.Any())
                
                    var parameterExpression = Expression.Parameter(typeof(TSender), "x");

                    Expression tailMemberExpression = parameterExpression;

                    foreach (var property in chainElementsWithoutTail.OfType<MemberExpression>())
                    
                        MemberExpression member = Expression.Property(tailMemberExpression, property.Member.Name);
                        tailMemberExpression = member;
                    

                    var trimmedUnaryExpression = Expression.Convert(tailMemberExpression, typeof(object));
                    var trimmedLambdaExpression = Expression.Lambda<Func<TSender, object>>(trimmedUnaryExpression, new[]  parameterExpression );

                    this.tailParentFunc = trimmedLambdaExpression.Compile();
                

                sender
                    .WhenAnyDynamic(expression.Body, x => (TRet)x.Value)
                    .Subscribe(this);
            

            public void OnCompleted()
            
                foreach (var observer in this.observers)
                
                    observer.OnCompleted();
                
            

            public void OnError(Exception error)
            
                foreach (var observer in this.observers)
                
                    observer.OnError(error);
                
            

            public void OnNext(TRet value)
            
                if (this.tailParentFunc == null)
                
                    return;
                

                var currentTailParent = this.tailParentFunc(this.sender);

                try
                
                    if (currentTailParent != this.previousTailParent)
                    
                        return;
                    

                    foreach (var observer in this.observers)
                    
                        observer.OnNext(value);
                    
                
                finally
                
                    this.previousTailParent = currentTailParent;
                
            

            public IDisposable Subscribe(IObserver<TRet> observer)
            
                if (!this.observers.Contains(observer))
                
                    this.observers.Add(observer);
                

                return new TailObserverSubscription<TRet>(this.observers, observer);
            

            private sealed class TailObserverSubscription<TValue> : IDisposable
            
                private readonly List<IObserver<TValue>> observers;
                private readonly IObserver<TValue> observer;

                public TailObserverSubscription(List<IObserver<TValue>> observers, IObserver<TValue> observer)
                
                    this.observers = observers;
                    this.observer = observer;
                

                public void Dispose()
                
                    if (this.observer != null && this.observers.Contains(this.observer))
                    
                        this.observers.Remove(this.observer);
                    
                
            
        
    

【讨论】:

以上是关于仅订阅属性链中的尾部属性的主要内容,如果未能解决你的问题,请参考以下文章

是否可以在引导程序中的检测阶段之前从链中的 MSI 文件中读取 MSI 属性?

您应该在 Angular2 父/子组件层次结构中的哪个点订阅 observable?

类型“订阅”缺少类型“可观察<StringMap<any>>”中的以下属性

[转] JavaScript中的属性:如何遍历属性

NSPredicate 按仅存在于其中一个子类中的属性过滤掉结果

如何仅覆盖一个属性:Qt StyleSheet 中的值对