DynamicData: Ändern von Sammlungen, MVVM-Entwurfsmustern und reaktiven Erweiterungen

Im Februar 2019 wurde ReactiveUI 9 , ein plattformübergreifendes Framework zum Erstellen von GUI-Anwendungen auf der Microsoft .NET-Plattform, veröffentlicht. ReactiveUI ist ein Tool zur engen Integration reaktiver Erweiterungen in das MVVM-Entwurfsmuster. Die Bekanntschaft mit dem Framework kann mit einer Reihe von Artikeln über Habré oder auf der Titelseite der Dokumentation begonnen werden . Das ReactiveUI 9-Update enthält viele Korrekturen und Verbesserungen . Die vielleicht interessanteste und bedeutendste Änderung ist jedoch die enge Integration in das DynamicData-Framework , das das Arbeiten mit sich ändernden Sammlungen in einem reaktiven Stil ermöglicht. Versuchen wir herauszufinden, in welchen Fällen DynamicData nützlich sein kann und wie dieses leistungsstarke reaktive Framework im Inneren angeordnet ist!

Hintergrund


Zunächst definieren wir den Aufgabenbereich, den DynamicData löst, und finden heraus, warum Standardtools für die Arbeit mit sich ändernden Datensätzen aus dem System.Collections.ObjectModel Namespace nicht zu uns passen.

Wie Sie wissen, umfasst die MVVM-Vorlage die Aufteilung der Verantwortung zwischen den Ebenen des Modells, der Präsentation und des Anwendungspräsentationsmodells. Die Modellebene wird durch Domänenentitäten und -dienste dargestellt und weiß nichts über das Präsentationsmodell. Die Modellschicht kapselt die gesamte komplexe Anwendungslogik, und das Präsentationsmodell delegiert die Operationen des Modells und gibt der Ansicht über beobachtbare Eigenschaften, Befehle und Sammlungen Zugriff auf Informationen zum aktuellen Status der Anwendung. Das Standardwerkzeug zum Arbeiten mit sich ändernden Eigenschaften ist die INotifyPropertyChanged Schnittstelle, der INotifyPropertyChanged zum Arbeiten mit Benutzeraktionen und INotifyCollectionChanged Implementieren von Sammlungen und zum Implementieren von ObservableCollection und ReadOnlyObservableCollection .



Die Implementierung von INotifyPropertyChanged und ICommand bleibt normalerweise dem Gewissen des Entwicklers und des verwendeten MVVM-Frameworks ICommand , aber die Verwendung von ObservableCollection einer Reihe von Einschränkungen für uns! Beispielsweise können wir eine Sammlung aus einem Hintergrundthread ohne Dispatcher.Invoke oder einen ähnlichen Aufruf nicht ändern. Dies kann hilfreich sein, wenn Sie mit Datenarrays arbeiten, die von einer Hintergrundoperation mit dem Server synchronisiert werden. Es ist zu beachten, dass in der idiomatischen MVVM die Modellschicht nicht über die Architektur der verwendeten GUI-Anwendung Bescheid wissen und mit dem Modell von MVC oder MVP kompatibel sein muss. Aus diesem Grund ermöglichen zahlreiche Dispatcher.Invoke den Zugriff auf die Benutzeroberfläche über den laufenden Hintergrundthread Verstoßen Sie in einem Domänendienst gegen das Prinzip der Aufteilung der Verantwortung zwischen den Anwendungsebenen.

Natürlich wäre es in einem Domänendienst möglich, ein Ereignis zu deklarieren und als Argument eines Ereignisses einen Block mit geänderten Daten zu übergeben. Abonnieren Sie dann das Ereignis, wickeln Sie den Aufruf von Dispatcher.Invoke in eine Schnittstelle ein, damit er nicht vom verwendeten GUI-Framework abhängt, verschieben Sie Dispatcher.Invoke in das Präsentationsmodell und ändern Sie die ObservableCollection Bedarf. Es gibt jedoch eine viel einfachere und elegantere Möglichkeit, den angegebenen Aufgabenbereich zu lösen, ohne ein Fahrrad schreiben zu müssen . Beginnen wir mit dem Studium!

