DynamicData: collections dynamiques, architecture MVVM et extensions réactives

Février 2019 a marqué la sortie de ReactiveUI 9 - le framework multiplateforme pour la construction d'applications GUI sur la plate-forme Microsoft .NET. ReactiveUI est un outil pour une intégration étroite des extensions réactives avec le modèle de conception MVVM. Vous pouvez vous familiariser avec le framework via une série de vidéos ou la page d'accueil de la documentation . La mise à jour ReactiveUI 9 comprend de nombreuses corrections et améliorations , mais la plus cruciale et la plus intéressante est probablement l' intégration avec le cadre DynamicData , vous permettant de travailler avec des collections dynamiques de façon réactive. Voyons à quoi nous pouvons utiliser DynamicData et comment ce puissant framework réactif fonctionne sous le capot!


Présentation


Déterminons d'abord les cas d'utilisation de DynamicData et découvrons ce que nous n'aimons pas des outils par défaut pour travailler avec des jeux de données dynamiques à partir de l'espace de noms System.Collections.ObjectModel .


Comme nous le savons, le modèle MVVM assume la répartition des responsabilités entre la couche modèle, la couche présentation et le modèle de présentation d'application, également appelé modèle de vue. La couche de modèle est représentée par des entités de domaine et des services et ne sait rien de la couche de modèle de vue. Le modèle encapsule toute la logique complexe de l'application, tandis que le modèle de vue délègue les opérations au modèle, fournissant l'accès à des informations sur l'état actuel de l'application via des propriétés, commandes et collections observables, à la vue. L'outil par défaut pour travailler avec les propriétés dynamiques est l'interface INotifyPropertyChanged , pour travailler avec les actions utilisateur - ICommand , et pour travailler avec les collections - l'interface INotifyCollectionChanged , ainsi que de telles implémentations, comme ObservableCollection<T> et ReadOnlyObservableCollection<T> .




Les implémentations des INotifyPropertyChanged et ICommand sont généralement à la charge du développeur et du framework MVVM utilisé, mais l'utilisation de la ObservableCollection<T> par défaut impose un certain nombre de limitations! Par exemple, nous ne pouvons pas muter la collection à partir d'un thread d'arrière-plan sans Dispatcher.Invoke ou un appel similaire, et cela aurait été très utile pour synchroniser les tableaux de données avec le serveur via une opération en arrière-plan. Il convient de noter que lors de l'utilisation de l'architecture MVVM propre, la couche modèle ne doit rien savoir du cadre GUI utilisé et doit être compatible avec la couche modèle dans la terminologie MVC ou MVP. C'est pourquoi ces nombreux appels Dispatcher.Invoke dans les services de domaine violent le principe de séparation des responsabilités.


Bien sûr, nous pourrions déclarer un événement dans un service de domaine et transmettre un morceau avec des éléments modifiés comme arguments d'événement, puis souscrire à l'événement, encapsuler l'appel Dispatcher.Invoke derrière une interface, afin que notre application ne dépende d'aucune interface graphique. , appelez cette interface à partir du modèle de vue et modifiez ObservableCollection<T> conséquence, mais il existe une manière beaucoup plus élégante de traiter ces problèmes sans avoir à réinventer la roue. Qu'attendons-nous donc?


Extensions réactives. Gestion des flux de données observables


Pour bien comprendre les abstractions introduites par DynamicData et comment travailler avec des ensembles de données réactifs changeants, rappelons ce qu'est la programmation réactive et comment l'utiliser dans le contexte de la plate-forme Microsoft .NET et du modèle de conception MVVM . La manière d'organiser l'interaction entre les composantes du programme peut être interactive ou réactive. Avec l'approche interactive, le consommateur reçoit les données du producteur de manière synchrone (basée sur l'extraction, T, IEnumerable), et avec l'approche réactive, le producteur envoie les données au consommateur de manière asynchrone (basée sur les push, Task, IObservable).




La programmation réactive consiste à programmer avec des flux de données asynchrones, et les extensions réactives sont l'implémentation de programmation réactive, basée sur les interfaces IObservable et IObserver de l'espace de noms système, définissant une série d'opérations de type LINQ sur l'interface IObservable , connue sous le nom de LINQ over Observable. Les extensions réactives prennent en charge .NET Standard et s'exécutent partout où Microsoft .NET s'exécute.




