
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

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 {
App.xaml.cs
public class App : Application { public override void Initialize() => AvaloniaXamlLoader.Load(this);
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

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;
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 masukViewModels / LoginViewModel.cs
[DataContract] public class LoginViewModel : ReactiveObject, IRoutableViewModel { private readonly ReactiveCommand<Unit, Unit> _login; private string _password; private string _username;
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() {
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() {
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 loginTampilan / 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> <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() {
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() {
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.jsonPerhatikan, 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.

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.
Tautan yang bermanfaat