Speichern des Routing-Status auf der Festplatte in einer plattformübergreifenden .NET Core-GUI-App mit ReactiveUI und Avalonia

Bild


Benutzeroberflächen moderner Unternehmensanwendungen sind recht komplex. Als Entwickler müssen Sie häufig die In-App-Navigation implementieren, Benutzereingaben überprüfen, Bildschirme basierend auf Benutzereinstellungen anzeigen oder ausblenden. Für eine bessere Benutzeroberfläche sollte Ihre App in der Lage sein, den Status auf der Festplatte zu speichern, wenn die App angehalten wird, und den Status wiederherzustellen, wenn die App fortgesetzt wird.


ReactiveUI bietet Funktionen, mit denen Sie den Anwendungsstatus beibehalten können, indem Sie den Ansichtsmodellbaum serialisieren, wenn die App heruntergefahren oder angehalten wird. Suspendierungsereignisse variieren je nach Plattform. ReactiveUI verwendet das Exit Ereignis für WPF, ActivityPaused für Xamarin.Android, DidEnterBackground für Xamarin.iOS und OnLaunched für UWP.


In diesem Tutorial erstellen wir eine Beispielanwendung, die die Verwendung der ReactiveUI Suspension- Funktion mit Avalonia demonstriert - einem plattformübergreifenden .NET Core XAML-basierten GUI-Framework. Es wird erwartet, dass Sie mit dem MVVM-Muster und den reaktiven Erweiterungen vertraut sind, bevor Sie diesen Hinweis lesen. Die im Lernprogramm beschriebenen Schritte sollten funktionieren, wenn Sie Windows 10 oder Ubuntu 18 verwenden und das .NET Core SDK installiert haben. Fangen wir an!


Bootstrapping des Projekts


Um das ReactiveUI-Routing in Aktion zu sehen, erstellen wir ein neues .NET Core-Projekt, das auf Avalonia-Anwendungsvorlagen basiert. Dann installieren wir das Avalonia.ReactiveUI Paket. Das Paket bietet plattformspezifische Avalonia-Lifecycle-Hooks, Routing- und Aktivierungsinfrastruktur . Denken Sie daran, .NET Core und git zu installieren, bevor Sie die folgenden Befehle ausführen.


 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 

Lassen Sie uns die App ausführen und sicherstellen, dass ein Fenster mit der Aufschrift "Willkommen in 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 

Bild


Installieren von Avalonia Preview Builds aus MyGet


Die neuesten Avalonia-Pakete werden jedes Mal in MyGet veröffentlicht, wenn ein neues Commit an den Hauptzweig des Avalonia-Repositorys auf GitHub gesendet wird. Um die neuesten Pakete von MyGet in unserer App zu verwenden, erstellen wir eine Datei nuget.config . sln generieren wir jedoch mithilfe der .NET Core CLI eine sln Datei für das zuvor erstellte Projekt:


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

Jetzt erstellen wir die Datei nuget.config 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> 

