ReactiveUI e o padrão MVVM em aplicativos WPF
Publicados: 2022-03-11A Programação Reativa é um paradigma de programação assíncrona preocupado com fluxos de dados e a propagação de mudanças. – Wikipédia
Depois de ler essa frase, você ainda pode acabar da mesma forma que eu fiz quando a li pela primeira vez: nada mais perto de entender sua relevância. Um pouco mais de dedicação aos conceitos básicos e você entenderá rapidamente sua importância. Basicamente, você poderia pensar em Programação Reativa inicialmente como: “Programação orientada a eventos com esteróides”. Imagine um manipulador de eventos como um fluxo e pense em cada disparo do manipulador como um novo dado no fluxo. Em poucas palavras, o que você acaba com é programação reativa.
Existem alguns conceitos que você pode querer entender antes de se aprofundar um pouco mais na Programação Reativa. Observáveis são os objetos que dão acesso aos fluxos sobre os quais falamos. O objetivo deles é fornecer uma janela para os dados no fluxo. Uma vez que a janela foi aberta, você pode ver os dados de qualquer maneira que você escolher usando Operadores e assim decidir quando e como seu aplicativo reage ao fluxo. Por fim, você define os Observadores no fluxo resultante para definir a ação que acontecerá toda vez que um novo dado for emitido pelo fluxo.
Em termos práticos, isso significa apenas que você obtém mais controle sobre a maneira como seu aplicativo reage ao que está acontecendo, seja o usuário clicando em um botão, seu aplicativo recebendo uma resposta HTTP ou recuperando-se de exceções. Uma vez que você comece a ver os benefícios de usar a Programação Reativa (que são muitos), dificilmente você poderá voltar atrás. Isso ocorre simplesmente porque a maioria das coisas que um aplicativo faz é reagir de uma certa maneira a uma determinada eventualidade.
Agora, isso não significa que não haja desvantagens nessa nova abordagem. Em primeiro lugar, sua curva de aprendizado pode ser bastante íngreme. Eu vi em primeira mão como os desenvolvedores (jovens, seniors e arquitetos, entre outros) lutam para descobrir o que devem escrever primeiro, em que ordem seu código está sendo executado ou como depurar erros. Minha recomendação ao introduzir esses conceitos pela primeira vez é mostrar muitos exemplos. Quando os desenvolvedores começarem a ver como as coisas devem funcionar e serem usadas, eles pegarão o jeito.
Eu trabalhava com aplicativos de desktop há mais de 10 anos (principalmente Visual Basic 6, Java Swing e Windows Forms) antes de colocar as mãos no Windows Presentation Foundation (WPF) pela primeira vez em 2010. Basicamente, o WPF foi criado para substituem o Windows Forms, que é a primeira estrutura de desenvolvimento de desktop do .NET.
As principais diferenças entre WPF e Windows Forms são substanciais, mas as mais importantes são:
- O WPF usa novos paradigmas de desenvolvimento que são mais robustos e foram exaustivamente testados.
- Com o WPF, você pode ter uma forte dissociação do design e da codificação da interface do usuário.
- O WPF permite muita personalização e controle sobre sua interface do usuário.
Quando comecei a aprender o WPF e seus recursos, adorei! Eu não podia acreditar como o padrão MVVM era fácil de implementar e como a vinculação de propriedade funcionava. Eu não achava que encontraria nada para melhorar essa maneira de trabalhar até me deparar com a Programação Reativa e seu uso com o WPF:
Neste post, espero poder mostrar uma implementação bem simples de um aplicativo WPF usando Programação Reativa com o padrão MVVM e acessar uma API REST.
O aplicativo será capaz de:
- Rastrear carros e suas localizações
- Obtenha informações extraídas de uma fonte simulada
- Exibir essas informações para o usuário em um controle WPF do Bing Maps
A arquitetura
Você construirá um cliente WPF que consome um serviço RESTful Web API Core 2.
O lado do cliente:
- WPF
- ReactiveUI
- Injeção de dependência
- Padrão MVVM
- Reequipar
- Controle WPF do Bing Maps
- Apenas para fins de teste, usaremos o Postman
O lado do servidor:
- .NET C# Web API Core 2
- Injeção de dependência
- Autenticação JWT
O que você precisará:
- Comunidade do Visual Studio 2017 (ou qualquer edição que você possa ter)
O back-end
Começo rápido
Inicie uma nova solução do Visual Studio com um aplicativo Web ASP.NET Core.
Configure-o para ser uma API, pois só o usaremos como back-end para nosso aplicativo WPF.
Devemos terminar com uma solução VS com uma estrutura semelhante a esta:
Até agora, temos tudo o que precisamos para iniciar nosso back-end da API REST. Se executarmos nosso projeto, ele carregará um navegador da Web (aquele que configuramos no Visual Studio) apontando para um site hospedado no IIS Express que mostrará uma resposta a uma chamada REST com um objeto JSON.
Agora, vamos configurar a autenticação JWT para nosso serviço REST.
No final do arquivo startup.cs
, adicione as seguintes linhas.
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 }; }); }
Além disso, dentro do método ConfigureServices
, chame o método que acabamos de criar antes que o método AddMvc
seja chamado.
public void ConfigureServices(IServiceCollection services) { LoadJwtAuthorization(services); services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); }
Por fim, ajuste o método Configure
para que fique assim:
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAuthentication(); app.UseMvc(); }
Até agora, estabelecemos a Autenticação JWT para ser usada em nossos controladores, caso seja definida. Em seguida, ajustaremos o controlador para que ele use a autenticação que descrevemos.
No ValuesController
, adicionaremos o AuthorizeAttribute
para que fique parecido com isto:
[Route("api/[controller]")] [ApiController] [Authorize] public class ValuesController : ControllerBase { ... }
Agora, se tentarmos executar nosso serviço, receberemos um erro 401 Unauthorized como este:
Então, precisaremos adicionar um método para autenticar nossos usuários. Para simplificar aqui, faremos isso na mesma classe 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); }
Agora criamos um método com acesso Anonymous, o que significa que todos os clientes, mesmo aqueles não autenticados, poderão chamá-lo usando uma mensagem POST contendo um objeto JSON e passando uma string para seu nome de usuário e uma string para sua senha.
Quando revisamos com o Postman, obtemos isso:
Como podemos ver, o resultado do método authenticate foi a própria string que agora precisamos usar como token para cada chamada que queremos fazer à API.
Uma vez que o token é incluído nos cabeçalhos da mensagem, ocorre a validação e, se os parâmetros corretos forem passados, o serviço executa o método e retorna seu valor. Por exemplo, se agora chamarmos o controlador de valores e passarmos o token, obteremos o mesmo resultado de antes:
Agora, vamos criar um método para obter a latitude e longitude do carro atual que estamos rastreando. Novamente, para simplificar, será apenas um método fictício que retornará inicialmente um local aleatório e começará a mover o carro por uma distância fixa toda vez que o método for chamado.
Primeiro, ajustamos o método Get(int id)
na classe ValuesController
para que fique assim:
[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(); }
Em seguida, adicionamos uma nova classe LocationHelper
que lidará com as localizações atuais e futuras dos carros rastreados.
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)>(); }
Isso é tudo para o back-end.
A extremidade dianteira:
Agora vamos criar um novo aplicativo WPF. Depois de criá-lo, o Visual Studio adicionará um novo projeto com a seguinte estrutura à nossa solução.
Controle do Bing Maps:
Para usar o controle WPF para Bing Maps, precisaremos instalar o SDK (referenciado acima) e adicioná-lo como referência ao nosso aplicativo WPF. Dependendo de onde você o instalou, a DLL pode estar em um caminho diferente. Eu instalei no local padrão e adicionei da seguinte forma:

Em seguida, adicionaremos pacotes nuget para reactiveui
, reactiveui-wpf
e refit
ao nosso projeto WPF, o que nos permitirá criar modelos de visualização usando Programação Reativa, além de consumir nossa API REST.
Agora vamos criar nosso ViewModel
. Adicione uma nova classe chamada MainViewModel.cs
e faça com que fique assim:
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 }
Para que a visualização saiba que há um ViewModel
anexado a ele e que pode ser usado, precisaremos fazer algumas alterações no arquivo 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); } }
Em seguida, modificaremos o arquivo MainWindow.xaml
para que fique assim:
<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>
É importante ajustar a propriedade CredentialsProvider
com sua própria chave do Bing Maps.
Para poder acessar nossa API REST, usaremos o refit. Tudo o que precisamos fazer é criar uma interface que descreva os métodos de APIs que usaremos. Assim, criamos uma nova interface chamada ITrackingService com o seguinte conteúdo:
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); }
Por fim, modificamos a classe App
para incluir injeção de dependência (usando Splat, que foi adicionado quando incluímos a referência a reactiveui
), definimos o ServerUri
(que você deve alterar para qualquer porta que obtenha ao executar a API REST) e simular nossa login no início do aplicativo.
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}"; } }
Por fim, quando executarmos nossa aplicação, poderemos ver uma simulação em tempo real de um carro em movimento com suas coordenadas sendo retiradas da API REST a cada 500ms. O usuário também pode alterar o carro que está sendo seguido para qualquer outro ID, e um novo conjunto de dados será criado para ele.
Espero que este pequeno exemplo tenha mostrado o básico de como lidar com uma API REST com Programação Reativa no WPF de maneira acessível.
Você sempre pode baixar todo o projeto de origem deste repositório.
Há algumas áreas para continuar com este exemplo que podem ajudá-lo a aprofundar sua compreensão:
- Crie uma janela de login e permita que o usuário faça login e logout.
- Valide os dados do usuário de um banco de dados.
- Crie diferentes funções de usuário e restrinja determinados métodos na API REST para que apenas um usuário com uma determinada função possa acessá-los.
- Entenda mais sobre Programação Reativa passando por todos os operadores e seu comportamento com Rx Marbles. Rx Marbles é um aplicativo elegante que permite interagir com fluxos e aplicar operadores aos pontos de dados neles.
Conclusão
A Programação Reativa pode ser benéfica ao tentar alcançar uma maneira controlada de usar a programação orientada a eventos sem se deparar com os problemas usuais inerentes a esse paradigma. Usá-lo para novos desenvolvimentos é tão simples quanto adicionar algumas referências a bibliotecas de código aberto bem suportadas. Mas, mais importante, incorporá-lo em bases de código existentes pode ser progressivo e não deve quebrar a compatibilidade com componentes que não o implementam. Este artigo tratou da Programação Reativa para WPF, mas existem portas para a maioria das principais linguagens e frameworks, o que torna a Programação Reativa uma boa aventura para qualquer tipo de desenvolvedor.
Como exercício, a seguir, você deve:
- Estenda o comportamento do projeto por
- Adicionando um banco de dados para usuários, carros e locais
- Obtendo a localização dos carros do banco de dados e mostrando-os ao usuário. Permitir que o usuário explore os movimentos de um carro durante um período de tempo
- Adicionando permissões de usuário. Permita que os usuários administradores criem novos carros e usuários e dê acesso somente leitura a usuários regulares. Adicione funções à autenticação JWT.
- Revise o código-fonte para extensões .NET reativas em https://github.com/dotnet/reactive