[RxUI] 1 – 集合

[RxUI] 1 – 集合

ReactiveUI推荐使用DynamicData作为基础操作的集合。

DynamicData已经替代了ReactiveList

DynamicData概述

DynamicData是基于Reactive Extensions for .NET的响应式集合。

每当对DynamicData集合进行更改时都会推送一个通知。该通知反映了集合中更改的内容。该通知由包含一个或多个更改的ChangeSet来表示。ChangeSet中的每一项都是一个Change,其中包含了自上一次通知以来的每个更改的信息。

更改的集合通常发布为IObservable<ChangeSet>

此类型是DynamicData的基础单元,在其上提供了丰富的Linq操作符,这些操作符允许以线程安全的方式对数据的更改进行查询和维护。

维护与使用数据

DynamicData提供了两个为IObservable<ChangeSet>专用的生产集合:

  1. SourceCache<TObject, TKey> 用于项有一个唯一键的情况。
  2. SourceList<T>用于项没有唯一键的情况。

这些对象都提供了用于维护数据的API,例如添加和删除这种典型的集合方法。可以使用这些集合维护数据,然后使用类似Linq-to-Objects的Linq API扩展方式动态查询数据。

为了将这些集合转换为IObservable<ChangeSet>可以调用Connect(),然后就能观察到通知并可以使用提供的Linq操作符了。DynamicData中规定,调用Connect的所有用户都将接收到集合中已经存在的项的通知以及后续更改的通知。

另外,还有几种其它方法可以从实现了INotifyCollectionChangedIEnumerable<T>接口的现有集合创建IObservable<ChangeSet>

不是什么

DynamicData集合不是ObservableCollection<T>的替代实现。它的体系结构首先基于领域驱动的概念。用途就是为提供的集合加载和维护数据,然后可以使用操作符来操作数据,而无需管理集合。它可以对集合做出任何想要的响应,无论是绑定到屏幕还是生成其它类型的通知。这些集合可以根据需要进行多次连接,单个集合也可以成为其他派生集合的源。

如何使用

如果已经在使用ObservableCollection<T>,那么最快、最简单的方式是使用ToObservableChangeSet扩展方法,该扩展生成一个IObservable<ChangeSet>,然后就可以使用DynamicData操作符了。

例如,存在一个ObservableCollection<T> myList响应式列表,那么就可以像下面这样做:

// 'myList' 为 ObservableCollection<T>
// 'myDerivedList' 为 IObservableList<T>
var myDerivedList = myList
    .ToObservableChangeSet()
    .Filter(t => t.Status == "Something")
    .AsObservableList();

这样就创建了一个过滤后的IObservableList。或者指定一个键:

// 'myList' 为 ObservableCollection<T>
// 'myDerivedCache' 为 IObservableCache<T, TKey>
var myDerivedCache = myList
    .ToObservableChangeSet(t => t.Id)
    .Filter(t => t.Status == "Something")
    .AsObservableCache();

这样就有了一个派生的IObservableCache

需要注意的是,如果使用的是myList,则可能不是线程安全的。假设myList是绑定到屏幕上的,那么应该在UI线程上创建和修改可观察的更改集,因为应该避免除绑定外的所有其他操作。另一种方式是创建数据源,然后再绑定。

var myList = new SourceList<T>()
var disposable = myList
    .Connect() // make the source an observable change set
    .\\some other operation

或者cache版本

var myCache = new SourceCache<T, int>(t => t.Id) 
var disposable = myCache
    .Connect() // make the source an observable change set
    .\\some other operation

自己创建数据源的优点是可以在后台线程中维护数据,从而节省主线程的宝贵时间。如果有需要,可以进行如下绑定:

ReadOnlyObservableCollection<T> bindingData;
var disposable = mySource
    .Connect() // make the source an observable change set
    .Sort(SortExpressionComparer<T>.Ascending(t => t.DateTime))
    .ObserveOn(RxApp.MainThreadScheduler) 
    // Make sure this line^^ is only right before the Bind()
    // This may be important to avoid threading issues if
    // 'mySource' is updated on a different thread.
    .Bind(out bindingData)
    .Subscribe(); 

对于cache和list的API与上面的相同。

SourceList与SourceCache有什么不同

