
As interfaces de usuário de aplicativos modernos geralmente são complexas - geralmente é necessário implementar o suporte à navegação de página, processar campos de entrada de vários tipos e exibir ou ocultar informações com base nos parâmetros selecionados pelo usuário. Ao mesmo tempo, para melhorar o UX, o aplicativo deve salvar o estado dos elementos da interface no disco durante a suspensão ou o desligamento, restaurar o estado do disco quando o programa for reiniciado.
A estrutura do ReactiveUI MVVM propõe preservar o estado de um aplicativo serializando o gráfico dos modelos de apresentação no momento em que o programa é suspenso, enquanto os mecanismos para determinar o momento da suspensão são diferentes para estruturas e plataformas. Portanto, para o WPF, o evento Exit
é usado, para Xamarin.Android - ActivityPaused
, para Xamarin.iOS - DidEnterBackground
, para sobrecarga UWP - OnLaunched
.
Neste artigo, consideraremos o uso do ReactiveUI para salvar e restaurar o estado do software com uma GUI, incluindo o estado de um roteador, usando a estrutura da GUI entre plataformas da Avalonia como exemplo . O material pressupõe uma compreensão básica do padrão de design MVVM e da programação reativa no contexto da linguagem C # e da plataforma .NET para o leitor. As etapas deste artigo se aplicam ao Windows 10 e Ubuntu 18.
Criação de projeto
Para tentar rotear em ação, crie um novo projeto .NET Core a partir do modelo Avalonia, instale o pacote Avalonia.ReactiveUI
- uma camada fina de integração entre Avalonia e ReactiveUI. Certifique-se de ter o .NET Core SDK e o git instalados antes de começar.
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
Verifique se o aplicativo é iniciado e exibe uma janela que diz 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

Conecte pré-compilações Avalonia do MyGet
Para conectar e usar as versões mais recentes do Avalonia que são publicadas automaticamente no MyGet quando a ramificação master
repositório do Avalonia no GitHub é alterada, usamos o arquivo de configuração da fonte do pacote nuget.config
. Para que o IDE e o .NET Core CLI vejam nuget.config
, é necessário gerar um arquivo sln
para o projeto criado acima. Usamos as ferramentas da CLI do .NET Core:
dotnet new sln # Ctrl+C dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj
Crie um arquivo nuget.config
em uma pasta com um arquivo .sln
do 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>
Pode ser necessário reiniciar o IDE ou descarregar e baixar a solução inteira. Atualizaremos os pacotes Avalonia para a versão necessária (pelo menos 0.9.1
) usando a interface do gerenciador de pacotes NuGet do seu IDE ou as ferramentas de linha de comando do Windows ou o 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
Agora, o arquivo de projeto ReactiveUI.Samples.Suspension.csproj
parece com o seguinte:
<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>
Crie as pastas Views/
e ViewModels/
na raiz do projeto, altere o nome da classe MainWindow
para MainView
por conveniência, mova-o para o diretório Views/
, alterando os espaços de nome de acordo com ReactiveUI.Samples.Suspension.Views
. App.xaml.cs
conteúdo dos App.xaml.cs
Program.cs
e App.xaml.cs
- aplique a chamada UseReactiveUI
ao construtor de aplicativos Avalonia, mova a inicialização da visualização principal para OnFrameworkInitializationCompleted
para atender às recomendações de gerenciamento do ciclo de vida do aplicativo:
Program.cs
class Program {
App.xaml.cs
public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this);
Você precisará adicionar o using Avalonia.ReactiveUI
ao Program.cs
. Certifique-se de que, após atualizar os pacotes, o projeto inicie e exiba a janela de boas-vindas padrão.
# Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0

Como regra, existem duas abordagens principais para implementar a navegação entre as páginas de um aplicativo .NET - exibir primeiro e exibir modelo primeiro. A abordagem View-first envolve controlar a pilha de navegação e a navegação entre páginas no nível View na terminologia MVVM - por exemplo, usando as classes Frame e Page no caso de UWP ou WPF e, ao usar a abordagem view-first-model, a navegação é implementada no nível dos modelos de apresentação. As ferramentas do ReactiveUI que organizam o roteamento no aplicativo concentram-se no uso da abordagem de exibição primeiro modelo. O roteamento do ReactiveUI consiste em uma implementação IScreen
que contém o estado do roteador, várias implementações do IRoutableViewModel
e o controle XAML RoutedViewHost
da plataforma, RoutedViewHost
.

