
Hari ini,
platform .NET adalah alat yang benar-benar universal - dengan bantuannya Anda dapat menyelesaikan berbagai tugas, termasuk pengembangan aplikasi aplikasi untuk sistem operasi populer, seperti Windows, Linux, MacOS, Android dan iOS. Pada artikel ini, kita akan melihat arsitektur aplikasi .NET lintas-platform menggunakan pola desain MVVM dan
pemrograman reaktif . Kami akan berkenalan dengan pustaka
ReactiveUI dan
Fody , mempelajari cara mengimplementasikan antarmuka INotifyPropertyChanged menggunakan atribut, menyentuh dasar-dasar
AvaloniaUI ,
Formulir Xamarin ,
Platform Windows Universal ,
Yayasan Windows Presentation Foundation dan
.NET Standar , dan mempelajari alat yang efektif untuk lapisan model pengujian dan model presentasi aplikasi.
Materi ini merupakan adaptasi dari artikel "
MVVM Reaktif Untuk .NET Platform " dan "
Cross-Platform .NET Apps Melalui Pendekatan MVVM Reaktif ", yang diterbitkan oleh penulis sebelumnya tentang sumber daya Menengah. Kode contoh
tersedia di GitHub .
Pendahuluan Arsitektur MVVM dan lintas-platform .NET
Saat mengembangkan aplikasi lintas-platform pada platform .NET, Anda harus menulis kode portabel dan didukung. Jika Anda bekerja dengan kerangka kerja yang menggunakan dialek XAML, seperti UWP, WPF, Xamarin Forms, dan AvaloniaUI, ini dapat dicapai menggunakan pola desain MVVM, pemrograman reaktif, dan strategi pemisahan kode .NET Standard. Pendekatan ini meningkatkan portabilitas aplikasi dengan memungkinkan pengembang untuk menggunakan basis kode umum dan pustaka perangkat lunak umum pada berbagai sistem operasi.
Kami akan melihat lebih dekat pada setiap lapisan aplikasi yang dibangun berdasarkan arsitektur MVVM - Model, Tampilan, dan Model View. Lapisan model mewakili layanan domain, objek transfer data, entitas basis data, repositori - semua logika bisnis dari program kami. Pandangan bertanggung jawab untuk menampilkan elemen antarmuka pengguna pada layar dan tergantung pada sistem operasi spesifik, dan model presentasi memungkinkan dua lapisan yang dijelaskan di atas untuk berinteraksi, mengadaptasi lapisan model untuk berinteraksi dengan pengguna manusia.
Arsitektur MVVM menyediakan pembagian tanggung jawab antara tiga lapisan perangkat lunak aplikasi, sehingga lapisan ini dapat ditempatkan dalam rakitan terpisah yang ditujukan untuk .NET Standard. Spesifikasi .NET Standard formal memungkinkan pengembang untuk membuat perpustakaan portabel yang dapat digunakan dalam berbagai implementasi .NET dengan satu set API tunggal. Dengan mengikuti arsitektur MVVM dan strategi pemisahan kode .NET Standard, kami akan dapat menggunakan lapisan model yang sudah jadi dan model presentasi ketika mengembangkan antarmuka pengguna untuk berbagai platform dan sistem operasi.

Jika kami menulis aplikasi untuk sistem operasi Windows menggunakan Windows Presentation Foundation, kami dapat dengan mudah porting ke kerangka kerja lain, seperti, misalnya, Avalonia UI atau Xamarin Forms - dan aplikasi kami akan bekerja pada platform seperti iOS, Android, Linux, OSX, dan antarmuka pengguna akan menjadi satu-satunya hal yang perlu ditulis dari awal.
Implementasi MVVM tradisional
Model presentasi biasanya menyertakan properti dan perintah di mana elemen markup XAML dapat diikat. Agar pengikatan data berfungsi, model tampilan harus mengimplementasikan antarmuka INotifyPropertyChanged dan memposting acara PropertyChanged setiap kali properti model tampilan berubah. Implementasi sederhana mungkin terlihat seperti ini:
public class ViewModel : INotifyPropertyChanged { public ViewModel() => Clear = new Command(() => Name = string.Empty); public ICommand Clear { get; } public string Greeting => $"Hello, {Name}!"; private string name = string.Empty; public string Name { get => name; set { if (name == value) return; name = value; OnPropertyChanged(nameof(Name)); OnPropertyChanged(nameof(Greeting)); } } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); } }
XAML yang menggambarkan UI aplikasi:
<StackPanel> <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{Binding Greeting, Mode=OneWay}"/> <Button Content="Clear" Command="{Binding Clear}"/> </StackPanel>
Dan itu berhasil! Ketika pengguna memasukkan namanya di kotak teks, teks di bawah ini langsung berubah, menyapa pengguna.

Tapi tunggu sebentar! UI kami hanya membutuhkan dua properti yang disinkronkan dan satu perintah, mengapa kita perlu menulis lebih dari dua puluh baris kode agar aplikasi kita berfungsi dengan benar? Apa yang terjadi jika kami memutuskan untuk menambahkan lebih banyak properti yang mencerminkan keadaan model tampilan kami? Kode akan menjadi lebih besar, kode akan menjadi lebih rumit dan rumit. Dan kita masih harus mendukungnya!
Resep # 1 Template Pengamat. Getter dan setter pendek. ReaktifUI
Faktanya, masalah implementasi verbose dan membingungkan dari antarmuka INotifyPropertyChanged bukanlah hal baru, dan ada beberapa solusi. Hal pertama yang harus Anda perhatikan adalah
ReactiveUI . Ini adalah kerangka kerja MVVM lintas-platform, fungsional, reaktif yang memungkinkan pengembang .NET untuk menggunakan ekstensi reaktif saat mengembangkan model presentasi.
Ekstensi reaktif adalah implementasi pola desain Observer yang ditentukan oleh antarmuka standar .NET library - IObserver dan IObservable. Pustaka juga mencakup lebih dari lima puluh operator yang memungkinkan Anda mengonversi aliran acara - filter, gabungkan, kelompokkan mereka - menggunakan sintaksis yang mirip dengan bahasa kueri terstruktur
LINQ . Baca lebih lanjut tentang ekstensi jet di
sini .
ReactiveUI juga menyediakan kelas dasar yang mengimplementasikan INotifyPropertyChanged - ReactiveObject. Mari kita menulis ulang kode sampel kita menggunakan fitur yang disediakan oleh framework.
public class ReactiveViewModel : ReactiveObject { public ReactiveViewModel() { Clear = ReactiveCommand.Create(() => Name = string.Empty); this.WhenAnyValue(x => x.Name) .Select(name => $"Hello, {name}!") .ToProperty(this, x => x.Greeting, out greeting); } public ReactiveCommand Clear { get; } private ObservableAsPropertyHelper<string> greeting; public string Greeting => greeting.Value; private string name = string.Empty; public string Name { get => name; set => this.RaiseAndSetIfChanged(ref name, value); } }
Model presentasi seperti itu tidak persis sama dengan yang sebelumnya, tetapi kode di dalamnya lebih kecil, lebih dapat diprediksi, dan semua hubungan antara sifat-sifat model presentasi dijelaskan di satu tempat menggunakan sintaks
LINQ to Observable . Tentu saja, kita bisa berhenti di sini, tetapi masih ada cukup banyak kode - kita harus secara eksplisit mengimplementasikan getter, setter dan field.
Resep # 2. Enkapsulasi INotifyPropertyChanged. Properti Reaktif
Solusi alternatif adalah menggunakan pustaka
ReactiveProperty , yang menyediakan kelas wrapper yang bertanggung jawab untuk mengirim pemberitahuan ke antarmuka pengguna. Dengan
ReactiveProperty, model tampilan tidak harus mengimplementasikan antarmuka apa pun, melainkan setiap properti mengimplementasikan INotifyPropertyChanged sendiri. Properti reaktif tersebut juga menerapkan IObservable, yang berarti bahwa kami dapat berlangganan perubahannya seolah-olah kami menggunakan
ReactiveUI . Ubah model tampilan kami menggunakan ReactiveProperty.
public class ReactivePropertyViewModel { public ReadOnlyReactiveProperty<string> Greeting { get; } public ReactiveProperty<string> Name { get; } public ReactiveCommand Clear { get; } public ReactivePropertyViewModel() { Clear = new ReactiveCommand(); Name = new ReactiveProperty<string>(string.Empty); Clear.Subscribe(() => Name.Value = string.Empty); Greeting = Name .Select(name => $"Hello, {name}!") .ToReadOnlyReactiveProperty(); } }
Kita hanya perlu mendeklarasikan dan menginisialisasi properti reaktif dan menggambarkan hubungan di antara mereka. Tidak ada kode boilerplate yang perlu ditulis selain dari inisialisasi properti. Tetapi pendekatan ini memiliki kelemahan - kita harus mengubah XAML kita agar binding data berfungsi. Properti reaktif adalah pembungkus, jadi UI harus diikat ke properti masing-masing pembungkus tersebut!
<StackPanel> <TextBox Text="{Binding Name.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{Binding Greeting.Value, Mode=OneWay}"/> <Button Content="Clear" Command="{Binding Clear}"/> </StackPanel>
Resep # 3. Mengubah perakitan pada waktu kompilasi. PropertyChanged.Fody + ReactiveUI
Dalam model presentasi yang khas, setiap properti publik harus dapat mengirim pemberitahuan ke antarmuka pengguna saat nilainya berubah. Dengan
PropertyChanged.Fody , Anda tidak perlu khawatir tentang hal itu. Satu-satunya hal yang diperlukan dari pengembang adalah menandai kelas model tampilan dengan atribut
AddINotifyPropertyChangedInterface - dan kode yang bertanggung jawab untuk menerbitkan acara PropertyChanged akan ditambahkan ke setter secara otomatis setelah proyek dibangun, bersama dengan implementasi antarmuka INotifyPropertyChanged, jika ada yang hilang. Jika perlu, ubah properti kami menjadi aliran perubahan nilai, kami selalu dapat menggunakan
metode ekstensi
WhenAnyValue dari pustaka
ReactiveUI . Mari kita menulis ulang sampel kita untuk ketiga kalinya, dan lihat betapa lebih ringkasnya model presentasi kita!
[AddINotifyPropertyChangedInterface] public class FodyReactiveViewModel { public ReactiveCommand Clear { get; } public string Greeting { get; private set; } public string Name { get; set; } = string.Empty; public FodyReactiveViewModel() { Clear = ReactiveCommand.Create(() => Name = string.Empty); this.WhenAnyValue(x => x.Name) .Select(name => $"Hello, {name}!") .Subscribe(x => Greeting = x); } }
Fody mengubah kode IL proyek pada waktu kompilasi. Pencarian add-on
PropertyChanged.Fody untuk semua kelas yang ditandai dengan atribut
AddINotifyPropertyChangedInterface atau mengimplementasikan antarmuka INotifyPropertyChanged, dan mengedit setter dari kelas tersebut. Anda dapat mempelajari lebih lanjut tentang bagaimana pembuatan kode bekerja dan tugas-tugas lain apa yang dapat diselesaikan dari laporan Andrei Kurosh "
Reflection.Emit. Praktek Penggunaan ".
Meskipun PropertyChanged.Fody memungkinkan kita untuk menulis kode yang bersih dan ekspresif, versi lama dari .NET Framework, termasuk 4.5.1 dan yang lebih baru, tidak lagi didukung. Ini berarti bahwa Anda, pada kenyataannya, dapat mencoba menggunakan ReactiveUI dan Fody dalam proyek Anda, tetapi dengan risiko Anda sendiri dan mempertimbangkan bahwa semua kesalahan yang ditemukan tidak akan pernah diperbaiki! Versi untuk .NET Core didukung sesuai
dengan kebijakan dukungan Microsoft .
Dari teori ke praktik. Memvalidasi formulir dengan ReactiveUI dan PropertyChanged.Fody
Sekarang kita siap untuk menulis model presentasi reaktif pertama kami. Mari kita bayangkan bahwa kita sedang mengembangkan sistem multi-pengguna yang kompleks, sambil memikirkan UX dan ingin mengumpulkan umpan balik dari pelanggan kami. Saat seorang pengguna mengirimi kami pesan, kami perlu tahu apakah itu laporan bug atau saran untuk meningkatkan sistem, kami juga ingin mengelompokkan ulasan ke dalam kategori. Pengguna tidak boleh mengirim surat sampai mereka mengisi semua informasi yang diperlukan dengan benar. Model presentasi yang memenuhi kondisi yang tercantum di atas mungkin terlihat seperti ini:
[AddINotifyPropertyChangedInterface] public sealed class FeedbackViewModel { public ReactiveCommand<Unit, Unit> Submit { get; } public bool HasErrors { get; private set; } public string Title { get; set; } = string.Empty; public int TitleLength => Title.Length; public int TitleLengthMax => 15; public string Message { get; set; } = string.Empty; public int MessageLength => Message.Length; public int MessageLengthMax => 30; public int Section { get; set; } public bool Issue { get; set; } public bool Idea { get; set; } public FeedbackViewModel(IService service) { this.WhenAnyValue(x => x.Idea) .Where(selected => selected) .Subscribe(x => Issue = false); this.WhenAnyValue(x => x.Issue) .Where(selected => selected) .Subscribe(x => Idea = false); var valid = this.WhenAnyValue( x => x.Title, x => x.Message, x => x.Issue, x => x.Idea, x => x.Section, (title, message, issue, idea, section) => !string.IsNullOrWhiteSpace(message) && !string.IsNullOrWhiteSpace(title) && (idea || issue) && section >= 0); valid.Subscribe(x => HasErrors = !x); Submit = ReactiveCommand.Create( () => service.Send(Title, Message), valid ); } }
Kami menandai model tampilan kami dengan atribut
AddINotifyPropertyChangedInterface - dengan demikian, semua properti akan memberi tahu UI tentang perubahan nilai mereka. Menggunakan metode
WhenAnyValue , kami akan berlangganan perubahan pada properti ini dan akan memperbarui properti lainnya. Tim yang bertanggung jawab untuk mengirimkan formulir akan tetap tidak aktif sampai pengguna melengkapi formulir dengan benar. Kami akan menyimpan kode kami di perpustakaan kelas yang ditujukan untuk .NET Standard dan beralih ke pengujian.
Pengujian Model Unit
Pengujian adalah bagian penting dari proses pengembangan perangkat lunak. Dengan tes, kita akan dapat mempercayai kode kita dan berhenti takut untuk memperbaikinya - lagi pula, untuk memeriksa operasi program yang benar, itu akan cukup untuk menjalankan tes dan memastikan mereka berhasil diselesaikan. Aplikasi yang menggunakan arsitektur MVVM terdiri dari tiga lapisan, dua di antaranya berisi logika platform-independen - dan kita dapat mengujinya menggunakan .NET Core dan kerangka
XUnit .
Untuk membuat mob dan
stubs , perpustakaan
NSubstitute berguna bagi kami, yang menyediakan API yang nyaman untuk menggambarkan reaksi terhadap tindakan sistem dan nilai yang dikembalikan oleh "benda palsu".
var sumService = Substitute.For<ISumService>(); sumService.Sum(2, 2).Returns(4);
Untuk meningkatkan keterbacaan kode dan pesan kesalahan dalam pengujian kami, kami menggunakan perpustakaan
FluentAssertions . Dengan itu, kita tidak hanya harus mengingat argumen mana dalam Assert. Sama menghitung nilai aktual, dan yang merupakan nilai yang diharapkan, tetapi IDE kita akan menulis kode untuk kita!
var fibs = fibService.GetFibs(10); fibs.Should().NotBeEmpty("because we've requested ten fibs"); fibs.First().Should().Be(1);
Mari kita menulis tes untuk model presentasi kami.
[Fact] public void ShouldValidateFormAndSendFeedback() {
UI untuk Windows Universal Platform
Ok, sekarang model presentasi kami diuji dan kami yakin semuanya berjalan sesuai harapan. Proses mengembangkan lapisan presentasi aplikasi kita cukup sederhana - kita perlu membuat proyek Universal Windows Platform yang bergantung pada platform dan menambahkan tautan ke pustaka .NET Standard yang berisi logika platform-independen dari aplikasi kita. Selanjutnya, hal kecil adalah mendeklarasikan kontrol di XAML, mengikat properti mereka ke properti model tampilan dan ingat untuk
menentukan konteks data dengan cara yang mudah. Ayo lakukan!
<StackPanel Width="300" VerticalAlignment="Center"> <TextBlock Text="Feedback" Style="{StaticResource TitleTextBlockStyle}"/> <TextBox PlaceholderText="Title" MaxLength="{Binding TitleLengthMax}" Text="{Binding Title, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Style="{StaticResource CaptionTextBlockStyle}"> <Run Text="{Binding TitleLength, Mode=OneWay}"/> <Run Text="letters used from"/> <Run Text="{Binding TitleLengthMax}"/> </TextBlock> <TextBox PlaceholderText="Message" MaxLength="{Binding MessageLengthMax}" Text="{Binding Message, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Style="{StaticResource CaptionTextBlockStyle}"> <Run Text="{Binding MessageLength, Mode=OneWay}"/> <Run Text="letters used from"/> <Run Text="{Binding MessageLengthMax}"/> </TextBlock> <ComboBox SelectedIndex="{Binding Section, Mode=TwoWay}"> <ComboBoxItem Content="User Interface"/> <ComboBoxItem Content="Audio"/> <ComboBoxItem Content="Video"/> <ComboBoxItem Content="Voice"/> </ComboBox> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <CheckBox Grid.Column="0" Content="Idea" IsChecked="{Binding Idea, Mode=TwoWay}"/> <CheckBox Grid.Column="1" Content="Issue" IsChecked="{Binding Issue, Mode=TwoWay}"/> </Grid> <TextBlock Visibility="{Binding HasErrors}" Text="Please, fill in all the form fields." Foreground="{ThemeResource AccentBrush}"/> <Button Content="Send Feedback" Command="{Binding Submit}"/> </StackPanel>
Akhirnya, formulir kami siap.

UI untuk Xamarin. Bentuk
Agar aplikasi dapat berfungsi pada perangkat seluler yang menjalankan sistem operasi Android dan iOS, Anda perlu membuat proyek
Xamarin baru dan
menjelaskan UI menggunakan kontrol Xamarin yang disesuaikan untuk perangkat seluler.

UI untuk Avalonia
Avalonia adalah .NET framework lintas platform yang menggunakan dialek XAML yang akrab bagi pengembang WPF, UWP, atau Xamarin.Forms. Avalonia mendukung Windows, Linux, dan OSX dan sedang dikembangkan oleh
komunitas penggemar di GitHub . Untuk bekerja dengan
ReactiveUI, Anda harus menginstal paket
Avalonia.ReactiveUI .
Jelaskan lapisan presentasi pada Avalonia XAML!

Kesimpulan
Seperti yang dapat kita lihat, .NET pada tahun 2018 memungkinkan kita untuk menulis
perangkat lunak lintas-platform yang benar - menggunakan UWP, Xamarin.Form, WPF dan AvaloniaUI, kita dapat memberikan dukungan untuk sistem operasi aplikasi Android, iOS, Windows, Linux, OSX. Pola desain dan perpustakaan MVVM seperti
ReactiveUI dan
Fody dapat menyederhanakan dan mempercepat proses pengembangan dengan menulis kode yang jelas, dapat dipelihara, dan portabel. Infrastruktur yang dikembangkan, dokumentasi terperinci, dan dukungan yang baik dalam editor kode membuat platform .NET semakin menarik bagi pengembang perangkat lunak.
Jika Anda menulis aplikasi desktop atau seluler di .NET dan belum terbiasa dengan ReactiveUI, pastikan untuk memperhatikannya - kerangka kerjanya menggunakan
salah satu klien GitHub paling populer untuk iOS , ekstensi
Visual Studio untuk GitHub , klien git
Atitian SourceTree dan
Slack untuk Windows 10 Mobile Serangkaian artikel tentang ReactiveUI tentang Habré dapat menjadi titik awal yang sangat baik. Untuk pengembang di Xamarin, kursus "
Membangun aplikasi iOS dengan C # " dari salah satu penulis ReactiveUI mungkin akan berguna. Anda dapat mempelajari lebih lanjut
tentang pengalaman pengembangan di AvaloniaUI
dari artikel tentang Egram - klien alternatif untuk Telegram pada .NET Core.
Sumber aplikasi lintas platform yang dijelaskan dalam artikel dan menunjukkan kemungkinan memvalidasi formulir dengan ReactiveUI dan Fody
dapat ditemukan di GitHub . Contoh aplikasi lintas platform yang berjalan di Windows, Linux, macOS dan Android, dan menunjukkan penggunaan ReactiveUI, ReactiveUI.Fody dan
Akavache juga tersedia di GitHub .