DynamicData:动态集合,MVVM体系结构和反应性扩展

2019年2月标志着ReactiveUI 9的发布-ReactiveUI 9是用于在Microsoft .NET平台上构建GUI应用程序的跨平台框架。 ReactiveUI是用于将反应式扩展与MVVM设计模式紧密集成的工具。 您可以通过一系列视频文档的欢迎页面熟悉框架。 ReactiveUI 9更新包括许多修复和改进 ,但最关键和最有趣的更新可能是与DynamicData框架集成 ,使您能够以Reactive方式处理动态集合。 让我们找出可用于DynamicData的功能以及这个强大的反应式框架如何在后台运行!


引言


首先,让我们确定DynamicData的用例,并找出我们对使用System.Collections.ObjectModel命名空间中的动态数据集的默认工具System.Collections.ObjectModel


众所周知,MVVM模板承担了模型层,表示层和应用程序表示模型(也称为视图模型)之间的责任划分。 模型层由域实体和服务表示,对视图模型层一无所知。 该模型封装了应用程序的整个复杂逻辑,而视图模型则将操作委派给该模型,从而通过可观察的属性,命令和集合向视图提供对应用程序当前状态的信息的访问。 用于处理动态属性的默认工具是INotifyPropertyChanged接口,用于处理用户操作INotifyCollectionChanged ,用于处理集合INotifyCollectionChanged接口,以及诸如ObservableCollection<T>ReadOnlyObservableCollection<T>




INotifyPropertyChangedICommand接口的实现通常取决于开发人员和所使用的MVVM框架,但是使用默认的ObservableCollection<T>有很多限制! 例如,没有Dispatcher.Invoke或类似的调用,我们无法从后台线程中更改集合,这对于通过后台操作与服务器同步数据数组非常有用。 值得注意的是,当使用干净的MVVM体系结构时,模型层对所使用的GUI框架一无所知,并且应与MVC或MVP术语中的模型层兼容。 这就是域服务中众多Dispatcher.Invoke调用违反职责分离原则的原因。


当然,我们可以在域服务中声明一个事件,并传输带有更改项的块作为事件参数,然后订阅该事件,将Dispatcher.Invoke调用封装在接口后面,因此我们的应用程序将不依赖于任何GUI框架,从视图模型调用该接口并相应地修改ObservableCollection<T> ,但是有很多更优雅的方式来处理此类问题,而无需重新发明。 那我们还在等什么呢?


反应性扩展。 管理可观察的数据流


为了完全理解DynamicData引入的抽象以及如何使用变化的反应式数据集,让我们回顾一下什么是反应式编程以及如何在Microsoft .NET平台和MVVM设计模式的上下文中使用它 。 组织程序组件之间的交互的方式可以是交互的或反应性的。 使用交互式方法,消费者可以同步地从生产者接收数据(基于拉,T,IEnumerable),而采用反应性方法,生产者可以异步将数据推送到消费者(基于推,Task,IObservable)。




反应式编程是使用异步数据流进行编程,反应式扩展是基于系统名称空间中的IObservableIObserver接口的反应式编程实现,在IObserver接口上定义了一系列类似于LINQ的操作,称为LINQ over Observable。 反应性扩展支持.NET Standard,并且可以在Microsoft .NET运行的任何位置运行。




