
Les interfaces utilisateur des applications modernes sont généralement complexes - il est souvent nécessaire de mettre en œuvre la prise en charge de la navigation de page, de traiter des champs de saisie de différents types et d'afficher ou de masquer des informations en fonction des paramètres sélectionnés par l'utilisateur. Dans le même temps, afin d'améliorer l'UX, l'application doit enregistrer l'état des éléments d'interface sur le disque pendant la suspension ou l'arrêt, restaurer l'état à partir du disque lorsque le programme est redémarré.
Le framework ReactiveUI MVVM propose de conserver l'état d'une application en sérialisant le graphe des modèles de présentation au moment où le programme est suspendu, tandis que les mécanismes de détermination du moment de suspension sont différents pour les frameworks et les plateformes. Ainsi, pour WPF, l'événement Exit
est utilisé, pour Xamarin.Android - ActivityPaused
, pour Xamarin.iOS - DidEnterBackground
, pour UWP - OnLaunched
surcharge.
Dans cet article, nous considérerons l'utilisation de ReactiveUI pour enregistrer et restaurer l'état du logiciel avec une interface graphique, y compris l'état d'un routeur, en utilisant le cadre d'interface graphique multiplateforme Avalonia comme exemple . Le matériel suppose une compréhension de base du modèle de conception MVVM et de la programmation réactive dans le contexte du langage C # et de la plate-forme .NET pour le lecteur. Les étapes de cet article s'appliquent à Windows 10 et Ubuntu 18.
Création de projet
Pour essayer le routage en action, créez un nouveau projet .NET Core à partir du modèle Avalonia, installez le package Avalonia.ReactiveUI
- une fine couche d'intégration Avalonia et ReactiveUI. Assurez-vous que le SDK .NET Core et git sont installés avant de commencer.
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
Assurez-vous que l'application démarre et affiche une fenêtre qui dit Bienvenue à Avalonia!
# Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0

Connecter les pré-builds Avalonia de MyGet
Pour vous connecter et utiliser les dernières versions d'Avalonia qui sont automatiquement publiées sur MyGet lorsque la branche master
référentiel Avalonia sur GitHub change, nous utilisons le fichier de configuration source du package nuget.config
. Pour que l'IDE et l' interface CLI .NET Core voient nuget.config
, vous devez générer un fichier sln
pour le projet créé ci-dessus. Nous utilisons les outils de la CLI .NET Core:
dotnet new sln # Ctrl+C dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj
Créez un fichier nuget.config
dans un dossier avec un fichier .sln
du 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>
Vous devrez peut-être redémarrer l'IDE ou décharger et télécharger la solution entière. Nous mettrons à jour les packages Avalonia vers la version requise (au moins 0.9.1
) en utilisant l'interface du gestionnaire de packages NuGet de votre IDE, ou en utilisant les outils de ligne de commande Windows ou le 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
ReactiveUI.Samples.Suspension.csproj
fichier de projet ReactiveUI.Samples.Suspension.csproj
ressemble maintenant à 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>
Créez les dossiers Views/
et ViewModels/
dans la racine du projet, changez le nom de la classe MainWindow
, déplacez-le dans le répertoire Views/
, en changeant les espaces de noms en conséquence en ReactiveUI.Samples.Suspension.Views
. App.xaml.cs
contenu des App.xaml.cs
et App.xaml.cs
- appliquez l'appel UseReactiveUI
au générateur d'application Avalonia, déplacez l'initialisation de la vue principale vers OnFrameworkInitializationCompleted
pour respecter les recommandations de gestion du cycle de vie de l' application:
Program.cs
class Program {
App.xaml.cs
public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this);
Vous devrez ajouter à l' using Avalonia.ReactiveUI
à Program.cs
. Assurez-vous qu'après la mise à jour des packages, le projet démarre et affiche la fenêtre de bienvenue par défaut.
# Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0

En règle générale, il existe deux approches principales pour implémenter la navigation entre les pages d'une application .NET: la vue en premier et la vue en premier. L'approche View-first implique de contrôler la pile de navigation et la navigation entre les pages au niveau View dans la terminologie MVVM - par exemple, en utilisant les classes Frame et Page dans le cas d'UWP ou WPF, et lorsque vous utilisez l'approche view model-first, la navigation est implémentée au niveau des modèles de présentation. Les outils ReactiveUI qui organisent le routage dans l'application se concentrent sur l'utilisation de l'approche du modèle de vue en premier. Le routage ReactiveUI consiste en une implémentation IScreen
contenant l'état du routeur, plusieurs implémentations IRoutableViewModel
et le contrôle XAML RoutedViewHost
la plate-forme, RoutedViewHost
.

L'état du routeur est représenté par un objet RoutingState
qui contrôle la pile de navigation. IScreen
est la racine de la pile de navigation et il peut y avoir plusieurs racines de navigation dans l'application. RoutedViewHost
surveille l'état de son routeur RoutingState
correspondant, répondant aux changements dans la pile de navigation en incorporant le IRoutableViewModel
correspondant du contrôle XAML. La fonctionnalité décrite sera illustrée par des exemples ci-dessous.
Enregistrement des modèles d'état de vue sur le disque
Prenons un exemple typique d'un écran de recherche d'informations.

Nous devons décider quels éléments du modèle de représentation d'écran enregistrer sur le disque pendant la pause ou l'arrêt de l'application, et quels éléments recréer à chaque démarrage. Il n'est pas nécessaire d'enregistrer l'état des commandes ReactiveUI qui implémentent l'interface ICommand
et sont attachées aux boutons - ReactiveCommand<TIn, TOut>
sont créés et initialisés dans le constructeur, tandis que l'état de l'indicateur CanExecute
dépend des propriétés du modèle de vue et est recalculé lorsqu'ils changent. La nécessité de sauvegarder les résultats de la recherche - un point discutable - dépend des spécificités de l'application, mais il serait sage de sauvegarder et de restaurer l'état du champ de saisie SearchQuery
!
ViewModels / SearchViewModel.cs
[DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery;
La classe du modèle de vue est marquée avec l'attribut [DataContract]
et les propriétés qui doivent être sérialisées avec les [DataMember]
. Cela suffit si le sérialiseur utilisé utilise l'approche opt-in - il enregistre uniquement les propriétés qui sont explicitement marquées avec des attributs sur le disque; dans le cas de l'approche opt-out, il est nécessaire de marquer avec les attributs [IgnoreDataMember]
les propriétés qui n'ont pas besoin d'être enregistrées sur le disque. De plus, nous implémentons l'interface IRoutableViewModel
dans notre modèle de vue afin qu'elle puisse plus tard faire partie du cadre de navigation du routeur d'application.
De même, nous implémentons le modèle de présentation des pages d'autorisationViewModels / LoginViewModel.cs
[DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username;
Les modèles de présentation pour deux pages de l'application sont prêts, implémentent l'interface IRoutableViewModel
et peuvent être intégrés au routeur IScreen
. Maintenant, nous implémentons directement IScreen
. Nous marquons à l'aide des attributs [DataContract]
propriétés du modèle de vue à sérialiser et celles à ignorer. Faites attention au setter public de la propriété marquée avec l' [DataMember]
, dans l'exemple ci-dessous - la propriété est intentionnellement ouverte pour l'écriture afin que le sérialiseur puisse modifier une instance fraîchement créée de l'objet pendant la désérialisation du modèle.
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 application, seul RoutingState
doit être enregistré sur le disque; pour des raisons évidentes, les commandes n'ont pas besoin d'être enregistrées sur le disque - leur état dépend entièrement du routeur. L'objet sérialisé doit inclure des informations étendues sur les types qui implémentent IRoutableViewModel
afin que la pile de navigation puisse être restaurée lors de la désérialisation. Nous décrivons la logique du MainViewModel
vue ViewModels/MainViewModel.cs
la classe dans ViewModels/MainViewModel.cs
et dans l'espace de noms ReactiveUI.Samples.Suspension.ViewModels
correspondant.

Routage dans l'application Avalonia
La logique de l'interface utilisateur au niveau des couches du modèle et du modèle de présentation de l'application de démonstration est implémentée et peut être déplacée vers un assemblage distinct destiné à la norme .NET, car elle ne sait rien du cadre GUI utilisé. Jetons un coup d'œil à la couche de présentation. Dans la terminologie MVVM, la couche de présentation est responsable du rendu de l'état du modèle de présentation à l'écran. Pour rendre l'état actuel du routeur RoutingState
, le contrôle XAML RoutedViewHost
contenu dans le package Avalonia.ReactiveUI
est Avalonia.ReactiveUI
. Nous implémentons l'interface graphique pour SearchViewModel
- pour cela, dans le répertoire Views/
, créez deux fichiers: SearchView.xaml
et SearchView.xaml.cs
.
Une description de l'interface utilisateur utilisant le dialecte XAML utilisé dans Avalonia est susceptible de sembler familière aux développeurs sur Windows Presentation Foundation, Universal Windows Platform ou Xamarin.Forms. Dans l'exemple ci-dessus, nous créons une interface triviale pour le formulaire de recherche - nous dessinons un champ de texte pour entrer la requête de recherche et un bouton qui démarre la recherche, tandis que nous lions les contrôles aux propriétés du modèle SearchViewModel
défini ci-dessus.
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() {
Le code-behind du contrôle SearchView.xaml
apparaîtra également aux développeurs WPF, UWP et XF familiers. L'appel WhenActivated est utilisé pour exécuter du code lorsque la vue ou le modèle de vue est activé et désactivé. Si votre application utilise des observables à chaud (minuteries, géolocalisation, connexion au bus de messages), il serait judicieux de les attacher au CompositeDisposable
appelant DisposeWith
afin que lorsque vous DisposeWith
contrôle XAML et son modèle de vue correspondant de l'arborescence visuelle, les observables à chaud cessent de publier de nouvelles valeurs et il n'y a pas de fuite. mémoire.
De même, nous implémentons la représentation de la page 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); } }
Modifiez les Views/MainView.xaml.cs
Views/MainView.xaml
et Views/MainView.xaml.cs
. RoutedViewHost
XAML RoutedViewHost
partir de l'espace de noms RoutedViewHost
sur l'écran principal, affectez l'état du routeur RoutingState
à la propriété RoutingState
. Ajoutez des boutons pour accéder aux pages de recherche et d'autorisation, associez-les aux propriétés ViewModels/MainViewModel
décrites ci-dessus.
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 application simple démontrant les capacités de routage ReactiveUI et Avalonia est prête. Lorsque vous cliquez sur les boutons Search
et Login
, les commandes correspondantes sont appelées, une nouvelle instance du modèle de vue est créée et le RoutingState
mis à jour. Le contrôle XAML RoutedViewHost
, qui souscrit aux modifications apportées à RoutingState
, essaie d'obtenir le type IViewFor<TViewModel>
, où TViewModel
est le type de modèle de vue de Locator.Current
. Si une implémentation enregistrée d' IViewFor<TViewModel>
trouvée, une nouvelle instance sera créée, intégrée à RoutedViewHost
et affichée dans la fenêtre de l'application Avalonia.

Nous enregistrons les composants nécessaires IViewFor<TViewModel>
et IScreen
dans la méthode App.OnFrameworkInitializationCompleted
de notre application en utilisant Locator.CurrentMutable
. IViewFor<TViewModel>
nécessaire pour que RoutedViewHost
fonctionne RoutedViewHost
, et l'enregistrement IScreen
nécessaire pour que lors de la désérialisation, les LoginViewModel
SearchViewModel
et LoginViewModel
puissent être correctement initialisés à l'aide du constructeur sans paramètres et Locator.Current
.
App.xaml.cs
public override void OnFrameworkInitializationCompleted() {
Exécutez l'application et assurez-vous que le routage fonctionne correctement. S'il y a des erreurs dans le balisage XAML, le compilateur XamlIl utilisé dans Avalonia nous dira exactement où, au stade de la compilation. XamlIl prend également en charge le débogage XAML directement dans le débogueur IDE !
dotnet run --framework netcoreapp3.0

Enregistrement et restauration de l'état complet de l'application
Maintenant que le routage est configuré et fonctionne, la partie la plus intéressante commence - vous devez implémenter l'enregistrement des données sur le disque lorsque vous fermez l'application et la lecture des données du disque au démarrage, ainsi que l'état du routeur. L'initialisation des hooks qui écoutent les événements de démarrage et de fermeture de l'application est gérée par une classe AutoSuspendHelper
spéciale, AutoSuspendHelper
à chaque plate-forme prise en charge par ReactiveUI . La tâche du développeur est d'initialiser cette classe au tout début de la racine de composition d'application. Il est également nécessaire d'initialiser la propriété RxApp.SuspensionHost.CreateNewAppState
fonction qui renverra l'état par défaut de l'application s'il n'y a aucun état enregistré ou si une erreur inattendue s'est produite ou si le fichier enregistré est endommagé.
Ensuite, vous devez appeler la méthode RxApp.SuspensionHost.SetupDefaultSuspendResume
, en lui passant l'implémentation d' ISuspensionDriver
, le pilote qui ISuspensionDriver
et lit l'objet d'état. Pour implémenter ISuspensionDriver
, ISuspensionDriver
utilisons la bibliothèque Newtonsoft.Json
et l'espace de noms System.IO
pour travailler avec le système de fichiers. Pour ce faire, installez le package 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.jsonVeuillez noter que chaque objet comprend un champ $type
contenant des informations sur le nom complet du type et le nom complet de l'assembly dans lequel se trouve le 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
. , — !
Liens utiles