MVVM My Way

Création d'une application WPF MVVM de A à Z

Auteur

Jounad Tourvieille

Compiler les différents frameworks existants pour créer une application graphique peut parfois être déconcertant. L'objectif ici et de proposer une solution clef en main pour un démarrage rapide lors de la création d'une application client lourd, type WPF.

- Titre : MVVM My Way

- Auteur : Jounad Tourvieille

- Parution : 9 février 2015

- Mise à jour : 9 février 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2015 jounad. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Propos

- I-B. Ce que vous obtiendrez

- I-C. Démarche

- II. Démarrage

- III. MVVM Light

- III-A. Installation des bibliothèques

- III-B. Structuration de la solution

- IV. Création de l'interface graphique

- IV-A. Création des Views/ViewModels associés

- IV-B. TabControl et MVVM

- V. Services, Repositories et Model

- V-A. Structure

- V-B. ServiceRepository

- V-C. UserRepository

- VI. Tests unitaires

- VII. Retour à la Vue/VueModel

- VIII. Automapper

- IX. Initialisation des services windows

- X. Gestion des erreurs à travers le Messenger

- XI. Réactivité de l'interface

- XII. Tests asynchrones et Moq

- XIII. Services

- XIII-A. MISE EN ŒUVRE

- XIII-B. EventToCommand

- XIII-C. Retour aux tests

- XIV. Conclusion

I - Introduction

I-A - Propos

Le but de cet article est de proposer une solution clef en main pour construire une application WPF en utilisant le pattern MVVM. Pourquoi MVVMyWay ? Parce que je vais introduire un ensemble d'outils et de frameworks avec lesquels j'ai l'habitude de travailler, et qui résolvent certaines difficultés lorsqu'on développe une application en client lourd. Vous pourrez trouver les sources de l'article à cette adresse : sources.

I-B - Ce que vous obtiendrez

L'application vous permettra de visualiser la liste des Utilisateurs et des Services déclarés sur votre machine. Toutes les couches View, ViewModel, Model, Repository seront bien séparées et étanches. Vous aurez également un projet de tests unitaires testant les Repositories et la couche ViewModel.

I-C - Démarche

Nous travaillerons sous forme itérative avec de l'amélioration continue. Parfois, nous rencontrerons des problèmes, voire des impasses, et nous verrons comment les contourner, ou comment refactorer notre code pour les éviter.

II - Démarrage

Tout d'abord, vous devez avoir Visual Studio sur votre machine. La version que j'utilise est la 2012/2013. Le code fourni est très certainement compatible avec d'autres versions, mais non testé.

Commençons par créer un projet de type WPF Application. Vous pouvez également cocher la case « créer un répertoire pour la solution ». Nommons le projet MonitoringPlatform.

Vous avez maintenant une Solution prête à l'emploi.

III - MVVM Light

III-A - Installation des bibliothèques

MVVM Light est une bibliothèque qui permet d'accélérer le développement d'applications MVVM. Il existe bien d'autres « concurrents » à ce framework (Caliburn Micro, Catel …). Ce que j'apprécie dans ce dernier, c'est qu'il fournit les principaux outils pour développer proprement : un ViewModelBase, un Messenger et un conteneur IoC. Nous étudierons plus en détail chacun de ces éléments.

Nous allons maintenant installer MVVM light. À travers NuGet, sélectionnez « MVVM light libraries only ».

Votre solution devrait maintenant avoir des références supplémentaires.

Nous allons maintenant créer les répertoires « Models », « ViewModels » et « Views », et transférer/renommer MainWindow dans Views. Nous devons également mettre à jour le App.xaml.

App.xaml

1. <Application x:Class="MonitoringPlatform.App"

2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

4. StartupUri="Views/MainView.xaml">

5. <Application.Resources>

7. </Application.Resources>

8. </Application>

MainView.xaml

1. <Window x:Class="MonitoringPlatform.Views.MainView"

2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

4. Title="MainView" Height="350" Width="525">

5. <Grid>

7. </Grid>

8. </Window>

MainView.xaml.cs

1. namespace MonitoringPlatform.Views

2. {

3. /// <summary>

4. /// Interaction logic for MainView.xaml

5. /// </summary>

6. public partial class MainView

7. {

8. public MainView()

9. {

10. InitializeComponent();

11. }

12. }

13. }

Vous pouvez lancer l'application, elle devrait s'afficher.

III-B - Structuration de la solution

Nous allons maintenant mettre en place certaines classes qui vont nous permettre de structurer notre application.

Nous avons notre MainView, mais toujours pas de ViewModel associé. Commençons par ajouter une classe MainViewModel dans le répertoire ViewModel. Laissons-la vide pour l'instant.

Ajoutons également une classe ViewModelLocator, toujours dans ViewModels. Cette classe va nous permettre d'identifier les différents ViewModels enregistrés dans notre application.

ViewModelLocator

1. using GalaSoft.MvvmLight.Ioc;

2. using Microsoft.Practices.ServiceLocation;

4. public class ViewModelLocator

5. {

6. static ViewModelLocator()

7. {

8. ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);

10. // Register Services in here

12. // Register ViewModels in here

13. SimpleIoc.Default.Register<MainViewModel>();

14. }

16. public MainViewModel Main

17. {

18. get

19. {

20. return ServiceLocator.Current.GetInstance<MainViewModel>();

21. }

22. }

23. }

Maintenant, nous devons associer notre View à notre ViewModel.

Pour cela, nous allons dérouler deux étapes.

Il nous faut tout d'abord exposer notre ViewModelLocator. Pour ce faire, allons dans App.xaml, et ajoutons quelques lignes pour obtenir le résultat suivant.

App.xaml

1. <Application x:Class="MonitoringPlatform.App"

2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

4. xmlns:viewModels="clr-namespace:MonitoringPlatform.ViewModels"

5. StartupUri="Views/MainView.xaml"

6. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"

7. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

8. mc:Ignorable="d">

9. <Application.Resources>

10. <viewModels:ViewModelLocator x:Key="Locator"

11. d:IsDataSource="True" />

12. </Application.Resources>

13. </Application>

Nous avons déclaré une ressource applicative qui nous permet d'accéder au ViewModelLocator depuis n'importe quelle partie de notre application. Au passage, vous aurez noté l'ajout de deux xmlns.

La seconde étape consiste tout simplement à utiliser cette référence dans notre MainView. Nous allons en effet lier le DataContext de la Window à la propriété « Main » de notre ressource « Locator ».

MainView.xaml

1. <Window x:Class="MonitoringPlatform.Views.MainView"

2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

4. Title="MainView" Height="350" Width="525"

5. DataContext="{Binding Main, Source={StaticResource Locator}}">

6. <Grid>

8. </Grid>

9. </Window>

Nous avons maintenant un lien entre notre View et notre ViewModel.

IV - Création de l'interface graphique

Créons maintenant une interface graphique avec deux vues principales. Nous aurons une première vue pour la liste des services qui tournent sur la machine, et une seconde qui nous listera les utilisateurs déclarés.

IV-A - Création des Views/ViewModels associés

Ajoutons à notre répertoire Views un UserControl WPF nommé ServicesView.

Ajoutons simplement un TextBlock pour identifier la vue.

ServicesView.xaml

1. <TextBlock Text="I'm the Services view"></TextBlock>

Ajoutons maintenant la seconde vue, nommée « UsersView », de la même manière.

UsersView.xaml

1. <TextBlock Text="I'm the Users view"></TextBlock>

Ces deux vues représenteront le contenu de nos deux pages Wpf. Nous allons utiliser le contrôle TabControl pour les afficher. Nous ne créons pas tout de suite le ViewModel associé, vous comprendrez pourquoi.

IV-B - TabControl et MVVM

Retournons à la MainView. Nous allons ajouter notre composant principal : le TabControl, qui va nous permettre de switcher entre les deux vues. Ajoutez le code suivant entre les balises Grid.

TabControl

1. <TabControl ItemsSource="{Binding Tabs}" Margin="12">

2. <TabControl.ItemTemplate>

3. <DataTemplate>

4. <TextBlock Text="{Binding TabName}"/>

5. </DataTemplate>

6. </TabControl.ItemTemplate>

7. </TabControl>

Ce code permet de définir un template personnalisé pour le rendu d'un Tab.

Nous avons maintenant lié notre ItemsSource du TabControl à la collection « Tabs » dans notre ViewModel.

La difficulté avec le TabControl est de faire en sorte que le nom du Tab soit en adéquation avec le contenu du Tab affiché. En effet, ce que nous voulons, c'est associer les vues précédemment créées (ServicesView et UsersView) à un Tab du contrôle TabControl. Nous allons créer pour cela un TabViewModelBase, qui héritera de ViewModelBase. Dans le répertoire ViewModels, ajoutons une classe dont voici le contenu.

TabViewModelBase.cs

1. using GalaSoft.MvvmLight;

3. public abstract class TabViewModelBase : ViewModelBase

4. {

5. public abstract string TabName { get; }

6. }

Nous avons maintenant notre base pour créer nos ServicesViewModel & UsersViewModel, que nous allons ajouter au répertoire ViewModels.

ServicesViewModel.cs

1. public class ServicesViewModel : TabViewModelBase

2. {

3. public override string TabName

4. {

5. get

6. {

7. return "Services";

8. }

9. }

10. }

UsersViewModel.cs

1. public class UsersViewModel : TabViewModelBase

2. {

3. public override string TabName

4. {

5. get

6. {

7. return "Users";

8. }

9. }

10. }

Nous allons maintenant revenir à notre MainView et y ajouter des ressources pour faire comprendre au TabControl quel ViewModel il doit utiliser lorsqu'une vue est activée. Pour plus de détails sur ce mécanisme, recherchez le mot clef DataTemplateSelector (ceci permet de sélectionner un DataTemplate spécifique en fonction du type de données affiché). Modifions le contenu xaml pour obtenir ceci.

MainView.xaml

1. <Window x:Class="MonitoringPlatform.Views.MainView"

2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

4. xmlns:views="clr-namespace:MonitoringPlatform.Views"

5. xmlns:viewModels="clr-namespace:MonitoringPlatform.ViewModels"

6. Title="MainView" Height="350" Width="525"

7. DataContext="{Binding Main, Source={StaticResource Locator}}">

9. <Window.Resources>

10. <DataTemplate DataType="{x:Type viewModels:ServicesViewModel}">

11. <views:ServicesView />

12. </DataTemplate>

13. <DataTemplate DataType="{x:Type viewModels:UsersViewModel}">

14. <views:UsersView />

15. </DataTemplate>

16. </Window.Resources>

19. <Grid>

20. <TabControl ItemsSource="{Binding Tabs}" Margin="12">

21. <TabControl.ItemTemplate>

22. <DataTemplate>

23. <TextBlock Text="{Binding TabName}"/>

24. </DataTemplate>

25. </TabControl.ItemTemplate>

26. </TabControl>

28. </Grid>

29. </Window>

Désormais, nos vues et vues-models sont liés. Il nous reste maintenant à ajouter la liste des Tabs que nous voulons afficher. Pour ceci, rendons-nous dans le code de la MainViewModel. Ajoutons nos Tabs à l'initialisation.

MainViewModel.cs

1. using System.Collections.ObjectModel;

2. using GalaSoft.MvvmLight;

4. public class MainViewModel : ViewModelBase

5. {

6. private ObservableCollection<TabViewModelBase> _tabs;

7. private readonly ServicesViewModel _servicesViewModel;

8. private readonly UsersViewModel _usersViewModel;

10. public MainViewModel(ServicesViewModel servicesViewModel, UsersViewModel usersViewModel)

11. {

12. this._servicesViewModel = servicesViewModel;

13. this._usersViewModel = usersViewModel;

15. BuildTabs();

16. }

18. private void BuildTabs()

19. {

20. if (Tabs == null)

21. Tabs = new ObservableCollection<TabViewModelBase>();

23. Tabs.Clear();

25. Tabs.Add(_servicesViewModel);

26. Tabs.Add(_usersViewModel);

27. }

29. public ObservableCollection<TabViewModelBase> Tabs

30. {

31. get

32. {

33. return _tabs;

34. }

35. set

36. {

37. if (value == _tabs)

38. return;

40. _tabs = value;

41. this.RaisePropertyChanged();

42. }

43. }

44. }

Nous voyons ici que le constructeur prend directement les deux ViewModels qu'il va exposer. Il est à noter que ces deux paramètres sont fournis par le conteneur IoC de notre application.

La dernière étape consiste à enregistrer nos ViewModel au niveau de l'IoC (classe ViewModelLocator).

ViewModelLocator

1. SimpleIoc.Default.Register<ServicesViewModel>();

2. SimpleIoc.Default.Register<UsersViewModel>();

Et voilà, la boucle est bouclée. Notre application peut se lancer et afficher notre magnifique TabControl.

V - Services, Repositories et Model

V-A - Structure

Nous allons maintenant ajouter des services qui vont nous aider à retrouver nos données.

