Enregistrement de l'état de routage sur le disque dans une application GUI .NET Core multiplateforme avec ReactiveUI et Avalonia

image


Les interfaces utilisateur des applications d'entreprise modernes sont assez complexes. En tant que développeur, vous devez souvent implémenter la navigation dans l'application, valider l'entrée utilisateur, afficher ou masquer les écrans en fonction des préférences de l'utilisateur. Pour une meilleure expérience utilisateur, votre application doit être capable d'enregistrer l'état sur le disque lorsque l'application est suspendue et de restaurer l'état lorsque l'application reprend.


ReactiveUI fournit des fonctionnalités vous permettant de conserver l'état de l'application en sérialisant l'arborescence du modèle de vue lorsque l'application s'arrête ou se suspend. Les événements de suspension varient selon la plate-forme. ReactiveUI utilise l'événement Exit pour WPF, ActivityPaused pour Xamarin.Android, DidEnterBackground pour Xamarin.iOS, OnLaunched pour UWP.


Dans ce didacticiel, nous allons créer un exemple d'application qui montre l'utilisation de la fonction de suspension ReactiveUI avec Avalonia - une infrastructure d'interface graphique multiplateforme basée sur .NET Core XAML. Vous êtes censé être familier avec le modèle MVVM et avec les extensions réactives avant de lire cette note. Les étapes décrites dans le didacticiel devraient fonctionner si vous utilisez Windows 10 ou Ubuntu 18 et que le SDK .NET Core est installé. Commençons!


Démarrage du projet


Pour voir le routage ReactiveUI en action, nous créons un nouveau projet .NET Core basé sur les modèles d'application Avalonia. Ensuite, nous installons le package Avalonia.ReactiveUI . Le package fournit des crochets de cycle de vie Avalonia spécifiques à la plate-forme, une infrastructure de routage et d' activation . N'oubliez pas d'installer .NET Core et git avant d'exécuter les commandes ci-dessous.


 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 

Lançons l'application et assurons qu'elle affiche une fenêtre affichant "Bienvenue en Avalonia!"


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

image


Installation d'Avalonia Preview Builds à partir de MyGet


Les derniers packages Avalonia sont publiés sur MyGet chaque fois qu'un nouveau commit est poussé vers la branche principale du référentiel Avalonia sur GitHub. Pour utiliser les derniers packages de MyGet dans notre application, nous allons créer un fichier nuget.config . Mais avant de faire cela, nous générons un fichier sln pour le projet créé précédemment, en utilisant .NET Core CLI :


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

Nous créons nuget.config fichier nuget.config avec le contenu suivant:


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

Habituellement, un redémarrage est nécessaire pour forcer notre IDE à détecter les packages du flux MyGet nouvellement ajouté, mais le rechargement de la solution devrait également aider. Ensuite, nous mettons à niveau les packages Avalonia vers la version la plus récente (ou au moins vers 0.9.1 ) à l'aide de l'interface graphique du gestionnaire de packages NuGet ou de la CLI .NET Core:


 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 

Le fichier ReactiveUI.Samples.Suspension.csproj devrait maintenant ressembler à ceci:


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

Nous créons deux nouveaux dossiers dans le répertoire racine du projet, nommés Views/ et ViewModels/ respectivement. Ensuite, nous MainWindow classe MainWindow et la déplaçons dans le dossier Views/ . N'oubliez pas de renommer les références à la classe modifiée dans le fichier XAML correspondant, sinon, le projet ne sera pas compilé. N'oubliez pas non plus de changer l'espace de noms de MainView en ReactiveUI.Samples.Suspension.Views pour des ReactiveUI.Samples.Suspension.Views de cohérence. Ensuite, nous App.xaml.cs deux autres fichiers, App.xaml.cs et App.xaml.cs Nous ajoutons un appel à UseReactiveUI au générateur d'application Avalonia, déplaçons le code d'initialisation de l'application vers la méthode OnFrameworkInitializationCompleted pour se conformer aux directives de gestion de la durée de vie des applications Avalonia:


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

Avant d'essayer de générer le projet, nous nous assurons que la directive using Avalonia.ReactiveUI est ajoutée en haut du fichier Program.cs . Notre IDE a probablement déjà importé cet espace de noms, mais si ce n'est pas le cas, nous obtiendrons une erreur au moment de la compilation. Enfin, il est temps de s'assurer que l'application se compile, s'exécute et affiche une nouvelle fenêtre:


 dotnet run --framework netcoreapp2.1 

image


Routage ReactiveUI multiplateforme


Il existe deux approches générales d'organisation de la navigation dans l'application dans une application .NET multiplateforme: la vue en premier et la vue en premier. La première approche suppose que la couche View gère la pile de navigation - par exemple, en utilisant des classes Frame et Page spécifiques à la plate-forme. Avec cette dernière approche, la couche de modèle de vue prend en charge la navigation via une abstraction indépendante de la plate-forme. L'outil ReactiveUI est construit en gardant à l'esprit l'approche du modèle de vue. Le routage ReactiveUI se compose d'une implémentation IScreen , qui contient l'état de routage actuel, plusieurs implémentations IRoutableViewModel et un contrôle XAML spécifique à la plate-forme appelé RoutedViewHost .




L'objet RoutingState encapsule la gestion de la pile de navigation. IScreen est la racine de navigation, mais malgré son nom, il n'a pas à occuper tout l'écran. RoutedViewHost réagit aux modifications de RoutingState lié et incorpore la vue appropriée pour le IRoutableViewModel actuellement sélectionné. La fonctionnalité décrite sera illustrée par des exemples plus complets ultérieurement.


État du modèle de vue persistante


Prenons l'exemple d'un modèle de vue d'écran de recherche.




Nous allons décider quelles propriétés du modèle de vue enregistrer lors de l'arrêt de l'application et lesquelles recréer. Il n'est pas nécessaire de sauvegarder l'état d'une commande réactive qui implémente l'interface ICommand . ReactiveCommand<TIn, TOut> est généralement initialisée dans le constructeur, son indicateur CanExecute dépend généralement entièrement des propriétés du modèle de vue et est recalculé chaque fois que l'une de ces propriétés change. C'est discutable si vous deviez conserver les résultats de la recherche, mais enregistrer la requête de recherche est une bonne idée.


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

Nous marquons la classe de modèle de vue entière avec l'attribut [DataContract] , [DataContract] propriétés que nous allons sérialiser avec l'attribut [DataMember] . Cela suffit si nous allons utiliser le mode de sérialisation opt-in. En ce qui concerne les modes de sérialisation, l'opt-out signifie que tous les champs et propriétés publics seront sérialisés, à moins que vous ne les [IgnoreDataMember] explicitement en annotant avec l'attribut [IgnoreDataMember] , opt-in signifie le contraire. De plus, nous implémentons l'interface IRoutableViewModel dans notre classe de modèle de vue. Ceci est nécessaire pendant que nous allons utiliser le modèle de vue en tant que partie d'une pile de navigation.


Détails d'implémentation du modèle de vue de connexion

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

Les deux modèles de vue implémentent l'interface IRoutableViewModel et sont prêts à être intégrés dans un écran de navigation. Il est maintenant temps de mettre en œuvre l'interface IScreen . Encore une fois, nous utilisons les attributs [DataContract] pour indiquer les parties à sérialiser et celles à ignorer. Dans l'exemple ci-dessous, le RoutingState propriétés RoutingState est délibérément déclaré public - cela permet à notre sérialiseur de modifier la propriété lorsqu'elle est désérialisée.


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

Dans notre modèle de vue principal, nous enregistrons un seul champ sur le disque - celui de type RoutingState . Nous n'avons pas à enregistrer l'état des commandes réactives, car leur disponibilité dépend entièrement de l'état actuel du routeur et change de manière réactive. Pour pouvoir restaurer le routeur dans son état exact, nous incluons des informations de type étendues de nos implémentations IRoutableViewModel lors de la sérialisation du routeur. Nous utiliserons le paramètre TypenameHandling.All de Newtonsoft.Json pour y parvenir plus tard. Nous plaçons le MainViewModel dans le dossier ViewModels/ , ajustons l'espace de noms pour qu'il soit ReactiveUI.Samples.Suspension.ViewModels .




Routage dans une application Avalonia


Pour l'instant, nous avons implémenté le modèle de présentation de notre application. Plus tard, les classes de modèle de vue pourraient être extraites dans un assemblage distinct ciblant .NET Standard , de sorte que la partie centrale de notre application pourrait être réutilisée dans plusieurs infrastructures d'interface graphique .NET. Il est maintenant temps de mettre en œuvre la partie GUI spécifique à Avalonia de notre application. Nous créons deux fichiers dans le dossier Views/ , nommés SearchView.xaml et SearchView.xaml.cs respectivement. Ce sont les deux parties d'une vue de recherche unique - la première est l'interface utilisateur décrite de manière déclarative en XAML, et la dernière contient du code C #. Il s'agit essentiellement de la vue du modèle de vue de recherche créée précédemment.


Le dialecte XAML utilisé dans Avalonia devrait être immédiatement familier aux développeurs issus de WPF, UWP ou XF. Dans l'exemple ci-dessus, nous créons une mise en page simple contenant une zone de texte et un bouton qui déclenche la recherche. Nous SearchViewModel les propriétés et les commandes du SearchViewModel aux contrôles déclarés dans le SearchView .


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

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

Les développeurs WPF et UWP peuvent également trouver du code derrière le fichier SearchView.xaml . Un appel à WhenActivated est ajouté pour exécuter la logique d'activation de la vue. L' WhenActivated jetable venant comme premier argument pour WhenActivated est supprimé lorsque la vue est désactivée. Si votre application utilise des observables à chaud (par exemple, des services de positionnement, des minuteries, des agrégateurs d'événements), il serait judicieux d'attacher les abonnements au jetable composite WhenActivated en ajoutant un appel DisposeWith , afin que la vue se désabonne de ces observables à chaud et les fuites de mémoire n'auront pas lieu.


Détails d'implémentation pour la vue de connexion

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

Vues / LoginView.xaml.cs


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

Nous Views/MainView.xaml.cs fichiers Views/MainView.xaml et Views/MainView.xaml.cs . Nous ajoutons le contrôle RoutedViewHost de l'espace de noms Avalonia.ReactiveUI à la disposition XAML de la fenêtre principale et RoutedViewHost.Router propriété Router de MainViewModel à la propriété RoutedViewHost.Router . Nous ajoutons deux boutons, l'un ouvre la page de recherche et l'autre ouvre la page d'autorisation.


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

Vues / MainView.xaml.cs


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

Une simple application de démonstration de routage Avalonia et ReactiveUI est prête maintenant. Lorsqu'un utilisateur appuie sur les boutons de recherche ou de connexion, une commande qui déclenche la navigation est invoquée et le RoutingState est mis à jour. Le contrôle RoutedViewHost XAML observe l'état de routage et tente de résoudre l' IViewFor<TViewModel> Locator.Current partir de Locator.Current . Si une IViewFor<TViewModel> est enregistrée, une nouvelle instance du contrôle est créée et incorporée dans la fenêtre Avalonia.




Nous enregistrons nos IViewFor et IScreen dans la méthode App.OnFrameworkInitializationCompleted , à l'aide de Locator.CurrentMutable . L'enregistrement des implémentations IViewFor est requis pour RoutedViewHost contrôle RoutedViewHost fonctionne. L'enregistrement d'un IScreen permet à nos SearchViewModel et LoginViewModel de s'initialiser lors de la désérialisation, en utilisant le constructeur sans paramètre.


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

Lançons notre application et assurons que le routage fonctionne comme il se doit. En cas de problème avec le balisage de l'interface utilisateur XAML, le compilateur Avalonia XamlIl nous informera de toute erreur au moment de la compilation. De plus, XamlIl prend en charge le débogage de XAML !


 dotnet run --framework netcoreapp3.0 



Enregistrement et restauration de l'état de l'application


Il est maintenant temps d'implémenter le pilote de suspension responsable de l'enregistrement et de la restauration de l'état de l'application lorsque l'application est suspendue et reprend. La classe AutoSuspendHelper spécifique à la plate-forme s'occupe d'initialiser les choses, vous, en tant que développeur, n'avez qu'à en créer une instance dans la racine de composition de l'application. De plus, vous devez initialiser la fabrique RxApp.SuspensionHost.CreateNewAppState . Si l'application n'a pas de données enregistrées ou si les données enregistrées sont corrompues, ReactiveUI appelle cette méthode d'usine pour créer une instance par défaut de l'objet d'état de l'application.


Ensuite, nous RxApp.SuspensionHost.SetupDefaultSuspendResume méthode RxApp.SuspensionHost.SetupDefaultSuspendResume et lui transmettons une nouvelle instance de ISuspensionDriver . ISuspensionDriver interface du ISuspensionDriver aide de Newtonsoft.Json et des classes de l'espace de noms System.IO .


 dotnet add package Newtonsoft.Json 

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

L'approche décrite a un inconvénient - certaines classes System.IO ne fonctionneront pas avec le cadre UWP, les applications UWP s'exécutent dans un bac à sable et font les choses différemment. C'est assez facile à résoudre - tout ce que vous devez faire est d'utiliser les classes StorageFile et StorageFolder au lieu de File and Directory lors du ciblage UWP. Pour lire la pile de navigation à partir du disque, un pilote de suspension doit prendre en charge la désérialisation des objets JSON en implémentations IRoutableViewModel concrètes, c'est pourquoi nous utilisons le paramètre de sérialisation TypeNameHandling.All Newtonsoft.Json. Nous enregistrons le pilote de suspension dans la racine de la composition de l'application, dans la méthode App.OnFrameworkInitializationCompleted :


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

La classe AutoSuspendHelper du package Avalonia.ReactiveUI configure des hooks de cycle de vie pour votre application, de sorte que le framework ReactiveUI sait quand écrire l'état de l'application sur le disque, à l'aide de l'implémentation ISuspensionDriver fournie. Après avoir lancé notre application, le pilote de suspension créera un nouveau fichier JSON nommé appstate.json . Après avoir apporté des modifications à l'interface utilisateur (par exemple, tapez un peu dans les champs de texte ou cliquez sur n'importe quel bouton), puis fermez l'application, le fichier appstate.json ressemblera à ce qui suit:


appstate.json

Notez que chaque objet JSON du fichier contient une clé $type avec un nom de type complet, y compris l'espace de noms.


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

Si vous fermez l'application puis la relancez, vous verrez le même contenu à l'écran que vous avez vu auparavant! La fonctionnalité décrite fonctionne sur chaque plate-forme prise en charge par ReactiveUI , y compris UWP, WPF, Xamarin Forms ou Xamarin Native.


image


Bonus: l' interface du ISuspensionDriver peut être implémentée à l'aide d' Akavache - un magasin de valeurs-clés asynchrone et persistant. Si vous stockez vos données dans la section UserAccount ou la section Secure , alors sur iOS et UWP vos données seront sauvegardées automatiquement dans le cloud et seront disponibles sur tous les appareils sur lesquels l'application est installée. En outre, un BundleSuspensionDriver existe dans le package ReactiveUI.AndroidSupport . Les API Xamarin.Essentials SecureStorage peuvent également être utilisées pour stocker des données. Vous pouvez également stocker l'état de votre application sur un serveur distant ou dans un service cloud indépendant de la plateforme.



  • Le code source de l'application décrite est disponible sur GitHub .
  • Voir la documentation Avalonia pour commencer à apprendre le framework.
  • Lisez le manuel ReactiveUI pour découvrir plus de fonctionnalités impressionnantes.

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


All Articles