Einführung in ReactiveUI: Sammlungen

  • Tutorial
Hallo habr

Teil 1: Einführung in ReactiveUI: Pump-Eigenschaften in ViewModel

Im vorherigen Artikel haben wir über Eigenschaften in ViewModel gesprochen und darüber, wie wir mit ReactiveUI damit umgehen können. Es ist uns gelungen, die Abhängigkeiten zwischen den Eigenschaften zu ordnen und an einer Stelle des Ansichtsmodells Informationen darüber zu sammeln, welche Abhängigkeiten zwischen den Eigenschaften enthalten sind.
Lassen Sie uns dieses Mal ein wenig mehr über Eigenschaften sprechen und dann mit Sammlungen fortfahren. Versuchen wir zu verstehen, welche Probleme es mit regulären Sammlungen gibt und warum neue mit Benachrichtigungen über Änderungen erstellt werden mussten. Und versuchen Sie natürlich, sie zu verwenden.

Ein paar Worte zu Eigenschaften


Bevor ich zum Hauptthema übergehe, möchte ich einige Worte zu den Eigenschaften sagen. Letztes Mal kamen wir zu folgender Syntax:
private string _firstName;
public string FirstName
{
    get { return _firstName; }
    set { this.RaiseAndSetIfChanged(ref _firstName, value); }
}

Es ist ziemlich verschwenderisch, 6 Codezeilen für jede Eigenschaft auszugeben, insbesondere wenn es viele solcher Eigenschaften gibt und die Implementierung immer dieselbe ist. Um dieses Problem zu lösen, wurden in C # gleichzeitig automatische Eigenschaften hinzugefügt, und das Leben ist einfacher. Was können wir in unserem Fall tun?
In den Kommentaren wurde Fody erwähnt - ein Tool, das den IL-Code nach dem Erstellen des Projekts ändern kann. Fügen Sie beispielsweise eine Änderungsbenachrichtigung zur Implementierung der automatischen Eigenschaften hinzu. Und für ReactiveUI gibt es sogar eine entsprechende Erweiterung: ReactiveUI.Fody . Versuchen wir es mal. Übrigens gibt es auch eine Erweiterung für die klassische Implementierung: PropertyChanged , die aber nicht zu uns passt, da RaiseAndSetIfChanged () aufgerufen werden muss .

Installation von NuGet:> Install-Package ReactiveUI.Fody
FodyWeavers.xml wird im Projekt angezeigt. Fügen Sie die installierte Erweiterung hinzu:


  


Und ändern Sie unsere Eigenschaften:
[Reactive] public string FirstName { get; set; }

Mit diesem Tool können Sie auch die FullName-Eigenschaft basierend auf ObservableAsPropertyHelper <> implementieren. Die Methode ist in der Dokumentation zu GitHub beschrieben, hier lassen wir sie weg. Ich halte zwei Zeilen für eine akzeptable Option und möchte nicht wirklich eine Drittanbieter-Methode anstelle von ToProperty () verwenden, mit der ReactiveUI.Fody diese Eigenschaft korrekt implementieren kann.

