Navegación en una aplicación multiplataforma .NET Core con guardar estado en disco usando el ejemplo de ReactiveUI y Avalonia



Las interfaces de usuario de las aplicaciones modernas suelen ser complejas: a menudo es necesario implementar el soporte de navegación de página, procesar campos de entrada de varios tipos y mostrar u ocultar información en función de los parámetros seleccionados por el usuario. Al mismo tiempo, para mejorar UX, la aplicación debe guardar el estado de los elementos de la interfaz en el disco durante la suspensión o el apagado, restaurar el estado del disco cuando se reinicia el programa.


El marco ReactiveUI MVVM propone preservar el estado de una aplicación serializando el gráfico de los modelos de presentación en el momento en que se suspende el programa, mientras que los mecanismos para determinar el momento de la suspensión son diferentes para los marcos y las plataformas. Entonces, para WPF, se usa el evento Exit , para Xamarin.Android - ActivityPaused , para Xamarin.iOS - DidEnterBackground , para UWP - OnLaunched overload.


En este artículo, consideraremos el uso de ReactiveUI para guardar y restaurar el estado del software con una GUI, incluido el estado de un enrutador, utilizando el marco de la GUI multiplataforma de Avalonia como ejemplo . El material asume una comprensión básica del patrón de diseño MVVM y la programación reactiva en el contexto del lenguaje C # y la plataforma .NET para el lector. Los pasos de este artículo se aplican a Windows 10 y Ubuntu 18.


Creación de proyectos


Para probar el enrutamiento en acción, cree un nuevo proyecto .NET Core a partir de la plantilla de Avalonia, instale el paquete Avalonia.ReactiveUI , una capa delgada de integración de Avalonia y ReactiveUI. Asegúrese de tener instalado .NET Core SDK y git antes de comenzar.


 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 

¡Asegúrese de que la aplicación se inicie y muestre una ventana que dice 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 



Conecte las compilaciones previas de Avalonia desde MyGet


Para conectar y utilizar las últimas compilaciones de Avalonia que se publican automáticamente en MyGet cuando cambia la rama master repositorio de Avalonia en GitHub , utilizamos el archivo de configuración de origen del paquete nuget.config . Para que IDE y .NET Core CLI vean nuget.config , debe generar un archivo sln para el proyecto creado anteriormente. Utilizamos las herramientas de .NET Core CLI:


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

Cree un archivo nuget.config en una carpeta con un archivo .sln del 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> 

Es posible que deba reiniciar el IDE, o descargar y descargar la solución completa. Actualizaremos los paquetes de Avalonia a la versión requerida (al menos 0.9.1 ) usando la interfaz del administrador de paquetes NuGet de su IDE, o usando las herramientas de línea de comandos de Windows o el 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 

Ahora el archivo de proyecto ReactiveUI.Samples.Suspension.csproj tiene este aspecto:


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

Cree las carpetas Views/ y ViewModels/ en la raíz del proyecto, cambie el nombre de la clase MainWindow a MainView por conveniencia, muévalo al directorio Views/ , cambiando los espacios de nombres en consecuencia a ReactiveUI.Samples.Suspension.Views . Program.cs App.xaml.cs contenido de los App.xaml.cs Program.cs y App.xaml.cs : aplique la llamada UseReactiveUI al UseReactiveUI de aplicaciones Avalonia, mueva la inicialización de la vista principal a OnFrameworkInitializationCompleted para cumplir con las recomendaciones de administración del ciclo de vida de la aplicación:


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

Deberá agregar using Avalonia.ReactiveUI a Program.cs . Asegúrese de que después de actualizar los paquetes, el proyecto se inicia y muestra la ventana de bienvenida predeterminada.


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



Enrutamiento multiplataforma ReactiveUI


Como regla, hay dos enfoques principales para implementar la navegación entre páginas de una aplicación .NET: ver primero y ver primero el modelo. El enfoque de Ver primero implica controlar la pila de navegación y la navegación entre páginas en el nivel de Vista en terminología MVVM, por ejemplo, usando las clases Marco y Página en el caso de UWP o WPF, y cuando se usa el enfoque de ver primero el modelo, la navegación se implementa en el nivel de los modelos de presentación. Las herramientas ReactiveUI que organizan el enrutamiento en la aplicación se centran en usar el enfoque de vista del modelo primero. El enrutamiento ReactiveUI consiste en una implementación IScreen contiene el estado del enrutador, varias implementaciones de IRoutableViewModel y el control XAML RoutedViewHost la plataforma, RoutedViewHost .