ReactiveUI propose aux développeurs d'applications de tirer parti de l'utilisation des implémentations réactives pour les interfaces ICommand et INotifyPropertyChanged , en fournissant des outils tels que ReactiveCommand<TIn, TOut> et WhenAnyValue . WhenAnyValue vous permet de convertir une propriété d'une classe qui implémente l'interface INotifyPropertyChanged en un flux d'événements de type IObservable<T> , cela simplifie l'implémentation des propriétés dépendantes.


 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> vous permet de travailler avec une commande comme avec IObservable<TOut> , qui est publiée chaque fois qu'une commande termine son exécution. En outre, toute commande possède une propriété ThrownExceptions de type 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 

Jusqu'à cette époque, nous travaillions avec IObservable<T> , comme avec un événement qui publie une nouvelle valeur de type T chaque fois que l'état de l'objet observé change. Autrement dit, IObservable<T> est un flux d'événements, une collection de type T étirée dans le temps.


Bien sûr, nous pourrions travailler avec des collections tout aussi facilement et naturellement - chaque fois qu'une collection change, nous pourrions publier une nouvelle collection avec des éléments modifiés. Dans ce cas, la valeur publiée serait de type IEnumerable<T> ou plus spécialisée, et le flux observable lui-même serait de type IObservable<IEnumerable<T>> . Mais, comme le note à juste titre un lecteur critique, cela pose des problèmes de performances critiques, surtout s'il n'y a pas une douzaine d'éléments dans notre collection, mais une centaine, voire quelques milliers!


Introduction à DynamicData


DynamicData est une bibliothèque qui vous permet d'utiliser la puissance des extensions réactives lorsque vous travaillez avec des collections. Rx est extrêmement puissant, mais prêt à l'emploi ne fournit rien pour aider à la gestion des collections, et DynamicData corrige cela. Dans la plupart des applications, il est nécessaire de mettre à jour dynamiquement les collections - généralement, une collection est remplie d'éléments au démarrage de l'application, puis la collection est mise à jour de manière asynchrone, synchronisant les informations avec un serveur ou une base de données. Les applications modernes sont assez complexes et il est souvent nécessaire de créer des projections de collections - filtrer, transformer ou trier des éléments. DynamicData a été conçu pour se débarrasser du code incroyablement complexe dont nous aurions besoin pour gérer des ensembles de données à changement dynamique. L'outil se développe et s'affine activement, et maintenant plus de 60 opérateurs sont pris en charge pour travailler avec les collections.




DynamicData n'est pas une implémentation alternative d' ObservableCollection<T> . L'architecture de DynamicData est basée principalement sur des concepts de programmation pilotés par domaine. L'idéologie d'utilisation est basée sur le fait que vous contrôlez une certaine source de données, une collection à laquelle le code responsable de la synchronisation et de la mutation des données a accès. Ensuite, vous appliquez une série d'opérateurs à la source de données, avec l'aide de ces opérateurs, vous pouvez transformer les données de manière déclarative sans avoir besoin de créer et de modifier manuellement d'autres collections. En fait, avec DynamicData, vous séparez les opérations de lecture et d'écriture, et vous ne pouvez lire que de manière réactive - par conséquent, les collections héritées resteront toujours synchronisées avec la source.


Au lieu du classique IObservable<T> , DynamicData définit les opérations sur IObservable<IChangeSet<T>> et IObservable<IChangeSet<TValue, TKey>> , où IChangeSet est un bloc contenant des informations sur le changement de la collection, y compris le type de changement et les éléments affectés. Cette approche peut améliorer considérablement les performances du code pour travailler avec des collections, écrites de manière réactive. Vous pouvez toujours transformer IObservable<IChangeSet<T>> en IObservable<IEnumerable<T>> , s'il devient nécessaire d'accéder à tous les éléments d'une collection à la fois. Si cela semble difficile - ne vous inquiétez pas, les exemples de code ci-dessous clarifieront tout!


Des données dynamiques en action


Examinons un certain nombre d'exemples afin de mieux comprendre comment DynamicData fonctionne, en quoi il diffère de System. Reactive et quelles tâches les développeurs ordinaires de logiciels GUI peuvent aider à résoudre. Commençons par un exemple complet publié sur GitHub . Dans cet exemple, la source de données est SourceCache<Trade, long> contenant une collection de transactions. L'objectif est de n'afficher que les transactions actives, de transformer les modèles en objets proxy, de trier la collection.


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

