如何将 WhenActivated 与 avalonia 中的属性一起使用

How to use WhenActivated with properties in avalonia

我正在尝试将 ReactiveUI 与 Avalonia 一起使用。由于 Avalonia 0.10 预览中的初始化顺序,以下代码失败:

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;
}

因为 WhenActivated 在视图绑定到 viewModel 之后调用(因此 _myProperty 为 null)。

我认为没有简单的解决方法需要大量 hack、手动提升属性等。

所以问题是:

如何在 Avalonia 中使用 OAPH 和 WhenActivated?

选项 #1

可让您解决问题的最明显模式是使用空合并运算符。通过使用此运算符,您可以通过将代码调整为如下所示来实现所需的行为:

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

在这里,我们使用新的 C# 可空注释将声明的字段显式标记为可空。我们这样做是因为在 WhenActivated 块被调用之前,_myProperty 字段被设置为 null。另外,我们在这里使用 _myProperty?.Value 语法,因为 MyProperty getter 在未初始化视图模型时应该 return null

选项 #2

另一个 绝对 更好的选择是将 ToProperty 订阅移到 WhenActivated 块之外,并将 ObservableAsPropertyHelper<T> 字段标记为readonly。如果您的计算 属性 不订阅 比视图模型 长寿的外部服务,那么您 不不需要 处理 return 由 ToProperty 编辑的订阅。在 90% 的情况下,您不需要将 ToProperty 调用保留在 WhenActivated 内。请参阅 When should I bother disposing of IDisposable objects? documentation page for more info. See also the Hot and Cold observables 文章,该文章也可以阐明该主题。所以在 90% 的情况下,像这样编写代码是一个很好的方法:

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

// In the view model constructor:
_myProperty = obs.ToProperty(this, x => x.MyProperty);

如果您实际上订阅了外部服务,例如通过构造函数注入视图模型,然后你可以将 MyProperty 转换为具有私有 setter 的读写 属性,并编写以下代码:

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);
    }
}

此外,如果 RaiseAndSetIfChanged 语法对您来说太冗长,请查看 ReactiveUI.Fody

选项#3(我推荐这个选项)

值得注意的是 Avalonia 支持 binding to Tasks and Observables。这是一个非常有用 的功能,我强烈建议您尝试一下。这意味着,在 Avalonia 中,您可以简单地将计算的 属性 声明为 IObservable<TValue>,Avalonia 将为您管理订阅的生命周期。所以在视图模型中这样做:

class ViewModel : IActivatableViewModel
{
    public ViewModel()
    {
        MyProperty =
          this.WhenAnyValue(x => x.AnotherProperty)
              .Select(value => $"Hello, {value}!");
    }

    public IObservable<TValue> MyProperty { get; }
    
    // lines omitted for brevity
}

并在视图中写入如下代码:

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

OAPH 是为无法执行此类操作的平台而发明的,但 Avalonia 非常擅长巧妙的标记扩展。因此,如果您的目标是多个 UI 框架并编写与框架无关的视图模型,那么 OAPH 是不错的选择。但是,如果您仅针对 Avalonia,则只需使用 {Binding ^}.

选项#4

或者,如果您更喜欢使用 code-behind ReactiveUI bindings,请将选项 3 中的视图模型代码与 xaml.cs 文件中视图端的以下代码隐藏相结合:

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

这里我们假设 xaml 文件如下所示:

<TextBlock x:Name="NamedTextBox" />

我们现在有一个 source generator 可能有助于生成 x:Name 引用 btw。