Guardar el estado de enrutamiento en el disco en una aplicación GUI multiplataforma .NET Core con ReactiveUI y Avalonia

imagen


Las interfaces de usuario de las aplicaciones empresariales modernas son bastante complejas. Usted, como desarrollador, a menudo necesita implementar la navegación en la aplicación, validar la entrada del usuario, mostrar u ocultar pantallas según las preferencias del usuario. Para una mejor experiencia de usuario, su aplicación debe ser capaz de guardar el estado en el disco cuando la aplicación se suspende y restaurar el estado cuando se reanuda.


ReactiveUI proporciona funciones que le permiten mantener el estado de la aplicación al serializar el árbol del modelo de vista cuando la aplicación se está cerrando o suspendiendo. Los eventos de suspensión varían según la plataforma. ReactiveUI usa el evento Exit para WPF, ActivityPaused para Xamarin.Android, DidEnterBackground para Xamarin.iOS, OnLaunched para UWP.


En este tutorial vamos a construir una aplicación de muestra que demuestre el uso de la función de suspensión ReactiveUI con Avalonia , un marco de GUI multiplataforma basado en .NET Core basado en XAML. Se espera que esté familiarizado con el patrón MVVM y con las extensiones reactivas antes de leer esta nota. Los pasos descritos en el tutorial deberían funcionar si está utilizando Windows 10 o Ubuntu 18 y tiene instalado .NET Core SDK. ¡Empecemos!


Bootstrapping el proyecto


Para ver el enrutamiento ReactiveUI en acción, creamos un nuevo proyecto .NET Core basado en plantillas de aplicación Avalonia. Luego instalamos el paquete Avalonia.ReactiveUI . El paquete proporciona enganches de ciclo de vida, enrutamiento y activación de Avalonia específicos de la plataforma. Recuerde instalar .NET Core y git antes de ejecutar los siguientes comandos.


 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 

Ejecutemos la aplicación y asegúrese de que muestre una ventana que muestre "¡Bienvenido a Avalonia!"


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

imagen


Instalación de compilaciones de vista previa de Avalonia desde MyGet


Los últimos paquetes de Avalonia se publican en MyGet cada vez que se envía una nueva confirmación a la rama master del repositorio de Avalonia en GitHub. Para usar los últimos paquetes de MyGet en nuestra aplicación, vamos a crear un archivo nuget.config . Pero antes de hacer esto, generamos un archivo sln para el proyecto creado anteriormente, usando .NET Core CLI :


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

Ahora creamos el archivo nuget.config con el siguiente contenido:


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

Por lo general, se requiere un reinicio para forzar a nuestro IDE a detectar paquetes de la fuente MyGet recién agregada, pero la recarga de la solución también debería ayudar. Luego, actualizamos los paquetes de Avalonia a la versión más reciente (o al menos a 0.9.1 ) usando la GUI del administrador de paquetes NuGet o la CLI de .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 

El archivo ReactiveUI.Samples.Suspension.csproj debería verse algo así como ahora:


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

Creamos dos carpetas nuevas dentro del directorio raíz del proyecto, denominadas Views/ y ViewModels/ respectivamente. A continuación, cambiamos el nombre de la clase MainWindow a MainView y la movemos a la carpeta Views/ . Recuerde cambiar el nombre de las referencias a la clase editada en el archivo XAML correspondiente; de ​​lo contrario, el proyecto no se compilará. Además, recuerde cambiar el espacio de nombres para MainView a ReactiveUI.Samples.Suspension.Views para MainView coherencia. Luego, editamos otros dos archivos, Program.cs y App.xaml.cs UseReactiveUI una llamada a UseReactiveUI al UseReactiveUI de la aplicación Avalonia, mueve el código de inicialización de la aplicación al método OnFrameworkInitializationCompleted para cumplir con las pautas de administración de por vida de la aplicación 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(); } } 

Antes de intentar compilar el proyecto, nos aseguramos de que la directiva que using Avalonia.ReactiveUI se agregue al principio del archivo Program.cs . Lo más probable es que nuestro IDE ya haya importado ese espacio de nombres, pero si no lo hizo, obtendremos un error en tiempo de compilación. Finalmente, es hora de asegurarse de que la aplicación compila, se ejecuta y muestra una nueva ventana:


 dotnet run --framework netcoreapp2.1 

imagen


Enrutamiento UI reactivo multiplataforma


Existen dos enfoques generales para organizar la navegación dentro de la aplicación en una aplicación .NET multiplataforma: ver primero y ver primero el modelo. El primer enfoque supone que la capa de Vista administra la pila de navegación, por ejemplo, usando clases de Marco y Página específicas de la plataforma. Con este último enfoque, la capa del modelo de vista se encarga de la navegación a través de una abstracción independiente de la plataforma. Las herramientas ReactiveUI se construyen teniendo en cuenta el enfoque de primer modelo de vista. El enrutamiento ReactiveUI consiste en una implementación IScreen , que contiene el estado de enrutamiento actual, varias implementaciones IRoutableViewModel y un control XAML específico de la plataforma llamado RoutedViewHost .




