I. Introduction▲
I-A. Propos▲
Le but de cet article est de proposer une solution clef en main pour construire une application WPFWindows Presentation Foundation en utilisant le pattern MVVMModel-View-ViewModel. 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 IoCInversion of Control. 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.
2.
3.
4.
5.
6.
7.
8.
<
Application
x
:
Class
=
"
MonitoringPlatform.App
"
xmlns
=
"
http://schemas.microsoft.com/winfx/2006/xaml/presentation
"
xmlns
:
x
=
"
http://schemas.microsoft.com/winfx/2006/xaml
"
StartupUri
=
"
Views/MainView.xaml
"
>
<
Application
.
Resources
>
<
/
Application
.
Resources
>
<
/
Application
>
2.
3.
4.
5.
6.
7.
8.
<
Window
x
:
Class
=
"
MonitoringPlatform.Views.MainView
"
xmlns
=
"
http://schemas.microsoft.com/winfx/2006/xaml/presentation
"
xmlns
:
x
=
"
http://schemas.microsoft.com/winfx/2006/xaml
"
Title
=
"
MainView
"
Height
=
"
350
"
Width
=
"
525
"
>
<
Grid
>
<
/
Grid
>
<
/
Window
>
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
namespace
MonitoringPlatform.
Views
{
///
<
summary
>
///
Interaction
logic
for
MainView
.
xaml
///
<
/
summary
>
public
partial class
MainView
{
public
MainView
(
)
{
InitializeComponent
(
);
}
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
using
GalaSoft.
MvvmLight.
Ioc;
using
Microsoft.
Practices.
ServiceLocation;
public
class
ViewModelLocator
{
static
ViewModelLocator
(
)
{
ServiceLocator.
SetLocatorProvider
(
(
) =
>
SimpleIoc.
Default);
//
Register
Services
in
here
//
Register
ViewModels
in
here
SimpleIoc.
Default.
Register<
MainViewModel>
(
);
}
public
MainViewModel Main
{
get
{
return
ServiceLocator.
Current.
GetInstance<
MainViewModel>
(
);
}
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
<
Application
x
:
Class
=
"
MonitoringPlatform.App
"
xmlns
=
"
http://schemas.microsoft.com/winfx/2006/xaml/presentation
"
xmlns
:
x
=
"
http://schemas.microsoft.com/winfx/2006/xaml
"
xmlns
:
viewModels
=
"
clr-namespace:MonitoringPlatform.ViewModels
"
StartupUri
=
"
Views/MainView.xaml
"
xmlns
:
d
=
"
http://schemas.microsoft.com/expression/blend/2008
"
xmlns
:
mc
=
"
http://schemas.openxmlformats.org/markup-compatibility/2006
"
mc
:
Ignorable
=
"
d
"
>
<
Application
.
Resources
>
<
viewModels
:
ViewModelLocator
x
:
Key
=
"
Locator
"
d
:
IsDataSource
=
"
True
"
/
>
<
/
Application
.
Resources
>
<
/
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 xmlnsXML NameSpace.
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 ».
2.
3.
4.
5.
6.
7.
8.
9.
<
Window
x
:
Class
=
"
MonitoringPlatform.Views.MainView
"
xmlns
=
"
http://schemas.microsoft.com/winfx/2006/xaml/presentation
"
xmlns
:
x
=
"
http://schemas.microsoft.com/winfx/2006/xaml
"
Title
=
"
MainView
"
Height
=
"
350
"
Width
=
"
525
"
DataContext
=
"
{Binding
Main,
Source={StaticResource
Locator}}
"
>
<
Grid
>
<
/
Grid
>
<
/
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.
<
TextBlock
Text
=
"
I'm
the
Services
view
"
>
<
/
TextBlock
>
Ajoutons maintenant la seconde vue, nommée « UsersView », de la même manière.
<
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.
2.
3.
4.
5.
6.
7.
<
TabControl
ItemsSource
=
"
{Binding
Tabs}
"
Margin
=
"
12
"
>
<
TabControl
.
ItemTemplate
>
<
DataTemplate
>
<
TextBlock
Text
=
"
{Binding
TabName}
"
/
>
<
/
DataTemplate
>
<
/
TabControl
.
ItemTemplate
>
<
/
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.
2.
3.
4.
5.
6.
using
GalaSoft.
MvvmLight;
public
abstract
class
TabViewModelBase :
ViewModelBase
{
public
abstract
string
TabName {
get;
}
}
Nous avons maintenant notre base pour créer nos ServicesViewModel & UsersViewModel, que nous allons ajouter au répertoire ViewModels.
2.
3.
4.
5.
6.
7.
8.
9.
10.
public
class
ServicesViewModel :
TabViewModelBase
{
public
override
string
TabName
{
get
{
return
"
Services
"
;
}
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
public
class
UsersViewModel :
TabViewModelBase
{
public
override
string
TabName
{
get
{
return
"
Users
"
;
}
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
<
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}}
"
>
<
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
"
>
<
TabControl
.
ItemTemplate
>
<
DataTemplate
>
<
TextBlock
Text
=
"
{Binding
TabName}
"
/
>
<
/
DataTemplate
>
<
/
TabControl
.
ItemTemplate
>
<
/
TabControl
>
<
/
Grid
>
<
/
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
using
System.
Collections.
ObjectModel;
using
GalaSoft.
MvvmLight;
public
class
MainViewModel :
ViewModelBase
{
private
ObservableCollection<
TabViewModelBase>
_tabs;
private
readonly
ServicesViewModel _servicesViewModel;
private
readonly
UsersViewModel _usersViewModel;
public
MainViewModel
(
ServicesViewModel servicesViewModel,
UsersViewModel usersViewModel)
{
this
.
_servicesViewModel =
servicesViewModel;
this
.
_usersViewModel =
usersViewModel;
BuildTabs
(
);
}
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
(
);
}
}
}
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).
2.
SimpleIoc.
Default.
Register<
ServicesViewModel>
(
);
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
namespace
MonitoringPlatform.
Repositories
{
using
System.
Collections.
Generic;
public
interface
IServicesRepository
{
IList<
ServiceModel>
GetServices
(
);
}
}
namespace
MonitoringPlatform.
Repositories
{
using
System.
Collections.
Generic;
public
interface
IUsersRepository
{
IList<
UserModel>
GetUsers
(
);
}
}
namespace
MonitoringPlatform.
Repositories
{
using
System.
Collections.
Generic;
public
class
ServicesRepository :
IServicesRepository
{
public
IList<
ServiceModel>
GetServices
(
)
{
//
TODO
return
null
;
}
}
}
namespace
MonitoringPlatform.
Repositories
{
using
System.
Collections.
Generic;
public
class
UsersRepository :
IUsersRepository
{
public
IList<
UserModel>
GetUsers
(
)
{
//
TODO
return
null
;
}
}
}
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.
2.
3.
4.
5.
6.
public
class
ServiceModel
{
}
public
class
UserModel
{
}
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
MonitoringPlatform.
Models;
Une fois ajouté, la compilation devrait se faire sans problème.
Mappons maintenant les services et leurs implémentations dans notre ViewModelLocator.
2.
SimpleIoc.
Default.
Register<
IServicesRepository,
ServicesRepository>
(
);
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
namespace
MonitoringPlatform.
Models
{
public
enum
WindowsServiceStatus
{
Intermediate,
Paused,
Running,
Stopped
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
namespace
MonitoringPlatform.
Repositories
{
using
System.
Collections.
Generic;
using
System.
Linq;
using
System.
ServiceProcess;
using
MonitoringPlatform.
Models;
public
class
ServicesRepository :
IServicesRepository
{
public
IList<
ServiceModel>
GetServices
(
)
{
var windowsServices =
ServiceController.
GetServices
(
);
array
IList<
ServiceModel>
serviceModels =
new
List<
ServiceModel>
(
windowsServices.
Length);
foreach
(
ServiceController serviceController in
windowsServices)
{
var serviceModel =
new
ServiceModel
(
);
serviceModel.
ServiceName =
serviceController.
ServiceName;
switch
(
serviceController.
Status)
{
case
ServiceControllerStatus.
ContinuePending:
case
ServiceControllerStatus.
PausePending:
case
ServiceControllerStatus.
StartPending:
case
ServiceControllerStatus.
StopPending:
serviceModel.
Status =
WindowsServiceStatus.
Intermediate;
break
;
case
ServiceControllerStatus.
Paused:
serviceModel.
Status =
WindowsServiceStatus.
Paused;
break
;
case
ServiceControllerStatus.
Running:
serviceModel.
Status =
WindowsServiceStatus.
Running;
break
;
case
ServiceControllerStatus.
Stopped:
break
;
}
serviceModels.
Add
(
serviceModel);
}
return
serviceModels;
}
}
}
Il faut ajouter les propriétés associées sur nos models. Mettons à jour la classe ServiceModel.
2.
3.
4.
5.
6.
public
class
ServiceModel
{
public
string
ServiceName {
get;
set;
}
public
WindowsServiceStatus Status {
get;
set;
}
}
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.
2.
3.
4.
public
class
UserModel
{
public
string
Name {
get;
set;
}
}
Concernant la classe UsersRepository, nous devons ajouter une référence vers System.DirectoryServices.dll.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
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
(
)
{
return
new
List<
UserModel>
{
new
UserModel {
Name =
"
User
1
"
}
,
new
UserModel {
Name =
"
User
2
"
}
}
;
}
}
}
À 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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
namespace
MonitoringPlatform.
Tests
{
using
MonitoringPlatform.
Repositories;
using
NUnit.
Framework;
[
TestFixture]
public
class
ServicesRepositoryTests
{
[
Test]
public
void
CheckThatGetServicesDoesNotReturnNull
(
)
{
//
Arrange
ServicesRepository subject =
new
ServicesRepository
(
);
//
Act
var result =
subject.
GetServices
(
);
//
Assert
Assert.
IsNotNull
(
result);
}
}
}
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
namespace
MonitoringPlatform.
Tests
{
using
MonitoringPlatform.
Repositories;
using
NUnit.
Framework;
[
TestFixture]
public
class
UsersRepositoryTests
{
[
Test]
public
void
CheckThatGetUsersDoesNotReturnNull
(
)
{
//
Arrange
UsersRepository subject =
new
UsersRepository
(
);
//
Act
var result =
subject.
GetUsers
(
);
//
Assert
Assert.
IsNotNull
(
result);
}
}
}
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
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.
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
(
);
}
}
}
}
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.
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.
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
ItemsSource
=
"
{Binding
Users}
"
Margin
=
"
2
"
>
<
ListView
.
View
>
<
GridView
>
<
GridViewColumn
Width
=
"
250
"
Header
=
"
User
Name
"
DisplayMemberBinding
=
"
{Binding
Name}
"
/
>
<
/
GridView
>
<
/
ListView
.
View
>
<
/
ListView
>
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.
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.
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
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.
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.
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.
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.
2.
3.
4.
5.
6.
7.
8.
9.
using
System;
namespace
MonitoringPlatform.
Messages
{
public
class
GenericErrorMessage
{
public
Exception Error {
get;
set;
}
}
}
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.
<
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.
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é.
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.
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.
namespace
MonitoringPlatform.
Services
{
using
System.
Collections.
Generic;
using
System.
Threading.
Tasks;
using
MonitoringPlatform.
Models;
public
interface
IUsersService
{
Task<
IList<
UserModel>
>
GetUsersAsync
(
);
}
}
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.
SimpleIoc.
Default.
Register<
IUsersService,
UsersService>
(
);
Enfin, mettons à jour notre implémentation de UsersViewModel pour utiliser le service et non plus le Repo directement.
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.
<
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à.
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.
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.
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.
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.
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 !