La différence entre Service et Repo est (à mon sens) la suivante : un service va vous permettre de manipuler des données (règles business par exemple), tandis qu'un Repo va simplement vous permettre d'y accéder (définition du contrat d'accès, des signatures des méthodes CRUD). Un Service peut utiliser un Repository, mais pas le contraire.

Ajoutez le répertoire Repositories au projet. Dedans, ajoutez les interfaces IServicesRepository et IUsersRepository. Nous allons également ajouter les implémentations associées : ServicesRepository et UsersRepository.

Repositories

1. namespace MonitoringPlatform.Repositories

2. {

3. using System.Collections.Generic;

5. public interface IServicesRepository

6. {

7. IList<ServiceModel> GetServices();

8. }

9. }

11. namespace MonitoringPlatform.Repositories

12. {

13. using System.Collections.Generic;

15. public interface IUsersRepository

16. {

17. IList<UserModel> GetUsers();

18. }

19. }

21. namespace MonitoringPlatform.Repositories

22. {

23. using System.Collections.Generic;

25. public class ServicesRepository : IServicesRepository

26. {

27. public IList<ServiceModel> GetServices()

28. {

29. // TODO

30. return null;

31. }

32. }

33. }

35. namespace MonitoringPlatform.Repositories

36. {

37. using System.Collections.Generic;

39. public class UsersRepository : IUsersRepository

40. {

41. public IList<UserModel> GetUsers()

42. {

43. // TODO

44. return null;

45. }

46. }

47. }

Comme vous pouvez le voir, nous avons ajouté des Entités qui représentent nos models. Les classes UserModel et ServiceModel doivent donc être ajoutées dans le répertoire Models.

Models

1. public class ServiceModel

2. {

3. }

4. public class UserModel

5. {

6. }

Une dernière chose à faire avant de pouvoir compiler : il faut tout simplement ajouter l'using correspondant à l'espace de nom Models dans les classes et interfaces des reporitories créées précédemment.

using

1. using MonitoringPlatform.Models;

Une fois ajouté, la compilation devrait se faire sans problème.

Mappons maintenant les services et leurs implémentations dans notre ViewModelLocator.

ViewModelLocator

1. SimpleIoc.Default.Register<IServicesRepository, ServicesRepository>();

2. SimpleIoc.Default.Register<IUsersRepository, UsersRepository>();

V-B - ServiceRepository

Avant d'implémenter le Repo à proprement parler, nous avons besoin de créer un enum qui va représenter l'état du service. Dans le répertoire Models, ajoutez une classe que nous allons changer en enum.

WindowsServiceStatus

1. namespace MonitoringPlatform.Models

2. {

3. public enum WindowsServiceStatus

4. {

5. Intermediate,

6. Paused,

7. Running,

8. Stopped

9. }

10. }

Nous pouvons maintenant implémenter le Repo de récupération de services windows. Commençons par ajouter une référence vers la dll System.ServiceProcess.dll. Nous allons ensuite implémenter la classe ServicesRepository.

ServicesRepository

1. namespace MonitoringPlatform.Repositories

2. {

3. using System.Collections.Generic;

4. using System.Linq;

5. using System.ServiceProcess;

6. using MonitoringPlatform.Models;

8. public class ServicesRepository : IServicesRepository

9. {

10. public IList<ServiceModel> GetServices()

11. {

12. var windowsServices = ServiceController.GetServices();  array

14. IList<ServiceModel> serviceModels = new List<ServiceModel>(windowsServices.Length);

16. foreach (ServiceController serviceController in windowsServices)

17. {

18. var serviceModel = new ServiceModel();

19. serviceModel.ServiceName = serviceController.ServiceName;

21. switch (serviceController.Status)

22. {

23. case ServiceControllerStatus.ContinuePending:

24. case ServiceControllerStatus.PausePending:

25. case ServiceControllerStatus.StartPending:

26. case ServiceControllerStatus.StopPending:

27. serviceModel.Status = WindowsServiceStatus.Intermediate;

28. break;

29. case ServiceControllerStatus.Paused:

30. serviceModel.Status = WindowsServiceStatus.Paused;

31. break;

32. case ServiceControllerStatus.Running:

33. serviceModel.Status = WindowsServiceStatus.Running;

34. break;

35. case ServiceControllerStatus.Stopped:

36. break;

37. }

39. serviceModels.Add(serviceModel);

40. }

42. return serviceModels;

43. }

44. }

45. }

Il faut ajouter les propriétés associées sur nos models. Mettons à jour la classe ServiceModel.

ServiceModel

1. public class ServiceModel

2. {

3. public string ServiceName { get; set; }

5. public WindowsServiceStatus Status { get; set; }

6. }

V-C - UserRepository

Comme pour le Repo de Services, nous allons ajouter les propriétés adéquates à nos entités. Modifiez la classe UserModel comme suit.

UserModel

1. public class UserModel

2. {

3. public string Name { get; set; }

4. }

Concernant la classe UsersRepository, nous devons ajouter une référence vers System.DirectoryServices.dll.

UsersRepository.cs

1. namespace MonitoringPlatform.Repositories

2. {

3. using System;

4. using System.Collections;

5. using System.Collections.Generic;

6. using System.DirectoryServices;

7. using MonitoringPlatform.Models;

9. public class UsersRepository : IUsersRepository

10. {

11. public IList<UserModel> GetUsers()

12. {

13. return new List<UserModel> { new UserModel { Name = "User 1" }, new UserModel { Name = "User 2" } };

14. }

15. }

16. }

À ce stage, le projet doit compiler.

VI - Tests unitaires

Maintenant, nous avons nos services prêts à fonctionner. Pourquoi ne pas les tester ?

Cette étape est très importante dans le développement. Elle n'est pas à prendre à la légère. En effet, aujourd'hui, vous vous demandez pourquoi tester ces méthodes, alors qu'elles sont toutes simples et n'ont pas vraiment de risque de mal se comporter. Oui, c'est vrai.

Par contre, ce à quoi on ne pense pas tout de suite, c'est la maintenabilité du code qui, souvent, est repris par d'autres personnes. Comment comprendre ce que fait une méthode si vous n'avez pas les spécifications sous le coude (situation qui arrive très très peu souvent en informatique ) ? D'abord, regarder le nom de la méthode. Si elle est bien nommée, pas de souci. Ensuite, parcourir le test unitaire associé permet de mieux comprendre son fonctionnement.

Un autre point important à mes yeux est la non-régression. Comment garantir qu'une modification de votre part ne créera pas de bogues sur une autre partie du code ? Difficile à évaluer ! Pour réduire ce risque, les tests unitaires peuvent nous aider. Regardons comment les mettre en place.

Attention, ce qui va suivre concerne uniquement la mise en place des tests. Leur pertinence reste à votre charge.

Commençons par ajouter un projet de type ClassLibrary que nous nommerons MonitoringPlatform.Tests.

Référençons notre projet principal.

Supprimons la classe Class1 et ajoutons ServicesRepositoryTests et UsersRepositoryTests.

Maintenant, il nous faut un framework de test. Il en existe plusieurs. Celui que je préfère est NUnit. Il est très répandu et couvre la majorité des cas de tests dont nous avons besoin. On peut le référencer via Nuget.

Nous pouvons commencer à ajouter des cas de tests. Je ne vous ferai pas le tour de découverte de NUnit ici, ce n'est pas le propos. Les classes de tests doivent correspondre à cela. Attention, la pertinence des tests ci-dessous peut très facilement être discutée. L'important est de voir comment organiser notre solution.

ServicesRepositoryTests

1. namespace MonitoringPlatform.Tests

2. {

3. using MonitoringPlatform.Repositories;

4. using NUnit.Framework;

5. [TestFixture]

6. public class ServicesRepositoryTests

7. {

8. [Test]

9. public void CheckThatGetServicesDoesNotReturnNull()

10. {

11. // Arrange

12. ServicesRepository subject = new ServicesRepository();

14. // Act

15. var result = subject.GetServices();

17. // Assert

18. Assert.IsNotNull(result);

19. }

20. }

21. }

UsersRepositoryTests

1. namespace MonitoringPlatform.Tests

2. {

3. using MonitoringPlatform.Repositories;

4. using NUnit.Framework;

6. [TestFixture]

7. public class UsersRepositoryTests

8. {

9. [Test]

10. public void CheckThatGetUsersDoesNotReturnNull()

11. {

12. // Arrange

13. UsersRepository subject = new UsersRepository();

15. // Act

16. var result = subject.GetUsers();

18. // Assert

19. Assert.IsNotNull(result);

20. }

21. }

22. }

Le problème avec NUnit est que le runner de tests de Visual Studio n'est pas compatible par défaut avec NUnit. Nous devons donc télécharger et installer un TestAdapter pour dérouler nos tests dans un environnement Visual Studio. Via Tools, Extensions and Updates menu, sélectionons « NUnit Test Adapter ».

Les tests peuvent maintenant être joués depuis Visual Studio. Pour ceci, ouvrez le menu « Test », et dans le sous-menu « Windows », sélectionnez « Test Explorer ». Vous aurez désormais accès à une fenêtre qui vous permet de gérer l'ensemble des tests, et de voir leur état lors de leur dernier lancement.

VII - Retour à la Vue/VueModel

Retournons maintenant à la ServicesView. Nous allons y ajouter une ListView qui va nous permettre de voir nos services.

ListView

<ListView ItemsSource="{Binding WindowsServices}" Margin="2">
<ListView.View>
<GridView>
<GridViewColumn Width="250" Header="Service Name" DisplayMemberBinding="{Binding ServiceName}" />
</GridView>
</ListView.View>
</ListView>

Quelle va être la collection exposée côté ViewModel ? On pourrait exposer une ObservableCollection<ServiceModel>. Ce qui me dérange, c'est de manipuler des objets Models directement dans la vue. En effet, ici, pas de souci, vu que nous créons nous-mêmes nos Models. Par contre, s'ils dépendent d'une bibliothèque tierce sur laquelle nous n'avons pas la main, nous ne pourrons pas les modifier. Pensons également à un changement de valeur côté ViewModel. Comment le rendre visible côté View sans la notification ? Enfin, dernier argument, qui ne s'applique pas ici, mais qui a également du sens : la validation. En effet, il va nous être possible de définir des règles de validation, et des messages d'erreur spécifiques directement dans cet objet transformé. Mon point de vue est donc plutôt de créer un objet similaire côté ViewModel, et d'exposer ce dernier. Ce dernier contiendra les notifications de changement, les éventuelles règles de validation, ainsi que des propriétés supplémentaires si besoin. Tout cela, sans toucher à la structure du Model intrinsèque. Pour ce faire, on va ajouter un sous-répertoire « ObservableObjects » au répertoire ViewModels.

Nos deux ViewModels héritent de ObservableObject, du framework MvvmLight.

ServiceOo

namespace MonitoringPlatform.ViewModels.ObservableObjects
{
using GalaSoft.MvvmLight;
using MonitoringPlatform.Models;

public class ServiceOo : ObservableObject
{
private WindowsServiceStatus _status;
private string _serviceName;

public string ServiceName
{
get
{
return this._serviceName;
}
set
{
if (value == _serviceName)
return;

this._serviceName = value;
this.RaisePropertyChanged();
}
}

public WindowsServiceStatus Status
{
get
{
return this._status;
}
set
{
if (value == _status)
return;

this._status = value;
this.RaisePropertyChanged();
}
}
}
}

UserOo

namespace MonitoringPlatform.ViewModels.ObservableObjects
{
using GalaSoft.MvvmLight;

public class UserOo : ObservableObject
{
private string _name;

public string Name
{
get
{
return this._name;
}
set
{
if (value == _name)
return;

this._name = value;
this.RaisePropertyChanged();
}
}
}
}

Cela va nous permettre d'agrémenter notre objet observable sans altérer notre Model. En effet, il est parfois difficile d'avoir accès au Model en lui-même, et par conséquent de le modifier. Il est toujours possible d'utiliser les méthodes d'extension, mais comme leur nom l'indique, il ne s'agit que de méthodes. Quid des propriétés, constructeurs…

VIII - Automapper

Maintenant que nous avons un Model et un ViewModel associé, ce qui serait intéressant, c'est de pouvoir transvaser les objets les uns dans les autres. Plutôt que de faire cela « à la main », utilisons un outil de mapping : Automapper. Comme son nom l'indique, il vous permet de mapper directement un type vers un autre. Pour les types dont les propriétés sont identiques, il fera le mapping automatiquement. Pour les autres, vous pouvez spécifier une façon de transformer.

Ajoutons Automapper à notre projet.

Automapper fonctionne en deux phases, un peu comme l'IoC : une phase de configuration, et une phase d'utilisation.

Nous allons définir nos mappings au démarrage de l'application. Ouvrez la classe App et collez le code suivant.

App.xaml.cs

namespace MonitoringPlatform
{
using System.Windows;
using AutoMapper;
using MonitoringPlatform.Models;
using MonitoringPlatform.ViewModels.ObservableObjects;

/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App
{
protected override void OnStartup(StartupEventArgs e)
{
InitializeAutomapper();
base.OnStartup(e);
}

private void InitializeAutomapper()
{
Mapper.CreateMap<ServiceModel, ServiceOo>();
Mapper.CreateMap<UserModel, UserOo>();
}
}
}

Nous avons créé là deux mappings qui définissent que passer d'une classe ServiceModel à une classe ServiceOo, ainsi que d'une classe UserModel à UserOo est possible, et que le mapping se fait suivant les règles standard. Les règles standard définies par AutoMapper indiquent que le transfert de données d'une source vers une destination se fait via réflexion, c'est-à-dire que les noms/types des propriétés de l'objet source et destination doivent concorder pour que le transfert se fasse sans problème.

Nous utiliserons plus tard les mappings ainsi créés.

IX - Initialisation des services windows

Grâce à IoC, nous pouvons initialiser notre IServicesRepository directement dans notre ServicesViewModel, et l'utiliser. Nous mapperons les résultats de notre service de Model vers ViewModel, en utilisant Automapper.

ServicesViewModel.cs

namespace MonitoringPlatform.ViewModels
{
using System;
using System.Collections.ObjectModel;
using System.Windows;
using AutoMapper;
using MonitoringPlatform.Models;
using MonitoringPlatform.Repositories;
using MonitoringPlatform.ViewModels.ObservableObjects;

public class ServicesViewModel : TabViewModelBase
{
private readonly IServicesRepository _servicesRepository;
private ObservableCollection<ServiceOo> _windowsServices;

public ServicesViewModel(IServicesRepository servicesRepository)
{
_servicesRepository = servicesRepository;

InitServices();
}

private void InitServices()
{
try
{
if (WindowsServices == null)
WindowsServices = new ObservableCollection<ServiceOo>();

WindowsServices.Clear();

var services = _servicesRepository.GetServices();
if (services != null)
{
WindowsServices = Mapper.Map<ObservableCollection<ServiceOo>>(services);
}
}
catch (Exception ex)
{
MessageBox.Show(string.Format("Error when getting windows services: {0}", ex.Message));
}
}

public override string TabName
{
get
{
return "Services";
}
}

public ObservableCollection<ServiceOo> WindowsServices
{
get
{
return this._windowsServices;
}
set
{
if (value == _windowsServices)
return;

this._windowsServices = value;
this.RaisePropertyChanged();
}
}
}
}

Faisons la même chose pour Users View/ViewModel.

ListView

<ListView ItemsSource="{Binding Users}" Margin="2">
<ListView.View>
<GridView>
<GridViewColumn Width="250" Header="User Name" DisplayMemberBinding="{Binding Name}" />
</GridView>
</ListView.View>
</ListView>

UsersViewModel.cs

namespace MonitoringPlatform.ViewModels
{
using System.Collections.ObjectModel;
using AutoMapper;
using MonitoringPlatform.Models;
using MonitoringPlatform.Repositories;
using MonitoringPlatform.ViewModels.ObservableObjects;

public class UsersViewModel : TabViewModelBase
{
private readonly IUsersRepository _usersRepository;
private ObservableCollection<UserOo> _users;

public UsersViewModel(IUsersRepository usersRepository)
{
_usersRepository = usersRepository;

InitUsers();
}

private void InitUsers()
{
if (Users == null)
Users = new ObservableCollection<UserOo>();

Users.Clear();

var users = _usersRepository.GetUsers();
if (users != null)
{
foreach (UserModel userModel in users)
{
Users.Add(Mapper.Map<UserModel, UserOo>(userModel));
}
}
}

public override string TabName
{
get
{
return "Users";
}
}

public ObservableCollection<UserOo> Users
{
get
{
return this._users;
}
set
{
if (value == _users)
return;

this._users = value;
this.RaisePropertyChanged();
}
}
}

}

La solution compile et s'affiche.

X - Gestion des erreurs à travers le Messenger

Le messenger de MVVM light permet de faire communiquer des objets entre eux. Dans le cas de MVVM, il est surtout utilisé pour :

- faire communiquer un ViewModel avec un autre ViewModel ;

- faire communiquer un ViewModel avec une View.

C'est le second cas que nous allons évoquer ici.

Nous allons utiliser ce principe pour afficher une MessageBox d'erreur côté View sans que nous ne référencions de classes WPF côté ViewModel. Ainsi, notre ViewModel sera toujours indépendant de la partie View.

Faisons en sorte de créer une erreur dans notre Repository des Users.

UsersRepository

namespace MonitoringPlatform.Repositories
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.DirectoryServices;
using MonitoringPlatform.Models;

public class UsersRepository : IUsersRepository
{
public IList<UserModel> GetUsers()
{
throw new ApplicationException("Error just for test");
IList<UserModel> users = new List<UserModel>();

DirectoryEntry localMachine = new DirectoryEntry("WinNT://" + Environment.MachineName);
DirectoryEntry admGroup = localMachine.Children.Find("users", "group");
object members = admGroup.Invoke("members", null);
foreach (object groupMember in (IEnumerable)members)
{
DirectoryEntry member = new DirectoryEntry(groupMember);
UserModel user = new UserModel();
user.Name = member.Name;
users.Add(user);
}

return users;
}
}
}

Nous allons tout d'abord ajouter le répertoire Messages et créer notre message d'erreur. Ce dernier nous servira à transmettre l'erreur.

GenericErrorMessage.cs

using System;

namespace MonitoringPlatform.Messages
{
public class GenericErrorMessage
{
public Exception Error { get; set; }
}
}

Adaptons maintenant le UserViewModel de manière à aller chercher la liste des utilisateurs une fois que la page est affichée. Cela laissera le temps à notre View de s'abonner à l'événement de réception de l'erreur. Regardons plus précisément le catch de la méthode InitUsers

UsersViewModel.cs

namespace MonitoringPlatform.ViewModels
{
using System;
using System.Collections.ObjectModel;
using AutoMapper;
using MonitoringPlatform.Messages;
using MonitoringPlatform.Models;
using MonitoringPlatform.Repositories;
using MonitoringPlatform.ViewModels.ObservableObjects;
using System.Collections.Generic;

public class UsersViewModel : TabViewModelBase
{
private readonly IUsersRepository _usersRepository;
private ObservableCollection<UserOo> _users;

public UsersViewModel(IUsersRepository usersRepository)
{
_usersRepository = usersRepository;
}

private void InitUsers()
{
_users = new ObservableCollection<UserOo>();

IList<UserModel> users = null;
try
{
users = _usersRepository.GetUsers();
}
catch (Exception ex)
{
MessengerInstance.Send(new GenericErrorMessage { Error = ex });
}

if (users != null)
{
foreach (UserModel userModel in users)
{
_users.Add(Mapper.Map<UserModel, UserOo>(userModel));
}
}
}

public override string TabName
{
get
{
return "Users";
}
}

public ObservableCollection<UserOo> Users
{
get
{
if (_users == null)
InitUsers();

return this._users;
}
}
}
}

Maintenant, nous voulons pouvoir écouter sur ce message. Allons dans UsersView et ajoutons une écoute.

UsersView.xaml.cs

namespace MonitoringPlatform.Views
{
using System.Windows;
using GalaSoft.MvvmLight.Messaging;
using MonitoringPlatform.Messages;

/// <summary>
/// Interaction logic for UsersView.xaml
/// </summary>
public partial class UsersView
{
public UsersView()
{
InitializeComponent();

Messenger.Default.Register<GenericErrorMessage>(this, ReactOnUserError);
}

private void ReactOnUserError(GenericErrorMessage error)
{
MessageBox.Show(error.Error.Message);
}
}
}

Votre solution doit compiler et se lancer.

XI - Réactivité de l'interface

Un aspect important d'une GUI est sa réactivité. Si vous proposez à un utilisateur une GUI qui freeze, sa réaction sera sans appel. Par contre, si vous proposez une GUI qui l'informe qu'un process long est en cours, qu'il doit patienter et qu'il peut toujours agir sur l'application, là, vous marquez des points. C'est en partie le principe « Fast and Fluid » prôné par Microsoft.

Un des principaux outils pour nous aider dans cette quête est l'asynchronisme. Nous allons utiliser un Thread de travail dans lequel notre action longue va se dérouler. Nous nous rebrancherons ensuite sur le Thread appelant pour synchroniser les données. En effet, n'oublions pas que les composants graphiques ne peuvent être utilisés que dans le Thread graphique.

Dans le UsersViewModel, ajoutons une propriété IsBusy.

IsBusy

private bool _isBusy ;
public bool IsBusy
{
get
{
return this._isBusy;
}
set
{
if (_isBusy == value)
return;

this._isBusy = value;
this.RaisePropertyChanged();
}
}

Et modifions la méthode InitUsers de façon à introduire de l'asynchronisme.

InitUsers

private void InitUsers()
{
IsBusy = true;

_users = new ObservableCollection<UserOo>();

IList<UserModel> users = null;

Task.Run(
() =>
{
try
{
users = _usersRepository.GetUsers();
}
catch (Exception ex)
{
MessengerInstance.Send(new GenericErrorMessage { Error = ex });
}

// The folowing line simulates a long task
// You can safely remove it
Thread.Sleep(2000);
}).ContinueWith(
previous =>
{
IsBusy = false;
if (users != null)
{
foreach (UserModel userModel in users)
{
_users.Add(Mapper.Map<UserModel, UserOo>(userModel));
}
}
}, TaskScheduler.FromCurrentSynchronizationContext());
}

Nous avons affecté une propriété IsBusy de manière à savoir si un traitement long est en cours.

Définissons la classe GenericErrorMessage.

GenericErrorMessage.cs

1. using System;

3. namespace MonitoringPlatform.Messages

4. {

5. public class GenericErrorMessage

6. {

7. public Exception Error { get; set; }

8. }

9. }

Nous allons ajouter un élément graphique qui représente ce temps d'attente.

Pour ceci, installons l'Extended WPF Toolkit de Xceed.

Nous allons, maintenant utiliser le composant BusyIndicator de cette bibliothèque. Ceci va nous permettre d'afficher un indicateur graphique.

UsersView.xaml

<UserControl x:Class="MonitoringPlatform.Views.UsersView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
d:DesignHeight="300" d:DesignWidth="300"
>
<Grid>
<xctk:BusyIndicator IsBusy="{Binding IsBusy}" DisplayAfter="00:00:00.2">
<ListView ItemsSource="{Binding Users}" Margin="2">
<ListView.View>
<GridView>
<GridViewColumn Width="250" Header="User Name" DisplayMemberBinding="{Binding Name}" />
</GridView>
</ListView.View>
</ListView>
</xctk:BusyIndicator>
</Grid>
</UserControl>

Votre écran devrait ressembler à cela lorsque la tâche de fond est en cours.

Le BusyIndicator n'est actif que lorsque la propriété IsBusy est vraie.

XII - Tests asynchrones et Moq

Nous allons maintenant pouvoir tester notre ViewModel. Dans le projet MonitoringPlatform.Tests, ajoutons la classe UsersViewModelTests. Cette dernière va nous permettre de tester notre ViewModel. Pour ce faire, nous aurons besoin de simuler le comportement du IUserRepository que nous fournissons au constructeur. Nous allons utiliser Moq, une bibliothèque de Mocking, c'est-à-dire de simulation de comportement.

Dans le projet de tests, ajoutons le framework Moq.

Vous devrez également référencer les bibliothèques de MvvmLight dans le projet de test, puisque nous testons le ViewModel, qui hérite d'une classe définie dans ce framework.

Faisons de même pour AutoMapper, puisqu'il est également utilisé dans notre ViewModel.

Écrivons maintenant une première version de notre Test. Dans notre projet de test, ajoutons une classe UsersViewModelTests.

UsersViewModelTests

namespace MonitoringPlatform.Tests
{
using System.Collections.Generic;
using System.Threading;
using AutoMapper;
using MonitoringPlatform.Models;
using MonitoringPlatform.Repositories;
using MonitoringPlatform.ViewModels;
using MonitoringPlatform.ViewModels.ObservableObjects;
using Moq;
using NUnit.Framework;

[TestFixture]
public class UsersViewModelTests
{
private Mock<IUsersRepository> _usersRepositoryMock;

private readonly List<UserModel> _users = new List<UserModel>
{
new UserModel {Name = "Fake User 1"},
new UserModel {Name = "Fake User 2"},
new UserModel {Name = "Fake User 3"}
};

private void InitUsersRepositoryMock()
{
_usersRepositoryMock = new Mock<IUsersRepository>();
_usersRepositoryMock.Setup(m => m.GetUsers()).Returns(
() =>
{
// Simulates a long running process
Thread.Sleep(1000);

// Returns a fake list
return _users;
});
}

private void SetupMapper()
{
Mapper.CreateMap<UserModel, UserOo>();
}

[SetUp]
public void TestSetUp()
{
SetupMapper();
}

[Test]
public void CheckThatUsersPropertyReturnsUsersFromUsersRepository()
{
// Arrange
InitUsersRepositoryMock();
var subject = new UsersViewModel(_usersRepositoryMock.Object);

// Act
var users = subject.Users;

// Assert
Assert.AreEqual(_users.Count, users.Count);
Assert.AreEqual(_users[0].Name, users[0].Name);
Assert.AreEqual(_users[1].Name, users[1].Name);
Assert.AreEqual(_users[2].Name, users[2].Name);
}
}
}

Nous avons mis en lumière ici l'utilisation de Moq qui permet de simuler le comportement de notre interface sur l'appel de la méthode GetUsers(). À travers la méthode Setup de l'objet Mock<T>, nous avons défini le comportement de la méthode GetUsers de l'interface IUsersRepository. Cela nous permet de simuler un grand nombre de cas différents (valeurs aux limites, gestion des exceptions, cas nominaux…). Mais ce test ne pourra pas être valide. Le Thread qui exécute le test unitaire se termine (et donc, passe par les Assert) avant que le Thread de travail n'ait terminé sa tâche. Voici un schéma illustrant ce qui est vraiment exécuté (traits pleins), versus ce que nous voudrions qu'il soit exécuté (traits pointillés, sans le lien 3 => 5).

Voyons comment remédier à ce problème.

Tout d'abord, commençons par ajouter une classe correspondant à un SynchronizationContext de test. Celui-ci va nous permettre d'être notifiés lorsqu'un job est terminé.

TestSyncContext.cs

namespace MonitoringPlatform.Tests
{
using System;
using System.Threading;

public class TestSyncContext : SynchronizationContext
{
public event EventHandler NotifyCompleted;

public override void Post(SendOrPostCallback d, object state)
{
d.Invoke(state);
NotifyCompleted(this, EventArgs.Empty);
}
}
}

Nous pouvons maintenant être alertés lorsque le contexte termine un job. Modifions notre test unitaire en ce sens.

UsersViewModelTests

