DynamicData:更改集合,MVVM设计模式和响应式扩展

2019年2月,发布了ReactiveUI 9 ,这是一个用于在Microsoft .NET平台上构建GUI应用程序的跨平台框架。 ReactiveUI是用于将反应式扩展与MVVM设计模式紧密集成的工具。 可以 Habré上一系列文章从文档的首页开始熟悉该框架。 ReactiveUI 9更新包括许多修复和改进 ,但最有趣和最重要的变化可能是与DynamicData框架的紧密集成,该框架允许以响应式样式更改集合。 让我们尝试找出在什么情况下DynamicData可以派上用场,以及如何在内部安排这种强大的反应式框架!

背景知识


首先,我们定义DynamicData解决的任务范围,并找出为什么使用System.Collections.ObjectModel命名空间中的更改数据集的标准工具不适合我们。

如您所知,MVVM模板涉及模型,表示和应用程序表示模型各层之间的职责划分。 模型层由域实体和服务表示,对表示模型一无所知。 模型层封装了整个复杂的应用程序逻辑,而表示模型则委托了模型的操作,从而通过可观察的属性,命令和集合为视图提供了有关应用程序当前状态信息的视图访问。 用于更改属性的标准工具是INotifyPropertyChanged接口,用于处理用户操作的INotifyPropertyChangedINotifyCollectionChanged实现集合并实现ObservableCollectionReadOnlyObservableCollection INotifyCollectionChanged



INotifyPropertyChangedICommand的实现通常仍取决于开发人员和所使用的MVVM框架的良知,但是ObservableCollection的使用对我们施加了许多限制! 例如,如果没有Dispatcher.Invoke或类似的调用,我们将无法从后台线程更改集合,这在处理某些后台操作与服务器同步的数据数组的情况下可能很有用。 应当注意,在惯用的MVVM中,模型层不需要了解所使用的GUI应用程序的体系结构,并且不需要与MVC或MVP中的模型兼容,这就是为什么许多Dispatcher.Invoke允许从运行中的后台线程访问用户界面控件的原因在域服务中,违反了在应用程序层之间分担责任的原则。

当然,在域服务中,可以声明一个事件,并作为事件的参数传递带有已更改数据的块。 然后订阅该事件,将Dispatcher.Invoke调用包装在接口中,以使其不依赖于所使用的GUI框架,将Dispatcher.Invoke移至表示模型并ObservableCollection需要更改ObservableCollection ,但是有一种更简单,更优雅的方法来解决指定范围的任务,而无需编写自行车。 让我们开始学习!

反应性扩展。 管理数据流


为了完全理解DynamicData引入的抽象以及更改反应式数据集的工作原理,让我们回顾一下什么是反应式编程以及如何在Microsoft .NET平台和MVVM设计模式的上下文中应用它 。 组织程序组件之间交互的一种方式可以是交互的和反应性的。 在交互式交互中,使用者功能从提供程序功能(基于拉的方法TIEnumerable )同步接收数据,在响应式交互中,使用者功能异步将数据传递到使用者功能(基于推方法, TaskIObservable )。



反应式编程是使用异步数据流进行编程,而反应式扩展是其实现的特例,它基于System名称空间中的IObservableIObserver ,该IObserverIObserver上定义了许多类似于LINQ的操作,称为LINQ over Observable。 反应性扩展支持.NET Standard,并且可以在Microsoft .NET平台工作的任何地方工作。



