DynamicData: colecciones cambiantes, patrón de diseño MVVM y extensiones reactivas

En febrero de 2019, se lanzó ReactiveUI 9 , un marco multiplataforma para crear aplicaciones GUI en la plataforma Microsoft .NET. ReactiveUI es una herramienta para integrar estrechamente las extensiones reactivas con el patrón de diseño MVVM. El conocimiento del marco puede iniciarse con una serie de artículos sobre Habré o desde la portada de la documentación . La actualización ReactiveUI 9 incluye muchas correcciones y mejoras , pero quizás el cambio más interesante y significativo es la estrecha integración con el marco DynamicData , que permite trabajar con colecciones cambiantes en un estilo reactivo. ¡Intentemos averiguar en qué casos DynamicData puede ser útil y cómo este poderoso marco reactivo está organizado dentro!

Antecedentes


Primero, definimos el rango de tareas que DynamicData resuelve y descubrimos por qué las herramientas estándar para trabajar con conjuntos de datos cambiantes desde el espacio de nombres System.Collections.ObjectModel no nos convienen.

La plantilla MVVM, como saben, implica la división de la responsabilidad entre las capas del modelo, la presentación y el modelo de presentación de la aplicación. La capa del modelo está representada por entidades de dominio y servicios, y no sabe nada sobre el modelo de presentación. La capa del modelo encapsula toda la lógica de la aplicación compleja, y el modelo de presentación delega las operaciones del modelo, dando a la vista acceso a información sobre el estado actual de la aplicación a través de propiedades, comandos y colecciones observables. La herramienta estándar para trabajar con propiedades cambiantes es la interfaz INotifyPropertyChanged , INotifyPropertyChanged para trabajar con acciones del usuario e INotifyCollectionChanged implementar colecciones e implementar ObservableCollection y ReadOnlyObservableCollection .



La implementación de INotifyPropertyChanged e ICommand generalmente permanece en la conciencia del desarrollador y el marco MVVM utilizado, ¡pero el uso de ObservableCollection impone una serie de restricciones! Por ejemplo, no podemos cambiar una colección de un subproceso en segundo plano sin Dispatcher.Invoke o una llamada similar, y esto podría ser útil en caso de trabajar con matrices de datos que alguna operación en segundo plano sincroniza con el servidor. Cabe señalar que en el MVVM idiomático, la capa del modelo no necesita conocer la arquitectura de la aplicación GUI utilizada, y ser compatible con el modelo de MVC o MVP, y es por eso que numerosos Dispatcher.Invoke permiten el acceso al control de la interfaz de usuario desde el hilo de fondo en ejecución en un servicio de dominio, viola el principio de compartir la responsabilidad entre las capas de la aplicación.

Por supuesto, en un servicio de dominio sería posible declarar un evento y, como argumento de un evento, pasar un fragmento con los datos modificados. Luego suscríbase al evento, envuelva el Dispatcher.Invoke llamada en una interfaz para que no dependa del marco GUI utilizado, mueva Dispatcher.Invoke al modelo de presentación y cambie la ObservableCollection sea ​​necesario, sin embargo, hay una manera mucho más simple y elegante de resolver el rango de tareas indicado sin tener que escribir una bicicleta . ¡Comencemos el estudio!

Extensiones reactivas. Administrar flujos de datos


Para una comprensión completa de las abstracciones introducidas por DynamicData y los principios de trabajar con conjuntos de datos reactivos cambiantes, recordemos qué es la programación reactiva y cómo aplicarla en el contexto de la plataforma Microsoft .NET y el patrón de diseño MVVM . Una forma de organizar la interacción entre los componentes del programa puede ser interactiva y reactiva. En la interacción interactiva, la función del consumidor recibe sincrónicamente datos de la función del proveedor (enfoque basado en extracción, T , IEnumerable ), y en la interacción reactiva, la función del consumidor entrega datos asincrónicamente a la función del consumidor (enfoque basado en inserción, Task , IObservable ).



La programación reactiva es la programación usando flujos de datos asíncronos, y las extensiones reactivas son un caso especial de su implementación, basada en las IObserver IObservable e IObserver del espacio de nombres del sistema, que define una serie de operaciones similares a LINQ en la interfaz IObservable , llamada LINQ over Observable. Las extensiones reactivas son compatibles con .NET Standard y funcionan donde sea que funcione la plataforma Microsoft .NET.



