Navigation in einer plattformübergreifenden .NET Core-Anwendung mit Speicherstatus auf der Festplatte am Beispiel von ReactiveUI und Avalonia



Die Benutzeroberflächen moderner Anwendungen sind normalerweise komplex. Oft ist es erforderlich, die Unterstützung für die Seitennavigation zu implementieren, Eingabefelder verschiedener Art zu verarbeiten und Informationen basierend auf vom Benutzer ausgewählten Parametern anzuzeigen oder auszublenden. Um UX zu verbessern, muss die Anwendung gleichzeitig den Status der Schnittstellenelemente während des Suspendierens oder Herunterfahrens auf der Festplatte speichern und den Status von der Festplatte wiederherstellen, wenn das Programm neu gestartet wird.


Das ReactiveUI MVVM-Framework schlägt vor, den Status einer Anwendung beizubehalten, indem das Diagramm der Präsentationsmodelle zum Zeitpunkt der Programmunterbrechung serialisiert wird, während die Mechanismen zur Bestimmung des Zeitpunkts der Unterbrechung für Frameworks und Plattformen unterschiedlich sind. Daher wird für WPF das Exit Ereignis für Xamarin.Android - ActivityPaused , für Xamarin.iOS - DidEnterBackground und für UWP - OnLaunched Überladung verwendet.


In diesem Artikel wird die Verwendung von ReactiveUI zum Speichern und Wiederherstellen des Softwarestatus mit einer GUI, einschließlich des Status eines Routers, am Beispiel des plattformübergreifenden Avalonia -GUI-Frameworks betrachtet. Das Material setzt ein grundlegendes Verständnis des MVVM-Entwurfsmusters und der reaktiven Programmierung im Kontext der C # -Sprache und der .NET-Plattform für den Leser voraus. Die Schritte in diesem Artikel gelten für Windows 10 und Ubuntu 18.


Projekterstellung


Um das Routing in Aktion zu testen, erstellen Sie ein neues .NET Core-Projekt aus der Avalonia-Vorlage und installieren Sie das Avalonia.ReactiveUI Paket - eine dünne Schicht aus Avalonia- und ReactiveUI-Integration. Stellen Sie sicher, dass Sie das .NET Core SDK und Git installiert haben, bevor Sie beginnen.


 git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates git --git-dir ./avalonia-dotnet-templates/.git checkout 9263c6b dotnet new --install ./avalonia-dotnet-templates dotnet new avalonia.app -o ReactiveUI.Samples.Suspension cd ./ReactiveUI.Samples.Suspension dotnet add package Avalonia.ReactiveUI dotnet add package Avalonia.Desktop dotnet add package Avalonia 

Stellen Sie sicher, dass die Anwendung gestartet wird und ein Fenster mit der Aufschrift Willkommen bei Avalonia angezeigt wird!


 # Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0 



Connect Avalonia Pre-Builds von MyGet


Um die neuesten Avalonia-Builds zu verbinden und zu verwenden, die automatisch in MyGet veröffentlicht werden, wenn sich der Avalonia-Repository-Hauptzweig auf GitHub ändert, verwenden wir die nuget.config Pakets nuget.config . Damit die IDE- und .NET Core-CLI nuget.config , müssen Sie eine sln Datei für das oben erstellte Projekt generieren. Wir verwenden die Tools der .NET Core CLI:


 dotnet new sln # Ctrl+C dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj 

Erstellen Sie eine Datei nuget.config in einem Ordner mit einer .sln Datei mit folgendem Inhalt:


 <?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" /> </packageSources> </configuration> 

Möglicherweise müssen Sie die IDE neu starten oder die gesamte Lösung hochladen und herunterladen. Wir aktualisieren Avalonia-Pakete über die NuGet-Paketmanager-Oberfläche Ihrer IDE oder über die Windows-Befehlszeilentools oder das Linux-Terminal auf die erforderliche Version (mindestens 0.9.1 ):


 dotnet add package Avalonia.ReactiveUI --version 0.9.1 dotnet add package Avalonia.Desktop --version 0.9.1 dotnet add package Avalonia --version 0.9.1 cat ReactiveUI.Samples.Suspension.csproj 