ReactiveUI框架邀请应用程序开发人员利用ICommandINotifyPropertyChanged的响应式实现,提供诸如ReactiveCommand<TIn, TOut>WhenAnyValue类的强大工具。 WhenAnyValue允许WhenAnyValue将实现INotifyPropertyChanged的类的属性转换为IObservable<T>类型的事件流,从而简化了相关属性的实现。

 public class ExampleViewModel : ReactiveObject { [Reactive] //  ReactiveUI.Fody,  // -  // OnPropertyChanged   Name. public string Name { get; set; } public ExampleViewModel() { //  OnPropertyChanged("Name"). 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(); // : 42 

一直以来,我们一直使用IObservable<T> ,并且每当被监视对象的状态发生变化时,事件就会发布类型T的新值。 简而言之, IObservable<T>是事件流,序列随时间延伸。

当然,我们也可以轻松自然地使用集合-每当集合发生更改时,发布具有更改元素的新集合。 在这种情况下,发布的值将为IEnumerable<T>类型或更专业,并且事件本身将为IObservable<IEnumerable<T>> 。 但是,正如具有批判性思维的读者正确指出的那样,这会给应用程序性能带来严重问题,特别是如果我们的收藏中没有十二个元素,而是一百甚至数千个元素!

DynamicData简介


DynamicData是一个库,可让您在使用集合时充分利用反应式扩展的全部功能。 开箱即用的反应式扩展未提供使用最佳方法来更改数据集,而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示例


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

 //    System.Collections.ObjectModel, //       . ReadOnlyObservableCollection<TradeProxy> list; //   ,   . //   Add, Remove, Insert   . var source = new SourceCache<Trade, long>(trade => trade.Id); var cancellation = source //      . //   IObservable<IChangeSet<Trade, long>> .Connect() //       . .Filter(trade => trade.Status == TradeStatus.Live) //    -. //   IObservable<IChangeSet<TrandeProxy, long>> .Transform(trade => new TradeProxy(trade)) //    . .Sort(SortExpressionComparer<TradeProxy> .Descending(trade => trade.Timestamp)) //  GUI    . .ObserveOnDispatcher() //    - //    System.Collections.ObjectModel. .Bind(out list) // ,       //    . .DisposeMany() .Subscribe(); 

在上面的示例中,当您更改作为数据源的SourceCache时, ReadOnlyObservableCollection也将相应地更改。 在这种情况下,从集合中删除元素时,将调用Dispose方法,该集合将始终仅在GUI流中进行更新,并保持排序和过滤状态。 很酷,没有Dispatcher.Invoke和复杂的代码!

SourceList和SourceCache数据源


DynamicData提供了两个专用集合,可以用作可变数据源。 这些集合的类型为SourceListSourceCache<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 { //    . private readonly SourceList<Trade> _trades; //     . //  ,     , //    Publish()  Rx. public IObservable<IChangeSet<Trade>> Connect() => _trades.Connect(); public BackgroundService() { _trades = new SourceList<Trade>(); _trades.Add(new Trade()); //    ! //    . } } 

DynamicData使用内置的.NET类型将数据映射到外界。 使用强大的DynamicData运算符,我们可以将IObservable<IChangeSet<Trade>>转换ReadOnlyObservableCollection我们的视图模型的ReadOnlyObservableCollection

 public class TradesViewModel : ReactiveObject { private readonly ReadOnlyObservableCollection<TradeVm> _trades; public ReadOnlyObservableCollection<TradeVm> Trades => _trades; public TradesViewModel(IBackgroundService background) { //   ,  ,  //     System.Collections.ObjectModel. 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

 //   . ReadOnlyObservableCollection<TradeVm> _derived; //    -. 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然后将所需属性的选择器与参数一起传递。 AutoRefesh和其他DynamicData运算符将使您轻松自然地验证屏幕上显示的大量表单和子表单!

 //  IObservable<bool> var isValid = databases .ToObservableChangeSet() //      IsValid. .AutoRefresh(database => database.IsValid) //       . .ToCollection() // ,    . .Select(x => x.All(y => y.IsValid)); //   ReactiveUI, IObservable<bool> //     ObservableAsProperty. _isValid = isValid .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, x => x.IsValid); 

基于DynamicData功能,您可以快速创建相当复杂的界面-对于显示大量实时数据的系统,即时消息传递系统和监视系统尤其如此。



结论


反应性扩展是一个功能强大的工具,可让您声明性地使用数据和用户界面,编写可移植和受支持的代码,并以简单优雅的方式解决复杂的问题。 ReactiveUI允许.NET开发人员通过提供INotifyPropertyChangedICommand响应式实现,使用MVVM体系结构将响应式扩展紧密集成到他们的项目中,而DynamicData通过实现INotifyCollectionChanged来处理集合同步,扩展INotifyCollectionChanged扩展的功能并关注性能。

ReactiveUIDynamicData库与.NET平台的大多数流行的GUI框架兼容,包括Windows Presentation Foundation,通用Windows平台,Avalonia,Xamarin.Android,Xamarin Forms,Xamarin.iOS。 您可以从相应的ReactiveUI文档页面开始学习DynamicData。 另外,请确保签出DynamicData Snippets项目,其中包含在所有场合下使用DynamicData的示例。

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


All Articles