Salvando o estado de roteamento no disco em um aplicativo GUI do .NET para várias plataformas com ReactiveUI e Avalonia

imagem


As interfaces de usuário de aplicativos corporativos modernos são bastante complexas. Você, como desenvolvedor, geralmente precisa implementar a navegação no aplicativo, validar a entrada do usuário, mostrar ou ocultar telas com base nas preferências do usuário. Para um melhor UX, seu aplicativo deve ser capaz de salvar o estado no disco quando o aplicativo estiver em suspensão e de restaurar o estado quando o aplicativo for reiniciado.


O ReactiveUI fornece recursos que permitem persistir o estado do aplicativo serializando a árvore do modelo de exibição quando o aplicativo está sendo encerrado ou suspenso. Os eventos de suspensão variam de acordo com a plataforma. O ReactiveUI usa o evento Exit para WPF, ActivityPaused para Xamarin.Android, DidEnterBackground para Xamarin.iOS, OnLaunched para UWP.


Neste tutorial, criaremos um aplicativo de exemplo que demonstra o uso do recurso de suspensão do ReactiveUI com o Avalonia - uma estrutura de GUI baseada em XAML do .NET Core de plataforma cruzada. Você deve estar familiarizado com o padrão MVVM e com extensões reativas antes de ler esta nota. As etapas descritas no tutorial devem funcionar se você estiver usando o Windows 10 ou Ubuntu 18 e tiver o .NET Core SDK instalado. Vamos começar!


Inicializando o projeto


Para ver o roteamento do ReactiveUI em ação, criamos um novo projeto .NET Core baseado nos modelos de aplicativo Avalonia. Em seguida, instalamos o pacote Avalonia.ReactiveUI . O pacote fornece ganchos de ciclo de vida Avalonia específicos da plataforma, infraestrutura de roteamento e ativação . Lembre-se de instalar o .NET Core e o git antes de executar os comandos abaixo.


 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 

Vamos executar o aplicativo e garantir que ele mostre uma janela exibindo "Bem-vindo ao Avalonia!"


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

imagem


Instalando o Avalonia Preview Builds a partir do MyGet


Os pacotes mais recentes do Avalonia são publicados no MyGet toda vez que um novo commit é enviado à ramificação master do repositório do Avalonia no GitHub. Para usar os pacotes mais recentes do MyGet em nosso aplicativo, vamos criar um arquivo nuget.config . Porém, antes de fazer isso, geramos um arquivo sln para o projeto criado anteriormente, usando a CLI do .NET Core :


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

Agora, criamos o arquivo nuget.config com o seguinte conteúdo:


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

Geralmente, é necessária uma reinicialização para forçar nosso IDE a detectar pacotes do feed MyGet recém-adicionado, mas recarregar a solução também deve ajudar. Em seguida, atualizamos os pacotes Avalonia para a versão mais recente (ou pelo menos para 0.9.1 ) usando a GUI do gerenciador de pacotes NuGet ou a CLI do .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 

O arquivo ReactiveUI.Samples.Suspension.csproj deve se parecer com o seguinte agora:


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

Criamos duas novas pastas dentro do diretório raiz do projeto, denominadas Views/ e ViewModels/ respectivamente. Em seguida, renomeamos a classe MainWindow para MainView e a movemos para a pasta Views/ . Lembre-se de renomear referências para a classe editada no arquivo XAML correspondente, caso contrário, o projeto não será compilado. Além disso, lembre-se de alterar o espaço para nome do MainView para ReactiveUI.Samples.Suspension.Views para obter consistência. Em seguida, editamos outros dois arquivos, Program.cs e App.xaml.cs Adicionamos uma chamada ao UseReactiveUI ao criador de aplicativos Avalonia, movemos o código de inicialização do aplicativo para o método OnFrameworkInitializationCompleted para conformar as diretrizes de gerenciamento da vida útil do aplicativo 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 tentar criar o projeto, garantimos que a diretiva using Avalonia.ReactiveUI seja adicionada à parte superior do arquivo Program.cs . Provavelmente, nosso IDE já importou esse espaço para nome, mas, se não, receberemos um erro em tempo de compilação. Finalmente, é hora de garantir que o aplicativo compile, execute e mostre uma nova janela:


 dotnet run --framework netcoreapp2.1 

imagem


Roteamento ReactiveUI de plataforma cruzada


Há duas abordagens gerais para organizar a navegação no aplicativo em um aplicativo .NET de plataforma cruzada - exibir primeiro e exibir modelo primeiro. A abordagem anterior pressupõe que a camada View gerencia a pilha de navegação - por exemplo, usando as classes Frame e Page específicas da plataforma. Com a última abordagem, a camada do modelo de visualização cuida da navegação por meio de uma abstração independente da plataforma. As ferramentas do ReactiveUI são criadas tendo em mente a abordagem de visualização do modelo primeiro. O roteamento ReactiveUI consiste em uma implementação IScreen , que contém o estado de roteamento atual, várias implementações IRoutableViewModel e um controle XAML específico da plataforma chamado RoutedViewHost .