Reaktive Erweiterungen. Datenströme verwalten


Um ein umfassendes Verständnis der von DynamicData eingeführten Abstraktionen und der Prinzipien der Arbeit mit sich ändernden reaktiven Datensätzen zu erhalten, erinnern wir uns daran, was reaktive Programmierung ist und wie sie im Kontext der Microsoft .NET-Plattform und des MVVM-Entwurfsmusters angewendet wird . Eine Möglichkeit, die Interaktion zwischen Programmkomponenten zu organisieren, kann interaktiv und reaktiv sein. In der interaktiven Interaktion empfängt die Consumer-Funktion synchron Daten von der Provider-Funktion (Pull-basierter Ansatz, T , IEnumerable ), und in der reaktiven Interaktion liefert die Consumer-Funktion asynchron Daten an die Consumer-Funktion (Push-basierter Ansatz, Task , IObservable ).



Reaktive Programmierung ist die Programmierung mit asynchronen Datenströmen, und reaktive Erweiterungen sind ein Sonderfall ihrer Implementierung, basierend auf den IObservable und IObserver aus dem System-Namespace, der eine Reihe von LINQ-ähnlichen Operationen auf der IObservable Schnittstelle definiert, die als LINQ over Observable bezeichnet werden. Reaktive Erweiterungen unterstützen den .NET-Standard und funktionieren überall dort, wo die Microsoft .NET-Plattform funktioniert.



Das ReactiveUI-Framework lädt Anwendungsentwickler ein, die reaktive Implementierung von ICommand und INotifyPropertyChanged und leistungsstarke Tools wie ReactiveCommand<TIn, TOut> und WhenAnyValue . WhenAnyValue können WhenAnyValue eine Eigenschaft einer Klasse, die INotifyPropertyChanged implementiert, in einen Ereignisstrom vom Typ IObservable<T> , wodurch die Implementierung abhängiger Eigenschaften vereinfacht wird.

 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> können Sie mit dem Befehl arbeiten, wie mit einem Ereignis vom Typ IObservable<TOut> , das veröffentlicht wird, wenn der Befehl die Ausführung abschließt. Außerdem verfügt jeder Befehl über eine ThrownExceptions Eigenschaft vom Typ 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 

IObservable<T> dieser ganzen Zeit haben wir mit IObservable<T> , wie bei einem Ereignis, das einen neuen Wert vom Typ T wenn sich der Status des überwachten Objekts ändert. Einfach ausgedrückt ist IObservable<T> ein Strom von Ereignissen, eine Sequenz, die sich über die Zeit erstreckt.

Natürlich können wir genauso einfach und natürlich mit Sammlungen arbeiten - wenn sich eine Sammlung ändert, veröffentlichen Sie eine neue Sammlung mit geänderten Elementen. In diesem Fall wäre der veröffentlichte Wert vom Typ IEnumerable<T> oder spezialisierter, und das Ereignis selbst wäre vom Typ IObservable<IEnumerable<T>> . Wie der kritisch denkende Leser jedoch richtig hervorhebt, ist dies mit ernsthaften Problemen bei der Anwendungsleistung behaftet, insbesondere wenn unsere Sammlung nicht ein Dutzend Elemente enthält, sondern hundert oder sogar mehrere Tausend!

Einführung in DynamicData


