Exemple d'application d'entreprises avec Silverlight 3 et .NET RIA Services - Partie 25 - ViewModel

| Brad Abrams | Philippe Vialatte (Traduction) |

Cet article fait partie d'une série de traductions d'articles de Brad Abrams sur le développement d'applications métier avec Silverlight et .NET RIA Services.

Retrouvez l'ensemble des articles de la série sur cette page : Exemple d'application métier pour Silverlight 3 et .NET RIA Services.

Commentez cet article : Commentez . ♪

- Titre : Exemple d'application d'entreprises avec Silverlight 3 et .NET RIA Services - Partie 25 - ViewModel

- Auteurs : Brad Abrams, Philippe Vialatte (Traduction)

- Parution : 25 mars 2011

- Mise à jour : 21 décembre 2011

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

Sommaire

- I. Introduction

- II. Orientation

- III. Chargement des données

- IV. Filtrage

- V. Pagination

- VI. Tri

- VII. Interagir avec la vue

- VIII. Unit Testing Tests unitaires

- IX. Conclusion

- X. Remerciements

I - Introduction

ViewModel (ou Modèle-Vue-ViewModel) est un pattern qui est en cours d'apparition dans le monde de Silverlight et de WPF qui permet une séparation des préoccupations similaires à celle du modèle MVC qui est populaire sur les applications web d'aujourd'hui (par exemple: ASP.NET MVC).

John Gossman a été le premier que j'ai entendu parler de ce pattern, durant ses jours de travail sur Expression Blend. Bien sûr, c'est tout simplement une mise en application du pattern de Martin Fowler Modèle de présentation

Dans cet exemple, je vais prendre notre application (toujours plus populaire) SuperEmployees et la réécrire selon le pattern ViewModel. Comme pour tous les patterns émergents, il y a beaucoup de variations, toutes avec leurs forces et leurs faiblesses… J'ai choisi l'approche avec laquelle je me sentais le plus à l'aise pour une introduction.

Vous pouvez voir la série complète ici.

Cette démo nécessite les éléments suivants (tout est 100% gratuit) :

- VS2008 SP1 ;

- Silverlight 3 RTM ;

- .NET RIA Services.

Consultez le site en ligne et téléchargez les fichiers de la démo.

Pour cet exemple, nous allons nous concentrer exclusivement sur le projet client (MyApp et MyApp.Tests)… revoyez les articles précédents pour plus d'informations sur le côté serveur de cette application.

II - Orientation

Modèle (SuperEmployeeDomainContext dans MyApp.Web.g.cs) - Responsable de l'accès aux données et de la logique métier. Vue (home.xaml) - Responsable des éléments de l'interface utilisateur ViewModel (SuperEmployeesViewModel.cs) - Spécialisation du modèle que la vue va utiliser pour la liaison de données.

Plus d'information sur le Pattern ViewModel dans Silverlight se trouve sur le blog de Nikhil.

Il y a quelques autres exemples de codes intéressants dans le dossier « PatternsFramework ».

Il contient des classes d'aide qui peuvent être applicables à d'autres applications ViewModel + RIA Services.

- ViewModelBase - Provient de l'exemple de ViewModel de Nihkil ;

- PagedCollectionView - Ajoute le support du paging (IPagedCollectionView) d'une manière assez classique ;

- EntityCollectionView - Provient du blog de Jeff Handley, gère la plupart des interfaces nécessaires pour la liaison, et fonctionne bien sûr super bien avec RIA Services ;

- PagedEntityCollectionView - Ajout du support de pagination. Cela va nous permettre la plupart des choses fournies par DomainDataSource, mais en plus simple à utiliser avec un ViewModel.

III - Chargement des données

Commençons juste par obtenir quelques données de base dans l'application. Je vais uniquement travailler avec des liaisons de données à destination de ma classe SuperEmployeesViewModel, que je vais mettre en place en tant que DataContext de la page.

public class SuperEmployeesViewModel : PagedViewModelBase {}

puis, dans home.xaml

<navigation:Page.DataContext>
<AppViewModel:SuperEmployeesViewModel />
</navigation:Page.DataContext>