El objeto RoutingState encapsula la gestión de la pila de navegación. IScreen es la raíz de navegación, pero a pesar del nombre, no tiene que ocupar toda la pantalla. RoutedViewHost reacciona a los cambios en el RoutingState vinculado e incrusta la vista adecuada para el IRoutableViewModel seleccionado actualmente. La funcionalidad descrita se ilustrará con ejemplos más completos más adelante.


Estado del modelo de vista persistente


Considere un modelo de vista de pantalla de búsqueda como ejemplo.




Vamos a decidir qué propiedades del modelo de vista guardar en el cierre de la aplicación y cuáles recrear. No es necesario guardar el estado de un comando reactivo que implementa la interfaz ICommand . ReactiveCommand<TIn, TOut> generalmente se inicializa en el constructor, su indicador CanExecute generalmente depende completamente de las propiedades del modelo de vista y se recalcula cada vez que cambia cualquiera de esas propiedades. Es discutible si tuviera que mantener los resultados de búsqueda, pero guardar la consulta de búsqueda es una buena idea.


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

Marcamos toda la clase de modelo de vista con el atributo [DataContract] , anotamos propiedades que vamos a serializar con el atributo [DataMember] . Esto es suficiente si vamos a utilizar el modo de serialización opcional. Teniendo en cuenta los modos de serialización, la opción de exclusión significa que todos los campos y propiedades públicas se serializarán, a menos que los ignore explícitamente al anotar con el atributo [IgnoreDataMember] , la opción de [IgnoreDataMember] significa lo contrario. Además, implementamos la interfaz IRoutableViewModel en nuestra clase de modelo de vista. Esto es necesario mientras vamos a usar el modelo de vista como parte de una pila de navegación.


Detalles de implementación para el modelo de vista de inicio de sesión

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

Los dos modelos de vista implementan la interfaz IRoutableViewModel y están listos para integrarse en una pantalla de navegación. Ahora es el momento de implementar la interfaz IScreen . Nuevamente, usamos los atributos [DataContract] para indicar qué partes serializar y cuáles ignorar. En el ejemplo a continuación, el RoutingState propiedades RoutingState se declara deliberadamente como público; esto permite que nuestro serializador modifique la propiedad cuando se deserializa.


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

En nuestro modelo de vista principal, guardamos solo un campo en el disco, el de tipo RoutingState . No tenemos que guardar el estado de los comandos reactivos, ya que su disponibilidad depende completamente del estado actual del enrutador y cambia de forma reactiva. Para poder restaurar el enrutador al estado exacto en el que estaba, incluimos información de tipo extendido de nuestras implementaciones de IRoutableViewModel al serializar el enrutador. Usaremos TypenameHandling.All configuración de Newtonsoft.Json para lograr esto más adelante. Ponemos el MainViewModel en la carpeta ViewModels/ , ajustamos el espacio de nombres para que sea ReactiveUI.Samples.Suspension.ViewModels .




Enrutamiento en una aplicación Avalonia


Por el momento, hemos implementado el modelo de presentación de nuestra aplicación. Más tarde, las clases de modelo de vista podrían extraerse en un ensamblaje separado dirigido a .NET Standard , por lo que la parte central de nuestra aplicación podría reutilizarse en múltiples marcos de .NET GUI. Ahora es el momento de implementar la parte GUI específica de Avalonia de nuestra aplicación. Creamos dos archivos en la carpeta Views/ , llamados SearchView.xaml y SearchView.xaml.cs respectivamente. Estas son las dos partes de una sola vista de búsqueda: la primera es la IU descrita declarativamente en XAML, y la segunda contiene código C # subyacente. Esta es esencialmente la vista para el modelo de vista de búsqueda creado anteriormente.


El dialecto XAML utilizado en Avalonia debería sentirse inmediatamente familiar para los desarrolladores que vienen de WPF, UWP o XF. En el ejemplo anterior, creamos un diseño simple que contiene un cuadro de texto y un botón que activa la búsqueda. Vinculamos propiedades y comandos del SearchViewModel a los controles declarados en el SearchView .


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

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

Los desarrolladores de WPF y UWP también pueden encontrar código subyacente para el archivo SearchView.xaml . Se WhenActivated una llamada a WhenActivated para ejecutar la lógica de activación de vista. El desechable que viene como primer argumento para WhenActivated se desecha cuando la vista se desactiva. Si su aplicación utiliza observables activos (por ejemplo, servicios de posicionamiento, temporizadores, agregadores de eventos), sería una buena decisión adjuntar las suscripciones al material desechable compuesto WhenActivated agregando una llamada DisposeWith , de modo que la vista se cancele la suscripción de esos observables DisposeWith y no se producirán pérdidas de memoria.


Detalles de implementación para la vista de inicio de sesión

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

Views / LoginView.xaml.cs


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