O objeto RoutingState encapsula o gerenciamento de pilha de navegação. IScreen é a raiz da navegação, mas, apesar do nome, não precisa ocupar a tela inteira. RoutedViewHost reage às alterações no RoutingState ligado e incorpora a exibição apropriada para o IRoutableViewModel atualmente selecionado. A funcionalidade descrita será ilustrada com exemplos mais abrangentes posteriormente.


Estado do modelo de vista persistente


Considere um modelo de visualização da tela de pesquisa como exemplo.




Vamos decidir quais propriedades do modelo de visualização salvar no encerramento do aplicativo e quais recriar. Não há necessidade de salvar o estado de um comando reativo que implementa a interface ICommand . ReactiveCommand<TIn, TOut> classe ReactiveCommand<TIn, TOut> geralmente é inicializada no construtor, seu indicador CanExecute geralmente depende totalmente das propriedades do modelo de exibição e é recalculado toda vez que alguma dessas propriedades é alterada. É discutível se você manter os resultados da pesquisa, mas salvar a consulta de pesquisa é uma boa ideia.


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 a classe do modelo de exibição com o atributo [DataContract] , anotamos as propriedades que vamos serializar com o atributo [DataMember] . Isso é suficiente se quisermos usar o modo de serialização opcional. Considerando os modos de serialização, opt-out significa que todos os campos e propriedades públicos serão serializados, a menos que você os ignore explicitamente anotando com o atributo [IgnoreDataMember] , opt-in significa o contrário. Além disso, implementamos a interface IRoutableViewModel em nossa classe de modelo de exibição. Isso é necessário enquanto vamos usar o modelo de exibição como parte de uma pilha de navegação.


Detalhes de implementação para o modelo de visualização de login

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

Os dois modelos de visualização implementam a interface IRoutableViewModel e estão prontos para serem incorporados em uma tela de navegação. Agora é hora de implementar a interface IScreen . Novamente, usamos os atributos [DataContract] para indicar quais partes serializar e quais ignorar. No exemplo abaixo, o RoutingState propriedade RoutingState é deliberadamente declarado como público - isso permite que nosso serializador modifique a propriedade quando ela for desserializada.


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

Em nosso modelo de visualização principal, salvamos apenas um campo no disco - o do tipo RoutingState . Não precisamos salvar o estado dos comandos reativos, pois sua disponibilidade depende totalmente do estado atual do roteador e muda reativamente. Para poder restaurar o roteador para o estado exato em que estava, incluímos informações de tipo estendidas de nossas implementações IRoutableViewModel ao serializar o roteador. Usaremos TypenameHandling.All configuração de Newtonsoft.Json para conseguir isso mais tarde. Colocamos o MainViewModel na pasta ViewModels/ , ajustamos o espaço para nome como ReactiveUI.Samples.Suspension.ViewModels .




Roteamento em um aplicativo Avalonia


No momento, implementamos o modelo de apresentação de nossa aplicação. Posteriormente, as classes de modelo de exibição podem ser extraídas em um assembly separado direcionado ao .NET Standard , para que a parte principal do nosso aplicativo possa ser reutilizada em várias estruturas da GUI do .NET. Agora é hora de implementar a parte GUI específica para Avalonia do nosso aplicativo. Criamos dois arquivos na pasta Views/ , denominados SearchView.xaml e SearchView.xaml.cs respectivamente. Essas são as duas partes de uma única exibição de pesquisa - a primeira é a interface do usuário descrita declarativamente em XAML e a segunda contém o C # code-behind. Essa é essencialmente a visualização do modelo de visualização de pesquisa criado anteriormente.


O dialeto XAML usado em Avalonia deve parecer imediatamente familiar para desenvolvedores provenientes de WPF, UWP ou XF. No exemplo acima, criamos um layout simples que contém uma caixa de texto e um botão que aciona a pesquisa. Vinculamos propriedades e comandos do SearchViewModel aos controles declarados no SearchView .


Visualizações / 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> 

Visualizações / 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); } } 

Os desenvolvedores de WPF e UWP também podem encontrar o code-behind do arquivo SearchView.xaml . Uma chamada para WhenActivated é adicionada para executar a lógica de ativação da visualização. A entrada descartável como o primeiro argumento para WhenActivated é descartada quando a exibição é desativada. Se o seu aplicativo estiver usando observáveis ​​quentes (por exemplo, serviços de posicionamento, temporizadores, agregadores de eventos), seria uma decisão sábia anexar as assinaturas ao descartável composto WhenActivated adicionando uma chamada DisposeWith , para que a visualização cancele a inscrição desses observáveis ​​quentes e vazamentos de memória não ocorrerão.


Detalhes de implementação para a visualização de login

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

Visualizações / LoginView.xaml.cs


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