Maintenant, nous sommes prêts à commencer. Comme vous vous en souvenez dans les articles précédents, l'application est très simple à configurer.

Commençons par mettre en place la liaison de données du DataGrid et des DataForm…

1. <data:DataGrid x:Name="dataGrid1" Height="380" Width="380"

2. IsReadOnly="True" AutoGenerateColumns="False"

3. HorizontalAlignment="Left"

4. SelectedItem="{Binding SelectedSuperEmployee, Mode=TwoWay}"

5. HorizontalScrollBarVisibility="Disabled"

6. ItemsSource="{Binding SuperEmployees}">

7. <dataControls:DataForm x:Name="dataForm1" Height="393" Width="331"

8. VerticalAlignment="Top"

9. Header="Product Details"

10. CurrentItem="{Binding SelectedSuperEmployee}"

11. HorizontalAlignment="Left" >

Notez que pour la DataGrid, à la ligne 6, nous nous lions à la propriété SuperEmployees sur le ViewModel. Nous allons voir plus loin comment cela est défini. Ensuite à la ligne 4, nous allons nous lier de façon bidirectionnelle à la propriété SelectedSuperEmployee.

Cela signifie que le DataGrid va mettre à jour cette propriété lorsque l'utilisateur sélectionne un élément. Enfin, dans la ligne 3 du DataForm, nous allons le lier à cette même propriété.

Dans SuperEmployeesViewModel.cs, nous voyons les propriétés SuperEmployees et SelectedSuperEmployee … Notez que nous allons remonter des notifications de changement de propriété de telle sorte que l'interface utilisateur puisse se mettre à jour lorsque ces valeurs sont modifiées.

PagedEntityCollectionView<SuperEmployee> _employees;
public PagedEntityCollectionView<SuperEmployee> SuperEmployees
{
get { return _employees; }
set
{
if (_employees != value)
{
_employees = value;
RaisePropertyChanged(SuperEmployeesChangedEventArgs);
}
}
}

private SuperEmployee _selectedSuperEmployee;
public SuperEmployee SelectedSuperEmployee
{
get { return _selectedSuperEmployee; }
set
{
if (SelectedSuperEmployee != value)
{
SuperEmployees.MoveCurrentTo(value);
_selectedSuperEmployee = value;
RaisePropertyChanged(SelectedSuperEmployeeChangedEventArgs);
}
}
}

Ok, le câblage est fait, mais comment _employees va obtenir sa valeur en premier lieu ? Comment les données sont-elles effectivement chargées ?

Eh bien, consultons le constructeur SuperEmployeesViewModel.

public SuperEmployeesViewModel()
{
_superEmployeeContext = new SuperEmployeeDomainContext();
SuperEmployees = new PagedEntityCollectionView<SuperEmployee>(
_superEmployeeContext.SuperEmployees, this);
}

Comme SuperEmployees est une PagedCollectionView, nous pouvons la passer en tant que IPagedCollectionView, de façon à ce qu'elle soit rappelée lorsque le chargement de données est nécessaire (par exemple, quand je passe à la page 1). La classe de base PageViewModelhandles contient toute la plomberie, mais nous avons encore besoin de gérer le chargement des données via notre implémentation de la méthode loadData ().

public override void LoadData()
{
if (IsLoading || _superEmployeeContext == null)
{
return;
}

IsLoading = true;
_superEmployeeContext.SuperEmployees.Clear();
var q = _superEmployeeContext.GetSuperEmployeesQuery();

_superEmployeeContext.Load(q, OnSuperEmployeesLoaded, null);
}

Comme vous pouvez le voir, c'est assez simple, il suffit d'effacer la liste de ce que nous pouvons avoir déjà téléchargé, puis de charger les données supplémentaires. Notez que nous ne gérons pas réellement le paging, nous y arriverons sous peu.

IV - Filtrage

Maintenant que nous avons ce filtre sur Origins, nous allons voir comment nous y connecter de façon à ne récupérer que les entités qui ont une certaine origine.

Il faut souligner que nous ne voulons pas récupérer toutes les entités, puis effectuer le filtre côté client, ce qui utiliserait beaucoup trop de bande passante.

