Is there a pattern for subscribing to hierarchical property changes with Reactive UI?
Asked Answered
H

2

10

Suppose I have the following view models:

public class AddressViewModel : ReactiveObject
{
    private string line;

    public string Line
    {
        get { return this.line; }
        set { this.RaiseAndSetIfChanged(x => x.Line, ref this.line, value); }
    }
}

public class EmployeeViewModel : ReactiveObject
{
    private AddressViewModel address;

    public AddressViewModel Address
    {
        get { return this.address; }
        set { this.RaiseAndSetIfChanged(x => x.Address, ref this.address, value); }
    }
}

Now suppose that in EmployeeViewModel I want to expose a property with the latest value of Address.Line:

public EmployeeViewModel()
{
    this.changes = this.ObservableForProperty(x => x.Address)
        .Select(x => x.Value.Line)
        .ToProperty(this, x => x.Changes);
}

private readonly ObservableAsPropertyHelper<string> changes;
public string Changes
{
    get { return this.changes.Value; }
}

This will only tick when a change to the Address property is made, but not when a change to Line within Address occurs. If I instead do this:

public EmployeeViewModel()
{
    this.changes = this.Address.Changed
        .Where(x => x.PropertyName == "Line")
        .Select(x => this.Address.Line)        // x.Value is null here, for some reason, so I use this.Address.Line instead
        .ToProperty(this, x => x.Changes);
}

This will only tick when a change to Line within the current AddressViewModel occurs, but doesn't take into account setting a new AddressViewModel altogether (nor does it accommodate a null Address).

I'm trying to get my head around the correct approach to solving this problem. I'm new to RxUI so I could be missing something obvious. I could manually hook into address changes and set up a secondary subscription, but this seems ugly and error-prone.

Is there a standard pattern or helper I should be using to achieve this?

Here is some code that can be copy/pasted to try this out:

ViewModels.cs:

namespace RxUITest
{
    using System;
    using System.Reactive.Linq;
    using System.Threading;
    using System.Windows.Input;
    using ReactiveUI;
    using ReactiveUI.Xaml;

    public class AddressViewModel : ReactiveObject
    {
        private string line1;

        public string Line1
        {
            get { return this.line1; }
            set { this.RaiseAndSetIfChanged(x => x.Line1, ref this.line1, value); }
        }
    }

    public class EmployeeViewModel : ReactiveObject
    {
        private readonly ReactiveCommand changeAddressCommand;
        private readonly ReactiveCommand changeAddressLineCommand;
        private readonly ObservableAsPropertyHelper<string> changes;
        private AddressViewModel address;
        private int changeCount;

        public EmployeeViewModel()
        {
            this.changeAddressCommand = new ReactiveCommand();
            this.changeAddressLineCommand = new ReactiveCommand();

            this.changeAddressCommand.Subscribe(x => this.Address = new AddressViewModel() { Line1 = "Line " + Interlocked.Increment(ref this.changeCount) });
            this.changeAddressLineCommand.Subscribe(x => this.Address.Line1 = "Line " + Interlocked.Increment(ref this.changeCount));

            this.Address = new AddressViewModel() { Line1 = "Default" };

            // Address-only changes
            this.changes = this.ObservableForProperty(x => x.Address)
                .Select(x => x.Value.Line1 + " CHANGE")
                .ToProperty(this, x => x.Changes);

            // Address.Line1-only changes
            //this.changes = this.Address.Changed
            //    .Where(x => x.PropertyName == "Line1")
            //    .Select(x => this.Address.Line1 + " CHANGE")        // x.Value is null here, for some reason, so I use this.Address.Line1 instead
            //    .ToProperty(this, x => x.Changes);
        }

        public ICommand ChangeAddressCommand
        {
            get { return this.changeAddressCommand; }
        }

        public ICommand ChangeAddressLineCommand
        {
            get { return this.changeAddressLineCommand; }
        }

        public AddressViewModel Address
        {
            get { return this.address; }
            set { this.RaiseAndSetIfChanged(x => x.Address, ref this.address, value); }
        }

        public string Changes
        {
            get { return this.changes.Value; }
        }
    }
}

MainWindow.cs:

using System.Windows;

namespace RxUITest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new EmployeeViewModel();
        }
    }
}

MainWindow.xaml:

<Window x:Class="RxUITest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <StackPanel>
        <TextBlock Text="{Binding Changes}"/>
        <Button Command="{Binding ChangeAddressCommand}">Change Address</Button>
        <Button Command="{Binding ChangeAddressLineCommand}">Change Address.Line1</Button>
    </StackPanel>
</Window>
Hypothesize answered 24/1, 2013 at 14:14 Comment(0)
R
15

Nope, there's a way easier way to do this:

this.WhenAny(x => x.Address.Line, x => x.Value)
    .Subscribe(x => Console.WriteLine("Either Address or Address.Line changed!"));
Rapeseed answered 24/1, 2013 at 23:35 Comment(4)
That's....brilliant! Just tried it and it worked. I had misunderstood the WhenAny operator as observing any number of "tail" properties (Line in the above example). I didn't realize it also had the smarts to observe any property in the chain. Thanks Paul.Hypothesize
is this.RaiseAndSetIfChanged(x => x.Address, ref this.address, value); syntax still supported? I am able to get only the following syntax; this.RaiseAndSetIfChanged(ref this.address, value);Wellnigh
No, the old syntax is deprecated, you should only use the new syntax now.Rapeseed
@ana Is there a way to get the changed property on the Line object? I'm struggling to track the changed property.Fecit
D
0

If you consider your Address could be a simple collection of Lines i.e.

class AddressViewModel 
 : ObservableCollection<LineViewModel>, INotifyPropertyChanged

then you would probably attach an event handler to propogate the changes

this.CollectionChanged += this.NotifyPropertyChanged(...);

So I would do the same kind of thing for the ReactiveObject but using Rx

public class EmployeeViewModel : ReactiveObject
{
private AddressViewModel address;
private IDisposable addressSubscription;

public AddressViewModel Address
{
    get { return this.address; }
    set 
    { 
        if (addressSubscription != null)
        {
            addressSubscription.Dispose();
            addressSubscription =null;
        }
        if (value != null)
        {
            addressSubscription = value.Subscribe(...);
        }
        this.RaiseAndSetIfChanged(x => x.Address, ref this.address, value); 
     }
}

}

Depriest answered 24/1, 2013 at 15:23 Comment(3)
This is the kind of approach my existing code base takes. However, as I'm integrating RxUI, I figure there must be a much cleaner way to do this.Hypothesize
Let me know if you find :-)Depriest
Never write anything in the setter in ReactiveUI other than RaiseAndSetIfChanged - if you are, you're definitely Doing It Wrong™Rapeseed

© 2022 - 2024 — McMap. All rights reserved.