Die Projektdatei ReactiveUI.Samples.Suspension.csproj sieht nun folgendermaßen aus:


 <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> </PropertyGroup> <ItemGroup> <Compile Update="**\*.xaml.cs"> <DependentUpon>%(Filename)</DependentUpon> </Compile> <AvaloniaResource Include="**\*.xaml"> <SubType>Designer</SubType> </AvaloniaResource> </ItemGroup> <ItemGroup> <PackageReference Include="Avalonia" Version="0.9.1" /> <PackageReference Include="Avalonia.Desktop" Version="0.9.1" /> <PackageReference Include="Avalonia.ReactiveUI" Version="0.9.1" /> </ItemGroup> </Project> 

Erstellen Sie die Ordner Views/ und ViewModels/ im Projektstamm, ändern Sie den Namen der MainWindow Klasse der MainWindow in MainView , verschieben Sie ihn in das Verzeichnis Views/ und ändern Sie die Namespaces entsprechend in ReactiveUI.Samples.Suspension.Views . App.xaml.cs Inhalt der Program.cs App.xaml.cs und " App.xaml.cs den UseReactiveUI Aufruf auf den Avalonia Application Builder an und verschieben Sie die Initialisierung der Hauptansicht in " OnFrameworkInitializationCompleted , um den Empfehlungen zur Verwaltung des Anwendungslebenszyklus zu entsprechen:


Program.cs


 class Program { //  .   API  Avalonia  , //  SynchronizationContext,    // OnFrameworkInitializationCompleted  App.xaml.cs:  //     -    . public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); //   Avalonia.    // ,      . public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>() .UseReactiveUI() // ! .UsePlatformDetect() .LogToDebug(); } 

App.xaml.cs


 public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this); //    .     //  MVVM , DI    .    //     ApplicationLifetime,    //     . public override void OnFrameworkInitializationCompleted() { new Views.MainView().Show(); base.OnFrameworkInitializationCompleted(); } } 

Sie müssen Program.cs using Avalonia.ReactiveUI hinzufügen. Stellen Sie sicher, dass das Projekt nach dem Aktualisieren der Pakete gestartet wird und das Standard-Begrüßungsfenster anzeigt.


 # Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0 



Plattformübergreifendes Routing ReactiveUI


In der Regel gibt es zwei Hauptansätze für die Implementierung der Navigation zwischen Seiten einer .NET-Anwendung: Ansicht zuerst und Ansicht Modell zuerst. Der View-First-Ansatz umfasst die Steuerung des Navigationsstapels und der Navigation zwischen Seiten auf View-Ebene in der MVVM-Terminologie - beispielsweise unter Verwendung der Frame- und Page- Klassen bei UWP oder WPF. Bei Verwendung des View-Model-First-Ansatzes wird die Navigation auf der Ebene von Präsentationsmodellen implementiert. ReactiveUI- Tools, die das Routing in der Anwendung organisieren, konzentrieren sich auf die Verwendung des View Model-First-Ansatzes. Das reaktive IScreen Routing besteht aus einer IScreen Implementierung, die den Status des Routers enthält, mehreren IRoutableViewModel Implementierungen und dem plattformspezifischen XAML-Steuerelement RoutedViewHost .




Der Status des Routers wird durch das RoutingState Objekt dargestellt, das den Navigationsstapel steuert. IScreen ist die Wurzel des Navigationsstapels, und die Anwendung enthält möglicherweise mehrere Navigationswurzeln. RoutedViewHost überwacht den Status des entsprechenden RoutingState Routers und reagiert auf Änderungen im Navigationsstapel, indem das entsprechende IRoutableViewModel des XAML-Steuerelements eingebettet wird. Die beschriebene Funktionalität wird anhand der folgenden Beispiele veranschaulicht.


Speichern der Ansichtsmodelle auf der Festplatte


Betrachten Sie ein typisches Beispiel für einen Informationssuchbildschirm.




Wir müssen entscheiden, welche Elemente des Bildschirmdarstellungsmodells während der Pause oder des Herunterfahrens der Anwendung auf der Festplatte gespeichert werden sollen und welche bei jedem Start neu erstellt werden sollen. Der Status von ReactiveUI-Befehlen , die die ICommand Schnittstelle implementieren und an Schaltflächen ReactiveCommand<TIn, TOut> werden. ReactiveCommand<TIn, TOut> wird im Konstruktor erstellt und initialisiert, während der Status des CanExecute Indikators von den Eigenschaften des Ansichtsmodells abhängt und bei Änderungen neu berechnet wird. Die Notwendigkeit, Suchergebnisse zu speichern - ein strittiger Punkt - hängt von den Besonderheiten der Anwendung ab. Es ist jedoch SearchQuery , den Status des SearchQuery Eingabefelds zu speichern und wiederherzustellen!