Stellen Sie sicher, dass nichts kaputt ist. Wie? Ich überprüfe mit Unit-Tests. ReactiveUI sie freundlich, kein Wunder , dass er eine der MVVM Rahmen <...> , um die elegante schaffen, prüfbare der Benutzer die Schnittstellen ... . Um die Auslösung des Ereignisses zu überprüfen, müssen Sie sich nicht manuell anmelden, sondern die ausgelösten Daten irgendwo speichern und dann verarbeiten. Observable.FromEventPattern ()
hilft uns dabei., wodurch Ereignisauslöser mit allen erforderlichen Informationen in IObservable <> umgewandelt werden. Und um IObservable <> in eine Liste von Ereignissen zu verwandeln und auf Richtigkeit zu prüfen, ist es bequem, die Erweiterungsmethode .CreateCollection () zu verwenden . Er erstellt eine Sammlung, in der, bis die Quelle OnComplete () oder Dispose () aufruft, die Elemente, die über IObservable <> eingegangen sind, Informationen zu ausgelösten Ereignissen hinzugefügt werden.
Beachten Sie, dass die Sammlung sofort an uns zurückgesandt wird und die Elemente später asynchron hinzugefügt werden. Dieses Verhalten unterscheidet sich beispielsweise von .ToList (), das die Steuerung nicht zurückgibt, und daher die Auflistung selbst von OnComplete (), das im Fall eines regulären Ereignisabonnements mit ewigem Warten behaftet ist.
[Test]
public void FirstName_WhenChanged_RaisesPropertyChangedEventForFirstNameAndFullNameProperties()
{
    var vm = new PersonViewModel("FirstName", "LastName");
    var evendObservable = Observable.FromEventPattern(
        a => vm.PropertyChanged += a,
        a => vm.PropertyChanged -= a);
    var raisedEvents = evendObservable.CreateCollection();
    using (raisedEvents)
    {
        vm.FirstName = "NewFirstName";
    }
    Assert.That(vm.FullName, Is.EqualTo("NewFirstName LastName"));
    Assert.That(raisedEvents, Has.Count.EqualTo(2));
    Assert.That(raisedEvents[0].Sender, Is.SameAs(vm));
    Assert.That(raisedEvents[0].EventArgs.PropertyName, Is.EqualTo(nameof(PersonViewModel.FirstName)));
    Assert.That(raisedEvents[1].Sender, Is.SameAs(vm));
    Assert.That(raisedEvents[1].EventArgs.PropertyName, Is.EqualTo(nameof(PersonViewModel.FullName)));
}  

Das Testskript selbst (Festlegen eines neuen Werts für die Eigenschaft) wird in using ausgeführt und die Prüfungen danach. Dies ist notwendig, damit bei der Prüfung ein Ereignis nicht versehentlich eintritt und unsere Sammlung nicht verderbt. Natürlich ist dies oft nicht notwendig, aber manchmal kann es wichtig sein.

Überprüfen wir nun, ob IObservable <> Changed dasselbe zurückgibt.
[Test]
public void FirstName_WhenChanged_PushesToPropertyChangedObservableForFirstNameAndFullNameProperties()
{
    var vm = new PersonViewModel("FirstName", "LastName");
    var notifications = vm.Changed.CreateCollection();
    using (notifications)
    {
        vm.FirstName = "NewFirstName";
    }
    Assert.That(vm.FullName, Is.EqualTo("NewFirstName LastName"));
    Assert.That(notifications, Has.Count.EqualTo(2));
    Assert.That(notifications[0].Sender, Is.SameAs(vm));
    Assert.That(notifications[0].PropertyName, Is.EqualTo(nameof(PersonViewModel.FirstName)));
    Assert.That(notifications[1].Sender, Is.SameAs(vm));
    Assert.That(notifications[1].PropertyName, Is.EqualTo(nameof(PersonViewModel.FullName)));
}

Und ... der Test fiel. Aber wir haben nur die Informationsquelle über die Änderung von Eigenschaften geändert! Versuchen wir zu verstehen, warum der Test fehlgeschlagen ist:
vm.Changed.Subscribe(n => Console.WriteLine(n.PropertyName));
vm.FirstName = "OMG";

Und wir bekommen:
FullName
Vorname

So. Dies scheint kein Framework-Fehler zu sein, sondern ein Implementierungsdetail. Dies kann verstanden werden: Beide Eigenschaften haben sich bereits geändert und die Benachrichtigungsreihenfolge ist nicht wichtig. Auf der anderen Seite widerspricht es der Reihenfolge der Ereignisse und erfüllt nicht die Erwartungen, die voller Probleme sein könnten. Natürlich ist es eine schlechte Idee, die Anwendungslogik auf der Grundlage der Benachrichtigungsreihenfolge zu erstellen. Wenn Sie jedoch beispielsweise das Anwendungsprotokoll lesen, werden Sie feststellen, dass die abhängige Eigenschaft über die Änderung VOR ihren Abhängigkeitsänderungen informiert wurde. Dies kann verwirrend sein. Denken Sie also unbedingt an diese Funktion.
Wir haben also sichergestellt, dass ReactiveUI.Fody ordnungsgemäß funktioniert und die Codemenge erheblich reduziert. Als nächstes werden wir es verwenden.