Views/MainView.xaml.cs arquivos Views/MainView.xaml e Views/MainView.xaml.cs . Adicionamos o controle RoutedViewHost do espaço de nomes Avalonia.ReactiveUI ao layout XAML da janela principal e vinculamos a propriedade Router de MainViewModel à propriedade RoutedViewHost.Router . Adicionamos dois botões, um abre a página de pesquisa e outro abre a página de autorização.


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

Visualizações / MainView.xaml.cs


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

Um simples aplicativo de demonstração de roteamento Avalonia e ReactiveUI está pronto agora. Quando um usuário pressiona os botões de pesquisa ou login, um comando que aciona a navegação é chamado e o RoutingState é atualizado. O controle RoutedViewHost XAML observa o estado de roteamento, tenta resolver a IViewFor<TViewModel> apropriada de IViewFor<TViewModel> partir de Locator.Current . Se uma IViewFor<TViewModel> for registrada, uma nova instância do controle será criada e incorporada à janela Avalonia.




Registramos nossas implementações IViewFor e IViewFor no método App.OnFrameworkInitializationCompleted , usando Locator.CurrentMutable . É necessário registrar IViewFor implementações IViewFor para que o controle RoutedViewHost funcione. O registro de um IScreen permite que nosso SearchViewModel e LoginViewModel sejam inicializados durante a desserialização, usando o construtor sem 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(); } 

Vamos iniciar nosso aplicativo e garantir que o roteamento funcione como deveria. Se algo der errado com a marcação da interface do usuário XAML, o compilador Avalonia XamlIl nos notificará sobre quaisquer erros no momento da compilação. Além disso, o XamlIl suporta a depuração de XAML !


 dotnet run --framework netcoreapp3.0 



Salvando e restaurando o estado do aplicativo


Agora é hora de implementar o driver de suspensão responsável por salvar e restaurar o estado do aplicativo quando o aplicativo estiver sendo suspenso e reiniciado. A classe AutoSuspendHelper específica da plataforma cuida da inicialização, você, como desenvolvedor, só precisa criar uma instância na raiz de composição do aplicativo. Além disso, você precisa inicializar a fábrica RxApp.SuspensionHost.CreateNewAppState . Se o aplicativo não tiver dados salvos ou se os dados salvos estiverem corrompidos, o ReactiveUI chama esse método de fábrica para criar uma instância padrão do objeto de estado do aplicativo.


Em seguida, invocamos o método RxApp.SuspensionHost.SetupDefaultSuspendResume e passamos uma nova instância do ISuspensionDriver para ele. Vamos implementar a interface ISuspensionDriver usando Newtonsoft.Json e classes no espaço para nome 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); } } 

A abordagem descrita tem uma desvantagem - algumas classes System.IO não funcionam com a estrutura UWP, os aplicativos UWP são executados em uma sandbox e fazem as coisas de maneira diferente. Isso é bastante fácil de resolver - tudo o que você precisa fazer é usar as classes StorageFolder e StorageFolder vez do File and Directory ao direcionar-se à UWP. Para ler a pilha de navegação do disco, um driver de suspensão deve dar suporte à desserialização de objetos JSON em implementações concretas do IRoutableViewModel , por isso usamos a configuração de serializador TypeNameHandling.All Newtonsoft.Json. Registramos o driver de suspensão na raiz da composição do aplicativo, no 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(); } 

A classe AutoSuspendHelper do pacote Avalonia.ReactiveUI configura ganchos de ciclo de vida para seu aplicativo, para que a estrutura ReactiveUI saiba quando gravar o estado do aplicativo no disco, usando a implementação ISuspensionDriver fornecida. Após o lançamento do aplicativo, o driver de suspensão criará um novo arquivo JSON chamado appstate.json . Depois de fazer alterações na interface do usuário (por exemplo, digite um pouco nos campos de texto ou clique em qualquer botão) e feche o aplicativo, o arquivo appstate.json será semelhante ao seguinte:


appstate.json

Observe que cada objeto JSON no arquivo contém uma chave $type com um nome de tipo completo, incluindo espaço para nome.


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

Se você fechar o aplicativo e depois iniciá-lo novamente, verá o mesmo conteúdo na tela como antes! A funcionalidade descrita funciona em cada plataforma suportada pelo ReactiveUI , incluindo UWP, WPF, Xamarin Forms ou Xamarin Native.


imagem


Bônus: A interface ISuspensionDriver pode ser implementada usando o Akavache - um armazenamento de valores-chave persistente e assíncrono. Se você armazenar seus dados na seção UserAccount ou na seção Secure , no iOS e na UWP, seus dados serão copiados automaticamente para a nuvem e estarão disponíveis em todos os dispositivos nos quais o aplicativo está instalado. Além disso, existe um BundleSuspensionDriver no pacote ReactiveUI.AndroidSupport . As APIs do Xamarin.Essentials SecureStorage também podem ser usadas para armazenar dados. Você também pode armazenar o estado do aplicativo em um servidor remoto ou em um serviço em nuvem independente da plataforma.



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


All Articles