How to avoid stale values when using WhenAnyValue and null propagation?

67 views Asked by At

I need to monitor for changes of nullable Foo, including Foo = null or changes of any property like Foo.Bar = 123.

So I am using this syntax (similar to recommended):

WhenAnyValue(o => o.Foo, o => o.Foo!.Bar, (foo, bar) => (foo, bar))

to subscribe to changes of both Foo and Bar of my view model:

public class VM : ReactiveObject
{
    [Reactive]
    public Foo? Foo { get; set; }
}

public class Foo : ReactiveObject
{
    [Reactive]
    public int Bar { get; set; }
}

However, this will run subscriber multiple time with stale values. After Foo = null subscriber is called with previous value of Bar and after Foo = new() subscriber is called twice: with previous value of Bar and with correct one. This can be demonstrated with following code:

var vm = new VM() { Foo = new Foo { Bar = 1 } };

vm.WhenAnyValue(o => o.Foo, o => o.Foo!.Bar, (foo, bar) => (foo, bar))
.Subscribe(o => Console.WriteLine($"{o.foo?.GetHashCode()} {o.bar}"));

vm.Foo = null; // #1

vm.Foo = new Foo { Bar = 2 }; // #2

The output looks like this:

58225482 1
 1                   << stale Bar
32347029 1           << stale Bar
32347029 2           << extra call

Instead I want something like this:

58225482 1
 0
32347029 2

In other words, I don't want to have stale values and extra subscriber call if possible.

The Foo == null check inside subscriber will solve #1. But maybe there is a different rx solution to such a problem? Or is moving null check outside of subscriber somehow is also ok?

As for #2, I have only idea of workaround with storing previous value of Foo and ignore first call if previous value was null. Again, it's a procedural way of programming. How this should be done in rx world of programming?

2

There are 2 answers

0
Sinatr On

To demonstrate the problem more clearly I have increased the number Foo properties:

public class Foo : ReactiveObject
{
    [Reactive]
    public int Bar { get; set; }
    [Reactive]
    public int Baz { get; set; }
    [Reactive]
    public int Qux { get; set; }
}

Then the output of

var vm = new VM();

vm.WhenAnyValue(o => o.Foo, o => o.Foo!.Bar, o => o.Foo!.Baz, o => o.Foo!.Qux,
    (foo, bar, baz, qux) => (foo, bar, baz, qux)).Subscribe(o =>
{
    Console.WriteLine($"{o.foo?.GetHashCode()} {o.bar} {o.baz} {o.qux}");
});

vm.Foo = new();
vm.Foo = null;
vm.Foo = new Foo { Bar = 1, Baz = 2, Qux = 3 };
vm.Foo = null;
vm.Foo = new();

will contain lots of stale states:

30631159 0 0 0
 0 0 0
56680499 0 0 0
56680499 1 0 0
56680499 1 2 0
56680499 1 2 3
 1 2 3
6444509 1 2 3
6444509 0 2 3
6444509 0 0 3
6444509 0 0 0

Here is a fixed version of subscriber:

{
    // discard stale state
    if (o.foo != null && (o.bar != o.foo.Bar || o.baz != o.foo.Baz || o.qux != o.foo.Qux))
        return;

    // Console.WriteLine($"{o.foo?.GetHashCode()} {o.bar} {o.baz} {o.qux}");
    // use actual null propagated values!
    Console.WriteLine($"{o.foo?.GetHashCode()} {o.foo?.Bar} {o.foo?.Baz} {o.foo?.Qux}");
}

Now the output is as expected, one line per change:

30631159 0 0 0

56680499 1 2 3

6444509 0 0 0

In other words:

  • the subscriber to null propagation will always require a check to discard stale states;
  • null propagated values should not be used inside subscriber, their purpose is to detect their individual changes, e.g. when vm.Foo.Bar = 123;

EDIT: The second statement is incorrect, with discard there is no stale state anymore and values are correct, unless subscriber takes too long to finish, but that's another story.

1
Lukasz Szczygielek On

.WhenAnyValue reacts on any observable change, and it do code by gathering all values that are observed.

If you are interested in only Bar, Baz, Qux, then you can omit Foo, and you will get less notifications:

vm.WhenAnyValue(o => o.Foo!.Bar, o => o.Foo!.Baz, o => o.Foo!.Qux,
        (bar, baz, qux) => (bar, baz, qux))
    .Subscribe(o =>
{
    Console.WriteLine($"{o.bar} {o.baz} {o.qux}");
});

// output
0 0 0
1 0 0
1 2 0
1 2 3
0 2 3
0 0 3
0 0 0

Based on your expected output it looks that you want to track only a Foo change:

vm.WhenAnyValue(o => o.Foo)
    .Where(o => o != null)
    .Subscribe(o => Console.WriteLine($"{o.Bar} {o.Baz} {o.Qux}"));

// output
0 0 0
1 2 3
0 0 0