
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 {
App.xaml.cs
public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this);
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

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;
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;
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() {
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() {
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> <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() {
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() {
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
. , — !
Enlaces utiles