ReactiveUI通过提供诸如ReactiveCommand<TIn, TOut>WhenAnyValue类的工具,为应用程序开发人员提供了利用ICommandINotifyPropertyChanged接口的反应式实现的优势。 WhenAnyValue允许您将实现INotifyPropertyChanged接口的类的属性转换为IObservable<T>类型的事件流时,这将简化从属属性的实现。


 public class ExampleViewModel : ReactiveObject { [Reactive] // Attribute from the ReactiveUI.Fody package, // takes care of aspect-oriented INPC implementation // for this particular property. public string Name { get; set; } public ExampleViewModel() { // Here we subscribe to OnPropertyChanged("Name") events. this.WhenAnyValue(x => x.Name) // IObservable<string> .Subscribe(Console.WriteLine); } } 

ReactiveCommand<TIn, TOut>允许您使用与IObservable<TOut>一样的命令,每当命令完成执行时都会发布该命令。 同样,任何命令都具有类型为IObservable<Exception>ThrownExceptions属性。


 // ReactiveCommand<Unit, int> var command = ReactiveCommand.Create(() => 42); command // IObservable<int> .Subscribe(Console.WriteLine); command .ThrownExceptions // IObservable<Exception> .Select(exception => exception.Message) // IObservable<string> .Subscribe(Console.WriteLine); command.Execute().Subscribe(); // Outputs: 42 

在此之前,我们一直在使用IObservable<T> ,以及一个事件,只要被观察对象的状态发生变化,该事件就会发布类型T的新值。 简而言之, IObservable<T>是事件流,是时间拉伸的T类型集合。


当然,我们可以轻松自然地使用集合-每当集合发生更改时,我们都可以发布元素已更改的新集合。 在这种情况下,发布的值将是IEnumerable<T>类型或更专业,并且可观察的流本身将是IObservable<IEnumerable<T>> 。 但是,正如一个思维敏锐的读者正确指出的那样,这充满了关键的性能问题,尤其是如果我们的收藏中没有十二个元素,而是一百甚至数千个元素!


DynamicData简介


DynamicData是一个库,可让您在处理集合时使用反应式扩展的功能。 Rx非常强大,但是开箱即用并没有提供任何帮助来管理集合,而DynamicData对此进行了修复。 在大多数应用程序中,需要动态更新集合-通常,在应用程序启动时,集合中会填充项目,然后异步更新集合,从而将信息与服务器或数据库同步。 现代应用程序非常复杂,通常有必要创建集合的投影-过滤,转换或排序元素。 DynamicData旨在摆脱我们管理动态变化的数据集所需的极其复杂的代码。 该工具正在积极开发和完善中,现在已支持60多个操作员使用集合。




DynamicData不是ObservableCollection<T>的替代实现。 DynamicData的体系结构主要基于域驱动的编程概念。 使用的意识形态基于以下事实:您控制某个数据源,一个负责同步和变异数据的代码可以访问该集合。 接下来,您将一系列运算符应用于数据源,在这些运算符的帮助下,您可以声明性地转换数据,而无需手动创建和修改其他集合。 实际上,使用DynamicData可以将读写操作分开,并且只能以反应方式进行读取-因此,继承的集合将始终与源保持同步。


代替传统的IObservable<T> ,DynamicData定义了对IObservable<IChangeSet<T>>IObservable<IChangeSet<TValue, TKey>> ,其中IChangeSet是包含有关集合更改信息(包括更改类型)的块以及受影响的元素。 这种方法可以显着提高以反应方式编写的用于集合的代码的性能。 如果有必要一次访问集合的所有元素,则始终可以将IObservable<IChangeSet<T>>转换为IObservable<IEnumerable<T>> 。 如果这听起来很难-不用担心,下面的代码示例将使一切变得清晰!


动态数据在行动


为了更好地理解DynamicData的工作原理,它与System.Reactive的区别以及GUI软件的普通开发人员可以解决的任务,我们来看一些示例。 让我们从发布在GitHub上的综合示例开始。 在此示例中,数据源是SourceCache<Trade, long>其中包含交易的集合。 目的是仅显示活动事务,将模型转换为代理对象,对集合进行排序。


 // The default collection from the System.Collections.ObjectModel // namespace, to which we bind XAML UI controls. ReadOnlyObservableCollection<TradeProxy> list; // The mutable data source, containing the list of transactions. // We can use Add, Remove, Insert and similar methods on it. var source = new SourceCache<Trade, long>(trade => trade.Id); var cancellation = source // Here we transform the data source to an observable change set. .Connect() // Now we have IObservable<IChangeSet<Trade, long>> here. // Filter only active transactions. .Filter(trade => trade.Status == TradeStatus.Live) // Transform the models into proxy objects. .Transform(trade => new TradeProxy(trade)) // No we have IObservable<IChangeSet<TrandeProxy, long>> // Order the trade proxies by timestamp. .Sort(SortExpressionComparer<TradeProxy> .Descending(trade => trade.Timestamp)) // Use the dispatcher scheduler to update the GUI. .ObserveOnDispatcher() // Bind the sorted objects to the collection from the // System.Collections.ObjectModel namespace. .Bind(out list) // Ensure that when deleting elements from the // collections, the resources will get disposed. .DisposeMany() .Subscribe(); 

在上面的示例中,当更改作为数据源的SourceCache时, ReadOnlyObservableCollection也相应地更改。 同时,从集合中删除项目时,将调用Dispose方法,该集合将始终仅从GUI线程进行更新,并保持排序和过滤状态。 太好了,现在我们没有Dispatcher.Invoke调用了,代码简单易读!


数据源。 SourceList和SourceCache


DynamicData提供了两个专用集合,可以用作可变数据源。 这些集合是SourceList<TObject>SourceCache<TObject, TKey> 。 建议每当TObject具有唯一键时都使用SourceCache ,否则请使用SourceList 。 这些对象为.NET开发人员提供了用于集合管理的熟悉的API,例如AddRemoveInsert等方法。 要将数据源转换为IObservable<IChangeSet<T>>IObservable<IChangeSet<T, TKey>> ,请使用IObservable<IChangeSet<T, TKey>> .Connect()运算符。 例如,如果您有一项在后台定期更新项目集合的服务,则可以轻松地将这些项目的列表与GUI同步,而无需Dispatcher.Invoke和类似的样板代码:


 public class BackgroundService : IBackgroundService { // Declare the mutable data source containing trades. private readonly SourceList<Trade> _trades; // Expose the observable change set to the outside world. // If we have more than one subscriber, it is recommended // to use the Publish() operator from reactive extensions. public IObservable<IChangeSet<Trade>> Connect() => _trades.Connect(); public BackgroundService() { _trades = new SourceList<Trade>(); _trades.Add(new Trade()); // Mutate the source list! // Even from the background thread. } } 

借助强大的DynamicData运算符,我们可以将IObservable<IChangeSet<Trade>>转换为在视图模型中声明的ReadOnlyObservableCollection


 public class TradesViewModel : ReactiveObject { private readonly ReadOnlyObservableCollection<TradeVm> _trades; public ReadOnlyObservableCollection<TradeVm> Trades => _trades; public TradesViewModel(IBackgroundService background) { // Connect to the data source, transform elements, bind // them to the read-only observable collection. background.Connect() .Transform(x => new TradeVm(x)) .ObserveOn(RxApp.MainThreadScheduler) .Bind(out _trades) .DisposeMany() .Subscribe(); } } 

除了TransformFilterSort运算符外,DynamicData还支持分组,逻辑运算,集合展平,聚合函数的使用,消除相同元素,元素计数甚至在视图模型级别进行虚拟化。 您可以在GitHub上的项目README中了解有关所有运算符的更多信息。




除了SourceListSourceCache之外,DynamicData库还包括一个单线程可变集合实现ObservableCollectionExtended 。 要同步视图模型中的两个集合,请将其中一个声明为ObservableCollectionExtended ,将另一个声明为ReadOnlyObservableCollection ,然后使用ToObservableChangeSet运算符,该运算符的功能与Connect几乎相同,但旨在与ObservableCollection


 // Declare the derived collection. ReadOnlyObservableCollection<TradeVm> _derived; // Declare and initialize the source collection. var source = new ObservableCollectionExtended<Trade>(); source.ToObservableChangeSet(trade => trade.Key) .Transform(trade => new TradeProxy(trade)) .Filter(proxy => proxy.IsChecked) .Bind(out _derived) .Subscribe(); 

DynamicData还支持实现INotifyPropertyChanged接口的类中的更改跟踪。 例如,如果您希望每次属性更改时都收到通知,请使用AutoRefresh运算符并传入所需的属性选择器。 AutoRefresh和其他DynamicData运算符可以让您毫不费力地验证屏幕上显示的大量表单和嵌套表单!


 // IObservable<bool> var isValid = databases .ToObservableChangeSet() // Subscribe only to IsValid property changes. .AutoRefresh(database => database.IsValid) // Materialize the collection. .ToCollection() // Determine if all forms are valid. .Select(x => x.All(y => y.IsValid)); // If ReactiveUI is used, you can transform the // IObservable<bool> variable to a property declared // as ObservableAsPropertyHelper<bool>, eg IsValid. _isValid = isValid .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, x => x.IsValid); 

您可以使用DynamicData功能创建复杂的UI,它尤其适用于实时显示大量数据的系统,例如即时消息传递应用程序和监视系统。





结论


ReactiveX是一个功能强大的工具,可让您使用事件流和UI进行操作,编写可移植且可维护的代码,并以简单而优雅的方式解决复杂的任务。 ReactiveUI允许.NET开发人员使用MVVM体系结构通过INotifyPropertyChangedICommand响应式实现将响应式扩展集成到他们的项目中,而DynamicData通过实现INotifyCollectionChanged管理集合管理,以性能为重点扩展INotifyCollectionChanged扩展的功能。


ReactiveUI和DynamicData库与.NET平台上的所有GUI框架完全兼容,包括Windows Presentation Foundation,通用Windows平台,Avalonia,Xamarin.Android,Xamarin Forms和Xamarin iOS。 您可以在ReactiveUI文档的相应页面上开始研究DynamicData。 还请注意使自己熟悉DynamicData Snippets项目,其中包含几乎所有可能需要的代码示例。

Source: https://habr.com/ru/post/zh-CN454074/


All Articles