
现代应用程序的用户界面通常很复杂-通常有必要实现页面导航支持,处理各种输入字段以及基于用户选择的参数显示或隐藏信息。 同时,为了改善UX,应用程序必须在挂起或关闭期间将接口元素的状态保存到磁盘,并在重新启动程序时从磁盘恢复状态。
ReactiveUI MVVM框架建议通过在程序暂停时对表示模型的图形进行序列化来保留应用程序的状态,而对于框架和平台而言,确定暂停时间的机制是不同的。 因此,对于WPF,对于Xamarin.Android- ActivityPaused
,对于Xamarin.iOS- DidEnterBackground
,对于UWP- OnLaunched
重载都使用Exit
事件。
在本文中,我们将以Avalonia跨平台GUI框架为例 ,考虑使用ReactiveUI通过GUI保存和恢复软件状态,包括路由器状态。 该材料假定读者对CVM语言和.NET平台中的MVVM设计模式和反应式编程有基本的了解。 本文中的步骤适用于Windows 10和Ubuntu 18。
项目创建
要尝试实际进行路由,请从Avalonia模板创建一个新的.NET Core项目,安装Avalonia.ReactiveUI
程序包-Avalonia和ReactiveUI集成的薄层。 开始之前,请确保已安装.NET Core SDK和git 。
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
确保应用程序启动并显示一个窗口,显示欢迎使用Avalonia!
# Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0

从MyGet连接Avalonia预构建
要连接并使用当GitHub上的nuget.config
存储库 master
分支更改时自动发布到MyGet的最新Avalonia构建,我们使用nuget.config
包源配置文件。 为了使IDE和.NET Core CLI查看nuget.config
,您需要为上面创建的项目生成一个sln
文件。 我们使用.NET Core CLI的工具:
dotnet new sln # Ctrl+C dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj
在包含以下内容的.sln
文件的文件夹中创建nuget.config
文件:
<?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" /> </packageSources> </configuration>
您可能需要重新启动IDE,或卸载并下载整个解决方案。 我们将使用IDE的NuGet包管理器界面,或使用Windows命令行工具或Linux终端,将Avalonia包更新为所需版本(至少为0.9.1
)。
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
项目文件如下所示:
<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>
在项目根目录中创建Views/
和ViewModels/
文件夹,为方便起见,将MainWindow
类名更改为MainView
,将其移动到Views/
目录,相应地将名称空间更改为ReactiveUI.Samples.Suspension.Views
。 App.xaml.cs
Program.cs
和App.xaml.cs
内容-将UseReactiveUI
调用应用于UseReactiveUI
应用程序构建器,将主视图的初始化移动到OnFrameworkInitializationCompleted
以符合应用程序生命周期管理建议 :
Program.cs
class Program {
App.xaml.cs
public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this);
您将需要using Avalonia.ReactiveUI
添加到Program.cs
。 确保更新软件包后,项目启动并显示默认的欢迎窗口。
# Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0

通常,在.NET应用程序的页面之间实现导航的主要方法有两种:视图优先和视图模型优先。 视图优先方法涉及以MVVM术语在视图级别控制导航堆栈和页面之间的导航-例如,在UWP或WPF的情况下使用Frame和Page类,而在使用视图模型优先方法的情况下,导航在表示模型级别实现。 在应用程序中组织路由的ReactiveUI工具专注于使用视图模型优先方法。 ReactiveUI路由包括一个包含路由器状态的IRoutableViewModel
实现IRoutableViewModel
多个IRoutableViewModel
实现以及RoutedViewHost
于平台的XAML控件RoutedViewHost
。

路由器的状态由控制导航堆栈的RoutingState
对象表示。 IScreen
是导航堆栈的根,在应用程序中可能有多个导航根。 RoutedViewHost
监视其相应的RoutingState
路由器的状态,通过嵌入XAML控件的相应IRoutableViewModel
来响应导航堆栈中的更改。 下面的示例将说明所描述的功能。
将视图模型的状态保存到磁盘
考虑信息搜索屏幕的典型示例。

我们必须决定在应用程序暂停或关闭期间将屏幕表示模型的哪些元素保存到磁盘,以及在每次启动时重新创建。 无需保存实现ICommand
接口并附加到按钮的ReactiveUI命令的状态ReactiveCommand<TIn, TOut>
在构造函数中创建和初始化,而CanExecute
指示器的状态取决于视图模型的属性,并在它们更改时重新计算。 是否需要保存搜索结果(有争议的点)取决于应用程序的具体情况,但是明智的做法是保存和恢复SearchQuery
输入字段的状态!
ViewModels / SearchViewModel.cs
[DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery;
视图模型的类标记有[DataContract]
属性,而需要序列化的[DataMember]
具有[DataMember]
。 如果所使用的序列化程序使用“选择加入”方法就足够了-它仅将使用属性明确标记的属性保存到磁盘;对于选择“退出”方法,则需要使用[IgnoreDataMember]
属性标记那些不需要保存到磁盘的属性。 另外,我们在视图模型中实现IRoutableViewModel
接口,以便以后它可以成为应用程序路由器导航堆栈的一部分。
同样,我们实现授权页面展示模型ViewModels / LoginViewModel.cs
[DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username;
该应用程序的两个页面的演示模型已准备就绪,可以实现IRoutableViewModel
接口,并且可以内置到IScreen
路由器中。 现在我们直接实现IScreen
。 我们借助[DataContract]
属性标记要序列化视图模型的哪些属性,以及忽略哪些属性。 在下面的示例中,请注意标有[DataMember]
的公共设置器-该属性有意开放供编写,以便序列化程序可以在模型反序列化期间修改对象的新创建实例。
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() {
在我们的应用程序中,只需要将RoutingState
保存到磁盘;出于明显的原因,不需要将命令保存到磁盘-它们的状态完全取决于路由器。 序列化的对象必须包含有关实现IRoutableViewModel
的类型的扩展信息,以便可以在反序列化时恢复导航堆栈。 我们描述MainViewModel
视图MainViewModel
的逻辑,将类放在ViewModels/MainViewModel.cs
和相应的ReactiveUI.Samples.Suspension.ViewModels
命名空间中。

在Avalonia应用中路由
在模型层和演示应用程序的表示模型的层级上的用户界面逻辑已实现,并且可以移动到针对.NET Standard的单独程序集中,因为它对所使用的GUI框架一无所知。 让我们看一下表示层。 在MVVM术语中,表示层负责在屏幕上呈现表示模型的状态,要呈现RoutingState
路由器的当前状态,请使用RoutedViewHost
包中包含的XAML RoutedViewHost
控件。 我们为SearchViewModel
实现GUI-为此,在Views/
目录中创建两个文件: SearchView.xaml
和SearchView.xaml.cs
。
Windows Presentation Foundation,Universal Windows Platform或Xamarin.Forms上的开发人员可能对使用Avalonia中使用的XAML方言的用户界面进行了描述。 在上面的示例中,我们为搜索表单创建了一个简单的界面-我们绘制一个文本框以输入搜索查询和一个按钮来启动搜索,同时将控件绑定到上面定义的SearchViewModel
模型的属性。
视图/ 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>
视图/ SearchView.xaml.cs
public sealed class SearchView : ReactiveUserControl<SearchViewModel> { public SearchView() {
熟悉的WPF,UWP和XF SearchView.xaml
也将看到SearchView.xaml
控件的代码。 在激活和停用视图或表示模型时, WhenActivated调用用于执行一些代码。 如果您的应用程序使用了热的可观察对象 (计时器,地理位置,到消息总线的连接),则最好CompositeDisposable
调用DisposeWith
将它们附加到CompositeDisposable
这样当您从可视树中DisposeWith
XAML控件及其对应的视图模型时, 热的可观察对象将停止发布新值,并且不会泄漏记忆。
同样,我们实现授权页面的表示视图/ 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>
视图/ LoginView.xaml.cs
public sealed class LoginView : ReactiveUserControl<LoginViewModel> { public LoginView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } }
编辑Views/MainView.xaml
和Views/MainView.xaml.cs
。 RoutedViewHost
来自RoutedViewHost
命名空间RoutedViewHost
XAML RoutedViewHost
主屏幕上,并将RoutingState
路由器的状态分配给RoutingState
属性。 添加按钮以导航到搜索和授权页面,将它们绑定到上述的ViewModels/MainViewModel
属性。
视图/ 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>
视图/ MainView.xaml.cs
public sealed class MainView : ReactiveWindow<MainViewModel> { public MainView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } }
一个演示ReactiveUI和Avalonia路由功能的简单应用程序已准备就绪。 单击“ Search
和“ Login
按钮后,将调用相应的命令,创建视图模型的新实例,并更新RoutingState
。 订阅对RoutedViewHost
的更改的XAML控件RoutedViewHost
尝试获取IViewFor<TViewModel>
,其中TViewModel
是Locator.Current
的视图模型类型。 如果找到IViewFor<TViewModel>
的注册实现, IViewFor<TViewModel>
创建一个新实例,将其内置到RoutedViewHost
并显示在RoutedViewHost
应用程序窗口中。

我们使用Locator.CurrentMutable
在应用程序的App.OnFrameworkInitializationCompleted
方法中注册必要的组件IViewFor<TViewModel>
和IScreen
。 IViewFor<TViewModel>
对于RoutedViewHost
IScreen
必需的,而IScreen
注册IScreen
必需的,以便在反序列化时,可以使用不带参数和Locator.Current
的构造函数正确初始化SearchViewModel
和LoginViewModel
。
App.xaml.cs
public override void OnFrameworkInitializationCompleted() {
运行该应用程序,并确保路由正确运行。 如果XAML标记中有任何错误,则Avalonia中使用的XamlIIl编译器将在编译阶段告诉我们确切的位置。 XamlIl还直接在IDE调试器中支持XAML调试 !
dotnet run --framework netcoreapp3.0

保存和还原整个应用程序状态
既然路由已经配置并且可以正常工作,最有趣的部分就开始了-您需要在关闭应用程序时实现将数据保存到磁盘,并在启动时从磁盘读取数据以及路由器的状态。 侦听应用程序启动和关闭事件的挂钩的初始化由特殊的AutoSuspendHelper
类处理,该类AutoSuspendHelper
于ReactiveUI支持的每个平台。 开发人员的任务是在应用程序组合根的最开始处初始化此类。 还需要RxApp.SuspensionHost.CreateNewAppState
函数初始化RxApp.SuspensionHost.CreateNewAppState
属性RxApp.SuspensionHost.CreateNewAppState
如果没有保存状态或发生意外错误,或者保存的文件已损坏,则该函数将返回应用程序的默认状态。
接下来,您需要调用RxApp.SuspensionHost.SetupDefaultSuspendResume
方法,并将其传递给RxApp.SuspensionHost.SetupDefaultSuspendResume
的实现, ISuspensionDriver
是读写状态对象的驱动程序。 为了实现ISuspensionDriver
, ISuspensionDriver
使用Newtonsoft.Json
库和System.IO
命名空间来处理文件系统。 为此,请安装Newtonsoft.Json
软件包:
dotnet add package Newtonsoft.Json
驱动程序/ 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
. , — !