namespace MonitoringPlatform.Tests
{
using System.Collections.Generic;
using System.Threading;
using AutoMapper;
using MonitoringPlatform.Models;
using MonitoringPlatform.Repositories;
using MonitoringPlatform.ViewModels;
using MonitoringPlatform.ViewModels.ObservableObjects;
using Moq;
using NUnit.Framework;

[TestFixture]
public class UsersViewModelTests
{
private Mock<IUsersRepository> _usersRepositoryMock;

private readonly List<UserModel> _users = new List<UserModel>
{
new UserModel {Name = "Fake User 1"},
new UserModel {Name = "Fake User 2"},
new UserModel {Name = "Fake User 3"}
};

private ManualResetEvent _manualResetEvent;

private void InitUsersRepositoryMock()
{
_usersRepositoryMock = new Mock<IUsersRepository>();
_usersRepositoryMock.Setup(m => m.GetUsers()).Returns(
() =>
{
// Simulates a long running process
Thread.Sleep(1000);

// Returns a fake list
return _users;
});
}

private void SetupMapper()
{
Mapper.CreateMap<UserModel, UserOo>();
}

private void SetupSynchronizationContext()
{
var context = new TestSyncContext();
SynchronizationContext.SetSynchronizationContext(context);
_manualResetEvent = new ManualResetEvent(false);
context.NotifyCompleted += (sender, args) => _manualResetEvent.Set();
}

[SetUp]
public void TestSetUp()
{
SetupMapper();
SetupSynchronizationContext();
}

[Test]
public void CheckThatUsersPropertyReturnsUsersFromUsersRepository()
{
// Arrange
InitUsersRepositoryMock();
var subject = new UsersViewModel(_usersRepositoryMock.Object);

// Act
var users = subject.Users;

_manualResetEvent.WaitOne();

// Assert
Assert.AreEqual(_users.Count, users.Count);
Assert.AreEqual(_users[0].Name, users[0].Name);
Assert.AreEqual(_users[1].Name, users[1].Name);
Assert.AreEqual(_users[2].Name, users[2].Name);
}
}
}

Notre _manualResetEvent.WaitOne() nous permet d'attendre que le Thread background ait terminé son travail avant de pouvoir continuer nos tests. Pensons également à retirer le délai artificiel que nous avions mis dans le InitUsers du ViewModel.

XIII - Services

XIII-A - MISE EN ŒUVRE

Au début de l'article, nous avons brièvement décrit la différence entre un Repository et un Service. Nous allons ici mettre en œuvre un Service.

Si nous regardons le point précédent, quelque chose n'est pas normal : notre test contient une ligne (_manualResetEvent.WaitOne();) qui est dérangeante. En effet, dans notre test, nous supposons que nous devons nous synchroniser avec un autre Thread. Ceci implique que nous connaissions l'implémentation effective qui se cache derrière. Or, l'implémentation d'une classe doit pouvoir évoluer de manière indépendante au(x) test(s) sous-jacent(s). Ce n'est pas le cas ici.

Supposons que nous n'avons pas la main sur notre source de données (représentée par le Repo). Nous allons utiliser notre couche Service comme un wrapper sur notre Repo, et y ajouter de l'asynchronisme.

Ajoutons un répertoire Services à notre solution, et ajoutons-y deux fichiers : IUsersService et UsersServices.

IUsersService.cs

namespace MonitoringPlatform.Services
{
using System.Collections.Generic;
using System.Threading.Tasks;
using MonitoringPlatform.Models;

public interface IUsersService
{
Task<IList<UserModel>> GetUsersAsync();
}
}

UsersService.cs

namespace MonitoringPlatform.Services
{
using System.Collections.Generic;
using System.Threading.Tasks;
using MonitoringPlatform.Models;
using MonitoringPlatform.Repositories;

public class UsersService : IUsersService
{
private readonly IUsersRepository _usersRepository;

public UsersService(IUsersRepository usersRepository)
{
this._usersRepository = usersRepository;
}

public async Task<IList<UserModel>> GetUsersAsync()
{
var taskResult = await Task.Run(() => this._usersRepository.GetUsers());
return taskResult;
}
}
}

N'oublions pas d'enregistrer l'implémentation de l'interface auprès de l'IoC. Dans le fichier ViewModelLocator, dans le constructeur statique, ajouter la ligne suivante.

IoC

SimpleIoc.Default.Register<IUsersService, UsersService>();

Enfin, mettons à jour notre implémentation de UsersViewModel pour utiliser le service et non plus le Repo directement.

UsersViewModel.cs

namespace MonitoringPlatform.ViewModels
{
using System;
using System.Collections.ObjectModel;
using AutoMapper;
using MonitoringPlatform.Messages;
using MonitoringPlatform.Models;
using MonitoringPlatform.Services;
using MonitoringPlatform.ViewModels.ObservableObjects;
using System.Collections.Generic;

public class UsersViewModel : TabViewModelBase
{
private readonly IUsersService _usersService;
private ObservableCollection<UserOo> _users;

private bool _isBusy;

public UsersViewModel(IUsersService usersService)
{
_usersService = usersService;
}

private async void InitUsers()
{
IsBusy = true;

_users = new ObservableCollection<UserOo>();

IList<UserModel> users = null;

try
{
users = await _usersService.GetUsers();
}
catch (Exception ex)
{
MessengerInstance.Send(new UsersRepositoryErrorMessage { Error = ex });
}

IsBusy = false;
if (users != null)
{
foreach (UserModel userModel in users)
{
_users.Add(Mapper.Map<UserModel, UserOo>(userModel));
}
}

}

public override string TabName
{
get
{
return "Users";
}
}

public ObservableCollection<UserOo> Users
{
get
{
if (_users == null)
InitUsers();

return this._users;
}
}

public bool IsBusy
{
get
{
return this._isBusy;
}
set
{
if (_isBusy == value)
return;

this._isBusy = value;
this.RaisePropertyChanged();
}
}
}
}

En lançant l'application, le comportement reste le même.

XIII-B - EventToCommand

Comme vous pouvez le remarquer, les tests unitaires ne sont plus à jour, et le projet associé ne compile plus. Pour pouvoir les corriger, nous devons tout d'abord procéder à quelques modifications. Il y a quelque chose de gênant dans le ViewModel que nous avons construit : le chargement de la liste des Users se fait directement dans la propriété. Les bonnes pratiques veulent que les traitements soient plutôt faits dans des méthodes, puis une affectation à une propriété. Le but est d'avoir une propriété la plus simple possible.

Mais comment faire pour charger la liste des utilisateurs au bon moment ? Et quel est ce bon moment ? Plusieurs réponses peuvent être considérées comme valables. Nous partirons de l'hypothèse que nous souhaitons que le chargement débute lorsqu'on clique sur l'onglet « Users ».

Pour créer une « action » à ce moment, nous allons utiliser l'EventToCommand du framework MvvmLight. Modifions la MainView.

MainView.xaml

<Window x:Class="MonitoringPlatform.Views.MainView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:MonitoringPlatform.Views"
xmlns:viewModels="clr-namespace:MonitoringPlatform.ViewModels"
Title="MainView" Height="350" Width="525"
DataContext="{Binding Main, Source={StaticResource Locator}}"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras"
xmlns:command="http://www.galasoft.ch/mvvmlight">

<Window.Resources>
<DataTemplate DataType="{x:Type viewModels:ServicesViewModel}">
<views:ServicesView />
</DataTemplate>
<DataTemplate DataType="{x:Type viewModels:UsersViewModel}">
<views:UsersView />
</DataTemplate>
</Window.Resources>

<Grid>
<TabControl ItemsSource="{Binding Tabs}" Margin="12">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<command:EventToCommand Command="{Binding TabControlSelectionChangedCommand}" PassEventArgsToCommand="True" />
</i:EventTrigger>
</i:Interaction.Triggers>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding TabName}"/>
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>