Nous ne voulons pas non plus faire ce filtre au niveau du serveur web, ce qui pourrait tout de même finir par surcharger la base de données. À la place, nous voulons faire ce filtrage au plus bas niveau, dans la base de données.

Nous allons faire cela « par magie », en utilisant la composition de requêtes Linq. Nous allons créer une requête sur le client, puis l'envoyer au serveur web, qui va tout simplement la transmettre (par Entity Framework dans cet exemple) à la base de données.

Tout d'abord, dans la vue Home.xaml, nous nous branchons à la liaison de données:

1. <StackPanel Orientation="Horizontal" Margin="0,0,0,10">

2. <TextBlock Text="Origin: " />

3. <TextBox x:Name="originFilterBox" Width="338" Height="30"

4. Text="{Binding OriginFilterText, Mode=TwoWay}"></TextBox>

5. </StackPanel>

À la ligne 4, nous faisons la liaison sur notre propriété OriginFilterText dans el ViewModel. Jetons un œil à cette propriété.

string _originFilterText;
public string OriginFilterText
{
get { return _originFilterText; }
set
{
if (_originFilterText != value)
{
_originFilterText = value;
RaisePropertyChanged(OriginFilterTextChangedEventArgs);

PageIndex = 0;
LoadData();
}
}
}

À chaque fois que le texte du filtre change, nous avons besoin de charger les données… Mais comme vous vous souvenez de la méthode LoadData ci-dessus, on charge toutes les données. Comment pouvons-nous modifier l'application, de façon à ce qu'elle ne charge que les données correspondant notre filtre ?

1. public override void LoadData ()

2. {

3. if (IsLoading || _superEmployeeContext == null)

4. {

5. return;

6. }

8. IsLoading = true;

10. _superEmployeeContext.SuperEmployees.Clear();

11. var q = _superEmployeeContext.GetSuperEmployeesQuery();

13. if (!String.IsNullOrEmpty(OriginFilterText))

14. {

15. q = q.Where(emp => emp.Origin.StartsWith(OriginFilterText));

16. }

18. _superEmployeeContext.Load(q, OnSuperEmployeesLoaded, null);

19. }

Comme vous le voyez, dans les lignes 13 à 16, nous avons juste ajouté une clause supplémentaire. Cette clause est sérialisée, envoyée au serveur sur lequel elle est interprétée par la couche d'accès aux données (EF dans ce cas) et enfin exécutée sur la base de données.

Pour le code du « deep-linking », nous avons besoin de filtrer sur l'employeeID que nous recevons depuis l'URL, ce qui nous permet de voir combien il serait facile d'ajouter un filtre par employeeID. Regardez les lignes 13-16.

1. public override void LoadData ()

2. {

3. if (IsLoading || _superEmployeeContext == null)

4. {

5. return;

6. }

8. IsLoading = true;

10. _superEmployeeContext.SuperEmployees.Clear();

11. var q = _superEmployeeContext.GetSuperEmployeesQuery();

13. if (!String.IsNullOrEmpty(EmployeeIDFilter))

14. {

15. q = q.Where(emp => emp.EmployeeID == Convert.ToInt32(EmployeeIDFilter));

16. }

18. if (!String.IsNullOrEmpty(OriginFilterText))

19. {

20. q = q.Where(emp => emp.Origin.StartsWith(OriginFilterText));

21. }

23. _superEmployeeContext.Load(q, OnSuperEmployeesLoaded, null);

24. }

Comme vous pouvez le voir, il nous suffit d'ajouter une autre clause WHERE selon le même schéma.

V - Pagination

La pagination ressemble au filtrage que l'on vient d'examiner. Nous allons lier des contrôles d'interface utilisateur à une propriété sur le ViewModel, puis personnaliser la requête basée sur cette propriété. Dans ce cas, l'interface est un DataPager et la propriété est CurrentPage.

