DynamicData: Colecciones dinámicas, arquitectura MVVM y extensiones reactivas

Febrero de 2019 marcó el lanzamiento de ReactiveUI 9 , el marco multiplataforma para crear aplicaciones GUI en la plataforma Microsoft .NET. ReactiveUI es una herramienta para la estrecha integración de extensiones reactivas con el patrón de diseño MVVM. Puede familiarizarse con el marco a través de una serie de videos o la página de bienvenida de la documentación . La actualización ReactiveUI 9 incluye numerosas correcciones y mejoras , pero probablemente la más crucial e interesante es la integración con el marco DynamicData , lo que le permite trabajar con colecciones dinámicas de manera reactiva. ¡ Averigüemos para qué podemos usar DynamicData y cómo funciona este poderoso marco reactivo bajo el capó!


Introduccion


Primero determinemos los casos de uso de DynamicData y descubramos lo que no nos gusta de las herramientas predeterminadas para trabajar con conjuntos de datos dinámicos desde el espacio de nombres System.Collections.ObjectModel .


La plantilla MVVM, como sabemos, asume la división de responsabilidad entre la capa del modelo, la capa de presentación y el modelo de presentación de la aplicación, también conocido como el modelo de vista. La capa del modelo está representada por entidades y servicios de dominio y no sabe nada sobre la capa del modelo de vista. El modelo encapsula toda la lógica compleja de la aplicación, mientras que el modelo de vista delega operaciones al modelo, proporcionando acceso a la información sobre el estado actual de la aplicación a través de propiedades, comandos y colecciones observables. La herramienta predeterminada para trabajar con propiedades dinámicas es la interfaz INotifyPropertyChanged , para trabajar con acciones del usuario - ICommand , y para trabajar con colecciones - la interfaz INotifyCollectionChanged , así como las implementaciones, como ObservableCollection<T> y ReadOnlyObservableCollection<T> .




Las implementaciones de las INotifyPropertyChanged e ICommand generalmente dependen del desarrollador y del marco MVVM utilizado, pero el uso de la ObservableCollection<T> predeterminada impone una serie de limitaciones. Por ejemplo, no podemos mutar la colección desde un subproceso en segundo plano sin Dispatcher.Invoke o una llamada similar, y eso habría sido muy útil para sincronizar matrices de datos con el servidor a través de una operación en segundo plano. Vale la pena señalar que cuando se usa la arquitectura MVVM limpia, la capa del modelo no debe saber nada sobre el marco GUI utilizado, y debe ser compatible con la capa del modelo en la terminología MVC o MVP. Es por eso que esas numerosas llamadas Dispatcher.Invoke en los servicios de dominio violan el principio de segregación de responsabilidad.


Por supuesto, podríamos declarar un evento en un servicio de dominio y transmitir un fragmento con elementos modificados como argumentos de evento, luego suscribirnos al evento, encapsular el Dispatcher.Invoke llamada detrás de una interfaz, por lo que nuestra aplicación no dependerá de ninguna GUI framework, llame a esa interfaz desde el modelo de vista y modifique ObservableCollection<T> consecuencia, pero hay una manera mucho más elegante de tratar tales problemas sin la necesidad de reinventar la rueda. ¿Qué estamos esperando entonces?


Extensiones reactivas. Gestión de flujos de datos observables


Para comprender completamente las abstracciones introducidas por DynamicData y cómo trabajar con conjuntos de datos reactivos cambiantes, recordemos qué es la programación reactiva y cómo usarla en el contexto de la plataforma Microsoft .NET y el patrón de diseño MVVM . La forma de organizar la interacción entre los componentes del programa puede ser interactiva o reactiva. Con el enfoque interactivo, el consumidor recibe datos del productor sincrónicamente (basado en pull, T, IEnumerable), y con el enfoque reactivo, el productor envía los datos al consumidor de forma asincrónica (basado en push, Task, IObservable).




La programación reactiva es la programación con flujos de datos asíncronos, y las extensiones reactivas son la implementación de la programación reactiva, basada en las interfaces IObservable e IObserver del espacio de nombres del sistema, que define una serie de operaciones similares a LINQ en la interfaz IObservable , conocida como LINQ over Observable. Las extensiones reactivas son compatibles con .NET Standard y se ejecutan donde sea que se ejecute Microsoft .NET.




ReactiveUI ofrece a los desarrolladores de aplicaciones que aprovechen el uso de implementaciones reactivas para las interfaces ICommand e INotifyPropertyChanged , proporcionando herramientas como ReactiveCommand<TIn, TOut> y WhenAnyValue . WhenAnyValue permite convertir una propiedad de una clase que implementa la interfaz INotifyPropertyChanged en una secuencia de eventos de tipo IObservable<T> , esto simplifica la implementación de propiedades dependientes.


 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> permite trabajar con un comando como con IObservable<TOut> , que se publica cada vez que un 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(); // Outputs: 42 

Hasta este momento, hemos estado trabajando 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á observando. En pocas palabras, IObservable<T> es una secuencia de eventos, una colección de tipo T extendida en el tiempo.


Por supuesto, podríamos trabajar con colecciones con la misma facilidad y naturalidad: cada vez que cambie una colección, podríamos publicar una nueva colección con elementos modificados. En este caso, el valor publicado sería del tipo IEnumerable<T> o más especializado, y la secuencia observable en sí sería del tipo IObservable<IEnumerable<T>> . Pero, como señala correctamente un lector con mentalidad crítica, esto está plagado de problemas críticos de rendimiento, especialmente si no hay una docena de elementos en nuestra colección, ¡sino cien, o incluso unos pocos miles!