DynamicData ist eine Bibliothek, mit der Sie bei der Arbeit mit Sammlungen die volle Leistung reaktiver Erweiterungen nutzen können. Reaktive Erweiterungen bieten keine optimalen Möglichkeiten, um mit sich ändernden Datasets zu arbeiten, und DynamicData hat die Aufgabe, diese zu beheben. In den meisten Anwendungsanwendungen müssen Sammlungen dynamisch aktualisiert werden. In der Regel wird eine Sammlung beim Start der Anwendung mit einigen Elementen gefüllt und dann asynchron aktualisiert, um Informationen mit einem Server oder einer Datenbank zu synchronisieren. Moderne Anwendungen sind recht komplex, und häufig müssen Sammlungsprojektionen erstellt werden - Elemente filtern, transformieren oder sortieren. DynamicData wurde entwickelt, um den unglaublich komplexen Code zu beseitigen, den wir zur Verwaltung sich dynamisch ändernder Datensätze benötigen würden. Das Tool wird aktiv entwickelt und finalisiert. Mittlerweile werden mehr als 60 Bediener für die Arbeit mit Sammlungen unterstützt.



DynamicData ist keine alternative Implementierung von ObservableCollection<T> . Die DynamicData- Architektur basiert hauptsächlich auf den Konzepten der domänenspezifischen Programmierung. Die Nutzungsideologie basiert auf der Tatsache, dass Sie eine Datenquelle verwalten, eine Sammlung, auf die der Code, der für die Synchronisierung und Änderung von Daten verantwortlich ist, Zugriff hat. Als Nächstes wenden Sie eine Reihe von Operatoren auf die Quelle an, mit denen Sie die Daten deklarativ transformieren können, ohne andere Sammlungen manuell erstellen und ändern zu müssen. Tatsächlich trennen Sie mit DynamicData Lese- und Schreibvorgänge und können nur reaktiv lesen. Daher werden geerbte Sammlungen immer mit der Quelle synchronisiert.

Anstelle des klassischen IObservable<T> definiert DynamicData Operationen für IObservable<IChangeSet<T>>> und IObservable<IChangeSet<TValue, TKey>> , wobei IChangeSet ein IChangeSet ist, der Informationen über die Sammlungsänderung enthält - die Art der Änderung und die betroffenen Elemente. Dieser Ansatz kann die Leistung von Code für die Arbeit mit Sammlungen, die in einem reaktiven Stil geschrieben wurden, erheblich verbessern. Gleichzeitig kann IObservable<IChangeSet<T>> immer in ein reguläres IObservable<IEnumerable<T>> wenn alle Elemente der Sammlung gleichzeitig verarbeitet werden müssen. Wenn es kompliziert klingt - seien Sie nicht beunruhigt, aus den Codebeispielen wird alles klar und transparent!

DynamicData-Beispiel


Schauen wir uns eine Reihe von Beispielen an, um besser zu verstehen, wie DynamicData funktioniert, wie es sich von System.Reactive und welche Aufgaben gewöhnliche Entwickler von Anwendungssoftware mit einer GUI lösen können. Beginnen wir mit einem umfassenden Beispiel, das von DynamicData auf GitHub veröffentlicht wurde . Im Beispiel ist die Datenquelle SourceCache<Trade, long> , der eine Sammlung von Transaktionen enthält. Die Aufgabe besteht darin, nur aktive Transaktionen anzuzeigen, Modelle in Proxy-Objekte umzuwandeln und die Sammlung zu sortieren.

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

Wenn Sie im SourceCache Beispiel den SourceCache ändern, bei dem es sich um die SourceCache , ändert sich auch SourceCache entsprechend. In diesem Fall wird beim Löschen von Elementen aus der Auflistung die Dispose Methode aufgerufen. Die Auflistung wird immer nur im GUI-Stream aktualisiert und bleibt sortiert und gefiltert. Cool, kein Dispatcher.Invoke und komplizierter Code!

SourceList- und SourceCache-Datenquellen


