Asynchrone Beobachtungen und Versprechungen in Angular
Neulich bei der Code-Review in einem Angular-Team (ein fiktiver Dialog):
Neulich bei der Code-Review in einem Angular-Team (ein fiktiver Dialog):
ReactiveUI ist ein cross-platform Mvvm-Framework. Wie der Name vermuten lässt, setzt ReactiveUI auf den Reactive Extensions (Rx) auf. Es bietet neben WPF und UWP auch eine erstklassige Unterstützung für Xamarin Native und natürlich Xamarin Forms. Desweiteren ist es Open-Source und wurde mittlerweile in die .NET Foundation aufgenommen.
Anhand folgendem Beispiel möchte ich zeige, wie mit ReactiveUI und Xamarin.Forms eine Live-Suche implementiert werden kann. Die App ist einfach gestaltet. Man kann per Eingabefeld nach Ortschaften bzw. ÖV Stationen suchen. Die Suchanfragen werden jeweils an eine REST-Schnittstelle von opendata.ch abgesetzt. Anschliessend werden die Resultate in Form einer Liste dargestellt. Wer den kompletten Source Code einsehen will, findet diesen auf Github.
Damit der Server jedoch nicht mit unnötigen Suchanfragen bombadiert wird, soll die Anzahl der Aufrufe begrenzt werden. Wenn der Benutzer schnell tippt, macht es wenig Sinn nach jedem Buchstaben sofort eine neue Suche abzusetzen. Die Suche soll erst starten, wenn man für eine bestimmte Zeit keine weitere Eingabe gemacht wird. Wählt man einen kurzen Grenzwert, so fallt dies dem Benutzer auch nicht negativ auf. In diesem Beispiel wurden 500ms verwendet:
Die Solution ist in drei Projekte aufgeteilt. Jeweils ein Plattformprojekt (iOS/Android) sowie ein Projekt mit einer .Net Standard Klassenbibliothek. In dieser befindet sich die Xamarin.Forms Teil mit View und ViewModel. Es kann somit die komplette Logik zwischen den beiden Apps geteilt werden.
Das SearchViewModel beinhaltet alle relevanten Properties. Weiter findet man darin auch die Logik für die Suche. Die basis ViewModel Implementation bei ReactiveUI heisst ReactiveObject. Diese Klasse implementiert bereits INotifyPropertyChanged. Somit kann man Properties direkt wie folgt definieren:
public string SearchQuery { get { return _searchQuery; } set { this.RaiseAndSetIfChanged(ref _searchQuery, value); } } public ReactiveCommand<string, List<Station>> Search { get { return _searchCommand; } private set { this.RaiseAndSetIfChanged(ref _searchCommand, value); } }
Im Konstruktor des ViewModels werden die einzelnen Properties initialisiert. Für den Search Command kann man die statische Hilfsmethode CreateFromTask von ReactiveUI verwenden:
Search = ReactiveCommand.CreateFromTask<string, List<Station>>(SearchAsync, CanSearch());
Der erste Parameter ist dabei die Methode, welche einen Suchtext entgegennimmt und eine Liste von Stationen zurück liefert. Beim zweiten Parameter handelt es sich um ein IObservable<bool>. Dieses Observable definiert, ob der Command ausgeführt werden darf oder nicht. Dabei wird auf folgende Bedingungen geachtet:
Mit Rx ausgedrückt, kann dies wie folgt aussehen:
Observable.CombineLatest( this.WhenAnyValue(vm => vm.SearchQuery) .Select(searchQuery => !string.IsNullOrEmpty(searchQuery)) .DistinctUntilChanged(), this.WhenAnyObservable(x => x.Search.IsExecuting) .DistinctUntilChanged(), (hasSearchQuery, isExecuting) => hasSearchQuery && !isExecuting) .Do(cps => System.Diagnostics.Debug.WriteLine($"Can Perform Search: {cps}")) .DistinctUntilChanged()
CombineLatest führt dabei zwei Observables so zusammen, dass jeweils immer nur der letzte Wert berücksichtigt wird. Der dritte Parameter ist eine Funktion, welche die beiden letzten Werte zu einem einzigen Boolean zusammenführt.
Die Live Suche kann man ebenfalls relativ einfach mit Rx und den Hilfsmethoden von ReactiveUI implementieren. Mit Hilfe der Throttle Funktion können schnell aufeinanderfolgende Eingaben ausgefiltert werden. Dies lässt sich gut mit einem sogenannten Marble Diagramm erklären.
Die obere Zeitachse stellt den Input Stream dar und die untere Zeitachse den Output Stream. Die Implementation im ViewModel sieht dann wie folg aus:
// erstellt ein IObservable<string> von SearchQuery this.WhenAnyValue(x => x.SearchQuery) // drosselt Änderungen von SearchQuery, sodas diese erst weitergereicht warden, // wenn für 500ms keine weiteren Änderungen passieren .Throttle(TimeSpan.FromMilliseconds(500), TaskPoolScheduler.Default) // Callback soll auf dem UI Thread stattfinden .ObserveOn(RxApp.MainThreadScheduler) // SearchCommand soll ausgeführt warden (sofern CanExecute == true) .InvokeCommand(Search)
An diesem Beispiel sieht man gut, wie ein komplexer Ablauf mit Hilfe von Rx auf einfache Weise umsätzen lässt. Würde man die gleiche Funktionalität in einem imperativen Still implementieren wollen, müsste man zusätzliche Zustands-Variablen und einen Timer verwenden. Rx erlaut jedoch eine deklartive Schreibweise. Diese führt meiner Meinung nach zu verständlicherem Code.
Nachdem wir nun das ViewModel implementiert haben, muss noch die View umgesetzt werden. Diese kann man entweder mit XAML oder per Code aufbauen. Bindings werden bei ReactiveUI jedoch bewusst im Code-Behind als Expressions geschrieben:
private void InitializeBindings() { // Search Query this.Bind(ViewModel, x => x.SearchQuery, c => c.SearchEntry.Text) .DisposeWith(_bindingsDisposable); // Search Command this.BindCommand(ViewModel, x => x.Search, c => c.SearchButton, vm => vm.SearchQuery) .DisposeWith(_bindingsDisposable); // Activity Indicator this.WhenAnyObservable(x => x.ViewModel.Search.IsExecuting) .BindTo(ActivityIndicator, c => c.IsRunning) .DisposeWith(_bindingsDisposable); // Results this.OneWayBind(ViewModel, x => x.SearchResults, c => c.SearchResults.ItemsSource) .DisposeWith(_bindingsDisposable); }
Das Command-Binding führt dabei nicht nur den Command bei einem Klick aus sonder kümmer sich auch darum den Button zu aktiviren und deaktivieren.
Bisher hatte ich zwar noch nicht die Gelegenheit ReactiveUI in grösseren Projekten einzusetzen. Tortzdem macht das Framework für mich auf den ersten Blick einen guten Eindruck. Es bringt alle Werkzeuge mit, welche ich von einem Mvvm-Framework erwarte. Darüberhinaus liefert es viele nütziche Extensions. Diese erlauben es Bindings und Commands mit Rx zu verknüpfen.
Wer allerdings noch nie mit Rx gearbeitet hat, wird zu beginn einen etwas schweren Einstieg habe, da ReactiveUI sehr stark darauf aufbaut. In diesem Fall sollte man nebst der Dokumentation auch unbedingt die Beispiele genau anschauen.