Navigasi dalam aplikasi .NET Core lintas platform dengan kondisi penyimpanan ke disk menggunakan contoh ReactiveUI dan Avalonia



Antarmuka pengguna aplikasi modern biasanya kompleks - seringkali perlu untuk mengimplementasikan dukungan navigasi halaman, memproses berbagai bidang input, dan menampilkan atau menyembunyikan informasi berdasarkan parameter yang dipilih oleh pengguna. Pada saat yang sama, untuk meningkatkan UX, aplikasi harus menyimpan keadaan elemen antarmuka ke disk selama suspensi atau shutdown, memulihkan keadaan dari disk ketika program dimulai kembali.


Kerangka kerja MVVM ReactiveUI mengusulkan untuk mempertahankan keadaan aplikasi dengan membuat serial model grafik presentasi pada saat penangguhan program, sementara mekanisme untuk menentukan saat penangguhan berbeda untuk kerangka dan platform. Jadi, untuk WPF, acara Exit digunakan, untuk Xamarin. Android - ActivityPaused , untuk Xamarin.iOS - DidEnterBackground , untuk UWP - OnLaunched overload.


Pada artikel ini, kami akan mempertimbangkan penggunaan ReactiveUI untuk menyimpan dan mengembalikan keadaan perangkat lunak dengan GUI, termasuk keadaan router, menggunakan kerangka kerja GUI lintas-platform Avalonia sebagai contoh . Materi mengasumsikan pemahaman dasar dari pola desain MVVM dan pemrograman reaktif dalam konteks bahasa C # dan platform .NET untuk pembaca. Langkah-langkah dalam artikel ini berlaku untuk Windows 10 dan Ubuntu 18.


Pembuatan proyek


Untuk mencoba perutean dalam tindakan, buat proyek .NET Core baru dari template Avalonia, instal paket Avalonia.ReactiveUI - lapisan tipis integrasi Avalonia dan ReactiveUI. Pastikan Anda telah menginstal .NET Core SDK dan git sebelum memulai.


 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 

Pastikan aplikasi dimulai dan menampilkan jendela bertuliskan Selamat Datang di Avalonia!


 # Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0 



Hubungkan pra-build Avalonia dari MyGet


Untuk menghubungkan dan menggunakan build Avalonia terbaru yang secara otomatis dipublikasikan ke MyGet ketika cabang master repositori Avalonia di GitHub berubah, kami menggunakan file konfigurasi sumber paket nuget.config . Agar IDE dan .NET Core CLI dapat melihat nuget.config , Anda perlu membuat file sln untuk proyek yang dibuat di atas. Kami menggunakan alat dari .NET Core CLI:


 dotnet new sln # Ctrl+C dotnet sln ReactiveUI.Samples.Suspension.sln add ReactiveUI.Samples.Suspension.csproj 

Buat file nuget.config dalam folder dengan file nuget.config dari konten berikut:


 <?xml version="1.0" encoding="utf-8"?> <configuration> <packageSources> <add key="AvaloniaCI" value="https://www.myget.org/F/avalonia-ci/api/v2" /> </packageSources> </configuration> 

Anda mungkin perlu me-restart IDE, atau membongkar dan mengunduh seluruh solusi. Kami akan memperbarui paket Avalonia ke versi yang diperlukan (setidaknya 0,9.1) menggunakan antarmuka manajer paket NuGet dari IDE Anda, atau menggunakan alat baris perintah Windows atau 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 

Sekarang file proyek ReactiveUI.Samples.Suspension.csproj terlihat seperti ini:


 <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> 

Buat Views/ dan ViewModels/ folder di root proyek, ubah nama kelas MainWindow ke MainView untuk kenyamanan, pindahkan ke direktori Views/ , ubah ruang nama sesuai dengan ReactiveUI.Samples.Suspension.Views . App.xaml.cs konten file Program.cs dan App.xaml.cs - terapkan panggilan UseReactiveUI ke pembuat aplikasi Avalonia, pindahkan inisialisasi tampilan utama ke OnFrameworkInitializationCompleted untuk mematuhi rekomendasi manajemen siklus hidup aplikasi:


Program.cs


 class Program { //  .   API  Avalonia  , //  SynchronizationContext,    // OnFrameworkInitializationCompleted  App.xaml.cs:  //     -    . public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); //   Avalonia.    // ,      . public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>() .UseReactiveUI() // ! .UsePlatformDetect() .LogToDebug(); } 

App.xaml.cs


 public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this); //    .     //  MVVM , DI    .    //     ApplicationLifetime,    //     . public override void OnFrameworkInitializationCompleted() { new Views.MainView().Show(); base.OnFrameworkInitializationCompleted(); } } 

Anda perlu menambahkan using Avalonia.ReactiveUI ke Program.cs . Pastikan bahwa setelah memperbarui paket, proyek dimulai dan menampilkan jendela selamat datang standar.


 # Use .NET Core version which you have installed. # It can be netcoreapp2.0, netcoreapp2.1 and so on. dotnet run --framework netcoreapp3.0 



Perutean lintas-platform ReactiveUI


Sebagai aturan, ada dua pendekatan utama untuk menerapkan navigasi antara halaman aplikasi .NET - view-first dan view model-first. Pendekatan Lihat pertama melibatkan pengontrolan tumpukan navigasi dan navigasi antara halaman pada tingkat Lihat dalam terminologi MVVM - misalnya, menggunakan kelas Frame dan Halaman dalam kasus UWP atau WPF, dan ketika menggunakan pendekatan tampilan model-pertama, navigasi dilaksanakan pada tingkat model presentasi. Alat reaktifUI yang mengatur perutean dalam aplikasi difokuskan pada penggunaan pendekatan tampilan model-pertama. Routing ReactiveUI terdiri dari implementasi IScreen yang berisi keadaan router, beberapa implementasi IRoutableViewModel dan kontrol XAML RoutedViewHost platform, RoutedViewHost .




Keadaan router diwakili oleh objek RoutingState , yang mengontrol tumpukan navigasi. IScreen adalah akar dari tumpukan navigasi, dan mungkin ada beberapa akar navigasi dalam aplikasi. RoutedViewHost memonitor status router RoutingState sesuai, merespons perubahan dalam tumpukan navigasi dengan menyematkan IRoutableViewModel dari kontrol XAML yang sesuai. Fungsi yang dijelaskan akan diilustrasikan dengan contoh di bawah ini.


Menyimpan keadaan model tampilan ke disk


Pertimbangkan contoh khas layar pencarian informasi.




Kita harus memutuskan elemen model representasi layar mana yang akan disimpan ke disk selama jeda atau shutdown aplikasi, dan elemen mana yang akan dibuat ulang setiap kali dimulai. Tidak perlu menyimpan status perintah ReactiveUI yang mengimplementasikan antarmuka ICommand dan dilampirkan ke tombol - ReactiveCommand<TIn, TOut> dibuat dan diinisialisasi dalam konstruktor, sedangkan status indikator CanExecute tergantung pada properti model tampilan dan dihitung ulang ketika mereka berubah. Kebutuhan untuk menyimpan hasil pencarian - titik diperdebatkan - tergantung pada spesifikasi aplikasi, tetapi akan lebih bijaksana untuk menyimpan dan mengembalikan keadaan bidang input SearchQuery !


ViewModels / SearchViewModel.cs


 [DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery; //   IScreen  ,   NULL //  IScreen  Splat.Locator.    //      . public SearchViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); //     SearchQuery , //     . var canSearch = this .WhenAnyValue(x => x.SearchQuery) .Select(query => !string.IsNullOrWhiteSpace(query)); //      ,  //     . _search = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), //    canSearch); } public IScreen HostScreen { get; } public string UrlPathSegment => "/search"; public ICommand Search => _search; [DataMember] public string SearchQuery { get => _searchQuery; set => this.RaiseAndSetIfChanged(ref _searchQuery, value); } } 

Kelas model tampilan ditandai dengan atribut [DataContract] , dan properti yang perlu diserialisasi dengan [DataMember] . Ini cukup jika serializer yang digunakan menggunakan pendekatan opt-in - ini hanya menyimpan properti yang secara eksplisit ditandai dengan atribut ke disk, dalam kasus pendekatan opt-out, perlu menandai dengan [IgnoreDataMember] atribut properti-properti yang tidak perlu disimpan ke disk. Selain itu, kami menerapkan antarmuka IRoutableViewModel dalam model tampilan kami sehingga nantinya dapat menjadi bagian dari bingkai navigasi router aplikasi.


Demikian pula, kami menerapkan model presentasi halaman otorisasi

ViewModels / LoginViewModel.cs


 [DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username; //   IScreen  ,   NULL //  IScreen  Splat.Locator.    //      . public LoginViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); //     Username  Password // ,     . var canLogin = this .WhenAnyValue( x => x.Username, x => x.Password, (user, pass) => !string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(pass)); //      ,  //    . _login = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), //    canLogin); } public IScreen HostScreen { get; } public string UrlPathSegment => "/login"; public ICommand Login => _login; [DataMember] public string Username { get => _username; set => this.RaiseAndSetIfChanged(ref _username, value); } //        ! public string Password { get => _password; set => this.RaiseAndSetIfChanged(ref _password, value); } } 

Model presentasi untuk dua halaman aplikasi siap, mengimplementasikan antarmuka IRoutableViewModel dan dapat dibangun ke dalam router IScreen . Sekarang kami langsung mengimplementasikan IScreen . Kami menandai dengan bantuan atribut [DataContract] properti mana dari model tampilan untuk diserialisasi dan yang diabaikan. Perhatikan setter publik dari properti yang ditandai dengan [DataMember] , dalam contoh di bawah ini - properti ini sengaja dibuka untuk ditulis sehingga serializer dapat memodifikasi contoh objek yang baru dibuat selama deserialisasi model.


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() { //       , //  ,   . var canLogin = this .WhenAnyObservable(x => x.Router.CurrentViewModel) .Select(current => !(current is LoginViewModel)); _login = ReactiveCommand.Create( () => { Router.Navigate.Execute(new LoginViewModel()); }, canLogin); //       , //  ,   . var canSearch = this .WhenAnyObservable(x => x.Router.CurrentViewModel) .Select(current => !(current is SearchViewModel)); _search = ReactiveCommand.Create( () => { Router.Navigate.Execute(new SearchViewModel()); }, canSearch); } [DataMember] public RoutingState Router { get => _router; set => this.RaiseAndSetIfChanged(ref _router, value); } public ICommand Search => _search; public ICommand Login => _login; } 

Dalam aplikasi kami, hanya RoutingState perlu disimpan ke disk, karena alasan yang jelas, perintah tidak perlu disimpan ke disk - statusnya sepenuhnya tergantung pada router. Objek berseri harus menyertakan informasi tambahan tentang jenis yang menerapkan IRoutableViewModel sehingga tumpukan navigasi dapat dipulihkan setelah deserialisasi. Kami menjelaskan logika MainViewModel tampilan MainViewModel , tempatkan kelas dalam ViewModels/MainViewModel.cs dan dalam ruang nama ReactiveUI.Samples.Suspension.ViewModels sesuai.




Routing di aplikasi Avalonia


Logika antarmuka pengguna di tingkat lapisan model dan model presentasi dari aplikasi demo diimplementasikan dan dapat dipindahkan ke perakitan terpisah yang ditujukan untuk .NET Standard, karena tidak tahu apa-apa tentang kerangka kerja GUI yang digunakan. Mari kita lihat pada layer presentasi. Dalam terminologi MVVM, lapisan presentasi bertanggung jawab untuk menampilkan keadaan model presentasi di layar Untuk membuat keadaan router RoutingState , kontrol XAML RoutedViewHost terkandung dalam paket Avalonia.ReactiveUI . Kami menerapkan GUI untuk SearchViewModel - untuk ini, di direktori Views/ , buat dua file: SearchView.xaml dan SearchView.xaml.cs .


Deskripsi antarmuka pengguna menggunakan dialek XAML yang digunakan di Avalonia mungkin terdengar asing bagi para pengembang di Windows Presentation Foundation, Universal Windows Platform, atau Xamarin.Forms. Dalam contoh di atas, kami membuat antarmuka yang sepele untuk formulir pencarian - kami menggambar bidang teks untuk memasukkan permintaan pencarian dan tombol yang memulai pencarian, sementara kami mengikat kontrol ke properti dari model SearchViewModel yang ditentukan di atas.


Tampilan / 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> 

Tampilan / SearchView.xaml.cs


 public sealed class SearchView : ReactiveUserControl<SearchViewModel> { public SearchView() { //  WhenActivated     //        . this.WhenActivated((CompositeDisposable disposable) => { }); AvaloniaXamlLoader.Load(this); } } 

Kode-belakang kontrol SearchView.xaml juga akan muncul untuk SearchView.xaml WPF, UWP, dan XF yang akrab. Panggilan WhenActivated digunakan untuk mengeksekusi beberapa kode ketika tampilan atau model tampilan diaktifkan dan dinonaktifkan. Jika aplikasi Anda menggunakan hot observable (timer, geolocation, koneksi ke bus pesan), akan lebih baik untuk melampirkannya ke CompositeDisposable memanggil DisposeWith sehingga ketika Anda DisposeWith kontrol XAML dan model tampilan yang sesuai dari pohon visual, observasi panas berhenti menerbitkan nilai-nilai baru dan tidak ada kebocoran memori.


Demikian pula, kami menerapkan representasi halaman otorisasi.

Tampilan / 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> 

Tampilan / LoginView.xaml.cs


 public sealed class LoginView : ReactiveUserControl<LoginViewModel> { public LoginView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } } 

Edit file Views/MainView.xaml dan Views/MainView.xaml.cs . RoutedViewHost XAML RoutedViewHost dari ruang nama RoutedViewHost di layar utama, tetapkan status router RoutingState ke properti RoutingState . Tambahkan tombol untuk menavigasi ke halaman pencarian dan otorisasi, ikat mereka ke properti ViewModels/MainViewModel yang dijelaskan di atas.


Tampilan / 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> <!--  ,   RoutingState,   View  ViewModel --> <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> 

Tampilan / MainView.xaml.cs


 public sealed class MainView : ReactiveWindow<MainViewModel> { public MainView() { this.WhenActivated(disposables => { }); AvaloniaXamlLoader.Load(this); } } 

Sebuah aplikasi sederhana yang menunjukkan kemampuan perutean ReactiveUI dan Avalonia sudah siap. Ketika Anda mengklik tombol Search dan Login , perintah yang sesuai dipanggil, contoh baru model tampilan dibuat dan RoutingState diperbarui. Kontrol XAML, RoutedViewHost , yang berlangganan perubahan pada RoutingState , mencoba untuk mendapatkan tipe IViewFor<TViewModel> , di mana TViewModel adalah tipe model tampilan dari Locator.Current . Jika implementasi terdaftar dari IViewFor<TViewModel> ditemukan, instance baru akan dibuat, dibuat di dalam RoutedViewHost dan ditampilkan di jendela aplikasi Avalonia.




Kami mendaftarkan komponen yang diperlukan IViewFor<TViewModel> dan IScreen di App.OnFrameworkInitializationCompleted metode aplikasi kami menggunakan Locator.CurrentMutable . IViewFor<TViewModel> diperlukan agar RoutedViewHost berfungsi RoutedViewHost , dan pendaftaran IScreen diperlukan agar saat deserialisasi, LoginViewModel dan LoginViewModel dapat diinisialisasi dengan benar menggunakan konstruktor tanpa parameter dan Locator.Current .


App.xaml.cs


 public override void OnFrameworkInitializationCompleted() { //   . Locator.CurrentMutable.RegisterConstant<IScreen>(new MainViewModel()); Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); //        . new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.OnFrameworkInitializationCompleted(); } 

Jalankan aplikasi dan pastikan perutean bekerja dengan benar. Jika ada kesalahan dalam markup XAML, kompiler XamlIl yang digunakan di Avalonia akan memberi tahu kita dengan tepat di mana, pada tahap kompilasi. XamlIl juga mendukung debugging XAML langsung di debugger IDE !


 dotnet run --framework netcoreapp3.0 



Menyimpan dan memulihkan seluruh status aplikasi


Sekarang setelah perutean dikonfigurasikan dan berfungsi, bagian yang paling menarik dimulai - Anda perlu menerapkan penyimpanan data ke disk saat Anda menutup aplikasi dan membaca data dari disk saat dijalankan, bersama dengan keadaan router. Inisialisasi kait yang mendengarkan acara awal dan tutup aplikasi ditangani oleh kelas AutoSuspendHelper khusus, AutoSuspendHelper untuk setiap platform yang didukung ReactiveUI . Tugas pengembang adalah menginisialisasi kelas ini di bagian paling awal dari akar komposisi aplikasi. Juga diperlukan untuk menginisialisasi properti RxApp.SuspensionHost.CreateNewAppState fungsi yang akan mengembalikan keadaan default aplikasi jika tidak ada status tersimpan atau kesalahan tak terduga terjadi, atau jika file yang disimpan rusak.


Selanjutnya, Anda perlu memanggil metode RxApp.SuspensionHost.SetupDefaultSuspendResume , melewati penerapan ISuspensionDriver , driver yang ISuspensionDriver dan membaca objek keadaan. Untuk menerapkan ISuspensionDriver , ISuspensionDriver menggunakan perpustakaan Newtonsoft.Json dan System.IO namespace untuk bekerja dengan sistem file. Untuk melakukan ini, instal paket 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() { //   . var suspension = new AutoSuspendHelper(ApplicationLifetime); RxApp.SuspensionHost.CreateNewAppState = () => new MainViewModel(); RxApp.SuspensionHost.SetupDefaultSuspendResume(new NewtonsoftJsonSuspensionDriver("appstate.json")); suspension.OnFrameworkInitializationCompleted(); //  ,      . var state = RxApp.SuspensionHost.GetAppState<MainViewModel>(); Locator.CurrentMutable.RegisterConstant<IScreen>(state); Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); //  . new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.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 AkavacheUserAccount Secure iOS UWP , , Android BundleSuspensionDriver ReactiveUI.AndroidSupport . JSON Xamarin.Essentials SecureStorage . , — !



Source: https://habr.com/ru/post/id457164/


All Articles