ReactiveUI i wzorzec MVVM w aplikacjach WPF

Opublikowany: 2022-03-11

Programowanie reaktywne to paradygmat programowania asynchronicznego dotyczący strumieni danych i propagacji zmian. – Wikipedia

Gdy przeczytasz to zdanie, możesz nadal skończyć w ten sam sposób, co ja, gdy czytałem je po raz pierwszy: Nigdzie nie jesteś bliżej zrozumienia jego znaczenia. Trochę więcej poświęcenia się podstawowym pojęciom, a szybko zrozumiesz ich znaczenie. Zasadniczo można początkowo pomyśleć o programowaniu reaktywnym jako: „Programowanie sterowane zdarzeniami na sterydach”. Wyobraź sobie procedurę obsługi zdarzeń jako strumień i pomyśl o każdym uruchomieniu procedury obsługi jako nowej dacie w strumieniu. Krótko mówiąc, otrzymujesz programowanie reaktywne.

Jest kilka koncepcji, które możesz chcieć zrozumieć, zanim zagłębisz się w programowanie reaktywne. Obserwable to obiekty, które dają dostęp do strumieni, o których mówiliśmy. Ich celem jest udostępnienie ci wglądu w dane w strumieniu. Po otwarciu tego okna możesz przeglądać dane w dowolny sposób, używając na nim Operatorów , a tym samym decydować, kiedy i jak Twoja aplikacja zareaguje na strumień. Na koniec definiujesz obserwatorów w strumieniu wynikowym, aby zdefiniować akcję, która będzie miała miejsce za każdym razem, gdy strumień emituje nowe dane.

W praktyce oznacza to po prostu większą kontrolę nad sposobem, w jaki aplikacja reaguje na to, co się dzieje, niezależnie od tego, czy będzie to kliknięcie przycisku przez użytkownika, otrzymanie przez aplikację odpowiedzi HTTP, czy odzyskiwanie z wyjątków. Gdy zaczniesz dostrzegać korzyści płynące z używania programowania reaktywnego (których jest wiele), z trudem będziesz mógł wrócić. Dzieje się tak po prostu dlatego, że większość rzeczy, które robi aplikacja, reaguje w określony sposób na daną ewentualność.

Nie oznacza to, że to nowe podejście nie ma wad. Po pierwsze, jego krzywa uczenia się może być dość stroma. Widziałem na własne oczy, jak programiści (młodsi, seniorzy i architekci) zmagają się z ustaleniem, co powinni najpierw napisać, w jakiej kolejności wykonywany jest ich kod, lub jak debugować błędy. Moją rekomendacją przy pierwszym wprowadzaniu tych pojęć jest pokazanie wielu przykładów. Kiedy programiści zaczną widzieć, jak rzeczy powinny działać i być używane, zrozumieją to.

Pracowałem z aplikacjami komputerowymi od ponad 10 lat (głównie Visual Basic 6, Java Swing i Windows Forms), zanim po raz pierwszy położyłem ręce na Windows Presentation Foundation (WPF) w 2010 roku. Zasadniczo WPF został stworzony, aby zastępuje Windows Forms, który jest pierwszym frameworkiem programistycznym .NET.

Główne różnice między WPF i Windows Forms są znaczne, ale najważniejsze z nich to:

  • WPF używa nowych paradygmatów programistycznych, które są bardziej niezawodne i zostały dokładnie przetestowane.
  • Dzięki WPF możesz mieć silne oddzielenie projektu i kodowania dla interfejsu użytkownika.
  • WPF pozwala na wiele dostosowywania i kontroli nad interfejsem użytkownika.

Kiedy zacząłem uczyć się WPF i jego możliwości, bardzo mi się to spodobało! Nie mogłem uwierzyć, jak łatwo było zaimplementować wzorzec MVVM i jak dobrze działało wiązanie właściwości. Nie sądziłem, że znajdę coś, co ulepszy ten sposób pracy, dopóki nie natknąłem się na programowanie reaktywne i jego użycie z WPF:

W tym poście mam nadzieję, że będę mógł pokazać bardzo prostą implementację aplikacji WPF za pomocą programowania reaktywnego z wzorcem MVVM i uzyskać dostęp do API REST.

Aplikacja będzie mogła:

  • Śledź samochody i ich lokalizacje
  • Weź informacje pobrane z symulowanego źródła
  • Wyświetl te informacje użytkownikowi w kontrolce WPF usługi Bing Maps

Architektura

Zbudujesz klienta WPF, który korzysta z usługi RESTful Web API Core 2.

Strona klienta:

  • WPF
  • Reaktywny interfejs użytkownika
  • Wstrzykiwanie zależności
  • Wzorzec MVVM
  • Remont
  • Kontrola WPF Map Bing
  • Tylko do celów testowych będziemy używać Postmana

Strona serwera:

  • Rdzeń internetowego interfejsu API .NET C# 2
  • Wstrzykiwanie zależności
  • Uwierzytelnianie JWT

Co będziesz potrzebował:

  • Społeczność programu Visual Studio 2017 (lub dowolna wersja, którą możesz mieć)

Zaplecze

Szybki start

Uruchom nowe rozwiązanie programu Visual Studio z aplikacją sieci Web ASP.NET Core.

wpf reactiveui: Nowa aplikacja internetowa ASP.NET Core Visual Studio

Skonfiguruj go jako interfejs API, ponieważ będziemy używać go tylko jako zaplecza dla naszej aplikacji WPF.

wpf reactiveui: Skonfiguruj jako API

Powinniśmy otrzymać rozwiązanie VS o strukturze podobnej do tej:

wpf reactiveui: przykład rozwiązania VS

Jak dotąd mamy wszystko, czego potrzebujemy, aby uruchomić nasz back-end REST API. Jeśli uruchomimy nasz projekt, załaduje on przeglądarkę internetową (tę, którą ustawiliśmy w Visual Studio) wskazującą witrynę internetową hostowaną w IIS Express, która pokaże odpowiedź na wywołanie REST z obiektem JSON.

Teraz skonfigurujemy uwierzytelnianie JWT dla naszej usługi REST.

Na końcu pliku startup.cs dodaj następujące wiersze.

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

Ponadto wewnątrz metody ConfigureServices wywołaj metodę, którą właśnie utworzyliśmy, zanim zostanie wywołana metoda AddMvc .

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

Na koniec dostosuj metodę Configure , aby wyglądała tak:

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

Do tej pory ustanowiliśmy uwierzytelnianie JWT, które ma być używane na naszych kontrolerach, jeśli jest zdefiniowane. Następnie dostosujemy nasz kontroler, aby używał opisanego przez nas uwierzytelniania.

W ValuesController dodamy AuthorizeAttribute , aby wyglądał podobnie:

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

Teraz, jeśli spróbujemy uruchomić naszą usługę, otrzymamy następujący błąd 401 Unauthorized:

Nieautoryzowany błąd listonosza

Dlatego musimy dodać metodę uwierzytelniania naszych użytkowników. Dla uproszczenia tutaj zrobimy to na tej samej klasie ValuesController .

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

Teraz stworzyliśmy metodę z dostępem anonimowym, co oznacza, że ​​wszyscy klienci, nawet ci nieuwierzytelnieni, będą mogli wywołać ją za pomocą wiadomości POST zawierającej obiekt JSON i przekazującej ciąg znaków jako nazwę użytkownika i ciąg znaków jako hasło.

Kiedy sprawdzamy to z Postmanem, otrzymujemy to:

Reaktywny WPF: uwierzytelnianie

Jak widzimy, wynikiem metody uwierzytelnienia był ten sam ciąg znaków, którego teraz potrzebujemy jako tokena dla każdego wywołania, które chcemy wykonać w interfejsie API.

Po umieszczeniu tokena w nagłówkach wiadomości następuje walidacja i w przypadku przekazania prawidłowych parametrów usługa uruchamia metodę i zwraca jej wartość. Na przykład, jeśli teraz wywołamy kontroler wartości i przekażemy token, otrzymamy taki sam wynik jak poprzednio:

Reaktywny WPF: Uwierzytelnianie 2

Teraz utworzymy metodę, aby uzyskać szerokość i długość geograficzną dla bieżącego samochodu, który śledzimy. Ponownie, dla uproszczenia, będzie to po prostu fikcyjna metoda, która najpierw zwróci losową lokalizację i zacznie przesuwać samochód o ustaloną odległość za każdym razem, gdy metoda zostanie wywołana.

Najpierw dostosowujemy metodę Get(int id) w klasie ValuesController , aby wyglądała tak:

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

Następnie dodajemy nową klasę LocationHelper , która będzie obsługiwać obecną i przyszłą lokalizację śledzonych samochodów.

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

To wszystko na zapleczu.

Przód:

Utworzymy teraz nową aplikację WPF. Po utworzeniu programu Visual Studio doda do naszego rozwiązania nowy projekt o następującej strukturze.

Struktura aplikacji WPF

Kontrola map Bing:

Aby użyć kontrolki WPF dla Bing Maps, musimy zainstalować zestaw SDK (wspomniany powyżej) i dodać go jako odwołanie do naszej aplikacji WPF. W zależności od tego, gdzie go zainstalowałeś, biblioteka DLL może znajdować się w innej ścieżce. Zainstalowałem go w domyślnej lokalizacji i dodałem w następujący sposób:

Krok 1: Kliknij prawym przyciskiem myszy sekcję Referencje dla projektu WPF, a następnie kliknij
Krok 1: Kliknij prawym przyciskiem myszy sekcję Referencje projektu WPF, a następnie kliknij „Dodaj odwołanie”.

Krok 2: Przejdź do ścieżki instalacji Bing Maps WPF Control.
Krok 2: Przejdź do ścieżki instalacji Bing Maps WPF Control.

Krok 3: Kliknij OK, aby dodać go do projektu.
Krok 3: Kliknij OK, aby dodać go do projektu.

Następnie dodamy pakiety nuget dla reactiveui , reactiveui-wpf i refit do naszego projektu WPF, co umożliwi nam tworzenie modeli widoków za pomocą programowania reaktywnego, a także wykorzystanie naszego interfejsu API REST.

Krok 1: Kliknij prawym przyciskiem myszy sekcję Odwołania projektu WPF i kliknij Zarządzaj pakietami NuGet.
Krok 1: Kliknij prawym przyciskiem myszy sekcję Odwołania projektu WPF i kliknij Zarządzaj pakietami NuGet.

Krok 2: Na karcie Przeglądaj wyszukaj `reactiveui`, kliknij zainstaluj, wyszukaj `reactiveui-wpf`, kliknij zainstaluj i na koniec wyszukaj `refit` i kliknij zainstaluj.
Krok 2: Na karcie Przeglądaj wyszukaj `reactiveui`, kliknij zainstaluj, wyszukaj `reactiveui-wpf`, kliknij zainstaluj i na koniec wyszukaj `refit` i kliknij zainstaluj.

Stworzymy teraz nasz ViewModel . Dodaj nową klasę o nazwie MainViewModel.cs i spraw, aby wyglądała tak:

 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 }

Aby widok wiedział, że jest do niego dołączony ViewModel i można go używać, musimy wprowadzić kilka zmian w pliku 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); } }

Następnie zmodyfikujemy plik MainWindow.xaml , aby wyglądał następująco:

 <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>

Ważne jest, aby dostosować właściwość CredentialsProvider za pomocą własnego klucza Bing Maps.

Aby móc uzyskać dostęp do naszego interfejsu API REST, użyjemy narzędzia refit. Wszystko, co musimy zrobić, to stworzyć interfejs opisujący metody API, których będziemy używać. Dlatego tworzymy nowy interfejs o nazwie ITrackingService z następującą zawartością:

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

Na koniec modyfikujemy klasę App , aby zawierała wstrzykiwanie zależności (używając Splat, który został dodany, gdy dołączyliśmy odwołanie do reactiveui ), ustawiamy ServerUri (który należy zmienić na dowolny port, który otrzymasz po uruchomieniu interfejsu API REST) ​​i symulujemy nasz zaloguj się na samym początku aplikacji.

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

Wreszcie, gdy uruchomimy naszą aplikację, będziemy mogli zobaczyć symulację poruszającego się samochodu w czasie rzeczywistym, której współrzędne są pobierane z interfejsu API REST co 500ms. Użytkownik może również zmienić śledzony samochód na dowolny inny identyfikator, a dla niego zostanie utworzony nowy zestaw danych.

Mam nadzieję, że ten mały przykład w przystępny sposób pokazał podstawy obsługi REST API z reaktywnym programowaniem w WPF.

Zawsze możesz pobrać cały projekt źródłowy z tego repozytorium.

Jest kilka obszarów, które można kontynuować z tym przykładem, które mogą pomóc w lepszym zrozumieniu:

  • Utwórz okno logowania i zezwól użytkownikowi na logowanie i wylogowanie.
  • Sprawdź poprawność danych użytkownika z bazy danych.
  • Twórz różne role użytkowników i ograniczaj niektóre metody w interfejsie API REST, aby dostęp do nich miał tylko użytkownik z określoną rolą.
  • Dowiedz się więcej o programowaniu reaktywnym, przechodząc przez wszystkie operatory i ich zachowanie dzięki marmurom Rx. Rx Marbles to zgrabna aplikacja, która pozwala na interakcję ze strumieniami i stosowanie operatorów do zawartych w nich punktów danych.

Wniosek

Programowanie reaktywne może okazać się korzystne, gdy dąży się do osiągnięcia kontrolowanego sposobu korzystania z programowania sterowanego zdarzeniami bez napotykania typowych problemów związanych z tym paradygmatem. Używanie go do nowych projektów jest tak proste, jak dodanie kilku odniesień do dobrze obsługiwanych bibliotek open source. Ale co najważniejsze, włączenie go do istniejących baz kodu może być progresywne i nie powinno naruszać wstecznej kompatybilności z komponentami, które go nie implementują. Ten artykuł dotyczył programowania reaktywnego dla WPF, ale istnieją porty do większości głównych języków i frameworków, co sprawia, że ​​programowanie reaktywne jest dobrą przygodą dla każdego rodzaju programisty.

Jako ćwiczenie, następnie powinieneś:

  • Rozszerz zachowanie projektu o
    • Dodawanie bazy danych dla Użytkowników, Samochodów i Lokalizacji
    • Pobieranie lokalizacji samochodów z bazy danych i wyświetlanie ich użytkownikowi. Pozwól użytkownikowi na obserwowanie ruchów samochodu przez pewien czas
    • Dodanie uprawnień użytkownika. Pozwól administratorom tworzyć nowe samochody i użytkowników, a zwykłym użytkownikom przyznaj dostęp tylko do odczytu. Dodaj role do uwierzytelniania JWT.
  • Przejrzyj kod źródłowy rozszerzeń .NET reaktywnych na https://github.com/dotnet/reactive