Avant tout, nous devons définir une PageSize (nombre d'entités à charger en une fois). Comme nous voulons que cela soit personnalisable par un designer, on va en faire une propriété dans la vue.

<navigation:Page.DataContext>
<AppViewModel:SuperEmployeesViewModel PageSize="13" />
</navigation:Page.DataContext>

Ensuite, nous allons lier le DataPager à cette valeur et à notre liste de SuperEmployees.

<data:DataPager x:Name ="pager1" PageSize="{Binding PageSize}" Width="379"
HorizontalAlignment="Left"
Source="{Binding SuperEmployees}"
Margin="0,0.2,0,0" />

J'ai défini la propriété PageSize dans le PagedViewModelBase parce qu'il est commun à toutes les données … Mais il est assez peu original.

int pageSize;
public int PageSize
{
get { return pageSize; }
set
{
if (pageSize != value)
{
pageSize = value;
RaisePropertyChanged(PageSizeChangedEventArgs);
}
}
}

DataPager fonctionne grâce à l'interface IPagedCollection qui est définie sur PagedViewModelBase. Ainsi, cette classe de base s'applique à toutes les fonctionnalités FirstPage, NextPage, MoveTo(page) et expose simplement une propriété PageIndex.

Nous pouvons l'utiliser dans notre méthode LoadData () pour écrire le code de pagination, qui devrait sembler familier à quiconque a fait de la pagination de données au cours des 20 dernières années. ;-)

public override void LoadData ()
{
if (IsLoading || _superEmployeeContext == null)
{
return;
}

IsLoading = true;

_superEmployeeContext.SuperEmployees.Clear();
var q = _superEmployeeContext.GetSuperEmployeesQuery();

if (!String.IsNullOrEmpty(EmployeeIDFilter))
{
q = q.Where(emp => emp.EmployeeID == Convert.ToInt32(EmployeeIDFilter));
}

if (!String.IsNullOrEmpty(OriginFilterText))
{
q = q.Where(emp => emp.Origin.StartsWith(OriginFilterText));
}

if (PageSize > 0)
{
q = q.Skip(PageSize * PageIndex);
q = q.Take(PageSize);
}

_superEmployeeContext.Load(q, OnSuperEmployeesLoaded, null);
}

Dans les lignes 24-28, nous ajoutons à la requête les instructions Skip() et Take().

Nous allons d'abord sauter le nombre d'entités fois l'index de notre page actuelle. Puis nous récupérons le prochain lot d'entités pour une page. Encore une fois, tout ceci finit par se transformer en code TSQL et s'exécuter sur le serveur SQL.

VI - Tri

Comme vous pourriez le deviner, le tri suit exactement le même modèle que la pagination. Certains éléments d'interface utilisateur dans la vue sont liés à certaines propriétés du ViewModel, auxquelles on accède dans LoadData () de façon à personnaliser la requête Linq qui est envoyée au serveur.

Dans notre cas, le DataGrid est lié à la EntityCollectionView qui implémente ICollectionView.SortDescriptions. De cette façon, lorsque l'on trie le DataGrid, on va changer la SortDescription.

On va donc, dans notre DataLoad (), avoir besoin d'accéder à SortDescription et ajouter à la requête Linq.

1. public override void LoadData ()

2. {

3. if (IsLoading || _superEmployeeContext == null)

4. {

5. return;

6. }

8. IsLoading = true;

10. _superEmployeeContext.SuperEmployees.Clear();

11. var q = _superEmployeeContext.GetSuperEmployeesQuery();

13. if (!String.IsNullOrEmpty(EmployeeIDFilter))

14. {

15. q = q.Where(emp => emp.EmployeeID == Convert.ToInt32(EmployeeIDFilter));

16. }

18. if (!String.IsNullOrEmpty(OriginFilterText))

19. {

20. q = q.Where(emp => emp.Origin.StartsWith(OriginFilterText));

21. }

23. if (SuperEmployees.SortDescriptions.Any())

24. {

25. bool isFirst = true;

26. foreach (SortDescription sd in SuperEmployees.SortDescriptions)

27. {

28. q = OrderBy(q, isFirst, sd.PropertyName, sd.Direction == ListSortDirection.Descending);

29. isFirst = false;

30. }

31. }

32. else

33. {

34. q = q.OrderBy(emp => emp.EmployeeID);

35. }

37. if (PageSize > 0)

38. {

39. q = q.Skip(PageSize * PageIndex);

40. q = q.Take(PageSize);

41. }

43. _superEmployeeContext.Load(q, OnSuperEmployeesLoaded, null);

44. }

Voyez les lignes 23 à 35, ou nous ajoutons un OrderBy à la requête LINQ via une petite méthode d'assistance.

1. private EntityQuery<SuperEmployee> OrderBy(EntityQuery<SuperEmployee> q, bool isFirst, string propertyName, bool descending)

2. {

3. Expression<Func<SuperEmployee, object>> sortExpression;

5. switch (propertyName)

6. {

7. case "Name":

8. sortExpression = emp => emp.Name;

9. break;

10. case "EmployeeID":

11. sortExpression = emp => emp.EmployeeID;

12. break;

13. case "Origin":

14. sortExpression = emp => emp.Origin;

15. break;

16. default:

17. sortExpression = emp => emp.EmployeeID;

18. break;

19. }

20. if (isFirst)

21. {

22. if (descending)

23. return q.OrderByDescending(sortExpression);

24. return q.OrderBy(sortExpression);

25. }

26. else

27. {

28. if (!descending)

29. return q.ThenByDescending(sortExpression);

30. return q.ThenBy(sortExpression);

31. }

32. }

Cette méthode d'assistance construit une expression de tri fonction d'une propertyname et d'un ordre de tri.

VII - Interagir avec la vue

Un des aspects intéressants du pattern ViewModel concerne l'interaction du ViewModel avec la vue. Jusqu'ici, nous avons examiné plusieurs exemples de définition des propriétés de la View sur le ViewModel, ainsi que la databinding entre la View et les valeurs du ViewModel, mais nous n'avons pas encore vu comment le ViewModel peut interagir avec l'interface utilisateur.

Un bon exemple de cela est la façon dont on va refactoriser la fonctionnalité ExportToExcel.

Commençons par la vue. Comme vous l'avez peut-être vu, elle comporte un bouton Exporter vers Excel.

<Button Content="Export to Excel"
Width="105" Height="28"
Margin="5,0,0,0" HorizontalAlignment="Left"
Click="ExportToExcel_Click" ></Button>

Le clic est géré depuis le code-behind, plutôt que depuis le ViewModel, car la logique est très spécifique à la vue (appel de FileOpenDialog).

private void ExportToExcel_Click(object sender, RoutedEventArgs e)
{
var dialog = new SaveFileDialog();

dialog.DefaultExt = "*.xml";
dialog.Filter = "Excel Xml (*.xml)|*.xml|All files (*.*)|*.*";

if (dialog.ShowDialog() == false) return;

using (var fileStream = dialog.OpenFile())
{
ViewModel.ExportToExcel(fileStream);
}
}

Puis, à la ligne 12, il y a un exemple logique réelle que nous pourrions vouloir réutiliser ou tester séparément, et que nous allons mettre dans le ViewModel.

public void ExportToExcel(Stream fileStream)
{
var s = Application.GetResourceStream(new Uri("excelTemplate.txt", UriKind.Relative));
var sr = new StreamReader(s.Stream);

var sw = new StreamWriter(fileStream);
while (!sr.EndOfStream)
{
var line = sr.ReadLine();
if (line == "***") break;
sw.WriteLine(line);
}

foreach (SuperEmployee emp in SuperEmployees)
{
sw.WriteLine("<Row>");
sw.WriteLine("<Cell><Data ss:Type=\"String\">{0}</Data></Cell>", emp.Name);
sw.WriteLine("<Cell><Data ss:Type=\"String\">{0}</Data></Cell>", emp.Origin);
sw.WriteLine("<Cell><Data ss:Type=\"String\">{0}</Data></Cell>", emp.Publishers);
sw.WriteLine("<Cell><Data ss:Type=\"Number\">{0}</Data></Cell>", emp.Issues);
sw.WriteLine("</Row>");
}
while (!sr.EndOfStream)
{
sw.WriteLine(sr.ReadLine());
}
}

Notez que l'on n'interagit pas du tout avec la vue. La façon dont la vue récupère le Stream pour écrire les données Excel est totalement gérée par la vue. Cela rend les tests unitaires plus faciles, et permet une séparation plus propre des responsabilités.

AddSuperEmployee et ErrorWindow vont fonctionner de façon très similaire.

VIII - Unit Testing Tests unitaires

Aucun post sur le ViewModel ne serait complet sans au moins une mention des tests unitaires.

L'un des principaux facteurs de motivation pour le pattern ViewModel est la possibilité de tester la logique de l'interface utilisateur de votre application sans avoir à vous soucier de l'automatisation. La chose la plus importante à faire lorsque vous écrivez des tests unitaires est de se concentrer sur les tests de VOTRE code. Je sais que Microsoft emploie beaucoup de grands testeurs et développeurs pour tester notre code… Vous devriez vous concentrer sur l'isolation de votre code et tester juste votre code.

Donc, en réalité, ce que nous voulons faire est de créer une autre vue pour notre ViewModel (dans ce cas, le code de test) et mocker la couche d'accès réseau\données.

Tout d'abord, nous allons créer un projet de test unitaire pour notre client Silverlight. Si vous avez installé le Silverlight Unit Test Framework correctement, vous devriez trouver un modèle de projet « Silverlight Test Project » (consultez l'excellent post de Jeff Wilco sur comment installer ce Frameork).