O estado do roteador é representado por um objeto RoutingState
que controla a pilha de navegação. IScreen
é a raiz da pilha de navegação e pode haver várias raízes de navegação no aplicativo. RoutedViewHost
monitora o status do roteador RoutingState
correspondente, respondendo às alterações na pilha de navegação incorporando o IRoutableViewModel
correspondente do controle XAML. A funcionalidade descrita será ilustrada pelos exemplos abaixo.
Salvando os Modelos de Estado de Visão em Disco
Considere um exemplo típico de uma tela de pesquisa de informações.

Devemos decidir quais elementos do modelo de representação de tela serão salvos em disco durante a suspensão ou o desligamento do aplicativo e quais - recriar cada vez que for iniciado. Não há necessidade de salvar o estado dos comandos ReactiveUI que implementam a interface ICommand
e estão anexados aos botões - ReactiveCommand<TIn, TOut>
é criado e inicializado no construtor, enquanto o estado do indicador CanExecute
depende das propriedades do modelo de exibição e é recalculado quando eles mudam. A necessidade de salvar os resultados da pesquisa - um ponto discutível - depende das especificidades do aplicativo, mas seria aconselhável salvar e restaurar o estado do campo de entrada SearchQuery
!
ViewModels / SearchViewModel.cs
[DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery;
A classe do modelo de exibição é marcada com o atributo [DataContract]
e as propriedades que precisam ser serializadas com os [DataMember]
. Isso é suficiente se o serializador usado usar a abordagem de inclusão - ele salva apenas propriedades marcadas explicitamente com atributos no disco; no caso da abordagem de exclusão, é necessário marcar com os atributos [IgnoreDataMember]
aquelas propriedades que não precisam ser salvas no disco. Além disso, implementamos a interface IRoutableViewModel
em nosso modelo de exibição para que mais tarde ela possa se tornar parte do quadro de navegação do roteador de aplicativos.
Da mesma forma, implementamos o modelo de apresentação da página de autorizaçãoViewModels / LoginViewModel.cs
[DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username;
Os modelos de apresentação para duas páginas do aplicativo estão prontos, implementam a interface IRoutableViewModel
e podem ser incorporados ao roteador IScreen
. Agora, implementamos diretamente o IScreen
. Marcamos com a ajuda dos atributos [DataContract]
quais propriedades do modelo de exibição serializar e quais ignorar. Preste atenção ao configurador público da propriedade marcada com o [DataMember]
, no exemplo abaixo - a propriedade é intencionalmente aberta para gravação, para que o serializador possa modificar uma instância recém-criada do objeto durante a desserialização do 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() {
Em nosso aplicativo, apenas o RoutingState
precisa ser salvo no disco; por razões óbvias, os comandos não precisam ser salvos no disco - seu status depende inteiramente do roteador. O objeto serializado deve incluir informações estendidas sobre os tipos que implementam o IRoutableViewModel
para que a pilha de navegação possa ser restaurada após a desserialização. Descrevemos a lógica do MainViewModel
visualização MainViewModel
, coloque a classe em ViewModels/MainViewModel.cs
e no espaço de nome ReactiveUI.Samples.Suspension.ViewModels
correspondente.

Roteamento no aplicativo Avalonia
A lógica da interface do usuário no nível da camada do modelo e o modelo de apresentação do aplicativo de demonstração é implementada e pode ser movida para um assembly separado voltado para o .NET Standard, porque ele não sabe nada sobre a estrutura da GUI usada. Vamos entrar na camada de apresentação. Na terminologia MVVM, a camada de apresentação é responsável por renderizar o estado do modelo de apresentação na RoutingState
renderizar o estado atual do roteador RoutingState
, o controle XAML RoutedViewHost
contido no pacote Avalonia.ReactiveUI
é Avalonia.ReactiveUI
. Implementamos a GUI do SearchViewModel
- para isso, no diretório Views/
, crie dois arquivos: SearchView.xaml
e SearchView.xaml.cs
.
Uma descrição da interface do usuário usando o dialeto XAML usado no Avalonia provavelmente parecerá familiar para os desenvolvedores no Windows Presentation Foundation, Universal Windows Platform ou Xamarin.Forms. No exemplo acima, criamos uma interface trivial para o formulário de pesquisa - desenhamos um campo de texto para inserir a consulta de pesquisa e um botão que inicia a pesquisa, enquanto vinculamos os controles às propriedades do modelo SearchViewModel
definido acima.
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() {
O código por trás do controle SearchView.xaml
também aparecerá para os desenvolvedores familiares de WPF, UWP e XF. A chamada WhenActivated é usada para executar algum código quando a exibição ou modelo de exibição é ativada e desativada. Se seu aplicativo usar observáveis ativos (temporizadores, geolocalização, conexão com o barramento de mensagens), seria aconselhável anexá-los ao CompositeDisposable
chamando DisposeWith
para que, quando você DisposeWith
controle XAML e seu modelo de visualização correspondente da árvore visual, os observáveis quentes parem de publicar novos valores e não haja vazamentos memória.
Da mesma forma, implementamos a representação da página de loginViews / 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); } }
Edite os Views/MainView.xaml.cs
Views/MainView.xaml
e Views/MainView.xaml.cs
. RoutedViewHost
XAML RoutedViewHost
no espaço para nome RoutedViewHost
na tela principal, atribua o estado do roteador RoutingState
à propriedade RoutingState
. Adicione botões para navegar até as páginas de pesquisa e autorização, vincule-os às propriedades ViewModels/MainViewModel
descritas acima.
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> <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 aplicativo simples que demonstra os recursos de roteamento ReactiveUI e Avalonia está pronto. Quando você clica nos botões Search
e Login
, os comandos correspondentes são chamados, uma nova instância do modelo de exibição é criada e o RoutingState
atualizado. O controle XAML RoutedViewHost
, que assina as alterações no RoutingState
, tenta obter o tipo IViewFor<TViewModel>
, em que TViewModel
é o tipo de modelo de exibição de Locator.Current
. Se uma implementação registrada de IViewFor<TViewModel>
encontrada, uma nova instância será criada, incorporada ao RoutedViewHost
e exibida na janela do aplicativo Avalonia.

Registramos os componentes necessários IViewFor<TViewModel>
e App.OnFrameworkInitializationCompleted
método App.OnFrameworkInitializationCompleted
de nosso aplicativo usando Locator.CurrentMutable
. IViewFor<TViewModel>
necessário para que o RoutedViewHost
funcione RoutedViewHost
e o registro IScreen
necessário para que, ao desserializar, os LoginViewModel
SearchViewModel
e LoginViewModel
possam ser corretamente inicializados usando o construtor sem parâmetros e Locator.Current
.
App.xaml.cs
public override void OnFrameworkInitializationCompleted() {
Execute o aplicativo e verifique se o roteamento funciona corretamente. Se houver algum erro na marcação XAML, o compilador XamlIl usado no Avalonia nos dirá exatamente onde, no estágio de compilação. O XamlIl também suporta depuração XAML diretamente no depurador IDE !
dotnet run --framework netcoreapp3.0

Salvando e restaurando todo o estado do aplicativo
Agora que o roteamento está configurado e funcionando, a parte divertida começa - você precisa implementar a gravação de dados em disco ao fechar o aplicativo e a leitura de dados do disco quando ele inicia, juntamente com o estado do roteador. A inicialização de ganchos que escutam os eventos de início e fechamento do aplicativo é tratada por uma classe AutoSuspendHelper
especial, AutoSuspendHelper
para cada plataforma que o ReactiveUI suporta. A tarefa do desenvolvedor é inicializar essa classe no início da raiz da composição do aplicativo. Também é necessário inicializar a propriedade RxApp.SuspensionHost.CreateNewAppState
função que retornará o estado padrão do aplicativo se não houver um estado salvo ou um erro inesperado, ou se o arquivo salvo estiver danificado.
Em seguida, você precisa chamar o método RxApp.SuspensionHost.SetupDefaultSuspendResume
, passando a implementação do ISuspensionDriver
, o driver que ISuspensionDriver
e lê o objeto de estado. Para implementar o ISuspensionDriver
, usamos a biblioteca Newtonsoft.Json
e o espaço para nome System.IO
para trabalhar com o sistema de arquivos. Para fazer isso, instale o pacote 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() {
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
Akavache — UserAccount
Secure
iOS UWP , , Android BundleSuspensionDriver ReactiveUI.AndroidSupport
. JSON Xamarin.Essentials SecureStorage
. , — !