TIL: ReactiveUI Gives No Guarantees About the Order in Which Observers Are Notified
I’m rewriting my Svelte/Go app for peronsal finances in C# using Avalonia. One popular choice is to use reactive programming through rx.net and with the help of the ReactiveUI library.
ReactiveUI provides a construct called ObservableAsPropertyHelper<T>
(read more) to make it easy to create properties that depend on the value of other properties.
The documentation even encourages the use of ObservableAsPropertyHelper<T>
(“OAPH” from now on):
When a property’s value depends on another property, a set of properties, or an observable stream, rather than set the value explicitly, use
ObservableAsPropertyHelper
withWhenAny
wherever possible.
It provides the following example:
firstName = this
.WhenAnyValue(x => x.Name)
.Select(name => name.Split(' ')[0])
.ToProperty(this, nameof(FirstName));
A more complete example would look like this:
class Person : ReactiveObject
{
private string _name;
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
private ObservableAsPropertyHelper<string> _firstName;
public string FirstName => _firstName.Value;
public Person()
{
_firstName = this
// Whenever the value of the property Person.Name changes...
.WhenAnyValue(x => x.Name)
// ...then compute a first name out of the new value...
.Select(name => name.Split(' ')[0])
// ...and use the computed value to set FirstName,
// raising a notification in the process.
ToProperty(this, nameof(FirstName));
}
}
This looks like you’re setting up an invariant: when inspecting the object’s state from outside, FirstName
will always be equal to the first part of Name
, until the first space.
However, this is not true. ReactiveUI makes no guarantees that your OAPH will be set before external observers observing changes to Name
get notified.
I had a situation that looked like this: A view model maintained a list of Person
-like objects. It also had one property providing a status based on the state of all these Person
s. To update this status, I set things up like this in the constructor:
class ViewModel : ReactiveObject
{
private ObservableCollection<Person> _people;
private string _status;
public string Status
{
get => _status;
private set => this.RaiseAndSetIfChanged(ref _status, value);
}
public ViewModel()
{
// In the collection '_people', ...
_people
// ... listen to changes in the collection ...
.ToObservableChangeSet()
// ... limited to changes to the items' Name property ...
.AutoRefresh(x => x.Name)
// ... and calculate the status based on the values. Note that
// I don't care about what kind of change happened: Any change should
// trigger this listener.
.Subscribe(_ => Status =
_people.Any(x => x.FirstName == "")
? "Something is weird"
: "OK");
{
}
So, listening to any changes in the _people
collection, I recalculated the value of Status
by iterating over the objects.
Note how this code reacts to changes to Name
but then uses FirstName
for its calculation. This is important.
With this very example, it turns out that the Status
listener will be called before the OAPH’s listener (I’m using ReactiveUI v20.1.63). When giving all Person
instances a Name
different from ""
for the first time, Status
may still be "Something is weird"
at the end of it because of this ordering issue.
I’m writing “may” above because the ordering can change, in particular when running inside a unit test framework. In my specific case, the app itself actually worked fine, but when writing a test to verify this behaviour, I stumbled upon this state inconsistency due to this difference.
On the other hand, listening to any single Person
through person.WhenAnyValue(x => x.Name)
does not exhibit this behaviour, so it seems like something specific to collections and AutoRefresh
.
So the takeaways are:
- There is no guarantee about the order in which listeners are called, not even in a single-threaded context.
- Make sure to listen for changes to the same property as the one you’ll use in the handler, especially with
ToObservableChangeSet().AutoRefresh()
. - It’s fine to use OAPH to compute values of observables connected to views, but not to maintain class invariants.
I’ve submitted a bug on GitHub about this. It’s quite possible my expectations are wrong about the order in which listeners are called, but then I think the documentation should be more explicit about it.