如果有唯一ID,就应该使用observable cache,因为它是基于字典的,这将确保不能添加任何重复项,并且会在添加、更新和删除时进行通知,而list允许重复且没有更新的概念。SourceCacheSourceList更具优势,因此,如果可能,应该尽可能使用SourceCache,而不是SourceList

还有另一个区别。dynamic data中的cache更加成熟,并且有更多的操作符。具有更多操作符是因为基于键的操作符在实现上有性能优势,并且不希望向Dynamic Data中添加性能差的东西。

在ReactiveUI中使用DynamicData

使用ReactiveUIDynamicData构建程序时,可以选择可变和不可变集合。在使用不可变集合时,简单情况下使用ObservableAsPropertyHelper<T>就足够了。ObservableAsPropertyHelper<T>表示一个Observable<T>,即一段时间内值的流。可以将这些值为事件,将新值视为事件参数。这意味着,如果使用不可变集合,就可以将其视为事件参数,并且在每次更改新的集合时更新一个参数。查看ObservableAsPropertyHelper章节来学习如何使用这些特性。请注意,为每个更新创建新的集合会降低性能,通常应避免使用,而是使用DynamicData

一个例子

假设程序需要一项服务,该服务公开一个由后台维护的变体集合。需要以某种方式从中获取更改通知,以使用户界面同步更新。这时DynamicData就派上用场了。将IObservable<IChangeSet<bool>>公开给外界,而DynamicData负责观察可变的SourceList项的更改。使用Connect操作符将SourceList<T>变为IObservable<IChangeSet<bool>>

public class Service 
{
    private readonly SourceList<bool> _items = new SourceList<bool>();

    // We expose the Connect() since we are interested in a stream of changes.
    // If we have more than one subscriber, and the subscribers are known, 
    // it is recommended you look into the Reactive Extension method Publish().
    public IObservable<IChangeSet<bool>> Connect() => _items.Connect();

    public Service()
    {        
        // With DynamicData you can easily manage mutable datasets,
        // even if they are extremely large. In this complex scenario 
        // a service mutates the collection, by using .Add(), .Remove(), 
        // .Clear(), .Insert(), etc. DynamicData takes care of
        // allowing you to observe all of those changes.
        _items.Add(true);
        _items.RemoveAt(0);
        _items.Add(false);
    }
}

DynamicData使用.NET类型公开给外界,例如ReadOnlyObservableCollection<T>,而不是公开其自身类型。IObservable<IChangeSet<T>>IObservable<IChangeSet<TObject, TKey>>是两个基础可观察对象,可以基于其基础功能创建派生类。IObservable<IChangeSet<T>>指示集合改变了什么。调用ToObservableChangeSet时将会推送集合的当前状态。

SourceListSourceCache具有多线程性,并可以通过优化来创建IObservable<IChangeSet<T>>IObservable<IChangeSet<TObject, TKey>>。通常,SourceList/SourceCache是类中私有的,可以使用Bind方法来公开。可以对它们使用Connect方法来生成更改集。

使用功能强大的DynamicData操作符,可以将IObservable<IChangeSet<T>>转换为ReadOnlyObservableCollection<T>,可以轻松的将平台特定的用户界面绑定到其上。通过声明一个只读字段或变量作为Bind操作符的out变量。

public class ViewModel : ReactiveObject
{
    private readonly ReadOnlyObservableCollection<bool> _items;
    public ReadOnlyObservableCollection<bool> Items => _items;

    public ViewModel()
    {
        var service = new Service();
        service.Connect()
            // Transform in DynamicData works like Select in
            // LINQ, it observes changes in one collection, and
            // projects it's elements to another collection.
            .Transform(x => !x)
            // Filter is basically the same as .Where() operator
            // from LINQ. See all operators in DynamicData docs.
            .Filter(x => x)
            // Ensure the updates arrive on the UI thread.
            .ObserveOn(RxApp.MainThreadScheduler)
            // We .Bind() and now our mutable Items collection 
            // contains the new items and the GUI gets refreshed.
            .Bind(out _items)
            .Subscribe();
    }
}

提示 如果在后台线程中更新observable list或observable cache,必须在调用Bind前添加ObserveOn(RxApp.MainThreadScheduler),来确保更新能推送到UI线程。

