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) :
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…
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
<
data:
DataGrid x:
Name=
"dataGrid1"
Height=
"380"
Width=
"380"
IsReadOnly=
"True"
AutoGenerateColumns=
"False"
HorizontalAlignment=
"Left"
SelectedItem=
"{Binding SelectedSuperEmployee, Mode=TwoWay}"
HorizontalScrollBarVisibility=
"Disabled"
ItemsSource=
"{Binding SuperEmployees}"
>
<
dataControls:
DataForm x:
Name=
"dataForm1"
Height=
"393"
Width=
"331"
VerticalAlignment=
"Top"
Header=
"Product Details"
CurrentItem=
"{Binding SelectedSuperEmployee}"
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:
2.
3.
4.
5.
<
StackPanel Orientation=
"Horizontal"
Margin=
"0,0,0,10"
>
<
TextBlock Text=
"Origin: "
/>
<
TextBox x:
Name=
"originFilterBox"
Width=
"338"
Height=
"30"
Text=
"{Binding OriginFilterText, Mode=TwoWay}"
></
TextBox>
</
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 ?
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
public
override
void
LoadData (
)
{
if
(
IsLoading ||
_superEmployeeContext ==
null
)
{
return
;
}
IsLoading =
true
;
_superEmployeeContext.
SuperEmployees.
Clear
(
);
var
q =
_superEmployeeContext.
GetSuperEmployeesQuery
(
);
if
(!
String.
IsNullOrEmpty
(
OriginFilterText))
{
q =
q.
Where
(
emp =>
emp.
Origin.
StartsWith
(
OriginFilterText));
}
_superEmployeeContext.
Load
(
q,
OnSuperEmployeesLoaded,
null
);
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
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));
}
_superEmployeeContext.
Load
(
q,
OnSuperEmployeesLoaded,
null
);
}
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.
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.
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
(
SuperEmployees.
SortDescriptions.
Any
(
))
{
bool
isFirst =
true
;
foreach
(
SortDescription sd in
SuperEmployees.
SortDescriptions)
{
q =
OrderBy
(
q,
isFirst,
sd.
PropertyName,
sd.
Direction ==
ListSortDirection.
Descending);
isFirst =
false
;
}
}
else
{
q =
q.
OrderBy
(
emp =>
emp.
EmployeeID);
}
if
(
PageSize >
0
)
{
q =
q.
Skip
(
PageSize *
PageIndex);
q =
q.
Take
(
PageSize);
}
_superEmployeeContext.
Load
(
q,
OnSuperEmployeesLoaded,
null
);
}
Voyez les lignes 23 à 35, ou nous ajoutons un OrderBy à la requête LINQ via une petite méthode d'assistance.
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.
private
EntityQuery<
SuperEmployee>
OrderBy
(
EntityQuery<
SuperEmployee>
q,
bool
isFirst,
string
propertyName,
bool
descending
)
{
Expression<
Func<
SuperEmployee,
object
>>
sortExpression;
switch
(
propertyName)
{
case
"Name"
:
sortExpression =
emp =>
emp.
Name;
break
;
case
"EmployeeID"
:
sortExpression =
emp =>
emp.
EmployeeID;
break
;
case
"Origin"
:
sortExpression =
emp =>
emp.
Origin;
break
;
default
:
sortExpression =
emp =>
emp.
EmployeeID;
break
;
}
if
(
isFirst)
{
if
(
descending
)
return
q.
OrderByDescending
(
sortExpression);
return
q.
OrderBy
(
sortExpression);
}
else
{
if
(!
descending
)
return
q.
ThenByDescending
(
sortExpression);
return
q.
ThenBy
(
sortExpression);
}
}
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.
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.
[TestMethod]
[Asynchronous]
public
void
TestLoadData_EmployeeIDFilter
(
)
{
//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
//TODO
//check results
EnqueueDelay
(
1000
);
EnqueueCallback
((
) =>
{
//TODO asserts
}
);
EnqueueTestComplete
(
);
}
À 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.