如何在Avalonia中使用WhenActivated和属性

6

我正在尝试使用ReactiveUI和Avalonia。由于Avalonia 0.10 preview中的初始化顺序,以下代码会失败:

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 为空)。

我没有看到任何简单的解决方法,需要大量的 hack、手动提高属性等。

因此问题是:

如何在 Avalonia 中使用 OAPH 和 WhenActivated

1个回答

7

选项 #1

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

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

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

另一种更好的选择是将ToProperty订阅移到WhenActivated块之外,并将ObservableAsPropertyHelper<T>字段标记为readonly。如果您的计算属性不会订阅超出视图模型生命周期的外部服务,则无需处理由ToProperty返回的订阅。在90%的情况下,您不需要将ToProperty调用放在WhenActivated内部。有关更多信息,请参见何时应该处理IDisposable对象?文档页面。还可以查看热和冷的可观察对象文章,这也可能会对此主题有所启示。因此,在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支持绑定到任务和可观察对象。这是一个非常有用的功能,我强烈推荐您尝试一下。这意味着,在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^}"/>

OAPHs是为不能执行此类技巧的平台而发明的,但Avalonia非常擅长聪明的标记扩展。因此,如果您针对多个UI框架并编写与框架无关的视图模型,则OAPHs可以使用。但如果您仅针对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" />

我们现在有一个 源代码生成器,可以帮助生成x:Name引用。

感谢您详细的回答!选项#1很糟糕,完全违背了做有趣的事情的初衷。选项#2有点无用,因为它都是关于外部依赖的。选项#3非常棒,但为什么我必须使用“^”?它看起来很奇怪。 - Shadow
2
^符号只是Avalonia标记扩展语法,允许将控件绑定到任务和可观察对象。它允许我们告诉Avalonia我们要绑定的值是一个“Task”或“IObservable”,并且绑定应该表现不同并跟踪订阅 https://avaloniaui.net/docs/binding/binding-to-tasks-and-observables 他们称其为“流绑定运算符”。 - Artyom
1
是的,但这不能作为默认行为吗?如果某个东西是可观察的,应该自动订阅,这个 ^ 如果需要将原始可观察对象传递给视图,则可以使用。 - Shadow
如果您想了解更多关于此主题的信息,我猜值得在Avalonia存储库https://github.com/avaloniaui/avalonia中打开一个问题或在他们的Gitter聊天https://gitter.im/AvaloniaUI/Avalonia中开始讨论。可能他们更喜欢使任务和可观察绑定更明显和严格,或者由于Avalonia内部的某些其他限制而起作用。我听说核心团队计划支持编译绑定。 - Artyom
1
我已经在使用编译绑定了。你只需要添加 x:CompileBindings=truex:DataType={local:MyViewModel},这个文件中的所有绑定都会被编译。这太棒了。 - Shadow
2
Avalonia+RxUI的学习曲线像是一座悬崖。找到这个答案真是太令人宽慰了。 - djeikyb

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接