Kommen wir nun zu den Sammlungen


Die INotifyPropertyChanged-Schnittstelle wird, wie wir wissen, beim Ändern der Eigenschaften eines Ansichtsmodells verwendet, um beispielsweise ein visuelles Element zu benachrichtigen, dass sich etwas geändert hat und die Schnittstelle neu gezeichnet werden muss. Was tun, wenn das Ansichtsmodell eine Sammlung vieler Elemente enthält (z. B. einen Newsfeed) und wir neue Einträge zu den bereits angezeigten hinzufügen müssen? Benachrichtigen, dass sich die Eigenschaft, in der die Sammlung liegt, geändert hat? Dies ist möglich, führt jedoch zu einer Neuanordnung der gesamten Liste in der Benutzeroberfläche. Dies kann insbesondere bei Mobilgeräten eine langsame Operation sein. Nein, das geht nicht. Es ist notwendig, dass die Sammlung selbst darüber informiert, dass sich etwas an ihr geändert hat. Zum Glück gibt es eine wunderbare Oberfläche:
public interface INotifyCollectionChanged
{
  /// Occurs when the collection changes.
  event NotifyCollectionChangedEventHandler CollectionChanged;
}

Wenn die Sammlung dies implementiert, dann beim Hinzufügen / Entfernen / Ersetzen usw. von CollectionChanged ausgelöste Ereignisse. Und jetzt müssen Sie die Nachrichtenliste nicht erneut erstellen und die Sammlung von Datensätzen im Allgemeinen anzeigen. Fügen Sie einfach neue Elemente hinzu, die durch die Veranstaltung kamen. Es gibt Sammlungen in .NET, die dies implementieren, aber wir sprechen von ReactiveUI. Was ist drin?
Eine ganze Reihe von Schnittstellen: IReactiveList, IReadOnlyReactiveList, IReadOnlyReactiveCollection, IReactiveCollection, IReactiveNotifyCollectionChanged, IReactiveNotifyCollectionItemChanged. Ich werde hier keine Beschreibung von jedem geben, ich denke, es sollte aus den Namen klar sein, was sie sind.
Aber schauen wir uns die Implementierung genauer an. Treffen Sie ReactiveList. Er setzt sie alle und noch viel mehr um. Da wir Änderungen in der Auflistung verfolgen möchten, sollten wir uns die entsprechenden Eigenschaften dieser Klasse ansehen.
Eigenschaften zum Verfolgen von Änderungen in IReactiveList <T>
Ziemlich viel! Es überwacht das Hinzufügen, Entfernen, Verschieben von Elementen, die Anzahl der Elemente, die Leere der Sammlung und die Notwendigkeit eines Zurücksetzens. Betrachten Sie dies alles genauer. Natürlich werden auch Ereignisse von INotifyCollectionChanged, INotifyPropertyChanged und deren gepaarten * Changind implementiert, aber wir werden nicht darüber sprechen, sie arbeiten Seite an Seite mit den im Bild gezeigten "beobachtbaren" Eigenschaften, und es gibt dort nichts Einzigartiges.
Für den Anfang ein einfaches Beispiel. Wir abonnieren einige Benachrichtigungsquellen und arbeiten ein wenig an der Sammlung:
var list = new ReactiveList();
list.BeforeItemsAdded.Subscribe(e => Console.WriteLine($"Before added: {e}"));
list.ItemsAdded.Subscribe(e => Console.WriteLine($"Added: {e}"));
list.BeforeItemsRemoved.Subscribe(e => Console.WriteLine($"Before removed: {e}"));
list.ItemsRemoved.Subscribe(e => Console.WriteLine($"Removed: {e}"));
list.CountChanging.Subscribe(e => Console.WriteLine($"Count changing: {e}"));
list.CountChanged.Subscribe(e => Console.WriteLine($"Count changed: {e}"));
list.IsEmptyChanged.Subscribe(e => Console.WriteLine($"IsEmpty changed: {e}"));
Console.WriteLine("# Add 'first'");
list.Add("first");
Console.WriteLine("\n# Add 'second'");
list.Add("second");
Console.WriteLine("\n# Remove 'first'");
list.Remove("first");


