DynamicData: Changing collections, MVVM design pattern, and reactive extensions

    In February 2019, ReactiveUI 9   , a cross-platform framework for building GUI applications on the Microsoft .NET platform, was released. ReactiveUI   is a tool for tightly integrating reactive extensions with the MVVM design pattern. Acquaintance with the framework can be started with a series of articles on Habré or from the front page of the documentation . The ReactiveUI 9 update includes many fixes and improvements , but perhaps the most interesting and significant change is tight integration with the DynamicData framework , which allows working with changing collections in a reactive style. Let's try to figure out in which cases DynamicData can come in handy . and how this powerful reactive framework is arranged inside!

    Background


    First, we define the range of tasks that DynamicData solves and find out why standard tools for working with changing data sets from the namespace do not suit us System.Collections.ObjectModel.

    The MVVM template, as you know, involves the division of responsibility between the layers of the model, presentation and presentation model of the application. The model layer is represented by domain entities and services, and knows nothing about the presentation model. The model layer encapsulates the entire complex application logic, and the presentation model delegates the model’s operations, giving the view access to information about the current state of the application through observable properties, commands, and collections. The standard tool for working with changing properties is the interface INotifyPropertyChanged, for working with user actions -ICommand, and to work with collections - INotifyCollectionChangedand implementations ObservableCollectionand ReadOnlyObservableCollection.



    Implementation INotifyPropertyChangedand ICommandusually remains on the conscience of the developer and used MVVM framework, but the use ObservableCollectionimposes a number of restrictions on us! For example, we cannot change a collection from a background thread without Dispatcher.Invokeor a similar call, and this could be useful in case of working with data arrays that some background operation synchronizes with the server. It should be noted that in the idiomatic MVVM the model layer does not need to know about the architecture of the GUI application used, and be compatible with the model from MVC or MVP, and that is why numerousDispatcher.Invoke, allowing access to the user interface control from the background thread running in the domain service, violate the principle of sharing responsibility between application layers.

    Of course, in a domain service it would be possible to declare an event, and as an argument of an event, pass a chunk with changed data. Then subscribe to the event, wrap the call Dispatcher.Invokein an interface so that it does not depend on the GUI framework used, move it Dispatcher.Invoketo the presentation model and change ObservableCollectionit as needed, however there is a much simpler and more elegant way to solve the indicated range of tasks without writing a bicycle. Let's start the study!

    Reactive extensions. Manage data streams


    For a complete understanding of the abstractions introduced by DynamicData and the principles of working with changing reactive datasets, let's recall what reactive programming is and how to apply it in the context of the Microsoft .NET platform and the MVVM design pattern . A way to organize the interaction between program components can be interactive and reactive. When the interaction function is interactive, the user receives synchronous data from the function-provider (pull-based approach T, IEnumerable), and interaction with reactive function provider provides data asynchronously consuming functions (push-based approach Task, IObservable).



    Reactive programmingThis is programming using asynchronous data streams, and reactive extensions are a special case of its implementation, based on interfaces IObservableand IObserverfrom the System namespace, which defines a number of LINQ-like operations on an interface IObservablecalled LINQ over Observable. Reactive extensions support the .NET Standard and work wherever the Microsoft .NET platform works.



    The ReactiveUI framework offers application developers to take advantage of the reactive implementation of interfaces ICommandand INotifyPropertyChangedby providing such powerful tools as and . allows you to convert a property of a class that implements INotifyPropertyChanged into an event stream of a type , which simplifies the implementation of dependent properties.ReactiveCommandWhenAnyValueWhenAnyValueIObservable

    public class ExampleViewModel : ReactiveObject
    {
      [Reactive] 
      // Атрибут ReactiveUI.Fody, занимается
      // аспектно-ориентированным внедрением
      // OnPropertyChanged в сеттер Name.
      public string Name { get; set; }
      public ExampleViewModel()
      {
        // Слушаем OnPropertyChanged("Name").
        this.WhenAnyValue(x => x.Name)
            // Работаем с IObservable
            .Subscribe(Console.WriteLine);
      }
    }
    

    ReactiveCommandallows you to work with the team as an event of the type that is published whenever the team completes execution. Also, any command has a type property .IObservableThrownExceptionsIObservable

    // ReactiveCommand
    var command = ReactiveCommand.Create(() => 42);
    command
      // Работаем с IObservable
      .Subscribe(Console.WriteLine);
    command
      .ThrownExceptions
      // Работаем с IObservable
      .Select(exception => exception.Message)
      // Работаем с IObservable
      .Subscribe(Console.WriteLine);
    command.Execute().Subscribe();
    // Вывод: 42
    

    All this time we worked with , as with an event that publishes a new type value whenever the state of the object being monitored changes. Simply put, it is a stream of events, a sequence stretched over time. Of course, we could just as easily and naturally work with collections - whenever a collection changes, publish a new collection with changed elements. In this case, the published value would have a type or more specialized, and the event itself would have a type . But, as the critically thinking reader correctly points out, this is fraught with serious problems with application performance, especially if there are not a dozen elements in our collection, but a hundred, or even several thousand!IObservableTIObservable

    IEnumerableIObservable>

    Introduction to DynamicData


    DynamicData   is a library that allows you to use the full power of reactive extensions when working with collections. Out of the box reactive extensions do not provide optimal ways to work with changing datasets, and the DynamicData task- to fix this. In most applications, there is a need to dynamically update collections - usually, a collection is filled with some elements when the application starts, and then it is updated asynchronously, synchronizing information with a server or database. Modern applications are quite complex, and often there is a need to create collections projections - filter, transform or sort elements. DynamicData was designed just to get rid of the incredibly complex code that we would need to manage dynamically changing data sets. The tool is actively developed and finalized, and now more than 60 operators for working with collections are supported. DynamicData is not an alternative implementation . Architecture



    ObservableCollectionDynamicData is based primarily on the concepts of subject-oriented programming. The ideology of use is based on the fact that you manage some data source, a collection that the code responsible for synchronizing and changing data has access to. Next, you apply a number of operators to the source, with which you can declaratively transform the data, without the need to manually create and modify other collections. In fact, with DynamicData you separate read and write operations, and you can only read in a reactive way - therefore, inherited collections will always be synchronized with the source.

    Instead of the classic , DynamicData defines operations on and , whereIObservableIObservable>>IObservable>IChangeSet- a chunk containing information about the change in the collection — the type of change and the elements that have been affected. This approach can significantly improve the performance of code for working with collections written in a reactive style. At the same time, you can always transform into ordinary , if it becomes necessary to process all the elements of the collection at once. If it sounds complicated - do not be alarmed, from the code examples everything will become clear and transparent!IObservable>IObservable>

    DynamicData example


    Let's look at a series of examples to better understand how DynamicData works, how it differs from System.Reactiveand what tasks ordinary developers of application software with a GUI can solve. Let's start with a comprehensive example posted by DynamicData on GitHub . In the example, the data source is a containing a collection of transactions. The task is to show only active transactions, transform models into proxy objects, sort the collection.SourceCache

    // Стандартная коллекция из System.Collections.ObjectModel,
    // к которой будет привязан графический элемент управления.
    ReadOnlyObservableCollection list;
    // Изменяемый источник данных, содержащий модели сделок.
    // Можем использовать Add, Remove, Insert и подобные методы.
    var source = new SourceCache(trade => trade.Id);
    var cancellation = source
      // Трансформируем источник в наблюдаемый набор изменений.
      // Имеем тип IObservable>
      .Connect()
      // Дальше по цепочке пропустим только активные сделки.
      .Filter(trade => trade.Status == TradeStatus.Live) 
      // Трансформируем модели в прокси-объекты.
      // Имеем тип IObservable>
      .Transform(trade => new TradeProxy(trade))
      // Отсортируем объекты по времени.
      .Sort(SortExpressionComparer
        .Descending(trade => trade.Timestamp))
      // Обновляем GUI только из главного потока. 
      .ObserveOnDispatcher()
      // Привяжем список отсортированных прокси-объектов
      // к коллекции из System.Collections.ObjectModel.
      .Bind(out list) 
      // Убедимся, что по мере удаления элементов из
      // коллекции ресурсы будут освобождены.
      .DisposeMany()
      .Subscribe();
    

    In the example above, a change SourceCachethat is a data source will ReadOnlyObservableCollectionalso change accordingly. At the same time, when deleting elements from the collection, the method will be called Dispose, the collection will always be updated only in the GUI stream and remain sorted and filtered. Cool, no Dispatcher.Invokecomplicated code!

    SourceList and SourceCache Data Sources


    DynamicData provides two specialized collections that can be used as a mutable data source. These collections are types SourceListand . It is recommended to use it whenever it has a unique key, otherwise use it . These objects provide familiar .NET API for developers to change the data - the methods , , and the like. To convert data sources to or , use the operator  . For example, if you have a service that updates the collection of elements in the background, you can easily synchronize the list of these elements with the GUI, without any architectural excesses:SourceCacheSourceCacheTObjectSourceListAddRemoveInsertIObservable>IObservable>.Connect()Dispatcher.Invoke

    public class BackgroundService : IBackgroundService
    {
      // Объявляем изменяемый список сделок.
      private readonly SourceList _trades;
      // Выставляем наружу поток изменений коллекции.
      // Если ожидается, что будет более одного подписчика,
      // рекомендуется использовать оператор Publish() из Rx.
      public IObservable> Connect() => _trades.Connect();
      public BackgroundService()
      {
        _trades = new SourceList();
        _trades.Add(new Trade());
        // Изменяем список как хотим!
        // Даже из фонового потока.
      }
    }
    

    DynamicData uses built-in .NET types to map data to the outside world. Using powerful DynamicData operators, we can transform views into our model.IObservable>ReadOnlyObservableCollection

    public class TradesViewModel : ReactiveObject
    { 
      private readonly ReadOnlyObservableCollection _trades;
      public ReadOnlyObservableCollection Trades => _trades;
      public TradesViewModel(IBackgroundService background)
      {
        // Подключимся к источнику, трансформируем элементы, привяжем
        // их к коллекции из System.Collections.ObjectModel.
        background.Connect()
          .Transform(x => new TradeVm(x))
          .ObserveOn(RxApp.MainThreadScheduler)
          .Bind(out _trades)
          .DisposeMany()
          .Subscribe();
      }
    }
    

    Additionally Transform, Filterand Sort, DynamicData includes numerous other operators supports grouping, logical operations, smoothing collection application aggregated functions exception identical elements, counting elements and even at the level virtualization model representation. Read more about all operators in the README project on GitHub .



    Single threaded collections and change tracking


    Besides SourceListand SourceCache, the DynamicData library also includes a single-threaded implementation of a mutable collection - ObservableCollectionExtended. To synchronize two collections in your view model, declare one of them as ObservableCollectionExtended, and the other as ReadOnlyObservableCollectionand use an operator ToObservableChangeSetthat behaves the same as it is Connect, but is designed to work with ObservableCollection.

    // Объявим производную коллекцию.
    ReadOnlyObservableCollection _derived;
    // Объявим и инициализиуем коллекцию-источник.
    var source = new ObservableCollectionExtended();
    source.ToObservableChangeSet(trade => trade.Key)
      .Transform(trade => new TradeProxy(trade))
      .Filter(proxy => proxy.IsChecked)
      .Bind(out _derived)
      .Subscribe();
    

    DynamicData also supports tracking changes to classes that implement the interface INotifyPropertyChanged. For example, if you want to be notified of a collection change whenever a property of an element changes, use the operator AutoRefreshand pass the selector of the desired property with the argument. AutoRefeshand other DynamicData operators will allow you to easily and naturally validate the huge number of forms and nested forms displayed on the screen!

    // Тип IObservable
    var isValid = databases
      .ToObservableChangeSet()
      // Подписываемся только на изменения свойства IsValid.
      .AutoRefresh(database => database.IsValid) 
      // Получаем доступ ко всем элементам коллекции одновременно.
      .ToCollection()
      // Проверяем, что все элементы валидны.
      .Select(x => x.All(y => y.IsValid));
    // Если используется ReactiveUI, IObservable
    // можно преобразовать в свойство ObservableAsProperty.
    _isValid = isValid
      .ObserveOn(RxApp.MainThreadScheduler)
      .ToProperty(this, x => x.IsValid);
    

    Based on the DynamicData functionality, you can quickly create fairly complex interfaces - this is especially true for systems that display a large amount of real-time data, instant messaging systems, and monitoring systems.



    Conclusion


    Reactive extensions are a powerful tool that allows you to declaratively work with data and the user interface, write portable and supported code, and solve complex problems in a simple and elegant way. ReactiveUI allows .NET developers to closely integrate reactive extensions into their projects using the MVVM architecture by providing reactive implementations INotifyPropertyChangedand ICommand, and DynamicData takes care of collection synchronization by implementing INotifyCollectionChanged, expanding the capabilities of reactive extensions and taking care of performance.

    Libraries ReactiveUI and DynamicDatacompatible with most popular .NET framework GUIs, including Windows Presentation Foundation, Universal Windows Platform, Avalonia , Xamarin.Android, Xamarin Forms, Xamarin.iOS. You can start learning DynamicData from the corresponding ReactiveUI documentation page . Also be sure to check out the DynamicData Snippets project , which contains examples of using DynamicData for all occasions.

    Also popular now: