Navigation dans une application multiplateforme .NET Core avec enregistrement de l'état sur le disque en utilisant l'exemple de ReactiveUI et Avalonia



Les interfaces utilisateur des applications modernes sont généralement complexes - il est souvent nécessaire de mettre en œuvre la prise en charge de la navigation de page, de traiter des champs de saisie de différents types et d'afficher ou de masquer des informations en fonction des paramètres sélectionnés par l'utilisateur. Dans le même temps, afin d'améliorer l'UX, l'application doit enregistrer l'état des éléments d'interface sur le disque pendant la suspension ou l'arrêt, restaurer l'état à partir du disque lorsque le programme est redémarré.


Le framework ReactiveUI MVVM propose de conserver l'état d'une application en sérialisant le graphe des modèles de présentation au moment où le programme est suspendu, tandis que les mécanismes de détermination du moment de suspension sont différents pour les frameworks et les plateformes. Ainsi, pour WPF, l'événement Exit est utilisé, pour Xamarin.Android - ActivityPaused , pour Xamarin.iOS - DidEnterBackground , pour UWP - OnLaunched surcharge.


Dans cet article, nous considérerons l'utilisation de ReactiveUI pour enregistrer et restaurer l'état du logiciel avec une interface graphique, y compris l'état d'un routeur, en utilisant le cadre d'interface graphique multiplateforme Avalonia comme exemple . Le matériel suppose une compréhension de base du modèle de conception MVVM et de la programmation réactive dans le contexte du langage C # et de la plate-forme .NET pour le lecteur. Les étapes de cet article s'appliquent à Windows 10 et Ubuntu 18.


Création de projet


Pour essayer le routage en action, créez un nouveau projet .NET Core à partir du modèle Avalonia, installez le package Avalonia.ReactiveUI - une fine couche d'intégration Avalonia et ReactiveUI. Assurez-vous que le SDK .NET Core et git sont installés avant de commencer.


 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 

Assurez-vous que l'application démarre et affiche une fenêtre qui dit Bienvenue à Avalonia!


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



Connecter les pré-builds Avalonia de MyGet


Pour vous connecter et utiliser les dernières versions d'Avalonia qui sont automatiquement publiées sur MyGet lorsque la branche master référentiel Avalonia sur GitHub change, nous utilisons le fichier de configuration source du package nuget.config . Pour que l'IDE et l' interface CLI .NET Core voient nuget.config , vous devez générer un fichier sln pour le projet créé ci-dessus. Nous utilisons les outils de la CLI .NET Core:


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

Créez un fichier nuget.config dans un dossier avec un fichier .sln du 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> 

Vous devrez peut-être redémarrer l'IDE ou décharger et télécharger la solution entière. Nous mettrons à jour les packages Avalonia vers la version requise (au moins 0.9.1 ) en utilisant l'interface du gestionnaire de packages NuGet de votre IDE, ou en utilisant les outils de ligne de commande Windows ou le terminal Linux:


 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 

ReactiveUI.Samples.Suspension.csproj fichier de projet ReactiveUI.Samples.Suspension.csproj ressemble maintenant à 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> 

Créez les dossiers Views/ et ViewModels/ dans la racine du projet, changez le nom de la classe MainWindow , déplacez-le dans le répertoire Views/ , en changeant les espaces de noms en conséquence en ReactiveUI.Samples.Suspension.Views . App.xaml.cs contenu des App.xaml.cs et App.xaml.cs - appliquez l'appel UseReactiveUI au générateur d'application Avalonia, déplacez l'initialisation de la vue principale vers OnFrameworkInitializationCompleted pour respecter les recommandations de gestion du cycle de vie de l' application:


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

Vous devrez ajouter à l' using Avalonia.ReactiveUI à Program.cs . Assurez-vous qu'après la mise à jour des packages, le projet démarre et affiche la fenêtre de bienvenue par défaut.


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



Routage multiplateforme ReactiveUI


En règle générale, il existe deux approches principales pour implémenter la navigation entre les pages d'une application .NET: la vue en premier et la vue en premier. L'approche View-first implique de contrôler la pile de navigation et la navigation entre les pages au niveau View dans la terminologie MVVM - par exemple, en utilisant les classes Frame et Page dans le cas d'UWP ou WPF, et lorsque vous utilisez l'approche view model-first, la navigation est implémentée au niveau des modèles de présentation. Les outils ReactiveUI qui organisent le routage dans l'application se concentrent sur l'utilisation de l'approche du modèle de vue en premier. Le routage ReactiveUI consiste en une implémentation IScreen contenant l'état du routeur, plusieurs implémentations IRoutableViewModel et le contrôle XAML RoutedViewHost la plate-forme, RoutedViewHost .




L'état du routeur est représenté par un objet RoutingState qui contrôle la pile de navigation. IScreen est la racine de la pile de navigation et il peut y avoir plusieurs racines de navigation dans l'application. RoutedViewHost surveille l'état de son routeur RoutingState correspondant, répondant aux changements dans la pile de navigation en incorporant le IRoutableViewModel correspondant du contrôle XAML. La fonctionnalité décrite sera illustrée par des exemples ci-dessous.


Enregistrement des modèles d'état de vue sur le disque


Prenons un exemple typique d'un écran de recherche d'informations.




Nous devons décider quels éléments du modèle de représentation d'écran enregistrer sur le disque pendant la pause ou l'arrêt de l'application, et quels éléments recréer à chaque démarrage. Il n'est pas nécessaire d'enregistrer l'état des commandes ReactiveUI qui implémentent l'interface ICommand et sont attachées aux boutons - ReactiveCommand<TIn, TOut> sont créés et initialisés dans le constructeur, tandis que l'état de l'indicateur CanExecute dépend des propriétés du modèle de vue et est recalculé lorsqu'ils changent. La nécessité de sauvegarder les résultats de la recherche - un point discutable - dépend des spécificités de l'application, mais il serait sage de sauvegarder et de restaurer l'état du champ de saisie SearchQuery !


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

La classe du modèle de vue est marquée avec l'attribut [DataContract] et les propriétés qui doivent être sérialisées avec les [DataMember] . Cela suffit si le sérialiseur utilisé utilise l'approche opt-in - il enregistre uniquement les propriétés qui sont explicitement marquées avec des attributs sur le disque; dans le cas de l'approche opt-out, il est nécessaire de marquer avec les attributs [IgnoreDataMember] les propriétés qui n'ont pas besoin d'être enregistrées sur le disque. De plus, nous implémentons l'interface IRoutableViewModel dans notre modèle de vue afin qu'elle puisse plus tard faire partie du cadre de navigation du routeur d'application.


De même, nous implémentons le modèle de présentation des pages d'autorisation

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

Les modèles de présentation pour deux pages de l'application sont prêts, implémentent l'interface IRoutableViewModel et peuvent être intégrés au routeur IScreen . Maintenant, nous implémentons directement IScreen . Nous marquons à l'aide des attributs [DataContract] propriétés du modèle de vue à sérialiser et celles à ignorer. Faites attention au setter public de la propriété marquée avec l' [DataMember] , dans l'exemple ci-dessous - la propriété est intentionnellement ouverte pour l'écriture afin que le sérialiseur puisse modifier une instance fraîchement créée de l'objet pendant la désérialisation du modèle.


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

Dans notre application, seul RoutingState doit être enregistré sur le disque; pour des raisons évidentes, les commandes n'ont pas besoin d'être enregistrées sur le disque - leur état dépend entièrement du routeur. L'objet sérialisé doit inclure des informations étendues sur les types qui implémentent IRoutableViewModel afin que la pile de navigation puisse être restaurée lors de la désérialisation. Nous décrivons la logique du MainViewModel vue ViewModels/MainViewModel.cs la classe dans ViewModels/MainViewModel.cs et dans l'espace de noms ReactiveUI.Samples.Suspension.ViewModels correspondant.




Routage dans l'application Avalonia


La logique de l'interface utilisateur au niveau des couches du modèle et du modèle de présentation de l'application de démonstration est implémentée et peut être déplacée vers un assemblage distinct destiné à la norme .NET, car elle ne sait rien du cadre GUI utilisé. Jetons un coup d'œil à la couche de présentation. Dans la terminologie MVVM, la couche de présentation est responsable du rendu de l'état du modèle de présentation à l'écran. Pour rendre l'état actuel du routeur RoutingState , le contrôle XAML RoutedViewHost contenu dans le package Avalonia.ReactiveUI est Avalonia.ReactiveUI . Nous implémentons l'interface graphique pour SearchViewModel - pour cela, dans le répertoire Views/ , créez deux fichiers: SearchView.xaml et SearchView.xaml.cs .


Une description de l'interface utilisateur utilisant le dialecte XAML utilisé dans Avalonia est susceptible de sembler familière aux développeurs sur Windows Presentation Foundation, Universal Windows Platform ou Xamarin.Forms. Dans l'exemple ci-dessus, nous créons une interface triviale pour le formulaire de recherche - nous dessinons un champ de texte pour entrer la requête de recherche et un bouton qui démarre la recherche, tandis que nous lions les contrôles aux propriétés du modèle SearchViewModel défini ci-dessus.


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() { //  WhenActivated     //        . this.WhenActivated((CompositeDisposable disposable) => { }); AvaloniaXamlLoader.Load(this); } } 

Le code-behind du contrôle SearchView.xaml apparaîtra également aux développeurs WPF, UWP et XF familiers. L'appel WhenActivated est utilisé pour exécuter du code lorsque la vue ou le modèle de vue est activé et désactivé. Si votre application utilise des observables à chaud (minuteries, géolocalisation, connexion au bus de messages), il serait judicieux de les attacher au CompositeDisposable appelant DisposeWith afin que lorsque vous DisposeWith contrôle XAML et son modèle de vue correspondant de l'arborescence visuelle, les observables à chaud cessent de publier de nouvelles valeurs et il n'y a pas de fuite. mémoire.


De même, nous implémentons la représentation de la page 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); } } 

Modifiez les Views/MainView.xaml.cs Views/MainView.xaml et Views/MainView.xaml.cs . RoutedViewHost XAML RoutedViewHost partir de l'espace de noms RoutedViewHost sur l'écran principal, affectez l'état du routeur RoutingState à la propriété RoutingState . Ajoutez des boutons pour accéder aux pages de recherche et d'autorisation, associez-les aux propriétés ViewModels/MainViewModel décrites ci-dessus.


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

Vues / MainView.xaml.cs


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

Une application simple démontrant les capacités de routage ReactiveUI et Avalonia est prête. Lorsque vous cliquez sur les boutons Search et Login , les commandes correspondantes sont appelées, une nouvelle instance du modèle de vue est créée et le RoutingState mis à jour. Le contrôle XAML RoutedViewHost , qui souscrit aux modifications apportées à RoutingState , essaie d'obtenir le type IViewFor<TViewModel> , où TViewModel est le type de modèle de vue de Locator.Current . Si une implémentation enregistrée d' IViewFor<TViewModel> trouvée, une nouvelle instance sera créée, intégrée à RoutedViewHost et affichée dans la fenêtre de l'application Avalonia.




Nous enregistrons les composants nécessaires IViewFor<TViewModel> et IScreen dans la méthode App.OnFrameworkInitializationCompleted de notre application en utilisant Locator.CurrentMutable . IViewFor<TViewModel> nécessaire pour que RoutedViewHost fonctionne RoutedViewHost , et l'enregistrement IScreen nécessaire pour que lors de la désérialisation, les LoginViewModel SearchViewModel et LoginViewModel puissent être correctement initialisés à l'aide du constructeur sans paramètres et 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(); } 

Exécutez l'application et assurez-vous que le routage fonctionne correctement. S'il y a des erreurs dans le balisage XAML, le compilateur XamlIl utilisé dans Avalonia nous dira exactement où, au stade de la compilation. XamlIl prend également en charge le débogage XAML directement dans le débogueur IDE !


 dotnet run --framework netcoreapp3.0 



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


Maintenant que le routage est configuré et fonctionne, la partie la plus intéressante commence - vous devez implémenter l'enregistrement des données sur le disque lorsque vous fermez l'application et la lecture des données du disque au démarrage, ainsi que l'état du routeur. L'initialisation des hooks qui écoutent les événements de démarrage et de fermeture de l'application est gérée par une classe AutoSuspendHelper spéciale, AutoSuspendHelper à chaque plate-forme prise en charge par ReactiveUI . La tâche du développeur est d'initialiser cette classe au tout début de la racine de composition d'application. Il est également nécessaire d'initialiser la propriété RxApp.SuspensionHost.CreateNewAppState fonction qui renverra l'état par défaut de l'application s'il n'y a aucun état enregistré ou si une erreur inattendue s'est produite ou si le fichier enregistré est endommagé.


Ensuite, vous devez appeler la méthode RxApp.SuspensionHost.SetupDefaultSuspendResume , en lui passant l'implémentation d' ISuspensionDriver , le pilote qui ISuspensionDriver et lit l'objet d'état. Pour implémenter ISuspensionDriver , ISuspensionDriver utilisons la bibliothèque Newtonsoft.Json et l'espace de noms System.IO pour travailler avec le système de fichiers. Pour ce faire, installez le package Newtonsoft.Json :


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

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

Veuillez noter que chaque objet comprend un champ $typecontenant des informations sur le nom complet du type et le nom complet de l'assembly dans lequel se trouve le 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 . , — !


Liens utiles


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


All Articles