Wir erhalten das Ergebnis:
#Add 'first'
Count change: 0
Vor dem Hinzufügen: first
Count
change : 1 IsEmpty change: False
Hinzugefügt: first

#Add 'second'
Count change: 1
Vor dem Hinzufügen: second
Count change : 2
Hinzugefügt: second

#Remove 'first'
Count Ändern: 2
Vor dem Entfernen: Erste
Anzahl geändert: 1
Entfernt: Erste

Wir werden darüber informiert, was hinzugefügt oder entfernt wurde, sowie über eine Änderung der Anzahl der Elemente und das Zeichen der Leere der Sammlung.
Außerdem:
- ItemsAdded / ItemsRemoved / BeforeItemsAdded / BeforeItemsRemoved gibt das hinzugefügte / gelöschte Element selbst zurück.
- CountChanging / CountChanged gibt die Anzahl der Elemente vor und nach der Änderung zurück.
- IsEmptyChanged gibt den neuen Wert des Attributs collection empty zurück

Es gibt eine Subtilität


Bisher ist alles vorhersehbar. Stellen Sie sich nun vor, dass wir nur auf der Grundlage von Hinzufügungs- und Entfernungsmitteilungen die Anzahl der Datensätze in der Sammlung zählen möchten. Was könnte einfacher sein?
var count = 0;
var list = new ReactiveList();
list.ItemsAdded.Subscribe(e => count++);
list.ItemsRemoved.Subscribe(e => count--);
for (int i = 0; i < 100; i++)
{
    list.Add(i);
}            
for (int i = 0; i < 100; i+=2)
{
    list.Remove(i);
}            
Assert.That(count, Is.EqualTo(list.Count));

Der Test war erfolgreich. Ändere das Prinzip des Füllens der Sammlung und füge viele Elemente gleichzeitig hinzu:
list.AddRange(Enumerable.Range(0, 10));
list.RemoveAll(Enumerable.Range(0, 5).Select(i => i * 2));

Erfolgreich. Es scheint keinen Haken zu geben. Obwohl warten ...
list.AddRange(Enumerable.Range(0, 100));
list.RemoveAll(Enumerable.Range(0, 50).Select(i => i * 2));

Oh! Der Test ist fehlgeschlagen und zählt == 0. Anscheinend haben wir etwas nicht berücksichtigt. Lass es uns richtig machen.

Die Sache ist die ReactiveListimplementiert nicht so primitiv, wie es scheinen mag. Wenn sich die Auflistung erheblich ändert, werden Benachrichtigungen deaktiviert, alle Änderungen vorgenommen, Benachrichtigungen wieder aktiviert und ein Rücksetzsignal gesendet:
list.ShouldReset.Subscribe(_ => Console.WriteLine("ShouldReset"));

