
Las interfaces de usuario de las aplicaciones empresariales modernas son bastante complejas. Usted, como desarrollador, a menudo necesita implementar la navegación en la aplicación, validar la entrada del usuario, mostrar u ocultar pantallas según las preferencias del usuario. Para una mejor experiencia de usuario, su aplicación debe ser capaz de guardar el estado en el disco cuando la aplicación se suspende y restaurar el estado cuando se reanuda.
ReactiveUI proporciona funciones que le permiten mantener el estado de la aplicación al serializar el árbol del modelo de vista cuando la aplicación se está cerrando o suspendiendo. Los eventos de suspensión varían según la plataforma. ReactiveUI usa el evento Exit
para WPF, ActivityPaused
para Xamarin.Android, DidEnterBackground
para Xamarin.iOS, OnLaunched
para UWP.
En este tutorial vamos a construir una aplicación de muestra que demuestre el uso de la función de suspensión ReactiveUI con Avalonia , un marco de GUI multiplataforma basado en .NET Core basado en XAML. Se espera que esté familiarizado con el patrón MVVM y con las extensiones reactivas antes de leer esta nota. Los pasos descritos en el tutorial deberían funcionar si está utilizando Windows 10 o Ubuntu 18 y tiene instalado .NET Core SDK. ¡Empecemos!
Bootstrapping el proyecto
Para ver el enrutamiento ReactiveUI en acción, creamos un nuevo proyecto .NET Core basado en plantillas de aplicación Avalonia. Luego instalamos el paquete Avalonia.ReactiveUI
. El paquete proporciona enganches de ciclo de vida, enrutamiento y activación de Avalonia específicos de la plataforma. Recuerde instalar .NET Core y git antes de ejecutar los siguientes comandos.
git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates git --git-dir ./avalonia-dotnet-templates/.git checkout 9263c6b dotnet new --install ./avalonia-dotnet-templates dotnet new avalonia.app -o ReactiveUI.Samples.Suspension cd ./ReactiveUI.Samples.Suspension dotnet add package Avalonia.ReactiveUI dotnet add package Avalonia.Desktop dotnet add package Avalonia
Ejecutemos la aplicación y asegúrese de que muestre una ventana que muestre "¡Bienvenido a Avalonia!"
# Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0

Instalación de compilaciones de vista previa de Avalonia desde MyGet
Los últimos paquetes de Avalonia se publican en MyGet cada vez que se envía una nueva confirmación a la rama master
del repositorio de Avalonia en GitHub. Para usar los últimos paquetes de MyGet en nuestra aplicación, vamos a crear un archivo nuget.config
. Pero antes de hacer esto, generamos un archivo sln
para el proyecto creado anteriormente, usando .NET Core CLI :
dotnet new sln # Ctrl+C dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj
Ahora creamos el archivo nuget.config
con el siguiente contenido:
<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" /> </packageSources> </configuration>
Por lo general, se requiere un reinicio para forzar a nuestro IDE a detectar paquetes de la fuente MyGet recién agregada, pero la recarga de la solución también debería ayudar. Luego, actualizamos los paquetes de Avalonia a la versión más reciente (o al menos a 0.9.1
) usando la GUI del administrador de paquetes NuGet o la CLI de .NET Core:
dotnet add package Avalonia.ReactiveUI --version 0.9.1 dotnet add package Avalonia.Desktop --version 0.9.1 dotnet add package Avalonia --version 0.9.1 cat ReactiveUI.Samples.Suspension.csproj
El archivo ReactiveUI.Samples.Suspension.csproj
debería verse algo así como ahora:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> </PropertyGroup> <ItemGroup> <Compile Update="**\*.xaml.cs"> <DependentUpon>%(Filename)</DependentUpon> </Compile> <AvaloniaResource Include="**\*.xaml"> <SubType>Designer</SubType> </AvaloniaResource> </ItemGroup> <ItemGroup> <PackageReference Include="Avalonia" Version="0.9.1" /> <PackageReference Include="Avalonia.Desktop" Version="0.9.1" /> <PackageReference Include="Avalonia.ReactiveUI" Version="0.9.1" /> </ItemGroup> </Project>
Creamos dos carpetas nuevas dentro del directorio raíz del proyecto, denominadas Views/
y ViewModels/
respectivamente. A continuación, cambiamos el nombre de la clase MainWindow
a MainView
y la movemos a la carpeta Views/
. Recuerde cambiar el nombre de las referencias a la clase editada en el archivo XAML correspondiente; de lo contrario, el proyecto no se compilará. Además, recuerde cambiar el espacio de nombres para MainView
a ReactiveUI.Samples.Suspension.Views
para MainView
coherencia. Luego, editamos otros dos archivos, Program.cs
y App.xaml.cs
UseReactiveUI
una llamada a UseReactiveUI
al UseReactiveUI
de la aplicación Avalonia, mueve el código de inicialización de la aplicación al método OnFrameworkInitializationCompleted
para cumplir con las pautas de administración de por vida de la aplicación Avalonia:
Program.cs
class Program {
App.xaml.cs
public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this);
Antes de intentar compilar el proyecto, nos aseguramos de que la directiva que using Avalonia.ReactiveUI
se agregue al principio del archivo Program.cs
. Lo más probable es que nuestro IDE ya haya importado ese espacio de nombres, pero si no lo hizo, obtendremos un error en tiempo de compilación. Finalmente, es hora de asegurarse de que la aplicación compila, se ejecuta y muestra una nueva ventana:
dotnet run --framework netcoreapp2.1

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

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