</Grid>
</Window>

Nous avons utilisé un Trigger pour nous signaler que le Tab sélectionné a changé. Nous pouvons dans une commande appropriée côté ViewModel agir à ce moment-là.

MainViewModel

namespace MonitoringPlatform.ViewModels
{
using System.Collections.ObjectModel;
using System.Windows.Controls;
using System.Windows.Input;
using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.CommandWpf;

public class MainViewModel : ViewModelBase
{
private ObservableCollection<TabViewModelBase> _tabs;
private readonly ServicesViewModel _servicesViewModel;
private readonly UsersViewModel _usersViewModel;

private readonly ICommand _tabControlSelectionChangedCommand;

public MainViewModel(ServicesViewModel servicesViewModel, UsersViewModel usersViewModel)
{
this._servicesViewModel = servicesViewModel;
this._usersViewModel = usersViewModel;
_tabControlSelectionChangedCommand = new RelayCommand<SelectionChangedEventArgs>(SelectedTabChangedAction);

BuildTabs();
}

private async void SelectedTabChangedAction(SelectionChangedEventArgs e)
{
if (e.AddedItems != null && e.AddedItems.Count > 0 && e.AddedItems[0] is TabViewModelBase)
{
TabViewModelBase tabVm = e.AddedItems[0] as TabViewModelBase;
await tabVm.SetFocusAsync();
}
}

private void BuildTabs()
{
if (Tabs == null)
Tabs = new ObservableCollection<TabViewModelBase>();

Tabs.Clear();

Tabs.Add(_servicesViewModel);
Tabs.Add(_usersViewModel);
}

public ObservableCollection<TabViewModelBase> Tabs
{
get
{
return _tabs;
}
set
{
if (value == _tabs)
return;

_tabs = value;
this.RaisePropertyChanged();
}
}

public ICommand TabControlSelectionChangedCommand
{
get
{
return _tabControlSelectionChangedCommand;
}
}
}
}

Nous devons modifier le TabViewModelBase pour ajouter la méthode SetFocusAsync.

TabViewModelBase.cs

namespace MonitoringPlatform.ViewModels
{
using GalaSoft.MvvmLight;

public abstract class TabViewModelBase : ViewModelBase
{
public abstract string TabName { get; }

public abstract Task SetFocusAsync();
}
}

ServicesViewModel l'implémente, mais n'a pas d'action particulière.

SetFocusAsync

public override Task SetFocusAsync()
{
return Task.FromResult<string>(string.Empty);
}

UsersViewModel l'implémente également, et contient le déclenchement de l'initialisation des users.

XIII-C - Retour aux tests

Nous pouvons désormais supprimer la ligne (_manualResetEvent.WaitOne();) qui nous gênait.

Nous allons simplement la remplacer par le cycle de vie le la GUI, c'est-à-dire le SetFocusAsync. Modifions la classe UsersViewModelTests.

UsersViewModelTests.cs

namespace MonitoringPlatform.Tests
{
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using AutoMapper;
using MonitoringPlatform.Models;
using MonitoringPlatform.Services;
using MonitoringPlatform.ViewModels;
using MonitoringPlatform.ViewModels.ObservableObjects;
using Moq;
using NUnit.Framework;

[TestFixture]
public class UsersViewModelTests
{
private Mock<IUsersService> _usersServiceMock;

private readonly List<UserModel> _users = new List<UserModel>
{
new UserModel {Name = "Fake User 1"},
new UserModel {Name = "Fake User 2"},
new UserModel {Name = "Fake User 3"}
};

private void InitUsersServiceMock()
{
_usersServiceMock = new Mock<IUsersService>();
_usersServiceMock.Setup(m => m.GetUsers()).Returns(
async () =>
{
var taskResult = Task.Run<IList<UserModel>>(() =>
{
// Simulates a long process
Thread.Sleep(2000);

// Returns data
return _users;
});
return await taskResult;
});
}

private void SetupMapper()
{
Mapper.CreateMap<UserModel, UserOo>();
}

[SetUp]
public void TestSetUp()
{
SetupMapper();
}

[Test]
public void CheckThatUsersPropertyReturnsUsersFromUsersRepository()
{
// Arrange
InitUsersServiceMock();
var subject = new UsersViewModel(_usersServiceMock.Object);

// Act
subject.SetFocusAsync().Wait();
var users = subject.Users;

// Assert
Assert.AreEqual(_users.Count, users.Count);
Assert.AreEqual(_users[0].Name, users[0].Name);
Assert.AreEqual(_users[1].Name, users[1].Name);
Assert.AreEqual(_users[2].Name, users[2].Name);
}
}
}

La dernière étape consistera à ajouter un test directement sur le UsersService. Ajoutons une classe UsersServiceTests.

UsersServiceTests.cs

namespace MonitoringPlatform.Tests
{
using System.Collections.Generic;
using MonitoringPlatform.Models;
using MonitoringPlatform.Repositories;
using MonitoringPlatform.Services;
using Moq;
using NUnit.Framework;

[TestFixture]
public class UsersServiceTests
{
private Mock<IUsersRepository> _usersRepositoryMock;

private readonly List<UserModel> _users = new List<UserModel>
{
new UserModel {Name = "Fake User 1"},
new UserModel {Name = "Fake User 2"},
new UserModel {Name = "Fake User 3"}
};

private void InitUsersRepositoryMock()
{
_usersRepositoryMock = new Mock<IUsersRepository>();
_usersRepositoryMock.Setup(m => m.GetUsers()).Returns(() => this._users);
}

[Test]
public void CheckThatUsersPropertyReturnsUsersFromUsersRepository()
{
// Arrange
InitUsersRepositoryMock();
UsersService subject = new UsersService(_usersRepositoryMock.Object);

// Act
var task = subject.GetUsers();
var users = task.Result;

// Assert
Assert.AreEqual(_users.Count, users.Count);
Assert.AreEqual(_users[0].Name, users[0].Name);
Assert.AreEqual(_users[1].Name, users[1].Name);
Assert.AreEqual(_users[2].Name, users[2].Name);
}
}
}

Notre code est désormais couvert sur les étages importants : ViewModels, Repositories et Services.

XIV - Conclusion

Nous avons passé en revue quelques-uns des outils que j'affectionne particulièrement, et qui nous aident à mettre en place des programmes structurés et maintenables.

Encore une fois, ce n'est pas la seule et unique manière de procéder. D'autres moyens existent, d'autres frameworks, d'autres approches…

Les tests que nous avons mis en place ne sont pas forcément les plus pertinents. Le but ici est simplement de voir comment on peut les mettre en place.

Enfin, dans nos exemples, nous avons utilisé notre couche Services comme un wrapper sur nos Repositories, car nous avions supposé ne pas avoir la main sur nos Repo. La tendance actuelle est plutôt de créer des Repo asynchrones. Je vous laisse le soin de découvrir ces techniques.

Je remercie Evrim S, tomlev et ClaudeLeloup pour la relecture de l'article !