Ensuite, comme Jeff le mentionne dans son post, vous devez ajouter des références à Microsoft.VisualStudio.QualityTools.UnitTesting.Silverlight.dll et Microsoft.Silverlight.Testing.dll. Vous aurez aussi besoin d'ajouter une référence de projet vers le projet MyApp, qui contient le code que nous voulons tester.

Vous verrez que notre premier test déjà en place.

[TestClass]
public class SuperEmployeesViewModelTest : SilverlightTest
{
[TestMethod]
public void TestMethod()
{
Assert.IsTrue(true);
}

Pour l'exécuter, il suffit de définir le nouveau projet MyApp.Tests en tant que projet de démarrage

et d'appuyer sur F5.

Le test passe… mais il n'est manifestement pas très intéressant … ajoutons un qui l'est plus.

Mais d'abord, rappelons la partie la plus importante des tests unitaires - tester uniquement le code que vous avez écrit. Par exemple, je ne veux pas tester le code qui dialogue avec le serveur, ou le code qui appelle la base de données sur le serveur, tout ceci est le code de quelqu'un d'autre.

Je vais donc mocker la connexion au serveur. Heureusement, le DomainContext est conçu de façon à permettre ce type de mocking. En effet, DomainContext a un DomainService qui est chargé de toute communication avec le serveur. Nous avons juste besoin de lui fournir notre propre MockDomainService, qui n'accèdera pas au serveur pour obtenir des données, mais utilisera ses propres données locales.

public class MockDomainClient : LocalDomainClient {

private IEnumerable<Entity> _mockEntities;

public MockDomainClient(IEnumerable<Entity> mockEntities) {
_mockEntities = mockEntities;
}

protected override IQueryable<Entity> Query(QueryDetails details,
IDictionary<string, object> parameters) {
var q = _mockEntities.AsQueryable();

return q;
}
}

Voici mon MockDomainClient de départ. Vous remarquerez que j'hérite de LocalDomainClient (voir l'excellent post sur ViewModel de Nikhil) et plus tard nous nous pencherons sur QueryDetails (voir le code de Jason Allor concernant LinqService).

