
Les interfaces utilisateur des applications d'entreprise modernes sont assez complexes. En tant que développeur, vous devez souvent implémenter la navigation dans l'application, valider l'entrée utilisateur, afficher ou masquer les écrans en fonction des préférences de l'utilisateur. Pour une meilleure expérience utilisateur, votre application doit être capable d'enregistrer l'état sur le disque lorsque l'application est suspendue et de restaurer l'état lorsque l'application reprend.
ReactiveUI fournit des fonctionnalités vous permettant de conserver l'état de l'application en sérialisant l'arborescence du modèle de vue lorsque l'application s'arrête ou se suspend. Les événements de suspension varient selon la plate-forme. ReactiveUI utilise l'événement Exit
pour WPF, ActivityPaused
pour Xamarin.Android, DidEnterBackground
pour Xamarin.iOS, OnLaunched
pour UWP.
Dans ce didacticiel, nous allons créer un exemple d'application qui montre l'utilisation de la fonction de suspension ReactiveUI avec Avalonia - une infrastructure d'interface graphique multiplateforme basée sur .NET Core XAML. Vous êtes censé être familier avec le modèle MVVM et avec les extensions réactives avant de lire cette note. Les étapes décrites dans le didacticiel devraient fonctionner si vous utilisez Windows 10 ou Ubuntu 18 et que le SDK .NET Core est installé. Commençons!
Démarrage du projet
Pour voir le routage ReactiveUI en action, nous créons un nouveau projet .NET Core basé sur les modèles d'application Avalonia. Ensuite, nous installons le package Avalonia.ReactiveUI
. Le package fournit des crochets de cycle de vie Avalonia spécifiques à la plate-forme, une infrastructure de routage et d' activation . N'oubliez pas d'installer .NET Core et git avant d'exécuter les commandes ci-dessous.
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
Lançons l'application et assurons qu'elle affiche une fenêtre affichant "Bienvenue en Avalonia!"
# Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0

Installation d'Avalonia Preview Builds à partir de MyGet
Les derniers packages Avalonia sont publiés sur MyGet chaque fois qu'un nouveau commit est poussé vers la branche principale du référentiel Avalonia sur GitHub. Pour utiliser les derniers packages de MyGet dans notre application, nous allons créer un fichier nuget.config
. Mais avant de faire cela, nous générons un fichier sln
pour le projet créé précédemment, en utilisant .NET Core CLI :
dotnet new sln # Ctrl+C dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj
Nous créons nuget.config
fichier nuget.config
avec le contenu suivant:
<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" /> </packageSources> </configuration>
Habituellement, un redémarrage est nécessaire pour forcer notre IDE à détecter les packages du flux MyGet nouvellement ajouté, mais le rechargement de la solution devrait également aider. Ensuite, nous mettons à niveau les packages Avalonia vers la version la plus récente (ou au moins vers 0.9.1
) à l'aide de l'interface graphique du gestionnaire de packages NuGet ou de la CLI .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
Le fichier ReactiveUI.Samples.Suspension.csproj
devrait maintenant ressembler à ceci:
<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>
Nous créons deux nouveaux dossiers dans le répertoire racine du projet, nommés Views/
et ViewModels/
respectivement. Ensuite, nous MainWindow
classe MainWindow
et la déplaçons dans le dossier Views/
. N'oubliez pas de renommer les références à la classe modifiée dans le fichier XAML correspondant, sinon, le projet ne sera pas compilé. N'oubliez pas non plus de changer l'espace de noms de MainView
en ReactiveUI.Samples.Suspension.Views
pour des ReactiveUI.Samples.Suspension.Views
de cohérence. Ensuite, nous App.xaml.cs
deux autres fichiers, App.xaml.cs
et App.xaml.cs
Nous ajoutons un appel à UseReactiveUI
au générateur d'application Avalonia, déplaçons le code d'initialisation de l'application vers la méthode OnFrameworkInitializationCompleted
pour se conformer aux directives de gestion de la durée de vie des applications Avalonia:
Program.cs
class Program {
App.xaml.cs
public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this);
Avant d'essayer de générer le projet, nous nous assurons que la directive using Avalonia.ReactiveUI
est ajoutée en haut du fichier Program.cs
. Notre IDE a probablement déjà importé cet espace de noms, mais si ce n'est pas le cas, nous obtiendrons une erreur au moment de la compilation. Enfin, il est temps de s'assurer que l'application se compile, s'exécute et affiche une nouvelle fenêtre:
dotnet run --framework netcoreapp2.1

Il existe deux approches générales d'organisation de la navigation dans l'application dans une application .NET multiplateforme: la vue en premier et la vue en premier. La première approche suppose que la couche View gère la pile de navigation - par exemple, en utilisant des classes Frame et Page spécifiques à la plate-forme. Avec cette dernière approche, la couche de modèle de vue prend en charge la navigation via une abstraction indépendante de la plate-forme. L'outil ReactiveUI est construit en gardant à l'esprit l'approche du modèle de vue. Le routage ReactiveUI se compose d'une implémentation IScreen
, qui contient l'état de routage actuel, plusieurs implémentations IRoutableViewModel
et un contrôle XAML spécifique à la plate-forme appelé RoutedViewHost
.

L'objet RoutingState
encapsule la gestion de la pile de navigation. IScreen
est la racine de navigation, mais malgré son nom, il n'a pas à occuper tout l'écran. RoutedViewHost
réagit aux modifications de RoutingState
lié et incorpore la vue appropriée pour le IRoutableViewModel
actuellement sélectionné. La fonctionnalité décrite sera illustrée par des exemples plus complets ultérieurement.
État du modèle de vue persistante
Prenons l'exemple d'un modèle de vue d'écran de recherche.

Nous allons décider quelles propriétés du modèle de vue enregistrer lors de l'arrêt de l'application et lesquelles recréer. Il n'est pas nécessaire de sauvegarder l'état d'une commande réactive qui implémente l'interface ICommand
. ReactiveCommand<TIn, TOut>
est généralement initialisée dans le constructeur, son indicateur CanExecute
dépend généralement entièrement des propriétés du modèle de vue et est recalculé chaque fois que l'une de ces propriétés change. C'est discutable si vous deviez conserver les résultats de la recherche, mais enregistrer la requête de recherche est une bonne idée.
ViewModels / SearchViewModel.cs
[DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery;
Nous marquons la classe de modèle de vue entière avec l'attribut [DataContract]
, [DataContract]
propriétés que nous allons sérialiser avec l'attribut [DataMember]
. Cela suffit si nous allons utiliser le mode de sérialisation opt-in. En ce qui concerne les modes de sérialisation, l'opt-out signifie que tous les champs et propriétés publics seront sérialisés, à moins que vous ne les [IgnoreDataMember]
explicitement en annotant avec l'attribut [IgnoreDataMember]
, opt-in signifie le contraire. De plus, nous implémentons l'interface IRoutableViewModel
dans notre classe de modèle de vue. Ceci est nécessaire pendant que nous allons utiliser le modèle de vue en tant que partie d'une pile de navigation.
Détails d'implémentation du modèle de vue de connexionViewModels / LoginViewModel.cs
[DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username;
Les deux modèles de vue implémentent l'interface IRoutableViewModel
et sont prêts à être intégrés dans un écran de navigation. Il est maintenant temps de mettre en œuvre l'interface IScreen
. Encore une fois, nous utilisons les attributs [DataContract]
pour indiquer les parties à sérialiser et celles à ignorer. Dans l'exemple ci-dessous, le RoutingState
propriétés RoutingState
est délibérément déclaré public - cela permet à notre sérialiseur de modifier la propriété lorsqu'elle est désérialisée.
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() {
Dans notre modèle de vue principal, nous enregistrons un seul champ sur le disque - celui de type RoutingState
. Nous n'avons pas à enregistrer l'état des commandes réactives, car leur disponibilité dépend entièrement de l'état actuel du routeur et change de manière réactive. Pour pouvoir restaurer le routeur dans son état exact, nous incluons des informations de type étendues de nos implémentations IRoutableViewModel
lors de la sérialisation du routeur. Nous utiliserons le paramètre TypenameHandling.All
de Newtonsoft.Json pour y parvenir plus tard. Nous plaçons le MainViewModel
dans le dossier ViewModels/
, ajustons l'espace de noms pour qu'il soit ReactiveUI.Samples.Suspension.ViewModels
.

Routage dans une application Avalonia
Pour l'instant, nous avons implémenté le modèle de présentation de notre application. Plus tard, les classes de modèle de vue pourraient être extraites dans un assemblage distinct ciblant .NET Standard , de sorte que la partie centrale de notre application pourrait être réutilisée dans plusieurs infrastructures d'interface graphique .NET. Il est maintenant temps de mettre en œuvre la partie GUI spécifique à Avalonia de notre application. Nous créons deux fichiers dans le dossier Views/
, nommés SearchView.xaml
et SearchView.xaml.cs
respectivement. Ce sont les deux parties d'une vue de recherche unique - la première est l'interface utilisateur décrite de manière déclarative en XAML, et la dernière contient du code C #. Il s'agit essentiellement de la vue du modèle de vue de recherche créée précédemment.
Le dialecte XAML utilisé dans Avalonia devrait être immédiatement familier aux développeurs issus de WPF, UWP ou XF. Dans l'exemple ci-dessus, nous créons une mise en page simple contenant une zone de texte et un bouton qui déclenche la recherche. Nous SearchViewModel
les propriétés et les commandes du SearchViewModel
aux contrôles déclarés dans le SearchView
.
Views / 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>
Views / SearchView.xaml.cs
public sealed class SearchView : ReactiveUserControl<SearchViewModel> { public SearchView() {
Les développeurs WPF et UWP peuvent également trouver du code derrière le fichier SearchView.xaml
. Un appel à WhenActivated
est ajouté pour exécuter la logique d'activation de la vue. L' WhenActivated
jetable venant comme premier argument pour WhenActivated
est supprimé lorsque la vue est désactivée. Si votre application utilise des observables à chaud (par exemple, des services de positionnement, des minuteries, des agrégateurs d'événements), il serait judicieux d'attacher les abonnements au jetable composite WhenActivated
en ajoutant un appel DisposeWith
, afin que la vue se désabonne de ces observables à chaud et les fuites de mémoire n'auront pas lieu.
Détails d'implémentation pour la vue de connexionVues / 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>
Vues / LoginView.xaml.cs
public sealed class LoginView : ReactiveUserControl<LoginViewModel> { public LoginView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } }
Nous Views/MainView.xaml.cs
fichiers Views/MainView.xaml
et Views/MainView.xaml.cs
. Nous ajoutons le contrôle RoutedViewHost
de l'espace de noms Avalonia.ReactiveUI
à la disposition XAML de la fenêtre principale et RoutedViewHost.Router
propriété Router
de MainViewModel
à la propriété RoutedViewHost.Router
. Nous ajoutons deux boutons, l'un ouvre la page de recherche et l'autre ouvre la page d'autorisation.
Vues / 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>
Vues / MainView.xaml.cs
public sealed class MainView : ReactiveWindow<MainViewModel> { public MainView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } }
Une simple application de démonstration de routage Avalonia et ReactiveUI est prête maintenant. Lorsqu'un utilisateur appuie sur les boutons de recherche ou de connexion, une commande qui déclenche la navigation est invoquée et le RoutingState
est mis à jour. Le contrôle RoutedViewHost
XAML observe l'état de routage et tente de résoudre l' IViewFor<TViewModel>
Locator.Current
partir de Locator.Current
. Si une IViewFor<TViewModel>
est enregistrée, une nouvelle instance du contrôle est créée et incorporée dans la fenêtre Avalonia.

Nous enregistrons nos IViewFor
et IScreen
dans la méthode App.OnFrameworkInitializationCompleted
, à l'aide de Locator.CurrentMutable
. L'enregistrement des implémentations IViewFor
est requis pour RoutedViewHost
contrôle RoutedViewHost
fonctionne. L'enregistrement d'un IScreen
permet à nos SearchViewModel
et LoginViewModel
de s'initialiser lors de la désérialisation, en utilisant le constructeur sans paramètre.
App.xaml.cs
public override void OnFrameworkInitializationCompleted() {
Lançons notre application et assurons que le routage fonctionne comme il se doit. En cas de problème avec le balisage de l'interface utilisateur XAML, le compilateur Avalonia XamlIl nous informera de toute erreur au moment de la compilation. De plus, XamlIl prend en charge le débogage de XAML !
dotnet run --framework netcoreapp3.0

Enregistrement et restauration de l'état de l'application
Il est maintenant temps d'implémenter le pilote de suspension responsable de l'enregistrement et de la restauration de l'état de l'application lorsque l'application est suspendue et reprend. La classe AutoSuspendHelper
spécifique à la plate-forme s'occupe d'initialiser les choses, vous, en tant que développeur, n'avez qu'à en créer une instance dans la racine de composition de l'application. De plus, vous devez initialiser la fabrique RxApp.SuspensionHost.CreateNewAppState
. Si l'application n'a pas de données enregistrées ou si les données enregistrées sont corrompues, ReactiveUI appelle cette méthode d'usine pour créer une instance par défaut de l'objet d'état de l'application.
Ensuite, nous RxApp.SuspensionHost.SetupDefaultSuspendResume
méthode RxApp.SuspensionHost.SetupDefaultSuspendResume
et lui transmettons une nouvelle instance de ISuspensionDriver
. ISuspensionDriver
interface du ISuspensionDriver
aide de Newtonsoft.Json et des classes de l'espace de noms 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); } }
L'approche décrite a un inconvénient - certaines classes System.IO
ne fonctionneront pas avec le cadre UWP, les applications UWP s'exécutent dans un bac à sable et font les choses différemment. C'est assez facile à résoudre - tout ce que vous devez faire est d'utiliser les classes StorageFile
et StorageFolder
au lieu de File
and Directory
lors du ciblage UWP. Pour lire la pile de navigation à partir du disque, un pilote de suspension doit prendre en charge la désérialisation des objets JSON en implémentations IRoutableViewModel
concrètes, c'est pourquoi nous utilisons le paramètre de sérialisation TypeNameHandling.All
Newtonsoft.Json. Nous enregistrons le pilote de suspension dans la racine de la composition de l'application, dans la méthode App.OnFrameworkInitializationCompleted
:
public override void OnFrameworkInitializationCompleted() {
La classe AutoSuspendHelper
du package Avalonia.ReactiveUI
configure des hooks de cycle de vie pour votre application, de sorte que le framework ReactiveUI sait quand écrire l'état de l'application sur le disque, à l'aide de l'implémentation ISuspensionDriver
fournie. Après avoir lancé notre application, le pilote de suspension créera un nouveau fichier JSON nommé appstate.json
. Après avoir apporté des modifications à l'interface utilisateur (par exemple, tapez un peu dans les champs de texte ou cliquez sur n'importe quel bouton), puis fermez l'application, le fichier appstate.json
ressemblera à ce qui suit:
appstate.jsonNotez que chaque objet JSON du fichier contient une clé $type
avec un nom de type complet, y compris l'espace de noms.
{ "$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 vous fermez l'application puis la relancez, vous verrez le même contenu à l'écran que vous avez vu auparavant! La fonctionnalité décrite fonctionne sur chaque plate-forme prise en charge par ReactiveUI , y compris UWP, WPF, Xamarin Forms ou Xamarin Native.

Bonus: l' interface du ISuspensionDriver
peut être implémentée à l'aide d' Akavache - un magasin de valeurs-clés asynchrone et persistant. Si vous stockez vos données dans la section UserAccount
ou la section Secure
, alors sur iOS et UWP vos données seront sauvegardées automatiquement dans le cloud et seront disponibles sur tous les appareils sur lesquels l'application est installée. En outre, un BundleSuspensionDriver
existe dans le package ReactiveUI.AndroidSupport
. Les API Xamarin.Essentials SecureStorage peuvent également être utilisées pour stocker des données. Vous pouvez également stocker l'état de votre application sur un serveur distant ou dans un service cloud indépendant de la plateforme.
Liens utiles