El marco ReactiveUI invita a los desarrolladores de aplicaciones a aprovechar la implementación reactiva de ICommand e INotifyPropertyChanged , proporcionando herramientas poderosas como ReactiveCommand<TIn, TOut> y WhenAnyValue . WhenAnyValue permite convertir una propiedad de una clase que implementa INotifyPropertyChanged en una secuencia de eventos de tipo IObservable<T> , lo que simplifica la implementación de propiedades dependientes.

 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> permite trabajar con el comando, como con un evento de tipo IObservable<TOut> , que se publica cada vez que el comando completa la ejecución. Además, cualquier comando tiene una propiedad ThrownExceptions de tipo IObservable<Exception> .

 // 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 

Todo este tiempo trabajamos con IObservable<T> , como con un evento que publica un nuevo valor de tipo T cada vez que cambia el estado del objeto que se está monitoreando. En pocas palabras, IObservable<T> es una secuencia de eventos, una secuencia que se extiende a lo largo del tiempo.

Por supuesto, podríamos trabajar de manera igual y fácil con colecciones: cada vez que cambie una colección, publique una nueva colección con elementos modificados. En este caso, el valor publicado sería del tipo IEnumerable<T> o más especializado, y el evento en sí sería del tipo IObservable<IEnumerable<T>> . Pero, como señala correctamente el lector crítico, esto está plagado de serios problemas con el rendimiento de la aplicación, especialmente si no hay una docena de elementos en nuestra colección, ¡sino cien, o incluso varios miles!

Introducción a DynamicData


DynamicData es una biblioteca que le permite utilizar todo el poder de las extensiones reactivas cuando trabaja con colecciones. Las extensiones reactivas listas para usar no proporcionan formas óptimas de trabajar con conjuntos de datos cambiantes, y el trabajo de DynamicData es solucionarlo. En la mayoría de las aplicaciones, existe la necesidad de actualizar dinámicamente las colecciones; por lo general, una colección se llena con algunos elementos cuando se inicia la aplicación, y luego se actualiza de forma asíncrona, sincronizando la información con un servidor o una base de datos. Las aplicaciones modernas son bastante complejas y, a menudo, es necesario crear proyecciones de colecciones: filtrar, transformar u ordenar elementos. DynamicData fue diseñado solo para deshacerse del código increíblemente complejo que necesitaríamos para administrar conjuntos de datos que cambian dinámicamente. La herramienta se desarrolla y finaliza activamente, y ahora se admiten más de 60 operadores para trabajar con colecciones.



DynamicData no es una implementación alternativa de ObservableCollection<T> . La arquitectura DynamicData se basa principalmente en los conceptos de programación específica del dominio. La ideología de uso se basa en el hecho de que administra una fuente de datos, una colección a la que tiene acceso el código responsable de sincronizar y cambiar los datos. A continuación, aplica varios operadores a la fuente, con los que puede transformar los datos de forma declarativa, sin la necesidad de crear y modificar manualmente otras colecciones. De hecho, con DynamicData separa las operaciones de lectura y escritura, y solo puede leer de manera reactiva; por lo tanto, las colecciones heredadas siempre se sincronizarán con la fuente.

En lugar del clásico IObservable<T> , DynamicData define operaciones en IObservable<IChangeSet<T>>> y IObservable<IChangeSet<TValue, TKey>> , donde IChangeSet es un fragmento que contiene información sobre el cambio de colección: el tipo de cambio y los elementos que se vieron afectados. Este enfoque puede mejorar significativamente el rendimiento del código para trabajar con colecciones escritas en un estilo reactivo. Al mismo tiempo, IObservable<IChangeSet<T>> siempre se puede transformar en un IObservable<IEnumerable<T>> si es necesario procesar todos los elementos de la colección a la vez. Si suena complicado, ¡no se alarme, a partir de los ejemplos de código todo se volverá claro y transparente!

Ejemplo de DynamicData


Veamos una serie de ejemplos para comprender mejor cómo funciona DynamicData, cómo se diferencia de System.Reactive y qué tareas pueden resolver los desarrolladores comunes de software de aplicaciones con una GUI. Comencemos con un ejemplo completo publicado por DynamicData en GitHub . En el ejemplo, la fuente de datos es SourceCache<Trade, long> , que contiene una colección de transacciones. La tarea es mostrar solo transacciones activas, transformar modelos en objetos proxy, ordenar la colección.

 //    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(); 

