
Aujourd'hui,
la plate -
forme .NET est un outil vraiment universel - avec son aide, vous pouvez résoudre un large éventail de tâches, y compris le développement d'applications d'application pour les systèmes d'exploitation populaires, tels que Windows, Linux, MacOS, Android et iOS. Dans cet article, nous examinerons l'architecture des applications multiplateformes .NET en utilisant le modèle de conception MVVM et
la programmation réactive . Nous allons
nous familiariser avec les bibliothèques
ReactiveUI et
Fody , apprendre à implémenter l'interface INotifyPropertyChanged à l'aide d'attributs, aborder les bases d'
AvaloniaUI ,
Xamarin Forms ,
Universal Windows Platform ,
Universal Presentation Foundation et
.NET Standard , et découvrir des outils efficaces pour tester les couches de modèle et les modèles de présentation d'application.
Le matériel est une adaptation des articles «
MVVM réactif pour la plate -forme .NET » et «
Applications .NET multiplateforme via l'approche MVVM réactive », publiés par l'auteur plus tôt sur la ressource Medium. Un exemple de code est
disponible sur GitHub .
Présentation Architecture MVVM et multiplateforme .NET
Lorsque vous développez des applications multiplates-formes sur la plate-forme .NET, vous devez écrire du code portable et pris en charge. Si vous travaillez avec des frameworks qui utilisent des dialectes XAML, tels que UWP, WPF, Xamarin Forms et AvaloniaUI, cela peut être réalisé en utilisant le modèle de conception MVVM, la programmation réactive et la stratégie de séparation de code .NET Standard. Cette approche améliore la portabilité des applications en permettant aux développeurs d'utiliser une base de code commune et des bibliothèques de logiciels communes sur divers systèmes d'exploitation.
Nous allons examiner de plus près chacune des couches d'une application construite sur la base de l'architecture MVVM - le modèle, la vue et le ViewModel. La couche modèle représente les services de domaine, les objets de transfert de données, les entités de base de données, les référentiels - toute la logique métier de notre programme. La vue est responsable de l'affichage des éléments de l'interface utilisateur à l'écran et dépend du système d'exploitation spécifique, et le modèle de présentation permet aux deux couches décrites ci-dessus d'interagir, adaptant la couche modèle pour interagir avec l'utilisateur humain.
L'architecture MVVM prévoit la répartition des responsabilités entre les trois couches logicielles de l'application, de sorte que ces couches peuvent être placées dans des assemblages distincts destinés à .NET Standard. La spécification formelle .NET Standard permet aux développeurs de créer des bibliothèques portables qui peuvent être utilisées dans diverses implémentations .NET avec un seul ensemble unifié d'API. En suivant strictement l'architecture MVVM et la stratégie de séparation de code .NET Standard, nous pourrons utiliser des couches de modèle et des modèles de présentation prêts à l'emploi lors du développement de l'interface utilisateur pour diverses plates-formes et systèmes d'exploitation.

Si nous avons écrit une application pour le système d'exploitation Windows à l'aide de Windows Presentation Foundation, nous pouvons facilement la porter vers d'autres cadres, tels que, par exemple, Avalonia UI ou Xamarin Forms - et notre application fonctionnera sur des plates-formes telles que iOS, Android, Linux, OSX et l'interface utilisateur seront la seule chose qui devra être écrite à partir de zéro.
Implémentation MVVM traditionnelle
Les modèles de présentation incluent généralement des propriétés et des commandes auxquelles les éléments de balisage XAML peuvent être liés. Pour que les liaisons de données fonctionnent, le modèle de vue doit implémenter l'interface INotifyPropertyChanged et publier l'événement PropertyChanged chaque fois que les propriétés du modèle de vue changent. Une implémentation simple pourrait ressembler à ceci:
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 décrivant l'interface utilisateur de l'application:
<StackPanel> <TextBox Text="{Binding Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/> <TextBlock Text="{Binding Greeting, Mode=OneWay}"/> <Button Content="Clear" Command="{Binding Clear}"/> </StackPanel>
Et ça marche! Lorsque l'utilisateur entre son nom dans la zone de texte, le texte ci-dessous change instantanément, saluant l'utilisateur.