Maintenant, nous allons ajouter notre premier vrai test… Nous allons vérifier que notre code qui gère les EmployeeIDFilter est correct. Il y a trois étapes pour chaque test unitaire: (1) configuration (2) test (3) vérification.

Regardons l'initialisation en premier.

1. [TestMethod]

2. [Asynchronous]

3. public void TestLoadData_EmployeeIDFilter()

4. {

5. //initialize

6. var entityList = new List<Entity>()

7. {

8. new SuperEmployee () {

9. Name = "One",

10. EmployeeID = 1,

11. },

12. new SuperEmployee () {

13. Name = "Two",

14. EmployeeID = 2

15. }

16. };

19. var client = new MockDomainClient(entityList);

20. var context = new SuperEmployeeDomainContext(client);

21. var vm = new SuperEmployeesViewModel(context);

23. vm.ErrorRaising += (s, arg) =>

24. {

25. throw new Exception("VM throw an exceptions", arg.Exception);

26. };

28. //run test

29. //TODO

30. //check results

31. EnqueueDelay(1000);

33. EnqueueCallback(() =>

34. {

35. //TODO asserts

36. });

38. EnqueueTestComplete();

39. }

À la ligne 2, nous déclarions ce test de façon asynchrone, parce que notre ViewModel renvoie ses résultats de façon asynchrone, et que nous avons besoin que notre test fasse de même. De la ligne 6 à la 16, nous l'initialisons les données. J'aime bien déclarer toutes les données dans le test de sorte qu'il est facile de voir ce qu'il se passe. Dans les lignes 19 à 21, nous créons un MockDomainClient et nous l'initialisons avec nos données de test, puis nous créons un SuperEmployeeDomainContext basé sur ce DomainClient et enfin, nous créons le ViewModel. Enfin, dans les lignes 23 à 36, nous gérons toutes les erreurs qui peuvent être remontées, ce qui nous permet de déboguer les tests.