En el ejemplo anterior, cuando cambia el SourceCache , que es el origen de datos, ReadOnlyObservableCollection también cambiará en consecuencia. En este caso, al eliminar elementos de la colección, se llamará al método Dispose , la colección siempre se actualizará solo en la secuencia de la GUI y permanecerá ordenada y filtrada. Genial, sin Dispatcher.Invoke y código complicado!

Fuentes de datos SourceList y SourceCache


DynamicData proporciona dos colecciones especializadas que pueden usarse como fuente de datos mutable. Estas colecciones son de los tipos SourceList y SourceCache<TObject, TKey> . Se recomienda usar SourceCache siempre que TObject tenga una clave única; de lo contrario, use SourceList . Estos objetos proporcionan la conocida API de desarrolladores de .NET para modificar datos: Add , Remove , Insert y similares. Para convertir fuentes de datos a IObservable<IChangeSet<T>> o IObservable<IChangeSet<T, TKey>> , use el operador .Connect() . Por ejemplo, si tiene un servicio que actualiza la colección de elementos en segundo plano, puede sincronizar fácilmente la lista de estos elementos con la GUI, sin Dispatcher.Invoke y excesos arquitectónicos:

 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 utiliza tipos .NET integrados para asignar datos al mundo exterior. Usando los poderosos operadores DynamicData, podemos transformar el IObservable<IChangeSet<Trade>> en ReadOnlyObservableCollection nuestro modelo de vista.

 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(); } } 

Además de Transform , Filter y Sort , DynamicData incluye una gran cantidad de otros operadores, admite agrupación, operaciones lógicas, suavizar una colección, usar funciones de agregación, excluir elementos idénticos, contar elementos e incluso virtualizar a nivel del modelo de presentación. Lea más sobre todos los operadores en el proyecto README en GitHub .



Colecciones de un solo hilo y seguimiento de cambios


Además de SourceList y SourceCache , la biblioteca DynamicData también incluye una implementación de subproceso único de una colección mutable: ObservableCollectionExtended . Para sincronizar dos colecciones en su modelo de vista, declare una como ObservableCollectionExtended y la otra como ReadOnlyObservableCollection y use el operador ToObservableChangeSet , que se comporta igual que Connect pero está diseñado para funcionar con 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 también admite el seguimiento de cambios en las clases que implementan la interfaz INotifyPropertyChanged . Por ejemplo, si desea que se le notifique un cambio de colección cada vez que cambie una propiedad de un elemento, use la AutoRefresh y pase el selector de la propiedad deseada con el argumento. AutoRefesh y otros operadores DynamicData le permitirán validar de manera fácil y natural la gran cantidad de formularios y AutoRefesh muestran en la pantalla!

 //  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); 

Basado en la funcionalidad DynamicData, puede crear rápidamente interfaces bastante complejas, esto es especialmente cierto para los sistemas que muestran una gran cantidad de datos en tiempo real, sistemas de mensajería instantánea y sistemas de monitoreo.



Conclusión


Las extensiones reactivas son una herramienta poderosa que le permite trabajar de manera declarativa con los datos y la interfaz de usuario, escribir código portátil y compatible, y resolver problemas complejos de una manera simple y elegante. ReactiveUI permite a los desarrolladores de .NET integrar estrechamente las extensiones reactivas en sus proyectos utilizando la arquitectura MVVM al proporcionar implementaciones reactivas de INotifyPropertyChanged e ICommand , mientras que DynamicData se encarga de la sincronización de la colección al implementar INotifyCollectionChanged , ampliando las capacidades de las extensiones reactivas y cuidando el rendimiento.

Las bibliotecas ReactiveUI y DynamicData son compatibles con los marcos de GUI más populares de la plataforma .NET, incluidas Windows Presentation Foundation, Universal Windows Platform, Avalonia , Xamarin.Android, Xamarin Forms, Xamarin.iOS. Puede comenzar a aprender DynamicData desde la página de documentación de ReactiveUI correspondiente . También asegúrese de revisar el proyecto DynamicData Snippets , que contiene ejemplos del uso de DynamicData para todas las ocasiones.

Source: https://habr.com/ru/post/445098/


All Articles