Warum wird das gemacht? Manchmal ändert sich eine Sammlung erheblich: Beispielsweise werden 100 Elemente zu einer leeren Sammlung hinzugefügt, die Hälfte der Elemente wird aus einer großen Sammlung entfernt oder sie wird vollständig bereinigt. In diesem Fall macht es keinen Sinn, auf jede kleine Änderung zu reagieren - es ist teurer, auf das Ende der Änderungsserie zu warten und so zu reagieren, als wäre die Sammlung komplett neu.
Im letzten Beispiel passiert Folgendes. ShouldReset ist vom Typ IObservable. Einheit ist im Wesentlichen nichtig, nur in Form eines Objekts. Es wird in Situationen verwendet, in denen Sie den Teilnehmer über ein Ereignis benachrichtigen müssen und es nur wichtig ist, dass es auftritt. Es ist nicht erforderlich, zusätzliche Daten zu übertragen. Nur unser Fall. Wenn wir es abonnieren würden, würden wir sehen, dass wir nach den Einfüge- und Löschvorgängen ein Rücksetzsignal erhalten haben. Dementsprechend müssen wir unser Beispiel leicht modifizieren, um den Zähler korrekt zu aktualisieren:
list.ItemsAdded.Subscribe(e => count++);
list.ItemsRemoved.Subscribe(e => count--);
list.ShouldReset.Subscribe(_ => count = list.Count);

Jetzt ist der Test bestanden und alles ist wieder in Ordnung. Vergessen Sie jedoch nicht, dass einige Benachrichtigungen möglicherweise nicht in der Lage sind, mit diesen Situationen umzugehen. Und vergessen Sie natürlich nicht, solche Situationen zu testen, wenn sich Sammlungen stark verändern.

Regeln zur Benachrichtigungsunterdrückung ändern

Wir haben gesehen, dass bei einer starken Änderung in der Sammlung ein Rücksetzsignal auftritt. Wie kann dieser Prozess gesteuert werden?
Im Konstruktor von ReactiveListEs gibt ein optionales Argument: double resetChangeThreshold = 0.3. Nachdem Sie die Liste erstellt haben, können Sie sie über die ResetChangeThreshold-Eigenschaft ändern. Wie wird es benutzt? Benachrichtigungen über Änderungen werden unterdrückt, wenn das Ergebnis der Division der Anzahl der hinzugefügten / gelöschten Elemente durch die Anzahl der Elemente in der Sammlung selbst diesen Wert überschreitet und wenn die Anzahl der hinzugefügten / gelöschten Elemente streng über 10 liegt. Dies ist aus dem Quellcode ersichtlich und niemand garantiert, dass diese Regeln gelten wird sich in Zukunft nicht ändern.
In unserem Beispiel 100/0> 0,3 und 50/100> 0,3 , sodass die Benachrichtigungen beide Male unterdrückt wurden. Natürlich können Sie den ResetChangeThreshold variieren und die Sammlung durch einen bestimmten Verwendungsort ersetzen.

Wie unterdrücken wir selbst Benachrichtigungen?

Im ersten Beispiel mit einem Zähler haben wir diesen Code gesehen:
for (int i = 0; i < 100; i++)
{
    list.Add(i);
}

Hier werden die Elemente einzeln hinzugefügt, sodass Änderungsmeldungen immer gesendet werden. Wir fügen jedoch viele Elemente hinzu und möchten Benachrichtigungen für eine Weile unterdrücken. Wie? Verwenden von SuppressChangeNotifications (). Alles, was in using enthalten ist, löst keine Änderungsmeldungen aus:
using (list.SuppressChangeNotifications())
{
    for (int i = 0; i < 100; i++)
    {
        list.Add(i);
    }
}


Was ist mit Änderungen an den Elementen der Sammlung selbst?


Wir haben das in ReactiveList gesehenEs gibt Benachrichtigungsquellen ItemChanged und ItemChanging - Änderungen an den Elementen selbst. Versuchen wir sie zu benutzen:
var list = new ReactiveList();
list.ItemChanged.Subscribe(e => Console.WriteLine(e.PropertyName));
var vm = new PersonViewModel("Name", "Surname");
list.Add(vm);
vm.FirstName = "NewName";

Es ist nichts passiert. Werden wir getäuscht und ReactiveList überwacht die Änderung von Elementen nicht wirklich? Ja, aber nur standardmäßig. Damit er Änderungen in seinen Elementen nachverfolgen kann, muss nur diese Funktion aktiviert werden:
var list = new ReactiveList() { ChangeTrackingEnabled = true };

Jetzt funktioniert alles:
FullName
Vorname

