ReactiveUI dan Pola MVVM di Aplikasi WPF

Diterbitkan: 2022-03-11

Pemrograman Reaktif adalah paradigma pemrograman asinkron yang berkaitan dengan aliran data dan penyebaran perubahan. – Wikipedia

Setelah Anda membaca kalimat itu, Anda mungkin masih akan berakhir seperti yang saya lakukan ketika saya pertama kali membacanya: Tidak ada tempat yang lebih dekat untuk memahami relevansinya. Sedikit lebih banyak dedikasi ke dalam konsep dasar dan Anda akan segera memahami pentingnya. Pada dasarnya, Anda dapat memikirkan Pemrograman Reaktif pada awalnya sebagai: "Pemrograman berbasis peristiwa pada steroid." Bayangkan pengendali acara sebagai aliran, dan pikirkan setiap pengaktifan pengendali sebagai datum baru di aliran. Singkatnya, yang Anda dapatkan adalah Pemrograman Reaktif.

Ada beberapa konsep yang mungkin ingin Anda pahami sebelum mempelajari Pemrograman Reaktif lebih jauh. Observable adalah objek yang memberi Anda akses ke aliran yang telah kita bicarakan. Tujuannya adalah untuk memberi Anda jendela ke data di aliran. Setelah jendela itu dibuka, Anda dapat melihat data dengan cara apa pun yang Anda pilih dengan menggunakan Operator di dalamnya dan dengan demikian memutuskan kapan dan bagaimana aplikasi Anda bereaksi terhadap aliran. Terakhir, Anda menentukan Pengamat pada aliran yang dihasilkan untuk menentukan tindakan yang akan terjadi setiap kali datum baru dipancarkan oleh aliran.

Dalam istilah praktis, itu berarti Anda mendapatkan lebih banyak kontrol atas cara aplikasi Anda bereaksi terhadap apa yang terjadi, apakah itu pengguna Anda mengklik tombol, aplikasi Anda menerima respons HTTP, atau memulihkan dari pengecualian. Begitu Anda mulai melihat manfaat menggunakan Pemrograman Reaktif (yang jumlahnya banyak), Anda tidak akan bisa kembali lagi. Itu hanya karena sebagian besar hal yang dilakukan aplikasi adalah bereaksi dengan cara tertentu terhadap kemungkinan tertentu.

Sekarang, ini tidak berarti tidak ada kerugian dari pendekatan baru ini. Pertama-tama, kurva belajarnya bisa sangat curam. Saya telah melihat secara langsung bagaimana pengembang (junior, senior, dan arsitek, antara lain) berjuang untuk mencari tahu apa yang harus mereka tulis terlebih dahulu, dalam urutan apa kode mereka dieksekusi, atau bagaimana men-debug kesalahan. Rekomendasi saya ketika pertama kali memperkenalkan konsep-konsep ini adalah untuk menunjukkan banyak contoh. Ketika pengembang mulai melihat bagaimana hal-hal seharusnya bekerja dan digunakan, mereka akan memahaminya.

Saya telah bekerja dengan aplikasi desktop selama lebih dari 10 tahun (kebanyakan Visual Basic 6, Java Swing, dan Windows Forms) sebelum saya menggunakan Windows Presentation Foundation (WPF) untuk pertama kalinya pada tahun 2010. Pada dasarnya, WPF dibuat untuk menggantikan Windows Forms, yang merupakan kerangka kerja pengembangan desktop pertama .NET.

Perbedaan utama antara WPF dan Windows Forms cukup besar, tetapi yang paling penting adalah:

  • WPF menggunakan paradigma pengembangan baru yang lebih kuat dan telah diuji secara menyeluruh.
  • Dengan WPF, Anda dapat memiliki pemisahan desain dan pengkodean yang kuat untuk UI.
  • WPF memungkinkan banyak penyesuaian dan kontrol atas UI Anda.

Setelah saya mulai mempelajari WPF dan kemampuannya, saya sangat menyukainya! Saya tidak percaya betapa mudahnya menerapkan pola MVVM dan seberapa baik pengikatan properti bekerja. Saya tidak berpikir saya akan menemukan apa pun untuk meningkatkan cara kerja itu sampai saya menemukan Pemrograman Reaktif dan penggunaannya dengan WPF:

Dalam posting ini, saya berharap dapat menunjukkan implementasi aplikasi WPF yang sangat sederhana menggunakan Pemrograman Reaktif dengan pola MVVM dan untuk mengakses REST API.

Aplikasi akan dapat:

  • Lacak mobil dan lokasinya
  • Ambil informasi yang diambil dari sumber simulasi
  • Tampilkan informasi ini kepada pengguna di Kontrol WPF Peta Bing

Arsitektur

Anda akan membangun klien WPF yang menggunakan layanan RESTful Web API Core 2.

Sisi klien:

  • WPF
  • UI Reaktif
  • Injeksi ketergantungan
  • pola MVVM
  • Mereparasi
  • Kontrol WPF Peta Bing
  • Untuk tujuan pengujian saja, kami akan menggunakan Postman

Sisi server:

  • .NET C# Web API Core 2
  • Injeksi ketergantungan
  • Otentikasi JWT

Yang Anda perlukan:

  • Komunitas Visual Studio 2017 (Atau edisi apa pun yang Anda miliki)

Bagian belakang

Mulai cepat

Mulai Solusi Visual Studio baru dengan aplikasi web ASP.NET Core.

wpf reactui: Aplikasi web ASP.NET Core studio visual baru

Konfigurasikan itu menjadi API karena kami hanya akan menggunakannya sebagai back-end untuk aplikasi WPF kami.

wpf reaktifui: Konfigurasi sebagai API

Kita harus berakhir dengan solusi VS dengan struktur yang mirip dengan ini:

wpf reaktifui: Contoh Solusi VS

Sejauh ini, kami memiliki semua yang kami butuhkan untuk memulai kembali REST API kami. Jika kami menjalankan proyek kami, itu akan memuat browser web (yang telah kami atur di Visual Studio) yang menunjuk ke situs web yang dihosting di IIS Express yang akan menampilkan respons terhadap panggilan REST dengan objek JSON.

Sekarang, kami akan menyiapkan Otentikasi JWT untuk Layanan REST kami.

Di akhir file startup.cs , tambahkan baris berikut.

 static readonly byte[] JwtKey = Encoding.ASCII.GetBytes(@"this is a test key"); private void LoadJwtAuthorization(IServiceCollection services) { services.AddAuthentication(x => { x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(x => { x.Events = new JwtBearerEvents { OnTokenValidated = context => { var userId = int.Parse(context.Principal.Identity.Name); if (userId == 0) { //Handle user validation against DB context.Fail("Unauthorized"); } return Task.CompletedTask; } }; x.RequireHttpsMetadata = false; x.SaveToken = true; x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(JwtKey), ValidateIssuer = false, ValidateAudience = false }; }); }

Juga, di dalam metode ConfigureServices , panggil metode yang baru saja kita buat sebelum metode AddMvc dipanggil.

 public void ConfigureServices(IServiceCollection services) { LoadJwtAuthorization(services); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }

Terakhir, sesuaikan metode Configure sehingga terlihat seperti ini:

 public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAuthentication(); app.UseMvc(); }

Sejauh ini, kami telah menetapkan Otentikasi JWT untuk digunakan pada pengontrol kami jika itu didefinisikan. Selanjutnya, kami akan menyesuaikan pengontrol sehingga menggunakan otentikasi yang kami jelaskan.

Pada ValuesController , kami akan menambahkan AuthorizeAttribute sehingga menyerupai ini:

 [Route("api/[controller]")] [ApiController] [Authorize] public class ValuesController : ControllerBase { ... }

Sekarang, jika kami mencoba menjalankan layanan kami, kami akan mendapatkan kesalahan 401 Tidak Diotorisasi seperti ini:

Kesalahan Tidak Sah pada Tukang Pos

Jadi, kita perlu menambahkan metode untuk mengautentikasi pengguna kita. Demi kesederhanaan di sini, kita akan melakukannya pada kelas ValuesController yang sama.

 [AllowAnonymous] [HttpPost("authenticate")] public IActionResult Authenticate([FromBody]JObject userInfo) { var username = userInfo["username"].ToString(); var password = userInfo["password"].ToString(); //We would validate against the DB if (username != "user" || password != "123") { return BadRequest(new { message = "Username or password is incorrect" }); } // return basic user info (without password) and token to store on the front-end return Ok(CreateUserToken(1)); } private string CreateUserToken(int userId) { var tokenHandler = new JwtSecurityTokenHandler(); var tokenDescriptor = new SecurityTokenDescriptor { Subject = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, userId.ToString()) }), Expires = DateTime.UtcNow.AddDays(7), SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(Startup.JwtKey), SecurityAlgorithms.HmacSha256Signature) }; var token = tokenHandler.CreateToken(tokenDescriptor); return tokenHandler.WriteToken(token); }

Sekarang kita telah membuat metode dengan akses Anonim, yang berarti bahwa semua klien, bahkan yang tidak diautentikasi, akan dapat memanggilnya menggunakan pesan POST yang berisi objek JSON dan meneruskan string untuk nama pengguna dan string untuk kata sandinya.

Ketika kami meninjaunya dengan Postman, kami mendapatkan ini:

WPF Reaktif: Otentikasi

Seperti yang bisa kita lihat, hasil dari metode autentikasi adalah string yang sekarang perlu kita gunakan sebagai token untuk setiap panggilan yang ingin kita lakukan ke API.

Setelah token disertakan dalam header pesan, validasi dilakukan dan, jika parameter yang benar dilewatkan, layanan menjalankan metode dan mengembalikan nilainya. Misalnya, jika sekarang kita memanggil pengontrol nilai dan meneruskan token, kita mendapatkan hasil yang sama seperti sebelumnya:

WPF Reaktif: Otentikasi 2

Sekarang, kita akan membuat metode untuk mendapatkan garis lintang dan garis bujur untuk mobil yang sedang kita lacak. Sekali lagi, untuk kesederhanaan, itu hanya akan menjadi metode dummy yang pada awalnya akan kembali ke lokasi acak dan mulai menggerakkan mobil dengan jarak yang tetap setiap kali metode dipanggil.

Pertama, kita sesuaikan metode Get(int id) di kelas ValuesController agar terlihat seperti ini:

 [HttpGet("{id}")] public ActionResult<string> Get(int id) { var location = LocationHelper.GetCurrentLocation(id); dynamic jsonObject = new JObject(); jsonObject.Latitude = location.latitude; jsonObject.Longitude = location.longitude; return jsonObject.ToString(); }

Kemudian, kami menambahkan kelas LocationHelper baru yang akan menangani lokasi mobil yang sedang dilacak saat ini dan yang akan datang.

 public static class LocationHelper { private static readonly Random Randomizer = new Random(); private const double PositionDelta = 0.0001d; internal static (double latitude, double longitude) GetCurrentLocation(int id) { if (!Locations.ContainsKey(id)) { Locations.Add(id, default((double latitude, double longitude))); } //This method updates the last known location for the car and simulates its movement UpdateLocation(id); return Locations[id]; } private static void UpdateLocation(int id) { (double latitude, double longitude)loc = Locations[id]; //If the default value is found, randomly assign a starting point. if (loc.latitude == default(double) && loc.longitude == default(double)) { loc = Locations[id] = GetRandomStartingPoint(); } if (Randomizer.Next(2) > 0) { //In this scenario we simulate an updated latitude loc.latitude = loc.latitude + PositionDelta; } else { //Simulated longitude change loc.longitude = loc.longitude + PositionDelta; } Locations[id] = loc; } private static (double latitude, double longitude) GetRandomStartingPoint() { //Set inside the continental US return (Randomizer.Next(31, 49), Randomizer.Next(-121, -75)); } private static readonly Dictionary<int, (double latitude, double longitude)> Locations = new Dictionary<int, (double latitude, double longitude)>(); }

Itu saja untuk bagian belakang.

Ujung depan:

Sekarang kita akan membuat aplikasi WPF baru. Setelah kami membuatnya, Visual Studio akan menambahkan proyek baru dengan struktur berikut ke solusi kami.

Struktur Aplikasi WPF

Kontrol Peta Bing:

Untuk menggunakan kontrol WPF untuk Bing Maps, kita perlu menginstal SDK (direferensikan di atas) dan menambahkannya sebagai referensi ke aplikasi WPF kita. Tergantung di mana Anda menginstalnya, DLL mungkin berada di jalur yang berbeda. Saya menginstalnya di lokasi default dan menambahkannya sebagai berikut:

Langkah 1: Klik kanan pada bagian Referensi untuk proyek WPF Anda dan kemudian klik
Langkah 1: Klik kanan pada bagian Referensi untuk proyek WPF Anda dan kemudian klik "Tambahkan Referensi."

Langkah 2: Jelajahi jalur instalasi Kontrol WPF Bing Maps Anda.
Langkah 2: Jelajahi jalur instalasi Kontrol WPF Bing Maps Anda.

Langkah 3: Klik OK untuk menambahkannya ke proyek.
Langkah 3: Klik OK untuk menambahkannya ke proyek.

Selanjutnya, kami akan menambahkan paket nuget untuk reactiveui , reaktifui reactiveui-wpf dan refit ke proyek WPF kami, yang akan memungkinkan kami untuk membuat model tampilan menggunakan Pemrograman Reaktif serta menggunakan REST API kami.

Langkah 1: Klik kanan pada bagian Referensi proyek WPF Anda dan klik Kelola Paket NuGet.
Langkah 1: Klik kanan pada bagian Referensi proyek WPF Anda dan klik Kelola Paket NuGet.

Langkah 2: Pada tab Browse, cari `reactiveui`, klik install, cari `reactiveui-wpf`, klik install, dan terakhir, cari `refit` dan klik install.
Langkah 2: Pada tab Browse, cari `reactiveui`, klik install, cari `reactiveui-wpf`, klik install, dan terakhir, cari `refit` dan klik install.

Sekarang kita akan membuat ViewModel kita. Tambahkan kelas baru bernama MainViewModel.cs dan buat seperti ini:

 public class MainViewModel : ReactiveObject { #region Private Members private readonly ITrackingService _service; private readonly ISubject<(double latitude, double longitude)> _locationUpdate; #endregion #region Methods public MainViewModel() { _service = Locator.Current.GetService<ITrackingService>(); _locationUpdate = new Subject<(double latitude, double longitude)>(); UpdateCar = ReactiveCommand.Create(() => { var parsedCorrectly = int.TryParse(NewCarToFollow, out int newCar); NewCarToFollow = null; if (!parsedCorrectly) { MessageBox.Show("There was an error reading the number of the car to follow. Please, review it.", "Car Tracking Service", MessageBoxButton.OK, MessageBoxImage.Warning); return; } FollowedCar = newCar; }, canExecute: this.WhenAnyValue(x => x.NewCarToFollow).Select(x => !string.IsNullOrWhiteSpace(x))); /*This Scheduled method is where we get the location for the car being followed every 500 ms. We call the service with the car id, our JWT Token, and transform the result to a ValueTuple (double latitude, double longitude) to pass to our Subject's OnNext method so it can be received by the view */ Scheduler.Default.SchedulePeriodic(TimeSpan.FromMilliseconds(500), () => _service.GetLocation(FollowedCar, App.GetToken()) .Select(jo => ( latitude: double.Parse(jo["Latitude"].ToString()), longitude: double.Parse(jo["Longitude"].ToString()) )).Subscribe(newLocation => _locationUpdate.OnNext(newLocation))); } #endregion #region Properties private string _newCarToFollow; public string NewCarToFollow { get => _newCarToFollow; set => this.RaiseAndSetIfChanged(ref _newCarToFollow, value); } private int _followedCar = 1; public int FollowedCar { get => _followedCar; set => this.RaiseAndSetIfChanged(ref _followedCar, value); } public IObservable<(double latitude, double longitude)> LocationUpdate => _locationUpdate; private ReactiveCommand _updateCar; public ReactiveCommand UpdateCar { get => _updateCar; set => this.RaiseAndSetIfChanged(ref _updateCar, value); } #endregion }

Untuk memberi tahu tampilan bahwa ada ViewModel yang dilampirkan padanya dan dapat digunakan, kita perlu membuat beberapa perubahan pada file MainView.xaml.cs .

 public partial class MainWindow : IViewFor<MainViewModel> { public MainWindow() { InitializeComponent(); ViewModel = Locator.CurrentMutable.GetService<MainViewModel>(); /*Our ViewModel exposes an IObservable with a parameter of type ValueTuple (double latitude, double longitude) and it gets called every time the ViewModel updates the car's location from the REST API.*/ ViewModel.LocationUpdate .ObserveOn(RxApp.MainThreadScheduler) .Subscribe(SetLocation); } private void SetLocation((double latitude, double longitude) newLocation) { //New location for the tracked vehicle. var location = new Location(newLocation.latitude, newLocation.longitude); //Remove previous pin myMap.Children.Clear(); //Center pin and keep same Zoom Level myMap.SetView(location, myMap.ZoomLevel); var pin = new Pushpin { Location = location, Background = Brushes.Green }; //Add new pin to the map myMap.Children.Add(pin); } /// <summary> /// Allows the ViewModel to be used on the XAML via a dependency property /// </summary> public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register("ViewModel", typeof(MainViewModel), typeof(MainWindow), new PropertyMetadata(default(MainViewModel))); /// <summary> /// Implementation for the IViewFor interface /// </summary> object IViewFor.ViewModel { get => ViewModel; set => ViewModel = (MainViewModel)value; } /// <summary> /// Regular property to use the ViewModel from this class /// </summary> public MainViewModel ViewModel { get => (MainViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } }

Kemudian, kita akan memodifikasi file MainWindow.xaml agar terlihat seperti ini:

 <Window x:Class="WpfApp.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 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" xmlns:wpf="clr-namespace:Microsoft.Maps.MapControl.WPF;assembly=Microsoft.Maps.MapControl.WPF" xmlns:local="clr-namespace:WpfApp" DataContext="{Binding ViewModel,RelativeSource={RelativeSource Self}}" d:DataContext="{d:DesignInstance Type=local:MainViewModel, IsDesignTimeCreatable=True}" mc:Ignorable="d" WindowStartupLocation="CenterScreen" Title="Car Tracker" Height="800" Width="1200"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <DockPanel> <StackPanel Orientation="Horizontal" Margin="10" DockPanel.Dock="Left"> <Label>Car to follow</Label> <TextBox Width="50" Text="{Binding NewCarToFollow, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center"/> <Button Margin="15,0,0,0" Content="Update Followed Car" Command="{Binding UpdateCar}"/> </StackPanel> <TextBlock Text="{Binding FollowedCar,StringFormat=Following Car: {0}}" Margin="0,0,10,0" HorizontalAlignment="Right" VerticalAlignment="Center" DockPanel.Dock="Right"/> </DockPanel> <wpf:Map x:Name="myMap" ZoomLevel="15" Grid.Row="1" Margin="10" CredentialsProvider="ENTER-YOUR-BING-MAPS-CREDENTIAL-HERE"/> </Grid> </Window>

Penting untuk menyesuaikan properti CredentialsProvider dengan kunci Bing Maps Anda sendiri.

Untuk dapat mengakses REST API kami, kami akan menggunakan reparasi. Yang perlu kita lakukan adalah membuat antarmuka yang menjelaskan metode API yang akan kita gunakan. Jadi, kami membuat antarmuka baru yang disebut ITrackingService dengan konten berikut:

 public interface ITrackingService { [Post("/api/values/authenticate")] IObservable<string> Authenticate([Body] JObject user); [Get("/api/values/{id}")] IObservable<JObject> GetLocation(int id, [Header("Authorization")] string authorization); }

Terakhir, kami memodifikasi kelas App untuk menyertakan injeksi dependensi (menggunakan Splat, yang ditambahkan saat kami menyertakan referensi ke reactiveui ), mengatur ServerUri (yang harus Anda ubah ke port apa pun yang Anda dapatkan saat menjalankan REST API) dan mensimulasikan login di awal aplikasi.

 public partial class App : Application { protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); SetDependencyInjection(); LogIn(); } private void SetDependencyInjection() { Locator.CurrentMutable.RegisterLazySingleton(() => RestService.For<ITrackingService>(ServerUri), typeof(ITrackingService)); Locator.CurrentMutable.RegisterLazySingleton(() => new MainViewModel(), typeof(MainViewModel)); } private static string Token; private const string ServerUri = "http://localhost:54587"; private void LogIn() { try { var userInfo = new JObject { ["username"] = "user", ["password"] = "123" }; Token = Locator.Current.GetService<ITrackingService>() .Authenticate(userInfo) .Wait(); } catch { MessageBox.Show("There was an error validating the user. Is the service up?"); Shutdown(); } } internal static string GetToken() { return $"Bearer {Token}"; } }

Terakhir, saat kita menjalankan aplikasi, kita akan dapat melihat simulasi real-time dari mobil yang bergerak dengan koordinatnya diambil dari REST API setiap 500 md. Pengguna juga dapat mengubah mobil yang diikuti ke ID lain, dan kumpulan data baru akan dibuat untuk itu.

Saya harap contoh kecil ini telah menunjukkan dasar-dasar penanganan REST API dengan Pemrograman Reaktif di WPF dengan cara yang dapat diakses.

Anda selalu dapat mengunduh seluruh proyek sumber dari repositori ini.

Ada beberapa area untuk melanjutkan contoh ini yang mungkin membantu Anda lebih memahami:

  • Buat jendela masuk dan izinkan pengguna untuk masuk dan keluar.
  • Validasi data pengguna dari database.
  • Buat peran pengguna yang berbeda dan batasi metode tertentu di REST API sehingga hanya pengguna dengan peran tertentu yang dapat mengaksesnya.
  • Pahami lebih banyak tentang Pemrograman Reaktif melalui semua operator dan perilaku mereka dengan Rx Marbles. Rx Marbles adalah aplikasi rapi yang memungkinkan Anda berinteraksi dengan aliran dan menerapkan operator ke titik data di dalamnya.

Kesimpulan

Pemrograman Reaktif terbukti bermanfaat ketika berusaha mencapai cara terkontrol menggunakan pemrograman yang digerakkan oleh peristiwa tanpa mengalami masalah biasa yang melekat pada paradigma ini. Menggunakannya untuk pengembangan baru semudah menambahkan beberapa referensi ke pustaka sumber terbuka yang didukung dengan baik. Tetapi, yang paling penting, memasukkannya ke dalam basis kode yang ada dapat bersifat progresif dan tidak boleh merusak kompatibilitas dengan komponen yang tidak mengimplementasikannya. Artikel ini membahas Pemrograman Reaktif untuk WPF, tetapi ada port untuk sebagian besar bahasa dan kerangka kerja utama yang menjadikan Pemrograman Reaktif petualangan yang baik untuk semua jenis pengembang.

Sebagai latihan, selanjutnya, Anda harus:

  • Perluas perilaku proyek dengan
    • Menambahkan database untuk Pengguna, Mobil, dan Lokasi
    • Mendapatkan lokasi mobil dari database dan menunjukkannya kepada pengguna. Izinkan pengguna untuk menjelajahi pergerakan mobil selama periode waktu tertentu
    • Menambahkan izin pengguna. Biarkan pengguna admin membuat mobil dan pengguna baru dan memberikan akses hanya baca ke pengguna biasa. Tambahkan peran ke Otentikasi JWT.
  • Tinjau kode sumber untuk ekstensi reaktif .NET di https://github.com/dotnet/reactive