Dans l'exemple ci-dessus, lors de la modification de SourceCache qui est la source des données, ReadOnlyObservableCollection change également en conséquence. Dans le même temps, lors de la suppression d'éléments de la collection, la méthode Dispose sera appelée, la collection sera toujours mise à jour uniquement à partir du thread GUI et restera triée et filtrée. Cool, maintenant nous n'avons plus d'appels Dispatcher.Invoke et le code est simple et lisible!


Sources de données. SourceList et SourceCache


DynamicData fournit deux collections spécialisées qui peuvent être utilisées comme source de données mutable. Ces collections sont SourceList<TObject> et SourceCache<TObject, TKey> . Il est recommandé d'utiliser SourceCache chaque fois que TObject a une clé unique, sinon utilisez SourceList . Ces objets fournissent le familier pour les développeurs .NET API pour la gestion de collection - des méthodes telles que Add , Remove , Insert . Pour convertir des sources de données en IObservable<IChangeSet<T>> ou en IObservable<IChangeSet<T, TKey>> , utilisez l'opérateur .Connect() . Par exemple, si vous disposez d'un service qui met à jour périodiquement une collection d'éléments en arrière-plan, vous pouvez facilement synchroniser la liste de ces éléments avec l'interface graphique, sans Dispatcher.Invoke et un code passe-partout similaire:


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

Avec l'aide des puissants opérateurs DynamicData, nous pouvons transformer IObservable<IChangeSet<Trade>> en ReadOnlyObservableCollection déclaré dans notre modèle de vue.


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

En plus des opérateurs de Transform , de Filter et de Sort , DynamicData prend en charge le regroupement, les opérations logiques, l'aplatissement de la collection, l'utilisation de fonctions d'agrégation, l'élimination d'éléments identiques, le comptage d'éléments et même la virtualisation au niveau du modèle de vue. Vous pouvez en savoir plus sur tous les opérateurs dans le fichier README du projet sur GitHub .




Outre SourceList et SourceCache , la bibliothèque DynamicData comprend une implémentation de collection mutable à un seul thread - ObservableCollectionExtended . Pour synchroniser deux collections dans votre modèle de vue, déclarez l'une d'entre elles comme ObservableCollectionExtended et l'autre comme ReadOnlyObservableCollection , puis utilisez l'opérateur ToObservableChangeSet , qui fait presque la même chose que Connect, mais est destiné à fonctionner avec 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 prend également en charge le suivi des modifications dans les classes qui implémentent l'interface INotifyPropertyChanged . Par exemple, si vous souhaitez recevoir des notifications chaque fois qu'une propriété change, utilisez l'opérateur AutoRefresh et passez le sélecteur de propriété requis. AutoRefresh et d'autres opérateurs DynamicData peuvent vous permettre de valider sans effort un grand nombre de formulaires et de formulaires imbriqués affichés à l'écran!


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

Vous pouvez créer des interfaces utilisateur complexes à l'aide de la fonctionnalité DynamicData, et cela est particulièrement pertinent pour les systèmes affichant une grande quantité de données en temps réel, comme les applications de messagerie instantanée et les systèmes de surveillance.





Conclusion


ReactiveX est un outil puissant vous permettant de travailler avec des flux d'événements et avec l'interface utilisateur, d'écrire du code portable et maintenable et de résoudre des tâches complexes de manière simple et élégante. ReactiveUI permet aux développeurs .NET d'intégrer des extensions réactives dans leurs projets à l'aide de l'architecture MVVM avec des implémentations réactives d' INotifyPropertyChanged et ICommand , tandis que DynamicData s'occupe de la gestion de la collection en implémentant INotifyCollectionChanged , étendant les capacités des extensions réactives en mettant l'accent sur les performances.


Les bibliothèques ReactiveUI et DynamicData sont entièrement compatibles avec tous les frameworks GUI sur la plate-forme .NET, y compris Windows Presentation Foundation, Universal Windows Platform, Avalonia , Xamarin.Android, Xamarin Forms et Xamarin iOS. Vous pouvez commencer à étudier DynamicData sur la page correspondante de la documentation ReactiveUI . Veillez également à vous familiariser avec le projet DynamicData Snippets , qui contient des exemples de code pour presque tout ce dont vous pourriez avoir besoin.

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


All Articles