WPF Uygulamalarında ReactiveUI ve MVVM Kalıbı
Yayınlanan: 2022-03-11Reaktif Programlama, veri akışları ve değişimin yayılmasıyla ilgili eşzamansız bir programlama paradigmasıdır. - Vikipedi
Bu cümleyi okuduktan sonra, hala ilk okuduğumda yaptığım gibi olabilirsin: Alaka düzeyini anlamaktan daha yakın bir yer yok. Temel kavramlara biraz daha bağlılık ve önemini çabucak anlayacaksınız. Temel olarak, Reaktif Programlamayı ilk başta şu şekilde düşünebilirsiniz: "Steroidler üzerinde olaya dayalı programlama." Bir olay işleyiciyi akış olarak hayal edin ve işleyicinin her tetiklenmesini akışta yeni bir veri olarak düşünün. Özetle, elde ettiğiniz şey Reaktif Programlamadır.
Reaktif Programlamaya biraz daha girmeden önce anlamak isteyebileceğiniz bazı kavramlar var. Gözlenebilirler , bahsettiğimiz akışlara erişmenizi sağlayan nesnelerdir. Amaçları, akıştaki verilere bir pencere açmaktır. Bu pencere açıldıktan sonra, üzerindeki Operatörleri kullanarak istediğiniz şekilde verilere bakabilir ve böylece uygulamanızın akışa ne zaman ve nasıl tepki vereceğine karar verebilirsiniz. Son olarak, akış tarafından her yeni bir veri yayınlandığında gerçekleşecek eylemi tanımlamak için ortaya çıkan akışta Gözlemcileri tanımlarsınız.
Pratik açıdan, bu, kullanıcınızın bir düğmeyi tıklaması, uygulamanızın bir HTTP yanıtı alması veya istisnalardan kurtulması gibi, uygulamanızın olup bitenlere tepki verme şekli üzerinde daha fazla kontrol sahibi olduğunuz anlamına gelir. Reaktif Programlama kullanmanın faydalarını görmeye başladığınızda (ki bunlardan çoktur), geri dönmeniz pek mümkün olmayacaktır. Bunun nedeni, bir uygulamanın yaptığı şeylerin çoğunun belirli bir olasılığa belirli bir şekilde tepki vermesidir.
Bu, bu yeni yaklaşımın olumsuz tarafları olmadığı anlamına gelmiyor. Her şeyden önce, öğrenme eğrisi oldukça dik olabilir. Geliştiricilerin (diğerlerinin yanı sıra gençler, yaşlılar ve mimarlar) ilk önce ne yazmaları gerektiğini, kodlarının hangi sırayla yürütüldüğünü veya hataların nasıl ayıklanacağını anlamaya nasıl uğraştığını ilk elden gördüm. Bu kavramları ilk tanıtırken tavsiyem bolca örnek göstermenizdir. Geliştiriciler, işlerin nasıl çalışması ve kullanılması gerektiğini görmeye başladıklarında, işin aslını anlayacaklardır.
2010'da ilk kez Windows Presentation Foundation'ı (WPF) kullanmadan önce 10 yılı aşkın süredir (çoğunlukla Visual Basic 6, Java Swing ve Windows Forms) masaüstü uygulamalarıyla çalışıyordum. Temel olarak, WPF, .NET'in ilk masaüstü geliştirme çerçevesi olan Windows Forms'un yerini alır.
WPF ve Windows Forms arasındaki büyük farklar önemlidir, ancak en önemlileri şunlardır:
- WPF, daha sağlam ve kapsamlı bir şekilde test edilmiş yeni geliştirme paradigmalarını kullanır.
- WPF ile, UI için tasarım ve kodlamayı güçlü bir şekilde ayırabilirsiniz.
- WPF, kullanıcı arayüzünüz üzerinde çok sayıda özelleştirme ve kontrol sağlar.
WPF'yi ve yeteneklerini öğrenmeye başladığımda kesinlikle onu sevdim! MVVM modelinin uygulanmasının ne kadar kolay olduğuna ve mülk bağlamanın ne kadar iyi çalıştığına inanamadım. Reaktif Programlama ve WPF ile kullanımına rastlayana kadar bu çalışma şeklini geliştirecek bir şey bulacağımı düşünmemiştim:
Bu yazıda, MVVM modeliyle Reaktif Programlama kullanarak bir WPF uygulamasının çok basit bir uygulamasını gösterebilmeyi ve bir REST API'sine erişmeyi umuyorum.
Uygulama şunları yapabilecektir:
- Arabaları ve konumlarını takip edin
- Simüle edilmiş bir kaynaktan alınan bilgileri alın
- Bu bilgileri bir Bing Haritalar WPF Denetiminde kullanıcıya görüntüleyin
Mimarlık
RESTful Web API Core 2 hizmetini kullanan bir WPF istemcisi oluşturacaksınız.
Müşteri tarafı:
- WPF
- reaktif kullanıcı arayüzü
- Bağımlılık enjeksiyonu
- MVVM modeli
- tamir
- Bing Haritalar WPF Kontrolü
- Yalnızca test amacıyla Postacı kullanacağız
Sunucu tarafı:
- .NET C# Web API Çekirdeği 2
- Bağımlılık enjeksiyonu
- JWT kimlik doğrulaması
Neye ihtiyacınız olacak:
- Visual Studio 2017 Topluluğu (Veya sahip olabileceğiniz herhangi bir sürüm)
arka uç
Hızlı başlangıç
ASP.NET Core web uygulamasıyla yeni bir Visual Studio Çözümü başlatın.
API olarak yapılandırın, çünkü onu yalnızca WPF uygulamamız için arka uç olarak kullanacağız.
Buna benzer bir yapıya sahip bir VS çözümü bulmalıyız:
Şimdiye kadar, REST API arka uçumuzu başlatmak için ihtiyacımız olan her şeye sahibiz. Projemizi çalıştırırsak, IIS Express'te barındırılan ve JSON nesnesiyle yapılan bir REST çağrısına yanıt gösterecek bir web sitesine işaret eden bir web tarayıcısı (Visual Studio'da ayarladığımız tarayıcı) yükleyecektir.
Şimdi, REST Hizmetimiz için JWT Kimlik Doğrulaması ayarlayacağız.
startup.cs
dosyasının sonuna aşağıdaki satırları ekleyin.
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 }; }); }
Ayrıca, ConfigureServices
yönteminin içinde, AddMvc
yöntemi çağrılmadan önce az önce oluşturduğumuz yöntemi çağırın.
public void ConfigureServices(IServiceCollection services) { LoadJwtAuthorization(services); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }
Son olarak, Configure
yöntemini şöyle görünecek şekilde ayarlayın:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAuthentication(); app.UseMvc(); }
Şimdiye kadar, tanımlanmışsa denetleyicilerimizde kullanılacak JWT Kimlik Doğrulamasını kurduk. Ardından, denetleyiciyi tanımladığımız kimlik doğrulamasını kullanacak şekilde ayarlayacağız.
ValuesController
üzerinde AuthorizeAttribute
şuna benzeyecek şekilde ekleyeceğiz:
[Route("api/[controller]")] [ApiController] [Authorize] public class ValuesController : ControllerBase { ... }
Şimdi, hizmetimizi çalıştırmayı denersek, aşağıdaki gibi bir 401 Yetkisiz hatası alacağız:
Bu nedenle, kullanıcılarımızın kimliğini doğrulamak için bir yöntem eklememiz gerekecek. Buradaki basitlik adına, bunu aynı ValuesController
sınıfında yapacağız.
[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); }
Şimdi, Anonim erişime sahip bir yöntem oluşturduk; bu, kimliği doğrulanmamış olanlar da dahil olmak üzere tüm istemcilerin, bir JSON nesnesi içeren ve kullanıcı adı için bir dize ve şifresi için bir dize ileten bir POST mesajı kullanarak onu çağırabileceği anlamına gelir.
Postman ile incelediğimizde şunu elde ederiz:
Gördüğümüz gibi, kimlik doğrulama yönteminin sonucu, artık API'ye yapmak istediğimiz her çağrı için belirteç olarak kullanmamız gereken dizeydi.
Belirteç mesajın başlıklarına eklendiğinde doğrulama gerçekleşir ve doğru parametreler iletilirse hizmet yöntemi çalıştırır ve değerini döndürür. Örneğin, şimdi değerler denetleyicisini çağırır ve belirteci geçirirsek, öncekiyle aynı sonucu alırız:
Şimdi, takip ettiğimiz mevcut arabanın enlem ve boylamını almak için bir yöntem oluşturacağız. Yine, basitlik için, ilk başta rastgele bir konuma dönecek ve yöntem her çağrıldığında arabayı sabit bir mesafe kadar hareket ettirmeye başlayacak kukla bir yöntem olacaktır.
İlk olarak, ValuesController
sınıfındaki Get(int id)
yöntemini şöyle görünmesi için ayarlıyoruz:
[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(); }
Ardından, izlenen arabaların mevcut ve gelecekteki konumlarını işleyecek yeni bir LocationHelper
sınıfı ekliyoruz.
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)>(); }
Arka taraf için bu kadar.
Ön uç:
Şimdi yeni bir WPF uygulaması oluşturacağız. Oluşturduğumuzda, Visual Studio çözümümüze aşağıdaki yapıya sahip yeni bir proje ekleyecektir.
Bing Haritalar Kontrolü:
Bing Haritalar için WPF kontrolünü kullanmak için, SDK'yı (yukarıda atıfta bulunulan) yüklememiz ve onu WPF uygulamamıza referans olarak eklememiz gerekecek. Yüklediğiniz yere bağlı olarak DLL farklı bir yolda olabilir. Varsayılan konuma yükledim ve aşağıdaki gibi ekledim:

Daha sonra, WPF projemize reactiveui
, reactiveui-wpf
ve refit
için nuget paketleri ekleyeceğiz, bu da Reaktif Programlama kullanarak görünüm modelleri oluşturmamıza ve REST API'mizi tüketmemize izin verecek.
Şimdi ViewModel
oluşturacağız. MainViewModel.cs
adında yeni bir sınıf ekleyin ve şöyle görünmesini sağlayın:
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 }
Görünümün kendisine eklenmiş bir ViewModel
olduğunu bilmesini sağlamak ve kullanabilmek için MainView.xaml.cs
dosyasında birkaç değişiklik yapmamız gerekecek.
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); } }
Ardından, MainWindow.xaml
dosyasını şöyle görünmesi için değiştireceğiz:
<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>
CredentialsProvider
özelliğini kendi Bing Haritalar anahtarınızla ayarlamanız önemlidir.
REST API'mize erişebilmek için refit kullanacağız. Tek yapmamız gereken, kullanacağımız API yöntemlerini açıklayan bir arayüz oluşturmak. Bu nedenle, aşağıdaki içeriğe sahip ITrackingService adlı yeni bir arayüz oluşturuyoruz:
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); }
Son olarak, App
sınıfını bağımlılık enjeksiyonunu içerecek şekilde değiştiriyoruz ( reactiveui
referansını eklediğimizde eklenen Splat kullanarak), ServerUri
(REST API'sini çalıştırdığınızda aldığınız bağlantı noktasına değiştirmelisiniz) ayarlayın ve bizim simülasyonumuzu simüle edin. uygulamanın en başında oturum açın.
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}"; } }
Son olarak, uygulamamızı çalıştırdığımızda, koordinatları her 500 ms'de bir REST API'sinden alınan hareket eden bir arabanın gerçek zamanlı simülasyonunu görebileceğiz. Kullanıcı ayrıca takip edilen arabayı başka bir ID ile değiştirebilir ve bunun için yeni bir veri seti oluşturulacaktır.
Umarım bu küçük örnek, WPF'de Reaktif Programlama ile bir REST API'sini erişilebilir bir şekilde kullanmanın temellerini göstermiştir.
Kaynak projenin tamamını her zaman bu depodan indirebilirsiniz.
Bu örnekle devam etmeniz gereken, anlayışınızı ilerletmenize yardımcı olabilecek bazı alanlar var:
- Bir oturum açma penceresi oluşturun ve kullanıcının oturum açıp kapatmasına izin verin.
- Bir veritabanından kullanıcı verilerini doğrulayın.
- Farklı kullanıcı rolleri oluşturun ve yalnızca belirli bir role sahip bir kullanıcının bunlara erişebilmesi için REST API'de belirli yöntemleri kısıtlayın.
- Rx Marbles ile tüm operatörlerden geçen Reaktif Programlama ve davranışlarını daha iyi anlayın. Rx Marbles, akışlarla etkileşime girmenize ve içlerindeki veri noktalarına operatörler uygulamanıza izin veren temiz bir uygulamadır.
Çözüm
Reaktif Programlama, olaya dayalı programlamayı bu paradigmanın doğasında bulunan olağan sorunlarla karşılaşmadan kontrollü bir şekilde kullanmaya çalışırken faydalı olabilir. Yeni gelişmeler için kullanmak, iyi desteklenen açık kaynak kitaplıklarına birkaç referans eklemek kadar basittir. Ancak en önemlisi, onu mevcut kod tabanlarına dahil etmek ilerici olabilir ve onu uygulamayan bileşenlerle geriye dönük uyumluluğu bozmamalıdır. Bu makale WPF için Reaktif Programlama ile ilgiliydi, ancak Reaktif Programlamayı her tür geliştirici için iyi bir macera haline getiren çoğu ana dil ve çerçeve için bağlantı noktaları vardır.
Bir egzersiz olarak, daha sonra şunları yapmalısınız:
- Projenin davranışını şu şekilde genişletin:
- Kullanıcılar, Arabalar ve Konumlar için bir veritabanı ekleme
- Veritabanından arabaların konumunu almak ve bunları kullanıcıya göstermek. Kullanıcının belirli bir süre boyunca bir arabanın hareketlerini keşfetmesine izin verin
- Kullanıcı izinleri ekleme. Yönetici kullanıcıların yeni arabalar ve kullanıcılar oluşturmasına ve normal kullanıcılara salt okuma erişimi vermesine izin verin. JWT Kimlik Doğrulamasına roller ekleyin.
- https://github.com/dotnet/reactive adresinde .NET reaktif uzantıları için kaynak kodunu inceleyin