ReactiveUI - 具有许多相关属性的模型对象

ReactiveUI - Model object with many related properties

我有一个 WPF MVVM 应用程序。我的模型对象是我可以完全控制的 POCO。这些对象中的某些属性之间存在关系。

例如:假设我有

public class Model
{
    public ObservableCollection<double> XCoordinates { get; set; } // Cumulative sum of DistancesBetweenXCoordinates.
    public ObservableCollection<double> DistancesBetweenXCoordinates { get; set; } // {x[1]-x[0], x[2]-x[1], ..., x[n]-x[n-1]}
    public double TotalLength { get; set; } // Sum of DistancesBetweenXCoordinates.
}

我希望最终用户能够编辑距离列表或 x 坐标列表,并且其他属性应自动更新。到目前为止,我已经使用 INPC 事件完成了这项工作,但这很快就会变得太混乱。一些 UI 更新是在每次 PropertyChange 之后完成的,所以我想对此进行优化。

由于我的真实应用程序中的一些 属性 更新对性能的影响可以忽略不计,我不想使用 "calculated properties" 例如

public double TotalLength => DistancesBetweenXCoordinates.Sum();

根据我的阅读,ReactiveUI 框架似乎具有我正在寻找的功能。不过我有一些问题:

1) 我目前正在使用另一个我不想完全放弃的框架 (Catel)。我可以使用 ReactiveUI 中的 .WhenAny() 等而不继承 ReactiveObject(例如仅通过实现 IReactiveObject)吗?

2) 我见过的几乎所有示例都继承自 ViewModel 中的 ReactiveObject。这是实现我想要的目标的首选方式吗?在我的模型中实现它不是更有意义吗?如果我的模型应该只是一个 "dumb" POCO,没有任何机制来保持所有相关属性都是最新的,那么这是我的 Reactive VM 的责任吗?

如果能提供一个简单的示例或其他指导,我将不胜感激。

tl;博士

  • 实现INotifyPropertyChanged你可以使用.WhenAny()
  • ReactiveObject 可用于 ViewModel 和 Model classes。它实际上只是一个 "reactive" 对象。即像 System.Object。 (不相信我?看看 the source。)
  • 对依赖于其他反应属性或事件的计算属性使用ObservableAsPropertyHelper。 (见docs

答案

有几种方法可以处理这个问题,但特别是你似乎想要推理出一些事情:

  1. 如何在不从我正在使用的另一个框架迁移的情况下利用响应式功能?
  2. ReactiveObject 是否应该仅限于 ViewModels?它在普通模型中有用吗 classes?

首先,正如您已经注意到的,ReactiveUI 的很多吸引力都伴随着 .WhenAny() 中包含的强大功能。框架之间可以使用这些方法吗,可以.WhenAny().WhenAnyValue().WhenAnyObservable() 方法适用于实现 INotifyPropertyChanged 任何 对象。 (Related Docs)

事实上,您现有的框架很可能已经在它们自己的许多类型上实现了 INotifyPropertyChanged,因此 .WhenAny() 自然会扩展以无缝处理这些对象。你几乎从不需要一个ReactiveObject。它只会让您的生活更轻松。

Note: This is actually one of the core values of ReactiveUI. That at the core, ReactiveUI is really just a bunch of extension methods designed to make working with observables easier in the existing .Net world. This makes interoperability with existing code one of ReactiveUI's most compelling features.

现在,ReactiveObject 应该用于普通 "dumb" 模型吗?我想这取决于你希望责任归于何处。如果您希望您的模型 class 只包含规范化状态而根本不包含任何逻辑,那么可能不会。但是,如果您的模型旨在处理状态和领域相关的逻辑,那为什么不呢?

Note: There's a larger philosophical debate here about the single responsibility principal, software architecture, and MVVM, but that's probably for Programmers SE.

在这种情况下,我们关心的是通知侦听器有关某些计算属性(例如 TotalLength)的更新。 (即我们的模型包含一些逻辑)ReactiveObject 能帮助我们做到这一点吗?我也这么认为。

在您的方案中,我们希望每当添加或更改元素或其他内容时,根据 DistancesBetweenXCoordinates 计算 TotalLength。我们可以使用 ReactiveObjectObservableAsPropertyHelper 的组合。 (Related Docs)

例如:

class Model : ReactiveObject
{
    // Other Properties Here...

    // ObservableAsPropertyHelper makes it easy to map
    // an observable sequence to a normal property.
    public double TotalLength => totalLength.Value;
    readonly ObservableAsPropertyHelper<double> totalLength;
    public Model()
    {
        // Create an observable that resolves whenever a distance changes or
        // gets added. 
        // You would probably need CreateObservable()
        // to use Observable.FromEventPattern to convert the
        // CollectionChanged event to an observable.
        var distanceChanged = CreateObservable();

        // Every time that a distance is changed:
        totalLength = distanceChanged

            // 1. Recalculate the new length.
            .Select(distances => CalculateTotalLength(distances))

            // 2. Save it to the totalLength property helper.
            // 3. Send a PropertyChanged event for the TotalLength property.
            .ToProperty(this, x => x.TotalLength);
    }
}

在上面,每次 distanceChanged observable 解析时都会重新计算 TotalLength。例如,每当 DistanceBetweenXCoordinates 发出 CollectionChanged 事件时。此外,因为这只是一个普通的可观察对象,您可以在后台线程上进行计算,从而使您可以在长时间操作期间保持 UI 响应。计算完成后,将发送 PropertyChanged 事件 TotalLength.

也可以使用ReactiveList代替ObservableCollection,例如:

public class Model : ReactiveObject
{
    private ReactiveList<double> distancesBetweenXCoordinates;
    private readonly ObservableAsPropertyHelper<double> totalLength;

    public Model()
    {
        // ChangeTrackingEnabled allow to raise changes notifications when individual values changes
        DistancesBetweenXCoordinates  = new ReactiveList<double> { ChangeTrackingEnabled = true };

        this.WhenAnyValue(x => x.DistancesBetweenXCoordinates, x => x.Sum())
            .ToProperty(this, x => x.TotalLength, out totalLength);
    }

    public ReactiveList<double> DistancesBetweenXCoordinates 
    { 
        get => distancesBetweenXCoordinates; 
        set => this.RaiseAndSetIfChanged(ref distancesBetweenXCoordinates, value); 
    } 
    public double TotalLength { get => totalLength.Value; } 
}

这样,对 DistancesBetweenXCoordinates 的单个元素的更改将更改 TotalLength 值。