DynamicData bietet zwei spezialisierte Sammlungen, die als veränderbare Datenquelle verwendet werden können. Diese Sammlungen sind vom Typ SourceList und SourceCache<TObject, TKey> . Es wird empfohlen, SourceCache zu verwenden, wenn TObject einen eindeutigen Schlüssel hat, andernfalls SourceList . Diese Objekte bieten die bekannte .NET-Entwickler-API zum Ändern von Daten - Add , Remove , Insert und dergleichen. Verwenden Sie den Operator .Connect() um Datenquellen in IObservable<IChangeSet<T>> oder IObservable<IChangeSet<T, TKey>> .Connect() . Wenn Sie beispielsweise über einen Dienst verfügen, der die Sammlung von Elementen im Hintergrund aktualisiert, können Sie die Liste dieser Elemente problemlos mit der GUI synchronisieren, ohne Dispatcher.Invoke und architektonische Exzesse:

 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 verwendet integrierte .NET-Typen, um Daten der Außenwelt zuzuordnen. Mit den leistungsstarken DynamicData-Operatoren können wir IObservable<IChangeSet<Trade>> in ReadOnlyObservableCollection unseres Ansichtsmodells IObservable<IChangeSet<Trade>> .

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

Neben Transform , Filter und Sort enthält DynamicData eine Vielzahl weiterer Operatoren, unterstützt Gruppierungen, logische Operationen, das Glätten einer Sammlung, die Verwendung von Aggregationsfunktionen, das Ausschließen identischer Elemente, das Zählen von Elementen und sogar die Virtualisierung auf der Ebene des Darstellungsmodells. Lesen Sie mehr über alle Operatoren im README-Projekt auf GitHub .



Sammlungen mit einem Thread und Änderungsverfolgung


Neben SourceList und SourceCache die DynamicData-Bibliothek auch eine Single-Thread-Implementierung einer veränderlichen Sammlung - ObservableCollectionExtended . Um zwei Sammlungen in Ihrem Ansichtsmodell zu synchronisieren, deklarieren Sie eine als ObservableCollectionExtended und die andere als ReadOnlyObservableCollection und verwenden Sie den Operator ToObservableChangeSet , der sich wie Connect verhält, jedoch für die Arbeit mit 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 unterstützt auch das Verfolgen von Änderungen in Klassen, die die INotifyPropertyChanged Schnittstelle implementieren. Wenn Sie beispielsweise über eine Auflistungsänderung benachrichtigt werden möchten, wenn sich eine Eigenschaft eines Elements ändert, verwenden Sie die AutoRefresh und übergeben Sie den Selektor der gewünschten Eigenschaft mit dem Argument. AutoRefesh und anderen DynamicData-Operatoren können Sie die große Anzahl von Formularen und Unterformularen, die auf dem Bildschirm angezeigt werden, einfach und natürlich überprüfen!

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

Basierend auf der DynamicData-Funktionalität können Sie schnell recht komplexe Schnittstellen erstellen. Dies gilt insbesondere für Systeme, die eine große Menge von Echtzeitdaten anzeigen, Instant Messaging-Systeme und Überwachungssysteme.



Fazit


Reaktive Erweiterungen sind ein leistungsstarkes Tool, mit dem Sie deklarativ mit Daten und der Benutzeroberfläche arbeiten, tragbaren und unterstützten Code schreiben und komplexe Probleme auf einfache und elegante Weise lösen können. Mit ReactiveUI können .NET-Entwickler mithilfe der MVVM-Architektur reaktive Erweiterungen eng in ihre Projekte integrieren, indem sie reaktive Implementierungen von INotifyPropertyChanged und ICommand . DynamicData kümmert sich um die Synchronisierung der Sammlung, indem INotifyCollectionChanged implementiert INotifyCollectionChanged , wodurch die Funktionen reaktiver Erweiterungen erweitert und die Leistung INotifyCollectionChanged .

Die ReactiveUI- und DynamicData-Bibliotheken sind mit den gängigsten GUI-Frameworks der .NET-Plattform kompatibel, einschließlich Windows Presentation Foundation, Universal Windows Platform, Avalonia , Xamarin.Android, Xamarin Forms und Xamarin.iOS. Sie können DynamicData auf der entsprechenden ReactiveUI-Dokumentationsseite lernen. Schauen Sie sich auch das DynamicData Snippets- Projekt an, das Beispiele für die Verwendung von DynamicData für alle Gelegenheiten enthält.

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


All Articles