Views/MainView.xaml.cs archivos Views/MainView.xaml y Views/MainView.xaml.cs . RoutedViewHost control RoutedViewHost del espacio de nombres Avalonia.ReactiveUI al diseño XAML de la ventana principal y MainViewModel propiedad Router de MainViewModel a la propiedad RoutedViewHost.Router . Agregamos dos botones, uno abre la página de búsqueda y otro abre la página de autorización.


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

Vistas / MainView.xaml.cs


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

Una simple aplicación de demostración de enrutamiento Avalonia y ReactiveUI está lista ahora. Cuando un usuario presiona los botones de búsqueda o inicio de sesión, se invoca un comando que activa la navegación y se actualiza RoutingState . El control RoutedViewHost XAML observa el estado de enrutamiento, intenta resolver la IViewFor<TViewModel> apropiada de IViewFor<TViewModel> desde Locator.Current . Si se registra una implementación IViewFor<TViewModel> , se crea una nueva instancia del control y se incrusta en la ventana de Avalonia.




Registramos nuestras implementaciones IViewFor e IScreen en el método App.OnFrameworkInitializationCompleted , usando Locator.CurrentMutable . Se requiere registrar implementaciones IViewFor para que RoutedViewHost control RoutedViewHost . El registro de una IScreen permite que IScreen y LoginViewModel inicialicen durante la deserialización, utilizando el constructor sin parámetros.


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

Iniciemos nuestra aplicación y garanticemos que el enrutamiento funcione como debería. Si algo sale mal con el marcado de XAML UI, el compilador Avalonia XamlIl nos notificará sobre cualquier error en el momento de la compilación. ¡Además, XamlIl admite la depuración de XAML !


 dotnet run --framework netcoreapp3.0 



Guardar y restaurar el estado de la aplicación


Ahora es el momento de implementar el controlador de suspensión responsable de guardar y restaurar el estado de la aplicación cuando la aplicación se suspende y se reanuda. La clase AutoSuspendHelper específica de la AutoSuspendHelper se encarga de inicializar las cosas, usted, como desarrollador, solo necesita crear una instancia de la misma en la raíz de composición de la aplicación. Además, debe inicializar la fábrica RxApp.SuspensionHost.CreateNewAppState . Si la aplicación no tiene datos guardados, o si los datos guardados están corruptos, ReactiveUI invoca ese método de fábrica para crear una instancia predeterminada del objeto de estado de la aplicación.


Luego, invocamos el método RxApp.SuspensionHost.SetupDefaultSuspendResume y le pasamos una nueva instancia de ISuspensionDriver . Implementemos la interfaz del ISuspensionDriver usando Newtonsoft.Json y las clases del espacio de nombres System.IO .


 dotnet add package Newtonsoft.Json 

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

El enfoque descrito tiene un inconveniente: algunas clases de System.IO no funcionarán con el marco UWP, las aplicaciones UWP se ejecutan en un entorno limitado y hacen las cosas de manera diferente. Eso es bastante fácil de resolver: todo lo que necesita hacer es usar las clases StorageFile y StorageFolder lugar de File and Directory cuando se dirige a UWP. Para leer la pila de navegación del disco, un controlador de suspensión debe admitir la deserialización de objetos JSON en implementaciones concretas de IRoutableViewModel , es por eso que utilizamos la configuración de serializador TypeNameHandling.All Newtonsoft.Json. Registramos el controlador de suspensión en la raíz de composición de la aplicación, en el método 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 clase AutoSuspendHelper del paquete Avalonia.ReactiveUI configura AutoSuspendHelper de ciclo de vida para su aplicación, por lo que el marco ReactiveUI sabrá cuándo escribir el estado de la aplicación en el disco, utilizando la implementación ISuspensionDriver proporcionada. Después de lanzar nuestra aplicación, el controlador de suspensión creará un nuevo archivo JSON llamado appstate.json . Después de hacer cambios en la interfaz de usuario (por ejemplo, escribir algo en los campos de texto o hacer clic en cualquier botón) y luego cerrar la aplicación, el archivo appstate.json tendrá un aspecto similar al siguiente:


appstate.json

Tenga en cuenta que cada objeto JSON en el archivo contiene una clave $type con un nombre de tipo totalmente calificado, incluido el espacio de nombres.


 { "$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 cierra la aplicación y luego la inicia de nuevo, verá el mismo contenido en la pantalla que había visto antes. La funcionalidad descrita funciona en cada plataforma compatible con ReactiveUI , incluidos UWP, WPF, Xamarin Forms o Xamarin Native.


imagen


Bonificación: la interfaz del ISuspensionDriver se puede implementar utilizando Akavache , un almacén de valores clave asíncrono y persistente. Si almacena sus datos en la sección UserAccount o en la sección Secure , en iOS y UWP sus datos se guardarán automáticamente en la nube y estarán disponibles en todos los dispositivos en los que esté instalada la aplicación. Además, existe un BundleSuspensionDriver en el paquete ReactiveUI.AndroidSupport . Las API de Xamarin.Essentials SecureStorage también podrían usarse para almacenar datos. También puede almacenar el estado de su aplicación en un servidor remoto o en un servicio en la nube independiente de la plataforma.



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


All Articles