Search code examples
c#reactiveuiavaloniauiavalonia

How to use WhenActivated with properties in avalonia


I am trying to use ReactiveUI along with Avalonia. Due to initialization order in Avalonia 0.10 preview following code fails:

class ViewModel : IActivatableViewModel
{
    public ViewModel(){
        this.WhenActivated(disposables => {
            _myProperty = observable.ToProperty(this, nameof(MyProperty)).DisposeWith(disposables).
        });
    }

    private ObservableAsPropertyHelper<object> _myProperty = null!;
    public object MyProperty => _myProperty.Value;
}

Because WhenActivated is called after view binds to viewModel (hence _myProperty is null).

I see no easy workaround requiring lots of hacks, manually raising properties and so on.

So the question is:

How to work with OAPH and WhenActivated in Avalonia?


Solution

  • Option #1

    The most obvious pattern that allows you to resolve the issue is to use the null coalescing operator. By using this operator, you can achieve the desired behavior by adjusting the code to look somewhat like this:

    private ObservableAsPropertyHelper<TValue>? _myProperty;
    public TValue MyProperty => _myProperty?.Value;
    

    Here, we are marking the declared field as nullable explicitly, using the new C# nullable annotations. We are doing this because until the WhenActivated block is called, the _myProperty field is set to null. Also, we use the _myProperty?.Value syntax here, as MyProperty getter should return null when the view model isn't initialized.

    Option #2

    Another option which is definitely better is to move the ToProperty subscription outside the WhenActivated block and to mark the ObservableAsPropertyHelper<T> field as readonly. If your computed property doesn't subscribe to external services that outlive the view model, then you don't need to dispose of the subscription returned by ToProperty. In 90% cases you don't need to keep ToProperty calls inside WhenActivated. See the When should I bother disposing of IDisposable objects? documentation page for more info. See also the Hot and Cold observables article that could also shed some light on this topic. So writing code like this is a good way to go in 90% cases:

    private readonly ObservableAsPropertyHelper<TValue> _myProperty;
    public TValue MyProperty => _myProperty.Value;
    
    // In the view model constructor:
    _myProperty = obs.ToProperty(this, x => x.MyProperty);
    

    If you are actually subscribing to external services e.g. injected into the view model via the constructor, then you could convert MyProperty into a read-write property with a private setter, and write the following code:

    class ViewModel : IActivatableViewModel
    {
        public ViewModel(IDependency dependency)
        {
            this.WhenActivated(disposables =>
            {
                // We are using 'DisposeWith' here as we are
                // subscribing to an external dependency that
                // could potentially outlive the view model. So
                // we need to dispose the subscription in order
                // to avoid the potential for a memory leak. 
                dependency
                    .ExternalHotObservable
                    .Subscribe(value => MyProperty = value)
                    .DisposeWith(disposables);
            });
        }
    
        private TValue _myProperty;
        public TValue MyProperty 
        {
            get => _myProperty;
            private set => this.RaiseAndSetIfChanged(ref _myProperty, value);
        }
    }
    

    Also, take a look at ReactiveUI.Fody if RaiseAndSetIfChanged syntax feels too verbose to you.

    Option #3 (I'd recommend this option)

    Worth noting that Avalonia supports binding to Tasks and Observables. This is a very useful feature that I'd highly recommend you trying out. This means, that in Avalonia you could simply declare a computed property as IObservable<TValue> and Avalonia will manage the lifetimes of subscriptions for you. So in the view model do this:

    class ViewModel : IActivatableViewModel
    {
        public ViewModel()
        {
            MyProperty =
              this.WhenAnyValue(x => x.AnotherProperty)
                  .Select(value => $"Hello, {value}!");
        }
    
        public IObservable<TValue> MyProperty { get; }
        
        // lines omitted for brevity
    }
    

    And in the view, write the following code:

    <TextBlock Text="{Binding MyProperty^}"/>
    

    OAPHs were invented for platforms that can't do such tricks, but Avalonia is pretty good at clever markup extensions. So if you are targeting multiple UI frameworks and writing framework-agnostic view models, then OAPHs are good to go. But if you are targeting Avalonia only, then just use {Binding ^}.

    Option #4

    Or, if you prefer using code-behind ReactiveUI bindings, combine the view model code from option 3 with the following code-behind on the view side in the xaml.cs file:

    this.WhenActivated(cleanup => {
        this.WhenAnyObservable(x => x.ViewModel.MyProperty)
            .BindTo(this, x => x.NamedTextBox.Text)
            .DisposeWith(cleanup);
    });
    

    Here we assume that the xaml file looks like this:

    <TextBlock x:Name="NamedTextBox" />
    

    We now have a source generator that could potentially help in generating x:Name references btw.