Normalerweise ist ein Neustart erforderlich, um unsere IDE zu zwingen, Pakete aus dem neu hinzugefügten MyGet-Feed zu erkennen. Das erneute Laden der Lösung sollte jedoch ebenfalls hilfreich sein. Anschließend aktualisieren wir Avalonia-Pakete mithilfe der NuGet-Paketmanager-GUI oder der .NET Core-CLI auf die neueste Version (oder mindestens auf 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 Datei ReactiveUI.Samples.Suspension.csproj sollte jetzt ungefähr so ​​aussehen:


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

Wir erstellen zwei neue Ordner im Projektstammverzeichnis mit dem Namen Views/ bzw. ViewModels/ . Als Nächstes benennen wir die MainWindow Klasse in MainView und verschieben sie in den Ordner Views/ . Denken Sie daran, Verweise auf die bearbeitete Klasse in der entsprechenden XAML-Datei umzubenennen, da sonst das Projekt nicht kompiliert wird. MainView auch daran, den Namespace für MainView aus MainView der MainView in ReactiveUI.Samples.Suspension.Views zu ändern. Dann bearbeiten wir zwei weitere Dateien, Program.cs und App.xaml.cs Wir fügen dem Avalonia-App-Builder einen Aufruf von UseReactiveUI und verschieben den App-Initialisierungscode in die OnFrameworkInitializationCompleted Methode, um die Richtlinien für die OnFrameworkInitializationCompleted von Avalonia- Anwendungen zu erfüllen:


Program.cs


 class Program { // The entry point. Things aren't ready yet, so at this point // you shouldn't use any Avalonia types or anything that // expects a SynchronizationContext to be ready. public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); // This method is required for IDE previewer infrastructure. // Don't remove, otherwise, the visual designer will break. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>() .UseReactiveUI() // required! .UsePlatformDetect() .LogToDebug(); } 

App.xaml.cs


 public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this); // The entrypoint for your application. Here you initialize your // MVVM framework, DI container and other components. You can use // the ApplicationLifetime property here to detect if the app // is running on a desktop platform or on a mobile platform (WIP). public override void OnFrameworkInitializationCompleted() { new Views.MainView().Show(); base.OnFrameworkInitializationCompleted(); } } 

Bevor Sie versuchen, das Projekt zu erstellen, stellen Sie sicher, dass die Direktive using Avalonia.ReactiveUI oben in der Datei Program.cs hinzugefügt wird. Höchstwahrscheinlich hat unsere IDE diesen Namespace bereits importiert. Andernfalls wird ein Fehler beim Kompilieren angezeigt. Schließlich ist es Zeit sicherzustellen, dass die App kompiliert, ausgeführt und ein neues Fenster angezeigt wird:


 dotnet run --framework netcoreapp2.1 

Bild


Plattformübergreifendes ReactiveUI-Routing


Es gibt zwei allgemeine Ansätze zum Organisieren der In-App-Navigation in einer plattformübergreifenden .NET-App: Ansicht zuerst und Modell zuerst anzeigen. Beim ersteren Ansatz wird davon ausgegangen, dass die Ansichtsebene den Navigationsstapel verwaltet - beispielsweise mithilfe plattformspezifischer Frame- und Page- Klassen. Bei letzterem Ansatz übernimmt die Ansichtsmodellebene die Navigation über eine plattformunabhängige Abstraktion. ReactiveUI-Tools wurden unter Berücksichtigung des Ansatzes des Ansichtsmodells entwickelt. Das reaktive IScreen Routing besteht aus einer IScreen Implementierung, die den aktuellen Routing-Status, mehrere IRoutableViewModel Implementierungen und einem plattformspezifischen XAML-Steuerelement namens RoutedViewHost .




Das RoutingState Objekt kapselt die Verwaltung des Navigationsstapels. IScreen ist das Navigationsstammverzeichnis, muss jedoch trotz des Namens nicht den gesamten Bildschirm einnehmen. RoutedViewHost reagiert auf Änderungen im gebundenen RoutingState und bettet die entsprechende Ansicht für das aktuell ausgewählte IRoutableViewModel . Die beschriebene Funktionalität wird später anhand umfassenderer Beispiele veranschaulicht.


Modellstatus der persistenten Ansicht


Betrachten Sie als Beispiel ein Modell der Suchbildschirmansicht.




Wir werden entscheiden, welche Eigenschaften des Ansichtsmodells beim Herunterfahren der Anwendung gespeichert und welche neu erstellt werden sollen. Es ist nicht erforderlich, den Status eines reaktiven Befehls zu speichern, der die ICommand Schnittstelle implementiert. ReactiveCommand<TIn, TOut> -Klasse wird normalerweise im Konstruktor initialisiert. Der CanExecute Indikator hängt normalerweise vollständig von den Eigenschaften des Ansichtsmodells ab und wird jedes Mal neu berechnet, wenn sich eine dieser Eigenschaften ändert. Es ist fraglich, ob Sie die Suchergebnisse behalten, aber das Speichern der Suchabfrage ist eine gute Idee.


ViewModels / SearchViewModel.cs


 [DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery; // We inject the IScreen implementation via the constructor. // If we receive null, we use Splat.Locator to resolve the // default implementation. The parameterless constructor is // required for the deserialization feature to work. public SearchViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); // Each time the search query changes, we check if the search // query is empty. If it is, we disable the command. var canSearch = this .WhenAnyValue(x => x.SearchQuery) .Select(query => !string.IsNullOrWhiteSpace(query)); // Buttons bound to the command will stay disabled // as long as the command stays disabled. _search = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), // emulate a long-running operation 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); } } 

Wir markieren die gesamte Ansichtsmodellklasse mit dem Attribut [DataMember] und kommentieren Eigenschaften, die wir mit dem Attribut [DataMember] serialisieren [DataMember] . Dies ist ausreichend, wenn wir den Opt-In-Serialisierungsmodus verwenden möchten. In Anbetracht der Serialisierungsmodi bedeutet Opt-out, dass alle öffentlichen Felder und Eigenschaften serialisiert werden, es sei denn, Sie ignorieren sie explizit durch Annotieren mit dem Attribut [IgnoreDataMember] . Opt-In bedeutet das Gegenteil. Zusätzlich implementieren wir die IRoutableViewModel Schnittstelle in unserer Ansichtsmodellklasse. Dies ist erforderlich, wenn wir das Ansichtsmodell als Teil eines Navigationsstapels verwenden.


Implementierungsdetails für das Anmeldeansichtsmodell

ViewModels / LoginViewModel.cs


 [DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username; // We inject the IScreen implementation via the constructor. // If we receive null, we use Splat.Locator to resolve the // default implementation. The parameterless constructor is // required for the deserialization feature to work. public LoginViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); // When any of the specified properties change, // we check if user input is valid. var canLogin = this .WhenAnyValue( x => x.Username, x => x.Password, (user, pass) => !string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(pass)); // Buttons bound to the command will stay disabled // as long as the command stays disabled. _login = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), // emulate a long-running operation 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); } // Note: Saving passwords to disk isn't a good idea. public string Password { get => _password; set => this.RaiseAndSetIfChanged(ref _password, value); } } 

Die beiden Ansichtsmodelle implementieren die IRoutableViewModel Schnittstelle und können in einen Navigationsbildschirm eingebettet werden. Jetzt ist es Zeit, die IScreen Oberfläche zu implementieren. Wieder verwenden wir [DataContract] -Attribute, um anzugeben, welche Teile serialisiert und welche ignoriert werden sollen. Im folgenden Beispiel wird der RoutingState Eigenschaftssetzer absichtlich als öffentlich deklariert. RoutingState kann unser Serializer die Eigenschaft ändern, wenn sie deserialisiert wird.


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() { // If the authorization page is currently shown, then // we disable the "Open authorization view" button. var canLogin = this .WhenAnyObservable(x => x.Router.CurrentViewModel) .Select(current => !(current is LoginViewModel)); _login = ReactiveCommand.Create( () => { Router.Navigate.Execute(new LoginViewModel()); }, canLogin); // If the search screen is currently shown, then we // disable the "Open search view" button. 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 unserem Hauptansichtsmodell speichern wir nur ein Feld auf der Festplatte - das vom Typ RoutingState . Wir müssen den Status reaktiver Befehle nicht speichern, da ihre Verfügbarkeit vollständig vom aktuellen Status des Routers abhängt und sich reaktiv ändert. Um den Router in den genauen Zustand IRoutableViewModel zu können, in dem er sich befand, IRoutableViewModel wir bei der Serialisierung des Routers erweiterte IRoutableViewModel unserer IRoutableViewModel Implementierungen hinzu. Wir werden TypenameHandling.All Einstellungen von Newtonsoft.Json verwenden , um dies später zu erreichen. Wir legen das MainViewModel im Ordner ViewModels/ und passen den Namespace an ReactiveUI.Samples.Suspension.ViewModels .




Routing in einer Avalonia App


Im Moment haben wir das Präsentationsmodell unserer Anwendung implementiert. Später könnten die Ansichtsmodellklassen in eine separate Assembly extrahiert werden, die auf .NET Standard abzielt , sodass der Kernteil unserer App in mehreren .NET GUI-Frameworks wiederverwendet werden kann. Jetzt ist es Zeit, den Avalonia-spezifischen GUI-Teil unserer App zu implementieren. Wir erstellen zwei Dateien im Ordner Views/ mit den Namen SearchView.xaml und SearchView.xaml.cs . Dies sind die beiden Teile einer einzelnen Suchansicht - der erste ist die in XAML deklarativ beschriebene Benutzeroberfläche, und der zweite enthält C # -Code-Behind. Dies ist im Wesentlichen die Ansicht für das zuvor erstellte Suchansichtsmodell.


Der in Avalonia verwendete XAML-Dialekt sollte Entwicklern aus WPF, UWP oder XF sofort bekannt vorkommen. Im obigen Beispiel erstellen wir ein einfaches Layout mit einem Textfeld und einer Schaltfläche, die die Suche auslöst. Wir binden Eigenschaften und Befehle aus dem SearchViewModel an Steuerelemente, die in der SearchView .


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() { // The call to WhenActivated is used to execute a block of code // when the corresponding view model is activated and deactivated. this.WhenActivated((CompositeDisposable disposable) => { }); AvaloniaXamlLoader.Load(this); } } 

WPF- und UWP-Entwickler finden möglicherweise auch Code- SearchView.xaml für die SearchView.xaml Datei bekannt. Ein Aufruf von WhenActivated wird hinzugefügt, um die Aktivierungslogik für die Ansicht auszuführen. Das Einweg-Kommen als erstes Argument für WhenActivated wird entsorgt, wenn die Ansicht deaktiviert wird. Wenn Ihre Anwendung Hot Observables verwendet (z. B. Positionierungsdienste, Timer, Ereignisaggregatoren), ist es eine kluge Entscheidung, die Abonnements durch Hinzufügen eines DisposeWith Aufrufs an das WhenActivated Composite Disposable DisposeWith , damit die Ansicht diese Hot Observables und abbestellt Speicherlecks treten nicht auf.


Implementierungsdetails für die Anmeldeansicht

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

Wir bearbeiten die Dateien Views/MainView.xaml und Views/MainView.xaml.cs . Wir fügen das RoutedViewHost Steuerelement aus dem Avalonia.ReactiveUI Namespace zum XAML-Layout des MainViewModel und binden die Router Eigenschaft von MainViewModel an die RoutedViewHost.Router Eigenschaft. Wir fügen zwei Schaltflächen hinzu, eine öffnet die Suchseite und eine andere öffnet die Autorisierungsseite.


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> <!-- The RoutedViewHost XAML control observes the bound RoutingState. It subscribes to changes in the navigation stack and embedds the appropriate view for the currently selected view model. --> <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 Avalonia- und ReactiveUI- Routing-Demo-App ist jetzt verfügbar. Wenn ein Benutzer die Such- oder RoutingState drückt, wird ein Befehl aufgerufen, der die Navigation auslöst, und der RoutingState wird aktualisiert. Das RoutedViewHost XAML-Steuerelement RoutedViewHost den Routing-Status und versucht, die entsprechende IViewFor<TViewModel> Locator.Current aus Locator.Current . Wenn eine IViewFor<TViewModel> registriert ist, wird eine neue Instanz des Steuerelements erstellt und in das Avalonia-Fenster eingebettet.




Wir registrieren unsere IViewFor und IScreen Implementierungen in der App.OnFrameworkInitializationCompleted Methode mithilfe von Locator.CurrentMutable . Das Registrieren von IViewFor Implementierungen ist erforderlich, damit das RoutedViewHost Steuerelement funktioniert. Durch das Registrieren eines IScreen können unser SearchViewModel und LoginViewModel während der Deserialisierung mithilfe des parameterlosen Konstruktors die Eigenschaften initialisieren.


App.xaml.cs


 public override void OnFrameworkInitializationCompleted() { // Here we register our view models. Locator.CurrentMutable.RegisterConstant<IScreen>(new MainViewModel()); Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); // Here we resolve the root view model and initialize main view data context. new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.OnFrameworkInitializationCompleted(); } 

Lassen Sie uns unsere Anwendung starten und sicherstellen, dass das Routing ordnungsgemäß funktioniert. Wenn beim XAML-UI-Markup etwas schief geht, benachrichtigt uns der Avalonia XamlIl-Compiler beim Kompilieren über Fehler. Darüber hinaus unterstützt XamlIl das Debuggen von XAML !


 dotnet run --framework netcoreapp3.0 



Speichern und Wiederherstellen des Anwendungsstatus


Jetzt ist es an der Zeit, den Suspension-Treiber zu implementieren, der für das Speichern und Wiederherstellen des App-Status verantwortlich ist, wenn die App angehalten und fortgesetzt wird. Die plattformspezifische AutoSuspendHelper Klasse kümmert sich um die Initialisierung. Sie als Entwickler müssen lediglich eine Instanz davon im Stammverzeichnis der App-Komposition erstellen. Außerdem müssen Sie die RxApp.SuspensionHost.CreateNewAppState Factory initialisieren. Wenn die App keine gespeicherten Daten hat oder wenn die gespeicherten Daten beschädigt sind, ruft ReactiveUI diese Factory-Methode auf, um eine Standardinstanz des Anwendungsstatusobjekts zu erstellen.


Anschließend rufen wir die Methode RxApp.SuspensionHost.SetupDefaultSuspendResume und übergeben ihr eine neue Instanz von ISuspensionDriver . Lassen Sie uns die ISuspensionDriver mit Newtonsoft.Json und Klassen aus dem System.IO Namespace System.IO .


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

Der beschriebene Ansatz hat einen Nachteil: Einige System.IO Klassen funktionieren nicht mit dem UWP-Framework, UWP-Apps werden in einer Sandbox ausgeführt und funktionieren anders. Das ist ziemlich einfach zu lösen - alles, was Sie tun müssen, ist, StorageFile und StorageFolder Klassen anstelle von File und Directory wenn Sie auf UWP abzielen. Um den Navigationsstapel von der Festplatte zu lesen, sollte ein Suspensionstreiber das Deserialisieren von JSON-Objekten in konkrete IRoutableViewModel Implementierungen unterstützen. Aus diesem Grund verwenden wir die Serializer-Einstellung TypeNameHandling.All Newtonsoft.Json. Wir registrieren den Suspension-Treiber im App-Kompositionsstamm in der App.OnFrameworkInitializationCompleted Methode:


 public override void OnFrameworkInitializationCompleted() { // Initialize suspension hooks. var suspension = new AutoSuspendHelper(ApplicationLifetime); RxApp.SuspensionHost.CreateNewAppState = () => new MainViewModel(); RxApp.SuspensionHost.SetupDefaultSuspendResume(new NewtonsoftJsonSuspensionDriver("appstate.json")); suspension.OnFrameworkInitializationCompleted(); // Read main view model state from disk. var state = RxApp.SuspensionHost.GetAppState<MainViewModel>(); Locator.CurrentMutable.RegisterConstant<IScreen>(state); // Register views. Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); // Show the main window. new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.OnFrameworkInitializationCompleted(); } 

Die AutoSuspendHelper Klasse aus dem Avalonia.ReactiveUI Paket richtet Lifecycle-Hooks für Ihre Anwendung ein, sodass das ReactiveUI-Framework mithilfe der bereitgestellten ISuspensionDriver Implementierung weiß, wann der Anwendungsstatus auf die Festplatte geschrieben werden ISuspensionDriver . Nach dem Start unserer Anwendung erstellt der Suspension-Treiber eine neue JSON-Datei mit dem Namen appstate.json . Nachdem wir Änderungen an der Benutzeroberfläche vorgenommen haben (z. B. etwas in die Textfelder appstate.json oder auf eine beliebige Schaltfläche klicken) und dann die App schließen, appstate.json Datei appstate.json wie folgt aus:


appstate.json

Beachten Sie, dass jedes JSON-Objekt in der Datei einen Schlüssel vom $type mit einem vollständig qualifizierten Typnamen einschließlich Namespace enthält.


 { "$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" } ] } } } 

Wenn Sie die App schließen und dann erneut starten, wird auf dem Bildschirm derselbe Inhalt angezeigt, den Sie zuvor gesehen haben! Die beschriebene Funktionalität funktioniert auf jeder von ReactiveUI unterstützten Plattform, einschließlich UWP, WPF, Xamarin Forms oder Xamarin Native.


Bild


Bonus: Die ISuspensionDriver Oberfläche kann mit Akavache implementiert werden - einem asynchronen, dauerhaften Schlüsselwertspeicher. Wenn Sie Ihre Daten entweder im Bereich UserAccount oder im Bereich Secure speichern, werden Ihre Daten unter iOS und UWP automatisch in der Cloud gesichert und sind auf allen Geräten verfügbar, auf denen die App installiert ist. Außerdem ist im Paket ReactiveUI.AndroidSupport ein BundleSuspensionDriver vorhanden. Xamarin.Essentials SecureStorage-APIs können auch zum Speichern von Daten verwendet werden. Sie können Ihren App-Status auch auf einem Remote-Server oder in einem plattformunabhängigen Cloud-Dienst speichern.



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


All Articles