ObservableAsPropertyHelper Again — Be Home Before Midnight!
Rx.NET is kinda cool, and ReactiveUI adds some nifty constructs on top of that. But it also provides ObservableAsPropertyHelper which, I think, has a fatal design flaw. (I’ve run into trouble in the past. This article is my second take on the subject.)
Consider this code:
public class FooClass : ReactiveObject
{
// Bar has reference semantics.
private readonly ObservableAsPropertyHelper<Bar> _myBar;
private bool _buzz = false;
// Note: Not nullable!
public Bar MyBar => _myBar.Value;
// Controls the value of MyBar.
public bool Buzz
{
get => _buzz;
set => this.RaiseAndSetIfChanged(ref _buzz, value);
}
public FooClass()
{
this.WhenAny(x => x.Buzz)
.Select(buzz => new Bar(buzz ? "Boom" : "Bang"))
.ToProperty(this, nameof(MyBar)); // Uses a default scheduler.
}
}
I could go on and provide a use case where this crashes, but the point can be made even without it: At the end of the constructor, MyBar will be null! This although our class definition says it isn’t (otherwise we would have declared it as public Bar? MyBar).
Now, the design idea is that updates should be asynchronous such as to keep the main thread humming and not blocking the UI. The .ToProperty() call has a scheduler parameter to provide more control over this. However, it is optional and, as far as I can tell, uses the CurrentThread scheduler by default which seems to delay the setting of the property.
The documentation says to use RxApp.MainThread for the scheduler parameter for UI properties. But since the library is called ReactiveUI, one would think that’s the main use case!
I guess OAPHs were invented before nullable types were introduced, hence ObservableAsPropertyHelper<T>.Value having type T instead of T? but still being initialized to null. And I also guess that changing this (or making the scheduler parameter for ToProperty mandatory) would break too many projects at this point.
So, lesson learned: When using ObservableAsPropertyHelper<> for a property that can’t be null, always pass RxApp.MainThreadScheduler to ToProperty, lest your class’ state will be invalid immediately after construction. (This also applies to other invariants, like one property’s value being derived from another one’s.)