
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.

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.

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() {
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.

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.

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!

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 .