MVVM My Way

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

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.

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

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.

final screen

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

Image non disponible

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.

Image non disponible

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

Image non disponible

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

Image non disponible

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

Image non disponible

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.

Image non disponible
App.xaml
Sélectionnez
1.
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>
MainView.xaml
Sélectionnez
1.
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>
MainView.xaml.cs
Sélectionnez
1.
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.

ViewModelLocator
Sélectionnez
1.
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.

App.xaml
Sélectionnez
1.
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 ».

MainView.xaml
Sélectionnez
1.
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.

Image non disponible
Image non disponible

Ajoutons simplement un TextBlock pour identifier la vue.

ServicesView.xaml
Sélectionnez
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
Sélectionnez
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
Sélectionnez
1.
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.

TabViewModelBase.cs
Sélectionnez
1.
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.

ServicesViewModel.cs
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
public class ServicesViewModel : TabViewModelBase
    {
        public override string TabName
        {
            get
            {
                return "Services";
            }
        }
    }
UsersViewModel.cs
Sélectionnez
1.
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.

MainView.xaml
Sélectionnez
1.
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.

MainViewModel.cs
Sélectionnez
1.
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).

ViewModelLocator
Sélectionnez
1.
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.

Image non disponible

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
Sélectionnez
1.
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.

Models
Sélectionnez
1.
2.
3.
4.
5.
6.
public class ServiceModel
    {
    }    
public class UserModel
    {
    }
Image non disponible

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
Sélectionnez
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
Sélectionnez
1.
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.

WindowsServiceStatus
Sélectionnez
1.
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.

ServicesRepository
Sélectionnez
1.
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.

ServiceModel
Sélectionnez
1.
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.

UserModel
Sélectionnez
1.
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.

UsersRepository.cs
Sélectionnez
1.
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.

Image non disponible
Image non disponible

Référençons notre projet principal.

Image non disponible

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.

Image non disponible

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
Sélectionnez
1.
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);
        }
    }
}
UsersRepositoryTests
Sélectionnez
1.
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 ».

Image non disponible

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.

Image non disponible

VII. Retour à la Vue/VueModel

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

ListView
Sélectionnez
<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.

Image non disponible

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

ServiceOo
Sélectionnez
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
Sélectionnez
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.

Image non disponible

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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
<ListView ItemsSource="{Binding Users}" Margin="2">
            <ListView.View>
                <GridView>
                    <GridViewColumn Width="250" Header="User Name" DisplayMemberBinding="{Binding Name}" />
                </GridView>
            </ListView.View>
        </ListView>
UsersViewModel.cs
Sélectionnez
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.

Image non disponible
Image non disponible

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
Sélectionnez
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.

Image non disponible
GenericErrorMessage.cs
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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.

Image non disponible

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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
1.
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.

Image non disponible

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

Image non disponible
UsersView.xaml
Sélectionnez
<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.

Image non disponible

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.

Image non disponible

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.

Image non disponible

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
Sélectionnez
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).

Image non disponible

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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
namespace MonitoringPlatform.Services
{
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using MonitoringPlatform.Models;

    public interface IUsersService
    {
        Task<IList<UserModel>> GetUsersAsync();
    }
}
UsersService.cs
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
<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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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
Sélectionnez
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 !

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

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