Vamos a decidir qué propiedades del modelo de vista guardar en el cierre de la aplicación y cuáles recrear. No es necesario guardar el estado de un comando reactivo que implementa la interfaz ICommand
. ReactiveCommand<TIn, TOut>
generalmente se inicializa en el constructor, su indicador CanExecute
generalmente depende completamente de las propiedades del modelo de vista y se recalcula cada vez que cambia cualquiera de esas propiedades. Es discutible si tuviera que mantener los resultados de búsqueda, pero guardar la consulta de búsqueda es una buena idea.
ViewModels / SearchViewModel.cs
[DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery;
Marcamos toda la clase de modelo de vista con el atributo [DataContract]
, anotamos propiedades que vamos a serializar con el atributo [DataMember]
. Esto es suficiente si vamos a utilizar el modo de serialización opcional. Teniendo en cuenta los modos de serialización, la opción de exclusión significa que todos los campos y propiedades públicas se serializarán, a menos que los ignore explícitamente al anotar con el atributo [IgnoreDataMember]
, la opción de [IgnoreDataMember]
significa lo contrario. Además, implementamos la interfaz IRoutableViewModel
en nuestra clase de modelo de vista. Esto es necesario mientras vamos a usar el modelo de vista como parte de una pila de navegación.
Detalles de implementación para el modelo de vista de inicio de sesiónViewModels / LoginViewModel.cs
[DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username;
Los dos modelos de vista implementan la interfaz IRoutableViewModel
y están listos para integrarse en una pantalla de navegación. Ahora es el momento de implementar la interfaz IScreen
. Nuevamente, usamos los atributos [DataContract]
para indicar qué partes serializar y cuáles ignorar. En el ejemplo a continuación, el RoutingState
propiedades RoutingState se declara deliberadamente como público; esto permite que nuestro serializador modifique la propiedad cuando se deserializa.
ViewModels / MainViewModel.cs
[DataContract] public class MainViewModel : ReactiveObject, IScreen { private readonly ReactiveCommand<Unit, Unit> _search; private readonly ReactiveCommand<Unit, Unit> _login; private RoutingState _router = new RoutingState(); public MainViewModel() {
En nuestro modelo de vista principal, guardamos solo un campo en el disco, el de tipo RoutingState
. No tenemos que guardar el estado de los comandos reactivos, ya que su disponibilidad depende completamente del estado actual del enrutador y cambia de forma reactiva. Para poder restaurar el enrutador al estado exacto en el que estaba, incluimos información de tipo extendido de nuestras implementaciones de IRoutableViewModel
al serializar el enrutador. Usaremos TypenameHandling.All
configuración de Newtonsoft.Json para lograr esto más adelante. Ponemos el MainViewModel
en la carpeta ViewModels/
, ajustamos el espacio de nombres para que sea ReactiveUI.Samples.Suspension.ViewModels
.

Enrutamiento en una aplicación Avalonia
Por el momento, hemos implementado el modelo de presentación de nuestra aplicación. Más tarde, las clases de modelo de vista podrían extraerse en un ensamblaje separado dirigido a .NET Standard , por lo que la parte central de nuestra aplicación podría reutilizarse en múltiples marcos de .NET GUI. Ahora es el momento de implementar la parte GUI específica de Avalonia de nuestra aplicación. Creamos dos archivos en la carpeta Views/
, llamados SearchView.xaml
y SearchView.xaml.cs
respectivamente. Estas son las dos partes de una sola vista de búsqueda: la primera es la IU descrita declarativamente en XAML, y la segunda contiene código C # subyacente. Esta es esencialmente la vista para el modelo de vista de búsqueda creado anteriormente.
El dialecto XAML utilizado en Avalonia debería sentirse inmediatamente familiar para los desarrolladores que vienen de WPF, UWP o XF. En el ejemplo anterior, creamos un diseño simple que contiene un cuadro de texto y un botón que activa la búsqueda. Vinculamos propiedades y comandos del SearchViewModel
a los controles declarados en el SearchView
.
Vistas / SearchView.xaml
<UserControl xmlns="https://github.com/avaloniaui" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DataContext="{d:DesignInstance viewModels:SearchViewModel}" xmlns:viewModels="clr-namespace:ReactiveUI.Samples.Suspension.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ReactiveUI.Samples.Suspension.Views.SearchView" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="Search view" Margin="5" /> <TextBox Grid.Row="1" Text="{Binding SearchQuery, Mode=TwoWay}" /> <Button Grid.Row="2" Content="Search" Command="{Binding Search}" /> </Grid> </UserControl>
Vistas / SearchView.xaml.cs
public sealed class SearchView : ReactiveUserControl<SearchViewModel> { public SearchView() {
Los desarrolladores de WPF y UWP también pueden encontrar código subyacente para el archivo SearchView.xaml
. Se WhenActivated
una llamada a WhenActivated
para ejecutar la lógica de activación de vista. El desechable que viene como primer argumento para WhenActivated
se desecha cuando la vista se desactiva. Si su aplicación utiliza observables activos (por ejemplo, servicios de posicionamiento, temporizadores, agregadores de eventos), sería una buena decisión adjuntar las suscripciones al material desechable compuesto WhenActivated
agregando una llamada DisposeWith
, de modo que la vista se cancele la suscripción de esos observables DisposeWith
y no se producirán pérdidas de memoria.
Detalles de implementación para la vista de inicio de sesiónVistas / LoginView.xaml
<UserControl xmlns="https://github.com/avaloniaui" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DataContext="{d:DesignInstance viewModels:LoginViewModel, IsDesignTimeCreatable=True}" xmlns:viewModels="clr-namespace:ReactiveUI.Samples.Suspension.ViewModels" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ReactiveUI.Samples.Suspension.Views.LoginView" xmlns:reactiveUi="http://reactiveui.net" mc:Ignorable="d"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="48" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <TextBlock Grid.Row="0" Text="Login view" Margin="5" /> <TextBox Grid.Row="1" Text="{Binding Username, Mode=TwoWay}" /> <TextBox Grid.Row="2" PasswordChar="*" Text="{Binding Password, Mode=TwoWay}" /> <Button Grid.Row="3" Content="Login" Command="{Binding Login}" /> </Grid> </UserControl>
Views / LoginView.xaml.cs
public sealed class LoginView : ReactiveUserControl<LoginViewModel> { public LoginView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } }
Views/MainView.xaml.cs
archivos Views/MainView.xaml
y Views/MainView.xaml.cs
. RoutedViewHost
control RoutedViewHost
del espacio de nombres Avalonia.ReactiveUI
al diseño XAML de la ventana principal y MainViewModel
propiedad Router
de MainViewModel
a la propiedad RoutedViewHost.Router
. Agregamos dos botones, uno abre la página de búsqueda y otro abre la página de autorización.
Vistas / MainView.xaml
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="ReactiveUI.Samples.Suspension.Views.MainView" xmlns:reactiveUi="http://reactiveui.net" Title="ReactiveUI.Samples.Suspension"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="48" /> </Grid.RowDefinitions> <reactiveUi:RoutedViewHost Grid.Row="0" Router="{Binding Router}"> <reactiveUi:RoutedViewHost.DefaultContent> <TextBlock Text="Default Content" /> </reactiveUi:RoutedViewHost.DefaultContent> </reactiveUi:RoutedViewHost> <Grid Grid.Row="1"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> <ColumnDefinition Width="*" /> </Grid.ColumnDefinitions> <Button Grid.Column="0" Command="{Binding Search}" Content="Search" /> <Button Grid.Column="1" Command="{Binding Login}" Content="Login" /> <Button Grid.Column="2" Command="{Binding Router.NavigateBack}" Content="Back" /> </Grid> </Grid> </Window>
Vistas / MainView.xaml.cs
public sealed class MainView : ReactiveWindow<MainViewModel> { public MainView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } }
Una simple aplicación de demostración de enrutamiento Avalonia y ReactiveUI está lista ahora. Cuando un usuario presiona los botones de búsqueda o inicio de sesión, se invoca un comando que activa la navegación y se actualiza RoutingState
. El control RoutedViewHost
XAML observa el estado de enrutamiento, intenta resolver la IViewFor<TViewModel>
apropiada de IViewFor<TViewModel>
desde Locator.Current
. Si se registra una implementación IViewFor<TViewModel>
, se crea una nueva instancia del control y se incrusta en la ventana de Avalonia.

Registramos nuestras implementaciones IViewFor
e IScreen
en el método App.OnFrameworkInitializationCompleted
, usando Locator.CurrentMutable
. Se requiere registrar implementaciones IViewFor
para que RoutedViewHost
control RoutedViewHost
. El registro de una IScreen
permite que IScreen
y LoginViewModel
inicialicen durante la deserialización, utilizando el constructor sin parámetros.
App.xaml.cs
public override void OnFrameworkInitializationCompleted() {
Iniciemos nuestra aplicación y garanticemos que el enrutamiento funcione como debería. Si algo sale mal con el marcado de XAML UI, el compilador Avalonia XamlIl nos notificará sobre cualquier error en el momento de la compilación. ¡Además, XamlIl admite la depuración de XAML !
dotnet run --framework netcoreapp3.0

Guardar y restaurar el estado de la aplicación
Ahora es el momento de implementar el controlador de suspensión responsable de guardar y restaurar el estado de la aplicación cuando la aplicación se suspende y se reanuda. La clase AutoSuspendHelper
específica de la AutoSuspendHelper
se encarga de inicializar las cosas, usted, como desarrollador, solo necesita crear una instancia de la misma en la raíz de composición de la aplicación. Además, debe inicializar la fábrica RxApp.SuspensionHost.CreateNewAppState
. Si la aplicación no tiene datos guardados, o si los datos guardados están corruptos, ReactiveUI invoca ese método de fábrica para crear una instancia predeterminada del objeto de estado de la aplicación.
Luego, invocamos el método RxApp.SuspensionHost.SetupDefaultSuspendResume
y le pasamos una nueva instancia de ISuspensionDriver
. Implementemos la interfaz del ISuspensionDriver
usando Newtonsoft.Json y las clases del espacio de nombres System.IO
.
dotnet add package Newtonsoft.Json
Controladores / NewtonsoftJsonSuspensionDriver.cs
public class NewtonsoftJsonSuspensionDriver : ISuspensionDriver { private readonly string _file; private readonly JsonSerializerSettings _settings = new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.All }; public NewtonsoftJsonSuspensionDriver(string file) => _file = file; public IObservable<Unit> InvalidateState() { if (File.Exists(_file)) File.Delete(_file); return Observable.Return(Unit.Default); } public IObservable<object> LoadState() { var lines = File.ReadAllText(_file); var state = JsonConvert.DeserializeObject<object>(lines, _settings); return Observable.Return(state); } public IObservable<Unit> SaveState(object state) { var lines = JsonConvert.SerializeObject(state, _settings); File.WriteAllText(_file, lines); return Observable.Return(Unit.Default); } }
El enfoque descrito tiene un inconveniente: algunas clases de System.IO
no funcionarán con el marco UWP, las aplicaciones UWP se ejecutan en un entorno limitado y hacen las cosas de manera diferente. Eso es bastante fácil de resolver: todo lo que necesita hacer es usar las clases StorageFile
y StorageFolder
lugar de File
and Directory
cuando se dirige a UWP. Para leer la pila de navegación del disco, un controlador de suspensión debe admitir la deserialización de objetos JSON en implementaciones concretas de IRoutableViewModel
, es por eso que utilizamos la configuración de serializador TypeNameHandling.All
Newtonsoft.Json. Registramos el controlador de suspensión en la raíz de composición de la aplicación, en el método App.OnFrameworkInitializationCompleted
:
public override void OnFrameworkInitializationCompleted() {
La clase AutoSuspendHelper
del paquete Avalonia.ReactiveUI
configura AutoSuspendHelper
de ciclo de vida para su aplicación, por lo que el marco ReactiveUI sabrá cuándo escribir el estado de la aplicación en el disco, utilizando la implementación ISuspensionDriver
proporcionada. Después de lanzar nuestra aplicación, el controlador de suspensión creará un nuevo archivo JSON llamado appstate.json
. Después de hacer cambios en la interfaz de usuario (por ejemplo, escribir algo en los campos de texto o hacer clic en cualquier botón) y luego cerrar la aplicación, el archivo appstate.json
tendrá un aspecto similar al siguiente:
appstate.jsonTenga en cuenta que cada objeto JSON en el archivo contiene una clave $type
con un nombre de tipo totalmente calificado, incluido el espacio de nombres.
{ "$type": "ReactiveUI.Samples.Suspension.ViewModels.MainViewModel, ReactiveUI.Samples.Suspension", "Router": { "$type": "ReactiveUI.RoutingState, ReactiveUI", "_navigationStack": { "$type": "System.Collections.ObjectModel.ObservableCollection`1[[ReactiveUI.IRoutableViewModel, ReactiveUI]], System.ObjectModel", "$values": [ { "$type": "ReactiveUI.Samples.Suspension.ViewModels.SearchViewModel, ReactiveUI.Samples.Suspension", "SearchQuery": "funny cats" }, { "$type": "ReactiveUI.Samples.Suspension.ViewModels.LoginViewModel, ReactiveUI.Samples.Suspension", "Username": "worldbeater" } ] } } }
Si cierra la aplicación y luego la inicia de nuevo, verá el mismo contenido en la pantalla que había visto antes. La funcionalidad descrita funciona en cada plataforma compatible con ReactiveUI , incluidos UWP, WPF, Xamarin Forms o Xamarin Native.

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