Maintenant, nous allons aborder les étapes de test et de vérification.

//run test
vm.EmployeeIDFilter = "1";

//check results
EnqueueDelay(1000);

EnqueueCallback(() =>
{
Assert.IsTrue(vm.SuperEmployees.Count() == 1);
var res = vm.SuperEmployees.FirstOrDefault();
Assert.IsNotNull(res.EmployeeID == 1);
});

EnqueueTestComplete();
}

Pour exécuter les tests, nous avons simplement mis l'EmployeeIDFilter à 1, ce qui a comme effet secondaire de nous charger les données… Puis, dans les lignes 11-13, nous faisons quelques assertions de base, pour s'assurer qu'exactement un article est retourné et qu'il a le bon EmployeeID.

Maintenant, nous le lançons et… il passe !

Le test de la méthode OriginFilter ressemble beaucoup au précédent…

[TestMethod]
[Asynchronous]
public void TestLoadData_EmployeeOriginFilter()
{
//initialize
var entityList = new List<Entity>()
{
new SuperEmployee () {
Name = "One",
EmployeeID = 1,
},
new SuperEmployee () {
Name = "Two",
EmployeeID = 2
}
};

var client = new MockDomainClient(entityList);
var context = new SuperEmployeeDomainContext(client);
var vm = new SuperEmployeesViewModel(context);

vm.ErrorRaising += (s, arg) =>
{
throw new Exception("VM throw an exceptions", arg.Exception);
};

//run test

vm.OriginFilterText = "Earth";

//check results
EnqueueDelay(1000);

EnqueueCallback(() =>
{
Assert.IsTrue(vm.SuperEmployees.Count() == 2);
foreach (var emp in vm.SuperEmployees)
{
Assert.IsTrue(emp.Origin == "Earth");
}
});

EnqueueTestComplete();
}

Puis on le lance, et il se déroule avec succès !

Je laisse comme un exercice au lecteur de finir d'écrire les tests ;-)

IX - Conclusion

J'espère que vous avez apprécié cet aperçu de ViewModel avec RIA Services. Vous pouvez télécharger les fichiers de la démo complétée.

Merci à Jeff Handley de m'avoir aidé avec cet article, et à Nikhil, John Papa et Pete Brown pour leurs commentaires.

Mise à jour : Vijay Upadya m'a aidé un peu pour ce qui est des tests unitaires avec LinqUtils…

Cet article conclut la partie sur l'approche ViewModel. La vingt-sixième et dernière partie de cette série d'articles sera consacrée à l'authentification et à la personnalisation de l'application.

X - Remerciements

Je tiens ici à remercier Brad Abrams pour son aimable autorisation de traduire l'article.
Je remercie également djibril pour sa relecture et ses propositions.