Introducción a DynamicData


DynamicData es una biblioteca que le permite utilizar el poder de las extensiones reactivas al trabajar con colecciones. Rx es extremadamente poderoso, pero listo para usar no proporciona nada para ayudar con la administración de colecciones, y DynamicData lo soluciona. En la mayoría de las aplicaciones, es necesario actualizar dinámicamente las colecciones; por lo general, una colección se llena con elementos cuando se inicia la aplicación, y luego la colección 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 para deshacerse del código increíblemente complejo que necesitaríamos para administrar conjuntos de datos que cambian dinámicamente. La herramienta se está desarrollando y refinando 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 de DynamicData se basa principalmente en conceptos de programación basados ​​en dominio. La ideología de uso se basa en el hecho de que usted controla una determinada fuente de datos, una colección a la que tiene acceso el código responsable de sincronizar y mutar datos. Luego, aplica una serie de operadores a la fuente de datos, con la ayuda de esos operadores puede transformar los datos declarativamente 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 permanecerán sincronizadas con la fuente.


En lugar del clásico IObservable<T> , DynamicData define operaciones en IObservable<IChangeSet<T>> e IObservable<IChangeSet<TValue, TKey>> , donde IChangeSet es un fragmento que contiene información sobre el cambio de la colección, incluido el tipo de cambio y los elementos afectados. Este enfoque puede mejorar significativamente el rendimiento del código para trabajar con colecciones, escrito de manera reactiva. Siempre puede transformar IObservable<IChangeSet<T>> en IObservable<IEnumerable<T>> , si es necesario acceder a todos los elementos de una colección a la vez. Si esto suena difícil, no se preocupe, ¡los ejemplos de código a continuación lo aclararán todo!


Datos dinámicos en acción


Veamos una serie de ejemplos para comprender mejor cómo funciona DynamicData, cómo se diferencia del Sistema. Reactivo y qué tareas pueden ayudar a resolver los desarrolladores comunes de software GUI. Comencemos con un ejemplo completo publicado en GitHub . En este ejemplo, la fuente de datos es SourceCache<Trade, long> contiene una colección de transacciones. El objetivo es mostrar solo las transacciones activas, transformar los modelos en objetos proxy, ordenar la colección.


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

En el ejemplo anterior, al cambiar SourceCache que es la fuente de los datos, ReadOnlyObservableCollection también cambia en consecuencia. Al mismo tiempo, al eliminar elementos de la colección, se llamará al método Dispose , la colección siempre se actualizará solo desde el hilo de la GUI y permanecerá ordenada y filtrada. Genial, ahora no tenemos Dispatcher.Invoke llamadas y el código es simple y legible!


Fuentes de datos SourceList y SourceCache


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


 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. } } 

Con la ayuda de los poderosos operadores DynamicData, podemos transformar IObservable<IChangeSet<Trade>> en ReadOnlyObservableCollection declarada en nuestro modelo de vista.


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

Además de los operadores Transform , Filter y Sort , DynamicData admite agrupación, operaciones lógicas, aplanamiento de colecciones, el uso de funciones agregadas, la eliminación de elementos idénticos, el recuento de elementos e incluso la virtualización a nivel de modelo de vista. Puede leer más sobre todos los operadores en el archivo README del proyecto en GitHub .




Además de SourceList y SourceCache , la biblioteca DynamicData incluye una implementación de colección mutable de un solo subproceso: ObservableCollectionExtended . Para sincronizar dos colecciones en su modelo de vista, declare una de ellas como ObservableCollectionExtended y la otra como ReadOnlyObservableCollection , y luego use el operador ToObservableChangeSet , que hace casi lo mismo que Connect, pero está destinado a funcionar con 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 también admite el seguimiento de cambios en clases que implementan la interfaz INotifyPropertyChanged . Por ejemplo, si desea recibir notificaciones cada vez que cambia una propiedad, use el operador AutoRefresh y pase el selector de propiedades requerido. AutoRefresh y otros operadores de DynamicData pueden permitirle validar sin esfuerzo un gran número de formularios y formularios anidados que se muestran en la pantalla!


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

Puede crear interfaces de usuario complejas utilizando la funcionalidad DynamicData, y es especialmente relevante para sistemas que muestran una gran cantidad de datos en tiempo real, como aplicaciones de mensajería instantánea y sistemas de monitoreo.





Conclusión


ReactiveX es una herramienta poderosa que le permite trabajar con flujos de eventos y con la interfaz de usuario, escribir código portátil y mantenible y resolver tareas complejas de una manera simple y elegante. ReactiveUI permite a los desarrolladores de .NET integrar extensiones reactivas en sus proyectos utilizando la arquitectura MVVM con implementaciones reactivas de INotifyPropertyChanged e ICommand , mientras que DynamicData se encarga de la gestión de la colección al implementar INotifyCollectionChanged , ampliando las capacidades de las extensiones reactivas con enfoque en el rendimiento.


Las bibliotecas ReactiveUI y DynamicData son totalmente compatibles con todos los marcos de GUI en la plataforma .NET, incluidas Windows Presentation Foundation, Universal Windows Platform, Avalonia , Xamarin.Android, Xamarin Forms y Xamarin iOS. Puede comenzar a estudiar DynamicData en la página correspondiente de la documentación de ReactiveUI . También tenga cuidado de familiarizarse con el proyecto DynamicData Snippets , que contiene ejemplos de código para casi todo lo que pueda necesitar.

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


All Articles