El estado del enrutador está representado por un objeto RoutingState que controla la pila de navegación. IScreen es la raíz de la pila de navegación, y puede haber varias raíces de navegación en la aplicación. RoutedViewHost supervisa el estado de su enrutador RoutingState correspondiente, respondiendo a los cambios en la pila de navegación incorporando el correspondiente IRoutableViewModel del control XAML. La funcionalidad descrita se ilustrará con los ejemplos a continuación.


Guardar el estado de los modelos de vista en el disco


Considere un ejemplo típico de una pantalla de búsqueda de información.




Debemos decidir qué elementos del modelo de representación de pantalla guardar en el disco durante la suspensión o el apagado de la aplicación, y cuáles, para recrear cada vez que se inicia. No es necesario guardar el estado de los comandos ReactiveUI que implementan la interfaz ICommand y están unidos a botones: ReactiveCommand<TIn, TOut> se crean e inicializan en el constructor, mientras que el estado del indicador CanExecute depende de las propiedades del modelo de vista y se recalcula cuando cambian. La necesidad de guardar los resultados de búsqueda, un punto discutible, depende de los detalles de la aplicación, ¡pero sería conveniente guardar y restaurar el estado del campo de entrada 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 clase del modelo de vista está marcada con el atributo [DataContract] y las propiedades que deben serializarse con los [DataMember] . Esto es suficiente si el serializador utilizado utiliza el enfoque de aceptación: solo guarda propiedades que están marcadas explícitamente con atributos en el disco; en el caso del enfoque de exclusión, es necesario marcar con los atributos [IgnoreDataMember] aquellas propiedades que no necesitan guardarse en el disco. Además, implementamos la interfaz IRoutableViewModel en nuestro modelo de vista para que luego pueda formar parte del marco de navegación del enrutador de la aplicación.


Del mismo modo, implementamos el modelo de presentación de la página de autorización.

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

Los modelos de presentación para dos páginas de la aplicación están listos, implementan la interfaz IRoutableViewModel y pueden integrarse en el enrutador IScreen . Ahora implementamos directamente IScreen . Marcamos con la ayuda de los atributos [DataContract] qué propiedades del modelo de vista serializar y cuáles ignorar. Preste atención al [DataMember] público de la propiedad marcada con el [DataMember] , en el ejemplo a continuación: la propiedad está abierta intencionalmente para escribir de modo que el serializador pueda modificar una instancia recién creada del objeto durante la deserialización del modelo.


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

En nuestra aplicación, solo RoutingState debe guardarse en el disco; por razones obvias, los comandos no necesitan guardarse en el disco; su estado depende completamente del enrutador. El objeto serializado debe incluir información extendida sobre los tipos que implementan el IRoutableViewModel para que la pila de navegación pueda restaurarse tras la deserialización. Describimos la lógica del MainViewModel vista MainViewModel , colocamos la clase en ViewModels/MainViewModel.cs y en el espacio de nombres ReactiveUI.Samples.Suspension.ViewModels correspondiente.




Enrutamiento en la aplicación Avalonia


La lógica de la interfaz de usuario en el nivel de capa del modelo y el modelo de presentación de la aplicación de demostración se implementan y se pueden mover a un ensamblaje separado dirigido al estándar .NET, porque no sabe nada sobre el marco de GUI utilizado. Echemos un vistazo a la capa de presentación. En la terminología de MVVM, la capa de presentación es responsable de representar el estado del modelo de presentación en la pantalla; para representar el estado actual del enrutador RoutedViewHost , se RoutedViewHost control XAML RoutedViewHost contenido en el paquete Avalonia.ReactiveUI . Implementamos la GUI para SearchViewModel ; para esto, en el directorio Views/ , cree dos archivos: SearchView.xaml y SearchView.xaml.cs .


