仅订阅属性链中的 tail 属性

Subscribe only to tail property in a chain of properties

我有一个视图模型,它有一个 属性 A。属性 A 是一个有 属性 B 的类型。现在我想订阅我的视图模型的构造函数属性 B 的任何直接更改。“直接”是指仅当 属性 B 的当前值 属性 A 正在更改时,我才想 运行 我的订阅但如果 属性 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

您可以使用 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 上可以观察到 one,并且不会注意到 A 的内部值因 Switch() 呼叫.

虽然我更喜欢@Progman 的回答并且接受它是正确的,但我想post 我自己的问题解决方案。它基于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);
                    }
                }
            }
        }
    }
}