ViewModels / SearchViewModel.cs


 [DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery; //   IScreen  ,   NULL //  IScreen  Splat.Locator.    //      . public SearchViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); //     SearchQuery , //     . var canSearch = this .WhenAnyValue(x => x.SearchQuery) .Select(query => !string.IsNullOrWhiteSpace(query)); //      ,  //     . _search = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), //    canSearch); } public IScreen HostScreen { get; } public string UrlPathSegment => "/search"; public ICommand Search => _search; [DataMember] public string SearchQuery { get => _searchQuery; set => this.RaiseAndSetIfChanged(ref _searchQuery, value); } } 

Die Klasse des Ansichtsmodells ist mit dem Attribut [DataMember] und den Eigenschaften gekennzeichnet, die mit den [DataMember] serialisiert [DataMember] . Dies ist ausreichend, wenn der verwendete Serializer den Opt-In-Ansatz verwendet. Er speichert nur Eigenschaften, die explizit mit Attributen auf der Festplatte gekennzeichnet sind. Im Fall des Opt-Out-Ansatzes müssen die Eigenschaften, die nicht auf der Festplatte gespeichert werden müssen, mit den Attributen [IgnoreDataMember] werden. Zusätzlich implementieren wir die IRoutableViewModel Schnittstelle in unser Ansichtsmodell, damit sie später Teil des Navigationsrahmens des Anwendungsrouters werden kann.


Ebenso implementieren wir das Präsentationsmodell der Autorisierungsseite

ViewModels / LoginViewModel.cs


 [DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username; //   IScreen  ,   NULL //  IScreen  Splat.Locator.    //      . public LoginViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); //     Username  Password // ,     . var canLogin = this .WhenAnyValue( x => x.Username, x => x.Password, (user, pass) => !string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(pass)); //      ,  //    . _login = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), //    canLogin); } public IScreen HostScreen { get; } public string UrlPathSegment => "/login"; public ICommand Login => _login; [DataMember] public string Username { get => _username; set => this.RaiseAndSetIfChanged(ref _username, value); } //        ! public string Password { get => _password; set => this.RaiseAndSetIfChanged(ref _password, value); } } 

Präsentationsmodelle für zwei Seiten der Anwendung sind fertig, implementieren die IRoutableViewModel Schnittstelle und können in den IScreen Router integriert werden. Jetzt implementieren wir direkt IScreen . Mit Hilfe der Attribute [DataContract] markieren wir, welche Eigenschaften des Ansichtsmodells serialisiert und welche ignoriert werden sollen. [DataMember] den öffentlichen Setter der Eigenschaft, die im folgenden Beispiel mit dem [DataMember] gekennzeichnet ist. Die Eigenschaft ist absichtlich zum Schreiben geöffnet, damit der Serializer eine frisch erstellte Instanz des Objekts während der Modelldeserialisierung ändern kann.


ViewModels / MainViewModel.cs


 [DataContract] public class MainViewModel : ReactiveObject, IScreen { private readonly ReactiveCommand<Unit, Unit> _search; private readonly ReactiveCommand<Unit, Unit> _login; private RoutingState _router = new RoutingState(); public MainViewModel() { //       , //  ,   . var canLogin = this .WhenAnyObservable(x => x.Router.CurrentViewModel) .Select(current => !(current is LoginViewModel)); _login = ReactiveCommand.Create( () => { Router.Navigate.Execute(new LoginViewModel()); }, canLogin); //       , //  ,   . var canSearch = this .WhenAnyObservable(x => x.Router.CurrentViewModel) .Select(current => !(current is SearchViewModel)); _search = ReactiveCommand.Create( () => { Router.Navigate.Execute(new SearchViewModel()); }, canSearch); } [DataMember] public RoutingState Router { get => _router; set => this.RaiseAndSetIfChanged(ref _router, value); } public ICommand Search => _search; public ICommand Login => _login; } 

In unserer Anwendung muss nur RoutingState auf der Festplatte gespeichert werden. Aus offensichtlichen Gründen müssen Befehle nicht auf der Festplatte gespeichert werden. Ihr Status hängt vollständig vom Router ab. Das serialisierte Objekt muss erweiterte Informationen zu den Typen enthalten, die das IRoutableViewModel implementieren, damit der Navigationsstapel bei der Deserialisierung wiederhergestellt werden kann. Wir beschreiben die Logik des MainViewModel Ansichtsmodells, platzieren die Klasse in ViewModels/MainViewModel.cs und im entsprechenden ReactiveUI.Samples.Suspension.ViewModels Namespace.




Routing in der Avalonia App


Die Benutzeroberflächenlogik auf der Ebenenebene des Modells und des Präsentationsmodells der Demoanwendung ist implementiert und kann in eine separate Assembly verschoben werden, die auf den .NET-Standard ausgerichtet ist, da sie nichts über das verwendete GUI-Framework weiß. Werfen wir einen Blick auf die Präsentationsebene. In der MVVM-Terminologie ist die Präsentationsschicht für das Rendern des Status des Präsentationsmodells auf dem Bildschirm verantwortlich. Um den aktuellen Status des RoutingState Routers zu rendern, wird das im Avalonia.ReactiveUI Paket enthaltene XAML RoutedViewHost Steuerelement Avalonia.ReactiveUI . Wir implementieren die GUI für SearchViewModel - erstellen Sie dazu im Verzeichnis Views/ zwei Dateien: SearchView.xaml und SearchView.xaml.cs .


Eine Beschreibung der Benutzeroberfläche unter Verwendung des in Avalonia verwendeten XAML-Dialekts kommt Entwicklern auf der Windows Presentation Foundation, der Universal Windows Platform oder Xamarin.Forms wahrscheinlich bekannt vor. Im obigen Beispiel erstellen wir eine einfache Schnittstelle für das Suchformular. Wir zeichnen ein Textfeld zur Eingabe der Suchabfrage und eine Schaltfläche zum SearchViewModel der Suche, während wir die Steuerelemente an die Eigenschaften des SearchViewModel definierten SearchViewModel Modells binden.


Ansichten / SearchView.xaml


 <UserControl xmlns="https://github.com/avaloniaui" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DataContext="{d:DesignInstance viewModels:SearchViewModel}" xmlns:viewModels="clr-namespace:ReactiveUI.Samples.Suspension.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ReactiveUI.Samples.Suspension.Views.SearchView" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="Search view" Margin="5" /> <TextBox Grid.Row="1" Text="{Binding SearchQuery, Mode=TwoWay}" /> <Button Grid.Row="2" Content="Search" Command="{Binding Search}" /> </Grid> </UserControl> 

Ansichten / SearchView.xaml.cs


 public sealed class SearchView : ReactiveUserControl<SearchViewModel> { public SearchView() { //  WhenActivated     //        . this.WhenActivated((CompositeDisposable disposable) => { }); AvaloniaXamlLoader.Load(this); } } 

Der Code- SearchView.xaml des SearchView.xaml Steuerelements wird auch bekannten SearchView.xaml und XF- SearchView.xaml . Der Aufruf WhenActivated wird verwendet, um Code auszuführen, wenn die Ansicht oder das Ansichtsmodell aktiviert und deaktiviert wird. Wenn Ihre Anwendung Hot Observables (Timer, Geolocation, Verbindung zum Nachrichtenbus) verwendet, sollten Sie diese CompositeDisposable Aufrufen von DisposeWith an CompositeDisposable DisposeWith damit Hot Observables beim DisposeWith XAML-Steuerelements und des entsprechenden Ansichtsmodells vom visuellen Baum keine neuen Werte mehr veröffentlichen und keine Lecks mehr auftreten Speicher.


Ebenso implementieren wir die Darstellung der Autorisierungsseite.

Ansichten / LoginView.xaml


 <UserControl xmlns="https://github.com/avaloniaui" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DataContext="{d:DesignInstance viewModels:LoginViewModel, IsDesignTimeCreatable=True}" xmlns:viewModels="clr-namespace:ReactiveUI.Samples.Suspension.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ReactiveUI.Samples.Suspension.Views.LoginView" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="Login view" Margin="5" /> <TextBox Grid.Row="1" Text="{Binding Username, Mode=TwoWay}" /> <TextBox Grid.Row="2" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" /> <Button Grid.Row="3" Content="Login" Command="{Binding Login}" /> </Grid> </UserControl> 

Ansichten / LoginView.xaml.cs


 public sealed class LoginView : ReactiveUserControl<LoginViewModel> { public LoginView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } } 

Bearbeiten Sie die Views/MainView.xaml.cs Views/MainView.xaml und Views/MainView.xaml.cs . RoutedViewHost XAML RoutedViewHost im Namespace RoutedViewHost auf dem Hauptbildschirm, und weisen RoutingState der RoutingState Eigenschaft den Status des RoutingState Routers zu. Fügen Sie Schaltflächen hinzu, um zu den Such- und Autorisierungsseiten zu navigieren, und binden Sie sie an die ViewModels/MainViewModel beschriebenen ViewModels/MainViewModel Eigenschaften.


Ansichten / MainView.xaml


 <Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ReactiveUI.Samples.Suspension.Views.MainView" xmlns:reactiveUi="http://reactiveui.net" Title="ReactiveUI.Samples.Suspension"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="48" /> </Grid.RowDefinitions> <!--  ,   RoutingState,   View  ViewModel --> <reactiveUi:RoutedViewHost Grid.Row="0" Router="{Binding Router}"> <reactiveUi:RoutedViewHost.DefaultContent> <TextBlock Text="Default Content" /> </reactiveUi:RoutedViewHost.DefaultContent> </reactiveUi:RoutedViewHost> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Button Grid.Column="0" Command="{Binding Search}" Content="Search" /> <Button Grid.Column="1" Command="{Binding Login}" Content="Login" /> <Button Grid.Column="2" Command="{Binding Router.NavigateBack}" Content="Back" /> </Grid> </Grid> </Window> 

Ansichten / MainView.xaml.cs


 public sealed class MainView : ReactiveWindow<MainViewModel> { public MainView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } } 

Eine einfache Anwendung, die die Routing- Funktionen von ReactiveUI und Avalonia demonstriert, ist bereit. Wenn Sie auf die Schaltflächen Search und RoutingState , werden die entsprechenden Befehle aufgerufen, eine neue Instanz des Ansichtsmodells erstellt und der RoutingState aktualisiert. Das XAML- RoutedViewHost , das die Änderungen am RoutingState , versucht, den IViewFor<TViewModel> , wobei TViewModel der Typ des Ansichtsmodells ist, von Locator.Current . Wenn eine registrierte Implementierung von IViewFor<TViewModel> gefunden wird, wird eine neue Instanz erstellt, in RoutedViewHost und im Avalonia-Anwendungsfenster angezeigt.




Wir registrieren die erforderlichen Komponenten IViewFor<TViewModel> und IScreen in der App.OnFrameworkInitializationCompleted Methode unserer Anwendung mit Locator.CurrentMutable . IViewFor<TViewModel> erforderlich, damit RoutedViewHost funktioniert, und IScreen Registrierung von IScreen erforderlich, damit beim Deserialisieren die LoginViewModel SearchViewModel und LoginViewModel mithilfe des Konstruktors ohne Parameter und Locator.Current korrekt initialisiert werden Locator.Current .


App.xaml.cs


 public override void OnFrameworkInitializationCompleted() { //   . Locator.CurrentMutable.RegisterConstant<IScreen>(new MainViewModel()); Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); //        . new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.OnFrameworkInitializationCompleted(); } 

Führen Sie die Anwendung aus und stellen Sie sicher, dass das Routing ordnungsgemäß funktioniert. Wenn das XAML-Markup fehlerhaft ist , teilt uns der in Avalonia verwendete XamlIl-Compiler in der Kompilierungsphase genau mit, wo. XamlIl unterstützt auch das XAML-Debugging direkt im IDE-Debugger !


 dotnet run --framework netcoreapp3.0 



Speichern und Wiederherstellen des gesamten Anwendungsstatus


Nachdem das Routing konfiguriert ist und funktioniert, beginnt der interessanteste Teil: Sie müssen das Speichern von Daten auf der Festplatte implementieren, wenn Sie die Anwendung schließen, und das Lesen von Daten von der Festplatte beim Start zusammen mit dem Status des Routers. Die Initialisierung von Hooks, die Ereignisse zum Starten und Schließen von Anwendungen abhören, wird von einer speziellen AutoSuspendHelper Klasse durchgeführt, die für jede von ReactiveUI unterstützte Plattform AutoSuspendHelper . Die Aufgabe des Entwicklers besteht darin, diese Klasse ganz am Anfang der Wurzel der Anwendungszusammensetzung zu initialisieren. Es ist auch erforderlich, die Eigenschaft RxApp.SuspensionHost.CreateNewAppState Funktion zu initialisieren, die den Standardstatus der Anwendung RxApp.SuspensionHost.CreateNewAppState wenn kein gespeicherter Status vorliegt oder ein unerwarteter Fehler aufgetreten ist oder wenn die gespeicherte Datei beschädigt ist.


Als Nächstes müssen Sie die Methode RxApp.SuspensionHost.SetupDefaultSuspendResume und die Implementierung von ISuspensionDriver - dem Treiber, der das ISuspensionDriver und liest. Um ISuspensionDriver zu implementieren, verwenden ISuspensionDriver die Newtonsoft.Json Bibliothek und den System.IO Namespace, um mit dem Dateisystem zu arbeiten. Installieren Sie dazu das Newtonsoft.Json Paket:


 dotnet add package Newtonsoft.Json 

Treiber / NewtonsoftJsonSuspensionDriver.cs


 public class NewtonsoftJsonSuspensionDriver : ISuspensionDriver { private readonly string _file; private readonly JsonSerializerSettings _settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; public NewtonsoftJsonSuspensionDriver(string file) => _file = file; public IObservable<Unit> InvalidateState() { if (File.Exists(_file)) File.Delete(_file); return Observable.Return(Unit.Default); } public IObservable<object> LoadState() { var lines = File.ReadAllText(_file); var state = JsonConvert.DeserializeObject<object>(lines, _settings); return Observable.Return(state); } public IObservable<Unit> SaveState(object state) { var lines = JsonConvert.SerializeObject(state, _settings); File.WriteAllText(_file, lines); return Observable.Return(Unit.Default); } } 

System.IO Universal Winows Platform, — File Directory StorageFile StorageFolder . , IRoutableViewModel , Newtonsoft.Json TypeNameHandling.All . Avalonia — App.OnFrameworkInitializationCompleted :


 public override void OnFrameworkInitializationCompleted() { //   . var suspension = new AutoSuspendHelper(ApplicationLifetime); RxApp.SuspensionHost.CreateNewAppState = () => new MainViewModel(); RxApp.SuspensionHost.SetupDefaultSuspendResume(new NewtonsoftJsonSuspensionDriver("appstate.json")); suspension.OnFrameworkInitializationCompleted(); //  ,      . var state = RxApp.SuspensionHost.GetAppState<MainViewModel>(); Locator.CurrentMutable.RegisterConstant<IScreen>(state); Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); //  . new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.OnFrameworkInitializationCompleted(); } 

AutoSuspendHelper Avalonia.ReactiveUI IApplicationLifetime — , ISuspensionDriver . ISuspensionDriver appstate.json :


appstate.json

$type , , .


 { "$type": "ReactiveUI.Samples.Suspension.ViewModels.MainViewModel, ReactiveUI.Samples.Suspension", "Router": { "$type": "ReactiveUI.RoutingState, ReactiveUI", "_navigationStack": { "$type": "System.Collections.ObjectModel.ObservableCollection`1[[ReactiveUI.IRoutableViewModel, ReactiveUI]], System.ObjectModel", "$values": [ { "$type": "ReactiveUI.Samples.Suspension.ViewModels.SearchViewModel, ReactiveUI.Samples.Suspension", "SearchQuery": "funny cats" }, { "$type": "ReactiveUI.Samples.Suspension.ViewModels.LoginViewModel, ReactiveUI.Samples.Suspension", "Username": "worldbeater" } ] } } } 

, , , , , , ! , , ReactiveUI — UWP WPF, Xamarin.Forms.




: ISuspensionDriver AkavacheUserAccount Secure iOS UWP , , Android BundleSuspensionDriver ReactiveUI.AndroidSupport . JSON Xamarin.Essentials SecureStorage . , — !


Nützliche Links


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


All Articles