Menyimpan Routing State ke Disk di Cross-Platform .NET Core GUI App dengan ReactiveUI dan Avalonia

gambar


Antarmuka pengguna aplikasi perusahaan modern cukup kompleks. Anda, sebagai pengembang, sering perlu menerapkan navigasi dalam aplikasi, memvalidasi input pengguna, menampilkan atau menyembunyikan layar berdasarkan preferensi pengguna. Untuk UX yang lebih baik, aplikasi Anda harus mampu menyimpan status ke disk saat aplikasi ditangguhkan dan memulihkan status saat aplikasi dilanjutkan.


ReactiveUI menyediakan fasilitas yang memungkinkan Anda untuk mempertahankan status aplikasi dengan membuat serial pohon model tampilan ketika aplikasi dimatikan atau ditangguhkan. Acara suspensi berbeda-beda per platform. ReactiveUI menggunakan acara Exit untuk WPF, ActivityPaused untuk Xamarin. DidEnterBackground , DidEnterBackground untuk Xamarin.iOS, OnLaunched untuk UWP.


Dalam tutorial ini kita akan membangun aplikasi sampel yang menunjukkan penggunaan fitur ReactiveUI Suspension dengan Avalonia - sebuah kerangka kerja platform GUI berbasis NET Core XAML. Anda diharapkan terbiasa dengan pola MVVM dan dengan ekstensi reaktif sebelum membaca catatan ini. Langkah-langkah yang dijelaskan dalam tutorial harus berfungsi jika Anda menggunakan Windows 10 atau Ubuntu 18 dan menginstal .NET Core SDK. Ayo mulai!


Bootstrap proyek


Untuk melihat perutean ReactiveUI dalam aksi, kami membuat proyek .NET Core baru yang didasarkan pada templat aplikasi Avalonia. Kemudian kami menginstal paket Avalonia.ReactiveUI . Paket ini menyediakan kait siklus hidup Avalonia khusus platform, perutean dan infrastruktur aktivasi . Ingatlah untuk menginstal .NET Core dan git sebelum menjalankan perintah di bawah ini.


 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 

Mari kita jalankan aplikasi dan memastikannya menampilkan jendela yang menampilkan "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 

gambar


Menginstal Build Avalonia Preview dari MyGet


Paket Avalonia terbaru diterbitkan ke MyGet setiap kali komit baru didorong ke cabang master dari repositori Avalonia di GitHub. Untuk menggunakan paket-paket terbaru dari MyGet di aplikasi kami, kami akan membuat file nuget.config . Tetapi sebelum melakukan ini, kami membuat file sln untuk proyek yang dibuat sebelumnya, menggunakan .NET Core CLI :


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

Sekarang kita membuat file nuget.config dengan 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> 

Biasanya, restart diperlukan untuk memaksa IDE kami mendeteksi paket dari umpan MyGet yang baru ditambahkan, tetapi memuat ulang solusi juga akan membantu. Kemudian, kami memutakhirkan paket Avalonia ke versi terbaru (atau setidaknya ke 0.9.1 ) menggunakan GUI manajer paket NuGet, atau .NET Core CLI:


 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 

File ReactiveUI.Samples.Suspension.csproj akan terlihat seperti berikut 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> 

Kami membuat dua folder baru di dalam direktori root proyek, masing-masing bernama Views/ dan ViewModels/ . Selanjutnya, kami mengubah nama kelas MainWindow ke MainView dan memindahkannya ke folder Views/ . Ingatlah untuk mengganti nama referensi ke kelas yang diedit dalam file XAML yang sesuai, jika tidak, proyek tidak akan dikompilasi. Juga, ingatlah untuk mengubah namespace untuk MainView menjadi ReactiveUI.Samples.Suspension.Views untuk konsistensi. Kemudian, kami mengedit dua file lainnya, Program.cs dan App.xaml.cs Kami menambahkan panggilan ke UseReactiveUI ke pembangun aplikasi Avalonia, memindahkan kode inisialisasi aplikasi ke metode OnFrameworkInitializationCompleted untuk mematuhi pedoman manajemen seumur hidup aplikasi Avalonia:


Program.cs


 class Program { // The entry point. Things aren't ready yet, so at this point // you shouldn't use any Avalonia types or anything that // expects a SynchronizationContext to be ready. public static void Main(string[] args) => BuildAvaloniaApp() .StartWithClassicDesktopLifetime(args); // This method is required for IDE previewer infrastructure. // Don't remove, otherwise, the visual designer will break. public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>() .UseReactiveUI() // required! .UsePlatformDetect() .LogToDebug(); } 

App.xaml.cs


 public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this); // The entrypoint for your application. Here you initialize your // MVVM framework, DI container and other components. You can use // the ApplicationLifetime property here to detect if the app // is running on a desktop platform or on a mobile platform (WIP). public override void OnFrameworkInitializationCompleted() { new Views.MainView().Show(); base.OnFrameworkInitializationCompleted(); } } 

Sebelum mencoba membangun proyek, kami memastikan using Avalonia.ReactiveUI arahan using Avalonia.ReactiveUI ditambahkan ke bagian atas file Program.cs . Kemungkinan besar IDE kami telah mengimpor namespace itu, tetapi jika tidak, kami akan mendapatkan kesalahan waktu kompilasi. Akhirnya, saatnya memastikan aplikasi mengkompilasi, menjalankan, dan menampilkan jendela baru:


 dotnet run --framework netcoreapp2.1 

gambar


Routing ReaktifUI Lintas-platform


Ada dua pendekatan umum dalam mengatur navigasi dalam aplikasi dalam aplikasi .NET lintas platform - view-first dan view model-first. Pendekatan sebelumnya mengasumsikan bahwa layer View mengelola tumpukan navigasi - misalnya, menggunakan kelas Frame dan Halaman khusus platform. Dengan pendekatan yang terakhir, lapisan model tampilan menangani navigasi melalui abstraksi platform-agnostik. Perkakas ReactiveUI dibangun dengan mempertimbangkan pendekatan model-first view. Routing ReactiveUI terdiri dari implementasi IScreen , yang berisi status routing saat ini, beberapa implementasi IRoutableViewModel dan kontrol XAML khusus platform yang disebut RoutedViewHost .




Objek RoutingState merangkum manajemen tumpukan navigasi. IScreen adalah root navigasi, tetapi terlepas dari namanya, IScreen tidak harus menempati seluruh layar. RoutedViewHost bereaksi terhadap perubahan di RoutingState terikat dan menyematkan tampilan yang sesuai untuk IRoutableViewModel saat ini dipilih. Fungsionalitas yang dijelaskan akan diilustrasikan dengan contoh yang lebih komprehensif nanti.


Status model tampilan bertahan


Pertimbangkan model tampilan layar pencarian sebagai contoh.




Kita akan memutuskan, sifat-sifat model tampilan mana yang akan disimpan pada aplikasi shutdown dan mana yang akan dibuat ulang. Tidak perlu menyimpan status perintah reaktif yang mengimplementasikan antarmuka ICommand . Kelas ReactiveCommand<TIn, TOut> biasanya diinisialisasi dalam konstruktor, indikator CanExecute -nya biasanya sepenuhnya tergantung pada properti model tampilan dan akan dihitung ulang setiap kali salah satu dari properti itu berubah. Dapat diperdebatkan jika Anda menyimpan hasil pencarian, tetapi menyimpan kueri pencarian adalah ide yang bagus.


ViewModels / SearchViewModel.cs


 [DataContract] public class SearchViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _search; private string _searchQuery; // We inject the IScreen implementation via the constructor. // If we receive null, we use Splat.Locator to resolve the // default implementation. The parameterless constructor is // required for the deserialization feature to work. public SearchViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); // Each time the search query changes, we check if the search // query is empty. If it is, we disable the command. var canSearch = this .WhenAnyValue(x => x.SearchQuery) .Select(query => !string.IsNullOrWhiteSpace(query)); // Buttons bound to the command will stay disabled // as long as the command stays disabled. _search = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), // emulate a long-running operation 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); } } 

Kami menandai seluruh kelas model tampilan dengan atribut [DataContract] , properti anotasi yang akan kami [DataMember] atribut [DataMember] . Ini cukup jika kita akan menggunakan mode serialisasi opt-in. Mempertimbangkan mode serialisasi, opt-out berarti bahwa semua bidang publik dan properti akan diserialisasi, kecuali jika Anda secara eksplisit mengabaikannya dengan membuat anotasi dengan atribut [IgnoreDataMember] , memilih ikut berarti sebaliknya. Selain itu, kami menerapkan antarmuka IRoutableViewModel di kelas model tampilan kami. Ini diperlukan saat kita akan menggunakan model tampilan sebagai bagian dari tumpukan navigasi.


Detail implementasi untuk model tampilan masuk

ViewModels / LoginViewModel.cs


 [DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username; // We inject the IScreen implementation via the constructor. // If we receive null, we use Splat.Locator to resolve the // default implementation. The parameterless constructor is // required for the deserialization feature to work. public LoginViewModel(IScreen screen = null) { HostScreen = screen ?? Locator.Current.GetService<IScreen>(); // When any of the specified properties change, // we check if user input is valid. var canLogin = this .WhenAnyValue( x => x.Username, x => x.Password, (user, pass) => !string.IsNullOrWhiteSpace(user) && !string.IsNullOrWhiteSpace(pass)); // Buttons bound to the command will stay disabled // as long as the command stays disabled. _login = ReactiveCommand.CreateFromTask( () => Task.Delay(1000), // emulate a long-running operation 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); } // Note: Saving passwords to disk isn't a good idea. public string Password { get => _password; set => this.RaiseAndSetIfChanged(ref _password, value); } } 

Kedua model tampilan mengimplementasikan antarmuka IRoutableViewModel dan siap untuk disematkan ke layar navigasi. Sekarang saatnya untuk mengimplementasikan antarmuka IScreen . Sekali lagi, kami menggunakan atribut [DataContract] untuk menunjukkan bagian mana yang akan diserialisasi dan mana yang diabaikan. Dalam contoh di bawah ini, setter properti RoutingState secara sengaja dinyatakan sebagai publik - ini memungkinkan serializer kami untuk memodifikasi properti ketika itu deserialized.


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() { // If the authorization page is currently shown, then // we disable the "Open authorization view" button. var canLogin = this .WhenAnyObservable(x => x.Router.CurrentViewModel) .Select(current => !(current is LoginViewModel)); _login = ReactiveCommand.Create( () => { Router.Navigate.Execute(new LoginViewModel()); }, canLogin); // If the search screen is currently shown, then we // disable the "Open search view" button. 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 model tampilan utama kami, kami hanya menyimpan satu bidang ke disk - yang merupakan jenis RoutingState . Kami tidak harus menyimpan status perintah reaktif, karena ketersediaannya sepenuhnya tergantung pada kondisi router saat ini dan secara reaktif berubah. Untuk dapat mengembalikan router ke keadaan semula, kami menyertakan informasi tipe diperpanjang dari implementasi IRoutableViewModel kami saat membuat serial router. Kami akan menggunakan pengaturan TypenameHandling.All dari Newtonsoft.Json untuk mencapai ini nanti. Kami menempatkan MainViewModel ke dalam folder ViewModels/ , menyesuaikan namespace menjadi ReactiveUI.Samples.Suspension.ViewModels .




Routing di Aplikasi Avalonia


Untuk saat ini, kami telah mengimplementasikan model presentasi aplikasi kami. Kemudian, kelas model tampilan dapat diekstraksi menjadi perakitan terpisah yang menargetkan .NET Standard , sehingga bagian inti dari aplikasi kami dapat digunakan kembali di beberapa kerangka kerja .NET GUI. Sekarang saatnya untuk mengimplementasikan bagian GUI khusus Avalonia dari aplikasi kami. Kami membuat dua file di Views/ folder, masing-masing bernama SearchView.xaml dan SearchView.xaml.cs . Ini adalah dua bagian dari tampilan pencarian tunggal - yang pertama adalah UI yang dideskripsikan secara deklaratif di XAML, dan yang terakhir berisi kode C # di belakang. Ini pada dasarnya adalah tampilan untuk model tampilan pencarian yang dibuat sebelumnya.


Dialek XAML yang digunakan dalam Avalonia harus segera terasa akrab bagi pengembang yang berasal dari WPF, UWP atau XF. Pada contoh di atas kami membuat tata letak sederhana yang berisi kotak teks dan tombol yang memicu pencarian. Kami mengikat properti dan perintah dari SearchViewModel ke kontrol yang dinyatakan dalam SearchView .


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() { // The call to WhenActivated is used to execute a block of code // when the corresponding view model is activated and deactivated. this.WhenActivated((CompositeDisposable disposable) => { }); AvaloniaXamlLoader.Load(this); } } 

Pengembang WPF dan UWP mungkin menemukan kode-belakang untuk file SearchView.xaml akrab. Panggilan ke WhenActivated ditambahkan untuk menjalankan logika aktivasi tampilan. WhenActivated disposable sebagai argumen pertama untuk WhenActivated dibuang ketika tampilan dinonaktifkan. Jika aplikasi Anda menggunakan hot observable (mis. Layanan penentuan posisi, timer, agregator acara), itu akan menjadi keputusan yang bijaksana untuk melampirkan langganan ke komposit WhenActivated dibuang dengan menambahkan panggilan DisposeWith , sehingga tampilan akan berhenti berlangganan dari hot observable dan kebocoran memori tidak akan terjadi.


Detail implementasi untuk tampilan login

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); } } 

Kami mengedit file Views/MainView.xaml dan Views/MainView.xaml.cs . Kami menambahkan kontrol RoutedViewHost dari Avalonia.ReactiveUI namespace ke tata letak XAML jendela utama dan mengikat properti Router dari MainViewModel ke properti RoutedViewHost.Router . Kami menambahkan dua tombol, satu membuka halaman pencarian dan satu lagi membuka halaman otorisasi.


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> <!-- The RoutedViewHost XAML control observes the bound RoutingState. It subscribes to changes in the navigation stack and embedds the appropriate view for the currently selected view model. --> <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); } } 

Aplikasi demo perutean Avalonia dan ReactiveUI yang sederhana sudah siap sekarang. Ketika pengguna menekan tombol pencarian atau login, perintah yang memicu navigasi dipanggil dan RoutingState diperbarui. RoutedViewHost XAML mengamati status perutean, berupaya menyelesaikan IViewFor<TViewModel> dari Locator.Current . Jika IViewFor<TViewModel> terdaftar, maka instance baru dari kontrol dibuat dan disematkan ke jendela Avalonia.




Kami mendaftarkan implementasi IScreen dan IScreen di metode App.OnFrameworkInitializationCompleted , menggunakan Locator.CurrentMutable . Mendaftarkan implementasi IViewFor diperlukan agar kontrol RoutedViewHost berfungsi. Mendaftarkan IScreen memungkinkan IScreen dan LoginViewModel untuk menginisialisasi properti selama deserialisasi, menggunakan konstruktor tanpa parameter.


App.xaml.cs


 public override void OnFrameworkInitializationCompleted() { // Here we register our view models. Locator.CurrentMutable.RegisterConstant<IScreen>(new MainViewModel()); Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); // Here we resolve the root view model and initialize main view data context. new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.OnFrameworkInitializationCompleted(); } 

Mari meluncurkan aplikasi kita dan memastikan perutean berjalan sebagaimana mestinya. Jika ada yang salah dengan markup UI XAML, kompilator Avalonia XamlIl akan memberi tahu kami tentang kesalahan pada waktu kompilasi. Selain itu, XamlIl mendukung debugging XAML !


 dotnet run --framework netcoreapp3.0 



Menyimpan dan Memulihkan Status Aplikasi


Sekarang saatnya menerapkan driver suspensi yang bertanggung jawab untuk menyimpan dan mengembalikan status aplikasi saat aplikasi ditangguhkan dan dilanjutkan. Kelas AutoSuspendHelper khusus platform menangani inisialisasi hal-hal, Anda, sebagai pengembang, hanya perlu membuat AutoSuspendHelper di root komposisi aplikasi. Juga, Anda perlu menginisialisasi pabrik RxApp.SuspensionHost.CreateNewAppState . Jika aplikasi tidak memiliki data yang disimpan, atau jika data yang disimpan rusak, ReactiveUI memanggil metode pabrik untuk membuat contoh default objek status aplikasi.


Kemudian, kita memanggil metode RxApp.SuspensionHost.SetupDefaultSuspendResume , dan mengirimkan instance baru dari ISuspensionDriver ke sana. Mari kita mengimplementasikan antarmuka ISuspensionDriver menggunakan Newtonsoft.Json dan kelas dari namespace 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); } } 

Pendekatan yang dijelaskan memiliki kelemahan - beberapa kelas System.IO tidak akan bekerja dengan kerangka kerja UWP, aplikasi UWP berjalan di kotak pasir dan melakukan berbagai hal secara berbeda. Itu agak mudah dipecahkan - yang perlu Anda lakukan adalah menggunakan kelas StorageFolder dan StorageFolder alih-alih File dan Directory saat menargetkan UWP. Untuk membaca tumpukan navigasi dari disk, driver suspensi harus mendukung deserializing objek JSON menjadi implementasi IRoutableViewModel konkret, itulah sebabnya kami menggunakan pengaturan TypeNameHandling.All Newizeroft.Json pengaturan serializer. Kami mendaftarkan driver suspensi di root komposisi aplikasi, di metode App.OnFrameworkInitializationCompleted :


 public override void OnFrameworkInitializationCompleted() { // Initialize suspension hooks. var suspension = new AutoSuspendHelper(ApplicationLifetime); RxApp.SuspensionHost.CreateNewAppState = () => new MainViewModel(); RxApp.SuspensionHost.SetupDefaultSuspendResume(new NewtonsoftJsonSuspensionDriver("appstate.json")); suspension.OnFrameworkInitializationCompleted(); // Read main view model state from disk. var state = RxApp.SuspensionHost.GetAppState<MainViewModel>(); Locator.CurrentMutable.RegisterConstant<IScreen>(state); // Register views. Locator.CurrentMutable.Register<IViewFor<SearchViewModel>>(() => new SearchView()); Locator.CurrentMutable.Register<IViewFor<LoginViewModel>>(() => new LoginView()); // Show the main window. new MainView { DataContext = Locator.Current.GetService<IScreen>() }.Show(); base.OnFrameworkInitializationCompleted(); } 

Kelas AutoSuspendHelper dari paket Avalonia.ReactiveUI mengatur kait siklus hidup untuk aplikasi Anda, sehingga kerangka kerja ReactiveUI akan mengetahui kapan harus menulis status aplikasi ke disk, menggunakan implementasi ISuspensionDriver disediakan. Setelah kami meluncurkan aplikasi kami, driver suspensi akan membuat file JSON baru bernama appstate.json . Setelah kami membuat perubahan di UI (mis. Ketik agak ke dalam bidang teks, atau klik tombol apa saja) dan kemudian tutup aplikasi, file appstate.json akan terlihat mirip dengan yang berikut:


appstate.json

Perhatikan, bahwa setiap objek JSON dalam file berisi kunci $type dengan nama tipe yang sepenuhnya memenuhi syarat, termasuk namespace.


 { "$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" } ] } } } 

Jika Anda menutup aplikasi dan kemudian meluncurkannya lagi, Anda akan melihat konten yang sama di layar seperti yang Anda lihat sebelumnya! Fungsi yang dijelaskan bekerja pada setiap platform yang didukung oleh ReactiveUI , termasuk UWP, WPF, Xamarin Forms atau Xamarin Native.


gambar


Bonus: ISuspensionDriver dapat diimplementasikan menggunakan Akavache - sebuah penyimpanan nilai kunci yang asinkron dan persisten. Jika Anda menyimpan data di bagian UserAccount atau bagian Secure , maka di iOS dan UWP data Anda akan dicadangkan secara otomatis ke cloud dan akan tersedia di semua perangkat tempat aplikasi diinstal. Juga, BundleSuspensionDriver ada dalam paket ReactiveUI.AndroidSupport . Xamarin.Essentials SecureStorage APIs dapat digunakan untuk menyimpan data juga. Anda juga dapat menyimpan status aplikasi Anda di server jarak jauh atau di layanan cloud platform-independen.



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


All Articles