ObservableCollectionExtended<T>是一个很好的单线程集合,无需执行基于派生的功能。要同步视图模型中的两个集合,将一个集合声明为ObservableCollectionExtended<T>,将另一个集合声明为ReadOnlyObservableCollection<T>。然后,将ToObservableChangeSet操作符应用在ObservableCollectionExtended<T>上将其转化为IObservable<IChangeSet<T>>

public class SynchronizedCollectionsViewModel : ReactiveObject
{
    private readonly ReadOnlyObservableCollection<bool> _derived;
    public ReadOnlyObservableCollection<bool> Derived => _derived;

    public ObservableCollectionExtended<bool> Source { get; }

    public SynchronizedCollectionsViewModel()
    {
        Source = new ObservableCollectionExtended<bool>();

        // Use the ToObservableChangeSet operator to convert
        // the observable collection to IObservable<IChangeSet<T>>
        // which describes the changes. Then, use any DD operators
        // to transform the collection. 
        Source.ToObservableChangeSet()
            .Transform(value => !value)
            // No need to use the .ObserveOn() operator here, as
            // ObservableCollectionExtended is single-threaded.
            .Bind(out _derived)
            .Subscribe();

        // Update the source collection and the derived
        // collection will update as well.
        Source.Add(true);
        Source.RemoveAt(0);
        Source.Add(false);
        Source.Add(true);
    }
}

追踪Reactive Objects中集合的更改

DynamicData支持对实现了INotifyCollectionChanged接口的类(ReactiveObjects)的更改追踪。例如,如果想对变化对象集合中的每个元素执行WhenAnyValue,可以使用DynamicData中的AutoRefresh操作符:

// 'collectionOfReactiveObjects' is ObservableCollection<T>
// Here, T inherits from the ReactiveObject class.
// 'databasesValid' is IObservable<bool>
var databasesValid = collectionOfReactiveObjects
    .ToObservableChangeSet()
    .AutoRefresh(model => model.IsValid) // Subscribe only to IsValid property changes
    .ToCollection()                      // Get the new collection of items
    .Select(x => x.All(y => y.IsValid)); // Verify all elements satisfy a condition etc.

// Then you can convert IObservable<bool> to a view model property.
// '_databasesValid' is of type ObservableAsPropertyHelper<bool> here.
_databasesValid = databasesValid.ToProperty(this, x => x.DatabasesValid);

提示 ToCollection的内部工作方式完全不同,其每次都会重新生成整个列表,而SourceList/SourceCacheBind进行添加/删除等。ToCollection仅用于每次都真正需要完整集合的基于集合的操作时。

将ReactiveList转换为DynamicData

如果使用的是ReactiveList<T>,并且仅在UI线程中添加/删除,可以使用ObservableCollectionExtended<T>。它在AddRange和取消通知的地方提供了类似的功能。仅当在单线程中执行操作时才应使用该方法。

即使单线程应用不是必须的,很多用户仍在尝试执行以下操作。

var myList = new SourceList<T>()
var disposable = myList
    .Connect() // make the source an observable change set
    .ObserveOn(RxApp.MainThreadScheduler)
    .Bind(out _myOutputList)
    .Subscribe();

重要提示 很多用户常犯的一个错误时向外界公开DynamicData类。应改用Bind方法通过ReadOnlyObservableCollection<T>字段来公开一个属性。

应在有意义的地方尝试重用IObservableChangeSet<T>。生成的操作很耗时,可以使用Reactive Extension的Publish方法。

// Use standard rx Publish() / Connect() to share published change sets.
// 'shared' is of type IObservable<IChangeSet<T>>
var shared = _source
    .Connect()
    .Publish();

// 'selectedChanged' if of type IObservable<Unit>
var selectedChanged = shared
    .WhenPropertyChanged(si => si.IsSelected)
    .Select(changes => Unit.Default)
    .StartWith(Unit.Default);

// Apply other operations to the shared connection.
shared.ToCollection().CombineLatest(selectedChanged, (items, _) => items);
shared.Maximum(i => i).Subscribe(max => Max = max);
shared.Connect();

DynamicData中的常见操作与Reactive Extension操作符的名称略有不同。

  • WhereFilter
  • SelectTransform
  • SelectManyTransformMany

探索DynamicData

原文https://www.reactiveui.net/docs/handbook/collections/

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注