Es probable que una descripción de la interfaz de usuario que usa el dialecto XAML utilizado en Avalonia les resulte familiar a los desarrolladores de Windows Presentation Foundation, Universal Windows Platform o Xamarin.Forms. En el ejemplo anterior, creamos una interfaz trivial para el formulario de búsqueda: dibujamos un cuadro de texto para ingresar la consulta de búsqueda y un botón que inicia la búsqueda, mientras vinculamos los controles a las propiedades del modelo SearchViewModel definido anteriormente.


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

El código subyacente del control SearchView.xaml también aparecerá para los desarrolladores familiares de WPF, UWP y XF. La llamada WhenActivated se usa para ejecutar algún código cuando la vista o el modelo de vista se activan y desactivan. Si su aplicación usa observables activos (temporizadores, geolocalización, conexión al bus de mensajes), sería conveniente adjuntarlos al CompositeDisposable llamando a DisposeWith para que cuando DisposeWith control XAML y su modelo de vista correspondiente del árbol visual, los observables activos dejen de publicar nuevos valores y no haya fugas. memoria


Del mismo modo, implementamos la representación de la página de autorizació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); } } 

Edite los Views/MainView.xaml.cs Views/MainView.xaml y Views/MainView.xaml.cs . RoutedViewHost XAML RoutedViewHost del espacio de nombres RoutedViewHost en la pantalla principal, asigne el estado del enrutador RoutingState a la propiedad RoutingState . Agregue botones para navegar a las páginas de búsqueda y autorización, ViewModels/MainViewModel propiedades ViewModels/MainViewModel descritas anteriormente.


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

Vistas / MainView.xaml.cs


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

Una aplicación simple que demuestra las capacidades de enrutamiento ReactiveUI y Avalonia está lista. Cuando se hace clic en los botones Search e Login se invocan los comandos correspondientes, se crea una nueva instancia del modelo de vista y RoutingState actualiza RoutingState . El RoutedViewHost XAML, RoutedViewHost , que se suscribe a los cambios en RoutingState , intenta obtener el tipo IViewFor<TViewModel> , donde TViewModel es el tipo de modelo de vista, desde Locator.Current . Si se encuentra una implementación registrada de IViewFor<TViewModel> una nueva instancia, integrada en RoutedViewHost y se mostrará en la ventana de la aplicación Avalonia.




Registramos los componentes necesarios IViewFor<TViewModel> e IScreen en el método App.OnFrameworkInitializationCompleted de nuestra aplicación usando Locator.CurrentMutable . IViewFor<TViewModel> necesario para que RoutedViewHost funcione RoutedViewHost , y el registro IScreen necesario para que al deserializar, los LoginViewModel SearchViewModel y LoginViewModel puedan inicializarse correctamente utilizando el constructor sin parámetros y 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(); } 

Ejecute la aplicación y asegúrese de que la ruta funcione correctamente. Si hay algún error en el marcado XAML, el compilador XamlIl utilizado en Avalonia nos dirá exactamente dónde, en la etapa de compilación. ¡XamlIl también admite la depuración de XAML directamente en el depurador IDE !


 dotnet run --framework netcoreapp3.0 



Guardar y restaurar todo el estado de la aplicación


Ahora que el enrutamiento está configurado y funciona, comienza la parte más interesante: debe implementar guardar datos en el disco cuando cierra la aplicación y leer los datos del disco cuando comienza, junto con el estado del enrutador. La inicialización de ganchos que escuchan eventos de inicio y cierre de aplicaciones se maneja mediante una clase especial AutoSuspendHelper , AutoSuspendHelper para cada plataforma que admite ReactiveUI . La tarea del desarrollador es inicializar esta clase al comienzo de la raíz de la composición de la aplicación. También es necesario inicializar la propiedad RxApp.SuspensionHost.CreateNewAppState función que devolverá el estado predeterminado de la aplicación si no hay un estado guardado o si ocurrió un error inesperado, o si el archivo guardado está dañado.


A continuación, debe llamar al método RxApp.SuspensionHost.SetupDefaultSuspendResume , pasándole la implementación de ISuspensionDriver , el controlador que ISuspensionDriver y lee el objeto de estado. Para implementar ISuspensionDriver , utilizamos la biblioteca Newtonsoft.Json y el espacio de nombres System.IO para trabajar con el sistema de archivos. Para hacer esto, instale el paquete Newtonsoft.Json :


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

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


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

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


appstate.json

$type , , .


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

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




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


Enlaces utiles


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


All Articles