Darüber hinaus kann es während der Arbeit ein- und ausgeschaltet werden. Wenn diese Option deaktiviert ist, werden vorhandene interne Abonnements für Elemente gelöscht und beim Aktivieren erstellt. Natürlich werden beim Hinzufügen / Entfernen von Abonnements auch Artikel gelöscht und hinzugefügt.



Geerbte Sammlungen


Wie oft treten Situationen auf, in denen Sie nur einen Teil der Elemente aus einer vorhandenen Sammlung auswählen, sortieren oder konvertieren müssen? Und wenn Sie die ursprüngliche Sammlung ändern, ändern Sie die abhängige. Solche Situationen sind keine Seltenheit, und ReactiveUI verfügt über ein Tool, mit dem dies einfach möglich ist. Sein Name ist DerivedCollection. Sie erben von ReactiveList, und daher sind die Funktionen dieselben, mit der Ausnahme, dass beim Versuch, eine solche Auflistung zu ändern, eine Ausnahme ausgelöst wird. Eine Sammlung kann sich nur ändern, wenn sich ihre Basissammlung ändert.
Änderungen werden nicht mehr berücksichtigt, alles ist wie es war. Mal sehen, welche Transformationen auf die Basissammlung angewendet werden können.
var list = new ReactiveList();
list.AddRange(Enumerable.Range(1, 5));
var derived = list.CreateDerivedCollection(
    selector: i => i*2, 
    filter: i => i % 2 != 0, 
    orderer:(a, b) => b.CompareTo(a));
Console.WriteLine(string.Join(", ", list));
Console.WriteLine(string.Join(", ", derived));
list.AddRange(Enumerable.Range(2, 3));
Console.WriteLine(string.Join(", ", list));
Console.WriteLine(string.Join(", ", derived));

Wir sehen, dass Sie den Wert konvertieren, die ursprünglichen Elemente (vor der Konvertierung!) Filtern und einen Komparator für die konvertierten Elemente übergeben können. Darüber hinaus ist nur der Selektor erforderlich, der Rest ist optional.
Es gibt auch Methodenüberladungen, mit denen nicht nur INotifyCollectionChanged, sondern auch IEnumerable <> als Basissammlung verwendet werden kann. Dann müssen Sie jedoch eine geerbte Sammlung bereitstellen, um ein Rücksetzsignal zu erhalten.
Hier nimmt die geerbte Sammlung ungerade Elemente aus dem Original, verdoppelt ihren Wert und sortiert sie von größer nach kleiner. In der Konsole wird sein:
1, 2, 3, 4, 5
10, 6, 2

1, 2, 3, 4, 5, 2, 3, 4
10, 6, 6, 2


Bleib dran


Dieses Mal haben wir einige Details der Arbeit mit Eigenschaften besprochen, die im letzten Teil nicht beschrieben wurden. Sie stellten sicher, dass die Eigenschaftsimplementierung eine Zeile einnahm, und stellten fest, dass der Reihenfolge der Benachrichtigungen über ihre Änderung nicht vertraut werden kann. Das Hauptthema waren Sammlungen. Wir haben herausgefunden, welche Benachrichtigungen Sie von ReactiveList erhalten können, wenn sich diese ändern. Wir haben herausgefunden, warum und unter welchen Bedingungen Benachrichtigungen automatisch unterdrückt werden und wie man sie mit eigenen Händen unterdrückt. Schließlich haben wir versucht, geerbte Auflistungen zu verwenden, und sichergestellt, dass sie die Daten in der Basissammlung als Reaktion auf ihre Änderungen filtern, transformieren und sortieren können.
Im nächsten Teil werden wir über Teams sprechen und das Problem des Testens eines Ansichtsmodells betrachten. Wir werden herausfinden, welche Probleme damit verbunden sind und wie sie gelöst werden. Gehen wir dann zu View + ViewModel und versuchen, eine kleine GUI-Anwendung zu implementieren, die die bereits beschriebenen Tools verwendet.

Bis dann!

Jetzt auch beliebt: