Desenvolvimento de plataforma cruzada com .NET, programação reativa, padrão MVVM e geração de código

MVVM reativo e padrão .NET

Hoje, a plataforma .NET é uma ferramenta verdadeiramente universal - com sua ajuda, você pode resolver uma ampla gama de tarefas, incluindo o desenvolvimento de aplicativos para sistemas operacionais populares, como Windows, Linux, MacOS, Android e iOS. Neste artigo, examinaremos a arquitetura de aplicativos .NET de plataforma cruzada usando o padrão de design MVVM e a programação reativa . Vamos nos familiarizar com as bibliotecas ReactiveUI e Fody , aprender como implementar a interface INotifyPropertyChanged usando atributos, abordar o básico do AvaloniaUI , Xamarin Forms , Universal Windows Platform , Windows Presentation Foundation e .NET Standard e aprender ferramentas eficazes para camadas de modelo de teste de unidade e modelos de apresentação de aplicativos.

O material é uma adaptação dos artigos " MVVM reativa para a plataforma .NET " e " Aplicativos .NET multiplataforma através da abordagem MVVM reativa ", publicados pelo autor anteriormente no recurso Médio. Código de exemplo está disponível no GitHub .

1. Introdução Arquitetura MVVM e .NET de plataforma cruzada


Ao desenvolver aplicativos de plataforma cruzada na plataforma .NET, você deve escrever código portátil e suportado. Se você trabalha com estruturas que usam dialetos XAML, como UWP, WPF, Xamarin Forms e AvaloniaUI, isso pode ser alcançado usando o padrão de design MVVM, a programação reativa e a estratégia de separação de código do .NET Standard. Essa abordagem melhora a portabilidade do aplicativo, permitindo que os desenvolvedores usem uma base de código comum e bibliotecas de software comuns em vários sistemas operacionais.

Examinaremos cada uma das camadas de um aplicativo criado com base na arquitetura MVVM - o Model, o View e o ViewModel. A camada do modelo representa serviços de domínio, objetos de transferência de dados, entidades de banco de dados, repositórios - toda a lógica comercial do nosso programa. A visualização é responsável por exibir os elementos da interface do usuário na tela e depende do sistema operacional específico, e o modelo de apresentação permite que as duas camadas descritas acima interajam, adaptando a camada do modelo para interagir com o usuário humano.

A arquitetura MVVM fornece a divisão de responsabilidades entre as três camadas de software do aplicativo, para que essas camadas possam ser colocadas em montagens separadas destinadas ao .NET Standard. A especificação formal do .NET Standard permite que os desenvolvedores criem bibliotecas portáteis que podem ser usadas em várias implementações do .NET com um único conjunto unificado de APIs. Seguindo rigorosamente a arquitetura MVVM e a estratégia de separação de código .NET Standard, poderemos usar camadas de modelos prontas e modelos de apresentação ao desenvolver a interface do usuário para várias plataformas e sistemas operacionais.

imagem

Se escrevemos um aplicativo para o sistema operacional Windows usando o Windows Presentation Foundation, podemos portá-lo facilmente para outras estruturas, como, por exemplo, Avalonia UI ou Xamarin Forms - e nosso aplicativo funcionará em plataformas como iOS, Android, Linux, OSX e a interface do usuário serão a única coisa que precisará ser escrita do zero.

Implementação tradicional do MVVM


Os modelos de apresentação geralmente incluem propriedades e comandos aos quais os elementos de marcação XAML podem ser ligados. Para que as ligações de dados funcionem, o modelo de exibição deve implementar a interface INotifyPropertyChanged e postar o evento PropertyChanged sempre que qualquer propriedade do modelo de exibição for alterada. Uma implementação simples pode ser assim:

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 que descreve a interface do usuário do aplicativo:

 <StackPanel> <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{Binding Greeting, Mode=OneWay}"/> <Button Content="Clear" Command="{Binding Clear}"/> </StackPanel> 

E funciona! Quando o usuário digita seu nome na caixa de texto, o texto abaixo muda instantaneamente, cumprimentando o usuário.

Amostra de Ligação MVVM

Mas espere um momento! Nossa interface do usuário precisa de apenas duas propriedades sincronizadas e um comando. Por que precisamos escrever mais de vinte linhas de código para que nosso aplicativo funcione corretamente? O que acontece se decidirmos adicionar mais propriedades que refletem o estado do nosso modelo de exibição? O código se tornará maior, o código se tornará mais complicado e complicado. E ainda temos que apoiá-lo!

Receita nº 1. Modelo de observador. Getters e setters curtos. ReactiveUI


De fato, o problema da implementação detalhada e confusa da interface INotifyPropertyChanged não é novo e há várias soluções. A primeira coisa que você deve prestar atenção ao ReactiveUI . Essa é uma estrutura MVVM reativa, multiplataforma, funcional que permite que os desenvolvedores .NET usem extensões reativas ao desenvolver modelos de apresentação.

Extensões reativas são uma implementação do padrão de design do Observer definido pelas interfaces da biblioteca .NET padrão - IObserver e IObservable. A biblioteca também inclui mais de cinquenta operadores que permitem converter fluxos de eventos - filtrar, combinar, agrupá-los - usando uma sintaxe semelhante à linguagem de consulta estruturada LINQ . Leia mais sobre extensões de jato aqui .

ReactiveUI também fornece uma classe base que implementa INotifyPropertyChanged - ReactiveObject. Vamos reescrever nosso código de exemplo usando os recursos fornecidos pela estrutura.

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

Esse modelo de apresentação faz exatamente o mesmo que o anterior, mas o código é menor, é mais previsível e todos os relacionamentos entre as propriedades do modelo de apresentação são descritos em um único local usando a sintaxe LINQ to Observable . É claro que poderíamos parar por aqui, mas ainda há muito código - precisamos implementar explicitamente getters, setters e campos.

Receita # 2. Encapsulamento INotifyPropertyChanged. ReactiveProperty


Uma solução alternativa é usar a biblioteca ReactiveProperty , que fornece classes de wrapper responsáveis ​​pelo envio de notificações para a interface do usuário. Com ReactiveProperty, o modelo de exibição não precisa implementar nenhuma interface; em vez disso, cada propriedade implementa o próprio INotifyPropertyChanged. Essas propriedades reativas também implementam IObservable, o que significa que podemos assinar suas alterações como se estivéssemos usando o ReactiveUI . Altere nosso modelo de exibição usando 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(); } } 

Só precisamos declarar e inicializar as propriedades reativas e descrever os relacionamentos entre elas. Nenhum código padrão precisa ser escrito além dos inicializadores de propriedades. Mas essa abordagem tem uma desvantagem - precisamos alterar nosso XAML para que as ligações de dados funcionem. As propriedades reativas são wrappers, portanto, a interface do usuário deve estar vinculada à própria propriedade de cada um desses wrappers!

 <StackPanel> <TextBox Text="{Binding Name.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{Binding Greeting.Value, Mode=OneWay}"/> <Button Content="Clear" Command="{Binding Clear}"/> </StackPanel> 


Receita # 3. Alterando a montagem em tempo de compilação. PropertyChanged.Fody + ReactiveUI


