Search code examples
c#system.reactivereactiveui

Subscribe only to tail property in a chain of properties


I have a view model which has a property A. Property A is of a type which has a property B. Now I want to subscribe in my view model's constructor to any direct change of property B. By "direct" I mean that I want to run my subscription only if property B of the current value of property A is changing but not if the value of property A is changed.

By now I have something like this:

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

However, this will of course also execute DoSomethingWithB if the value of property A changes. I have already tried around whether WhenAnyObservable or Switch extension methods can be used but up to now I couldn't figure out what it has to look like.

Edit:

Since I don't know whether my initial question was clear enough I have added now a working example covering all cases that I need to take into account. For simplicity, property B is of type int and I have added an ID property to TypeA to be able to distinguish them.

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

Output:

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

Solution

  • You can use ObservableForProperty() to build an IObservable<T> for the properties you have, which will not fire the initial value they have. This in combination with Switch() allows you to build an IObservable<T> for the property B, which will only fire changes to the property B, but not when the property A is changed. The code might look like this:

    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;
    

    This will generate the following output:

    B is: 2
    B is: 3
    

    As you see, it will only trigger when the property B is changed, but will not trigger when A is changed. Also, you have one observable on the property B and will not notice that the inner value for A is changed due to the Switch() call.