Mais attendez un instant! Notre interface utilisateur n'a besoin que de deux propriétés synchronisées et d'une commande, pourquoi devons-nous écrire plus de vingt lignes de code pour que notre application fonctionne correctement? Que se passe-t-il si nous décidons d'ajouter plus de propriétés qui reflètent l'état de notre modèle de vue? Le code deviendra plus grand, le code deviendra plus compliqué et compliqué. Et nous devons encore le soutenir!
Recette n ° 1. Modèle d'observateur. Getters et setters courts. ReactiveUI
En fait, le problème de l'implémentation verbeuse et confuse de l'interface INotifyPropertyChanged n'est pas nouveau, et il existe plusieurs solutions. La première chose que vous devez faire attention à
ReactiveUI . Il s'agit d'un framework MVVM multiplateforme, fonctionnel et réactif qui permet aux développeurs .NET d'utiliser des extensions réactives lors du développement de modèles de présentation.
Les extensions réactives sont une implémentation du modèle de conception Observer défini par les interfaces de la bibliothèque .NET standard - IObserver et IObservable. La bibliothèque comprend également plus de cinquante opérateurs qui vous permettent de convertir des flux d'événements - les filtrer, les combiner, les regrouper - en utilisant une syntaxe similaire au langage de requête structuré
LINQ . En savoir plus sur les extensions de jet
ici .
ReactiveUI fournit également une classe de base qui implémente INotifyPropertyChanged - ReactiveObject. Réécrivons notre exemple de code à l'aide des fonctionnalités fournies par le framework.
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); } }
Un tel modèle de présentation fait exactement la même chose que le précédent, mais le code qu'il contient est plus petit, il est plus prévisible et toutes les relations entre les propriétés du modèle de présentation sont décrites en un seul endroit à l'aide de la syntaxe
LINQ to Observable . Bien sûr, nous pourrions nous arrêter ici, mais il y a encore beaucoup de code - nous devons implémenter explicitement les getters, les setters et les champs.
Recette n ° 2. Encapsulation INotifyPropertyChanged. ReactiveProperty
Une autre solution consiste à utiliser la bibliothèque
ReactiveProperty , qui fournit des classes d'encapsuleur chargées d'envoyer des notifications à l'interface utilisateur. Avec
ReactiveProperty, le modèle de vue n'a pas besoin d'implémenter d'interfaces; à la place, chaque propriété implémente INotifyPropertyChanged elle-même. Ces propriétés réactives implémentent également IObservable, ce qui signifie que nous pouvons souscrire à leurs modifications comme si nous utilisions
ReactiveUI . Modifiez notre modèle de vue à l'aide de 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(); } }
Nous avons juste besoin de déclarer et d'initialiser les propriétés réactives et de décrire les relations entre elles. Aucun code standard ne doit être écrit en dehors des initialiseurs de propriété. Mais cette approche a un inconvénient - nous devons changer notre XAML pour que les liaisons de données fonctionnent. Les propriétés réactives sont des wrappers, donc l'interface utilisateur doit être liée à la propre propriété de chacun de ces 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>
Recette n ° 3. Modification de l'assemblage au moment de la compilation. PropertyChanged.Fody + ReactiveUI
Dans un modèle de présentation typique, chaque propriété publique doit pouvoir envoyer des notifications à l'interface utilisateur lorsque sa valeur change. Avec
PropertyChanged.Fody , vous n'avez pas à vous en soucier. La seule chose qui est exigée du développeur est de marquer la classe de modèle de vue avec l'attribut
AddINotifyPropertyChangedInterface - et le code responsable de la publication de l'événement PropertyChanged sera ajouté aux setters automatiquement après la construction du projet, ainsi que l'implémentation de l'interface INotifyPropertyChanged, s'il en manque une. Si nécessaire, transformez nos propriétés en flux de valeurs changeantes, nous pouvons toujours utiliser la
méthode d' extension
WhenAnyValue de la bibliothèque
ReactiveUI . Réécrivons notre échantillon pour la troisième fois et voyons à quel point notre modèle de présentation sera plus concis!
[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 modifie le code IL du projet au moment de la compilation. Le module complémentaire
PropertyChanged.Fody recherche toutes les classes marquées avec l'attribut
AddINotifyPropertyChangedInterface ou implémentant l'interface INotifyPropertyChanged et modifie les paramètres de ces classes. Vous pouvez en savoir plus sur le fonctionnement de la génération de code et sur les autres tâches qui peuvent être résolues dans le rapport d'Andrei Kurosh "
Reflection.Emit. Practice of Use ".
Bien que PropertyChanged.Fody nous permette d'écrire du code propre et expressif, les versions héritées du .NET Framework, y compris 4.5.1 et versions ultérieures, ne sont plus prises en charge. Cela signifie que vous pouvez en fait essayer d'utiliser ReactiveUI et Fody dans votre projet, mais à vos risques et périls et en tenant compte du fait que toutes les erreurs trouvées ne seront jamais corrigées! Les versions pour .NET Core sont prises en charge conformément
à la politique de support de Microsoft .
De la théorie à la pratique. Validation des formulaires avec ReactiveUI et PropertyChanged.Fody
Nous sommes maintenant prêts à écrire notre premier modèle de présentation réactive. Imaginons que nous développons un système multi-utilisateur complexe, tout en pensant à l'UX et que nous voulons recueillir les commentaires de nos clients. Lorsqu'un utilisateur nous envoie un message, nous devons savoir s'il s'agit d'un rapport de bogue ou d'une suggestion pour améliorer le système, nous voulons également regrouper les avis en catégories. Les utilisateurs ne doivent pas envoyer de lettres avant d'avoir rempli correctement toutes les informations nécessaires. Un modèle de présentation qui remplit les conditions énumérées ci-dessus peut ressembler à ceci:
[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 ); } }
Nous marquons notre modèle de vue avec l'attribut
AddINotifyPropertyChangedInterface - ainsi, toutes les propriétés notifieront l'interface utilisateur d'un changement dans leurs valeurs. En utilisant la méthode
WhenAnyValue , nous nous
abonnerons aux modifications apportées à ces propriétés et mettrons à jour d'autres propriétés. L'équipe responsable de l'envoi du formulaire restera fermée jusqu'à ce que l'utilisateur remplisse correctement le formulaire. Nous allons enregistrer notre code dans la bibliothèque de classes destinée à la norme .NET et passer aux tests.
Test de modèle unitaire
Les tests sont une partie importante du processus de développement logiciel. Avec les tests, nous pourrons faire confiance à notre code et cesser d'avoir peur de le refactoriser - après tout, pour vérifier le bon fonctionnement du programme, il suffira d'exécuter les tests et de s'assurer qu'ils sont terminés avec succès. Une application utilisant l'architecture MVVM se compose de trois couches, dont deux contiennent une logique indépendante de la plate-forme - et nous pouvons la tester à l'aide du .NET Core et du framework
XUnit .
Pour créer des mobs et des
stubs , la bibliothèque
NSubstitute est utile pour nous, qui fournit une API pratique pour décrire les réactions aux actions du système et les valeurs renvoyées par les «faux objets».
var sumService = Substitute.For<ISumService>(); sumService.Sum(2, 2).Returns(4);
Pour améliorer la lisibilité du code et des messages d'erreur dans nos tests, nous utilisons la bibliothèque
FluentAssertions . Avec cela, nous devrons non seulement nous rappeler quel argument dans Assert.Equal compte la valeur réelle, et quelle est la valeur attendue, mais notre IDE écrira le code pour nous!
var fibs = fibService.GetFibs(10); fibs.Should().NotBeEmpty("because we've requested ten fibs"); fibs.First().Should().Be(1);
Écrivons un test pour notre modèle de présentation.
[Fact] public void ShouldValidateFormAndSendFeedback() {
UI pour Windows Universal Platform
Ok, maintenant notre modèle de présentation est testé et nous sommes sûrs que tout fonctionne comme prévu. Le processus de développement de la couche de présentation de notre application est assez simple - nous devons créer un nouveau projet de plateforme Windows universelle dépendant de la plate-forme et ajouter un lien vers la bibliothèque .NET Standard contenant la logique indépendante de la plate-forme de notre application. Ensuite, la petite chose est de déclarer les contrôles en XAML, de lier leurs propriétés aux propriétés du modèle de vue et de ne pas oublier de
spécifier le contexte de données de manière pratique. Faisons-le!
<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>
Enfin, notre formulaire est prêt.

Interface utilisateur pour Xamarin.Forms
Pour que l'application fonctionne sur les appareils mobiles exécutant les systèmes d'exploitation Android et iOS, vous devez créer un nouveau projet
Xamarin.Forms et
décrire l'interface utilisateur à l' aide des contrôles Xamarin adaptés aux appareils mobiles.

Interface utilisateur pour Avalonia
Avalonia est un framework .NET multiplateforme qui utilise le dialecte XAML familier aux développeurs WPF, UWP ou Xamarin.Forms. Avalonia prend en charge Windows, Linux et OSX et est développé par une
communauté de passionnés sur GitHub . Pour travailler avec
ReactiveUI, vous devez installer le package
Avalonia.ReactiveUI .
Décrivez la couche de présentation sur Avalonia XAML!

Conclusion
Comme nous pouvons le voir, .NET en 2018 nous permet d'écrire des
logiciels véritablement multiplateformes - en utilisant UWP, Xamarin.Forms, WPF et AvaloniaUI, nous pouvons fournir un support pour nos systèmes d'exploitation d'application Android, iOS, Windows, Linux, OSX. Les modèles de conception MVVM et les bibliothèques telles que
ReactiveUI et
Fody peuvent simplifier et accélérer le processus de développement en écrivant du code clair, maintenable et portable. L'infrastructure développée, la documentation détaillée et la bonne prise en charge des éditeurs de code rendent la plate-forme .NET de plus en plus attrayante pour les développeurs de logiciels.
Si vous écrivez des applications de bureau ou mobiles dans .NET et que vous ne connaissez pas encore ReactiveUI, assurez-vous d'y prêter attention - le framework utilise l'
un des clients GitHub les plus populaires pour iOS , l'extension
Visual Studio pour GitHub , le client git
Atitian SourceTree et
Slack pour Windows 10. Mobile La série d'articles sur ReactiveUI sur Habré peut devenir un excellent point de départ. Pour les développeurs sur Xamarin, le cours "
Construire une application iOS avec C # " de l'un des auteurs de ReactiveUI sera probablement utile. Vous pouvez en savoir plus
sur l' expérience de développement sur AvaloniaUI à
partir de l'article sur Egram - un client alternatif pour Telegram sur .NET Core.
Les sources de l'application multiplateforme décrites dans l'article et démontrant les possibilités de validation des formulaires avec ReactiveUI et Fody
se trouvent sur GitHub . Un exemple d'application multiplateforme fonctionnant sous Windows, Linux, macOS et Android, et démontrant l'utilisation de ReactiveUI, ReactiveUI.Fody et
Akavache est également disponible sur GitHub .