Em um modelo de apresentação típico, cada propriedade pública deve poder enviar notificações para a interface do usuário quando seu valor for alterado. Com PropertyChanged.Fody , você não precisa se preocupar com isso. A única coisa necessária ao desenvolvedor é marcar a classe do modelo de exibição com o atributo AddINotifyPropertyChangedInterface - e o código responsável pela publicação do evento PropertyChanged será adicionado aos setters automaticamente após a criação do projeto, juntamente com a implementação da interface INotifyPropertyChanged, se estiver faltando. Se necessário, transforme nossas propriedades em fluxos de valores variáveis, sempre podemos usar o método de extensão WhenAnyValue da biblioteca ReactiveUI . Vamos reescrever nossa amostra pela terceira vez e ver quão mais conciso será o nosso modelo de apresentação!

 [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 altera o código IL do projeto em tempo de compilação. O complemento PropertyChanged.Fody pesquisa todas as classes marcadas com o atributo AddINotifyPropertyChangedInterface ou implementa a interface INotifyPropertyChanged e edita os setters de tais classes. Você pode aprender mais sobre como a geração de código funciona e que outras tarefas podem ser resolvidas no relatório de Andrei Kurosh, " Reflection.Emit. Practice of Use ".

Embora PropertyChanged.Fody nos permita escrever código limpo e expressivo, as versões herdadas do .NET Framework, incluindo 4.5.1 e posterior, não são mais suportadas. Isso significa que você, de fato, pode tentar usar o ReactiveUI e o Fody em seu projeto, mas por seu próprio risco e levando em consideração que todos os erros encontrados nunca serão corrigidos! As versões do .NET Core são suportadas de acordo com a política de suporte da Microsoft .

Da teoria à prática. Validando formulários com ReactiveUI e PropertyChanged.Fody


Agora estamos prontos para escrever nosso primeiro modelo de apresentação reativa. Vamos imaginar que estamos desenvolvendo um complexo sistema multiusuário, enquanto pensamos no UX e queremos coletar feedback de nossos clientes. Quando um usuário nos envia uma mensagem, precisamos saber se é um relatório de bug ou uma sugestão para melhorar o sistema, também queremos agrupar revisões em categorias. Os usuários não devem enviar cartas até que preencham todas as informações necessárias corretamente. Um modelo de apresentação que atenda às condições listadas acima pode ser assim:

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

Marcamos nosso modelo de exibição com o atributo AddINotifyPropertyChangedInterface - portanto, todas as propriedades notificarão a interface do usuário de uma alteração em seus valores. Usando o método WhenAnyValue , assinaremos as alterações nessas propriedades e atualizaremos outras propriedades. A equipe responsável por enviar o formulário permanecerá desativada até que o usuário preencha o formulário corretamente. Salvaremos nosso código na biblioteca de classes destinada ao .NET Standard e passaremos para o teste.

Teste do modelo de unidade


O teste é uma parte importante do processo de desenvolvimento de software. Com os testes, poderemos confiar em nosso código e deixar de ter medo de refatorá-lo - afinal, para verificar a operação correta do programa, será suficiente executar os testes e garantir que eles sejam concluídos com êxito. Um aplicativo que usa a arquitetura MVVM consiste em três camadas, duas das quais contêm lógica independente de plataforma - e podemos testá-lo usando o .NET Core e a estrutura XUnit .

Para criar mobs e stubs , a biblioteca NSubstitute é útil para nós, que fornece uma API conveniente para descrever reações a ações e valores do sistema retornados por "objetos falsos".

 var sumService = Substitute.For<ISumService>(); sumService.Sum(2, 2).Returns(4); 

Para melhorar a legibilidade das mensagens de código e de erro em nossos testes, usamos a biblioteca FluentAssertions . Com isso, não precisaremos apenas lembrar qual argumento em Assert.Equal conta o valor real e qual é o valor esperado, mas nosso IDE escreverá o código para nós!

 var fibs = fibService.GetFibs(10); fibs.Should().NotBeEmpty("because we've requested ten fibs"); fibs.First().Should().Be(1); 

Vamos escrever um teste para o nosso modelo de apresentação.

 [Fact] public void ShouldValidateFormAndSendFeedback() { //    , //    . var service = Substitute.For<IService>(); var feedback = new FeedbackViewModel(service); feedback.HasErrors.Should().BeTrue(); //   . feedback.Message = "Message!"; feedback.Title = "Title!"; feedback.Section = 0; feedback.Idea = true; feedback.HasErrors.Should().BeFalse(); //    , //   Send()  IService  //    . feedback.Submit.Execute().Subscribe(); service.Received(1).Send("Title!", "Message!"); } 


UI para Plataforma Universal do Windows


Ok, agora nosso modelo de apresentação foi testado e temos certeza de que tudo funciona como esperado. O processo de desenvolvimento da camada de apresentação de nosso aplicativo é bastante simples - precisamos criar um novo projeto da Plataforma Universal do Windows, dependente da plataforma, e adicionar um link à biblioteca .NET Standard que contenha lógica independente do aplicativo. Em seguida, o pequeno problema é declarar os controles em XAML, vincular suas propriedades às propriedades do modelo de exibição e lembre-se de especificar o contexto de dados de qualquer maneira conveniente. Vamos fazer isso!

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

Finalmente, nosso formulário está pronto.

amostra uwp mvvm

UI para Xamarin.Forms


Para que o aplicativo funcione em dispositivos móveis com sistemas operacionais Android e iOS, é necessário criar um novo projeto Xamarin.Forms e descrever a interface do usuário usando os controles Xamarin adaptados para dispositivos móveis.

Amostra xamarin.forms mvvm

UI para Avalonia


O Avalonia é uma estrutura .NET de plataforma cruzada que usa o dialeto XAML, familiar aos desenvolvedores WPF, UWP ou Xamarin.Forms. O Avalonia suporta Windows, Linux e OSX e está sendo desenvolvido por uma comunidade de entusiastas no GitHub . Para trabalhar com o ReactiveUI, você deve instalar o pacote Avalonia.ReactiveUI . Descreva a camada de apresentação no Avalonia XAML!

amostra avalonia mvvm

Conclusão


Como podemos ver, o .NET em 2018 nos permite criar software verdadeiramente multiplataforma - usando UWP, Xamarin.Forms, WPF e AvaloniaUI, podemos fornecer suporte para nossos sistemas operacionais de aplicativos Android, iOS, Windows, Linux, OSX. Os padrões de design e bibliotecas do MVVM, como ReactiveUI e Fody, podem simplificar e acelerar o processo de desenvolvimento, escrevendo códigos claros, de manutenção e portáteis. A infraestrutura desenvolvida, a documentação detalhada e o bom suporte nos editores de código tornam a plataforma .NET cada vez mais atraente para os desenvolvedores de software.

Se você está escrevendo aplicativos móveis ou de desktop no .NET e ainda não está familiarizado com o ReactiveUI, preste atenção: a estrutura usa um dos clientes GitHub mais populares para iOS , a extensão do Visual Studio para GitHub , o cliente git Atitian SourceTree e o Slack para Windows 10 Celular A série de artigos sobre ReactiveUI no Habré pode se tornar um excelente ponto de partida. Para os desenvolvedores do Xamarin, o curso " Criando um aplicativo iOS com C # " de um dos autores do ReactiveUI provavelmente será útil. Você pode aprender mais sobre a experiência de desenvolvimento no AvaloniaUI no artigo sobre o Egram - um cliente alternativo para o Telegram no .NET Core.

As fontes do aplicativo de plataforma cruzada descritas no artigo e demonstrando as possibilidades de validação de formulários com ReactiveUI e Fody podem ser encontradas no GitHub . Um exemplo de aplicativo de plataforma cruzada executando no Windows, Linux, macOS e Android e demonstrando o uso do ReactiveUI, ReactiveUI.Fody e Akavache também está disponível no GitHub .

Source: https://habr.com/ru/post/pt418007/


All Articles