Partie 6 - Construire un framework MVVM▲
Dans cette partie, je vais parler de la construction d'un framework MVVM.
Comme je le disais dans la partie précédente, cette partie devrait être sur l'OrderView. Cette vue devrait permettre aux utilisateurs de choisir des éléments d'une liste de produits, les ajouter à la commande en cours, choisir les quantités, voir le prix total actuel et soumettre la commande. Aussi, l'utilisateur doit être en mesure de filtrer, trier et avoir une pagination sur la liste des produits. Je suis sûr que vous avez vu plein d'articles et de billets de blogs d'autres personnes parlant de ce sujet, c'est-à-dire obtenir le produit sélectionné, l'ajouter à une autre collection ou créer un détail de la commande basé dessus, puis mettre à jour certaines autres données sur l'interface utilisateur et, enfin, soumettre les modifications au serveur. Le truc, c'est que tout le monde a sa propre façon de programmer et lorsque vous vous retrouvez éventuellement dans une équipe, il se peut que vous constatiez que deux personnes ont codé la même chose d'une manière différente, et que l'une a un bogue dans la situation A et l'autre a un bogue dans la situation B. Avoir un bon framework MVVM avec une méthodologie bien définie est indispensable pour prévenir ces situations. Dans cette partie, je souhaite parler des composants essentiels que vous devez avoir dans un bon framework MVVM. Je vous décrirai par la suite un framework MVVM sur lequel je travaille qui était basé sur WCF RIA Services, mais dont il ne dépend pas vraiment.
Puisque nous suivons les bonnes pratiques, nous savons qu'en utilisant une bonne architecture MVVM nous pouvons arriver à une solution dont la logique d'extraction, de filtrage, de tri, de pagination est entièrement séparée de la vue, nous permettant d'avoir aussi des vues différentes pour le même viewmodel. Par exemple, nous pouvons commencer par utiliser un DataGrid et un DataPager pour afficher nos articles, mais plus tard, fournir une nouvelle vue qui utilise des combobox pour sélectionner les options de tri, une listbox de type album pour afficher les articles et des boutons personnalisés pour la pagination. De même, nous devrions être en mesure de séparer toute cette logique de sa logique d'accès aux données réelles pour pouvoir utiliser des objets factices pour notre modèle et d'effectuer des tests unitaires pour nos viewmodels. Ce n'est pas une tâche facile mais c'est ce que je souhaite réaliser à partir de maintenant.
Eh bien, pour commencer, .NET/Silverlight nous offre déjà quelques classes et interfaces qui sont très pratiques dans le cadre de scénarios MVVM.
- INotifyPropertyChanged - Utilisée pour déclencher un événement lorsqu'une propriété change. Le framework de liaison WPF/Silverlight utilise cette interface pour mettre à jour la vue lorsqu'une propriété change.
- INotifyCollectionChanged - Utilisée pour déclencher un événement lorsqu'une opération d'insertion, de suppression, d'effacement ou de remplacement a été effectuée sur une collection. Les contrôles WPF/Silverlight qui ont une propriété ItemsSource utilisent d'habitude cette interface pour créer ou supprimer des éléments visuels dans un conteneur. Par exemple, des ListBox affichent de nouvelles ListBoxItem, des DataGrid affichent de nouvelles DataGridRow.
- ICollectionView - Utilisée pour fournir le filtrage, des descriptions de tri, des descriptions du groupe, et la sélection d'items pour une collection IEnumerable et faire que la vue n'affiche que les éléments filtrés, triés selon les descriptions de tri et mettre en évidence l'élément sélectionné. (Possède davantage de fonctionnalités, mais ce sont les plus pertinentes dans le cadre de cet article.)
- IPagedCollectionView - Utilisée pour fournir des options de pagination à une collection IEnumerable. Celles-ci sont utilisées essentiellement par des DataPager, qui font des appels à la méthode MoveToPage(int pageIndex) et nous permettent d'enregistrer dans l'événement PageChanging et d'aller chercher une nouvelle page d'entités à afficher.
- Il existe d'autres interfaces importantes telles que IEditableObject et IEditableCollectionView, mais je ne les aborderai pas dans cet article. Elles sont utilisées pour mettre à jour les valeurs des propriétés d'un objet de façon atomique.
Malheureusement, les collections WCF RIA Services n'implémentent pas ICollectionView ou IPagedCollectionView, donc nous devons fournir notre propre mécanisme de filtrage, de tri et de pagination des données. Toutefois, cela est assez facile à faire avec LINQ et la classe Expression et nous pouvons composer des requêtes avec certains opérateurs de LINQ avec WCF RIA Services. Silverlight a sa propre implémentation de IPagedCollectionView, appelée System.Windows.Data.PagedCollectionView mais elle a été conçue pour fonctionner avec les collections en mémoire et non pas les « collections » (sources de données) côté serveur. Dans cette série, je fournis ma propre implémentation IPagedCollectionView/ICollectionView que j'ai conçue pour fonctionner avec n'importe quel type de sources de données. Quelle que soit l'option utilisée cela revient entièrement au modèle dans MVVM.
Donc, voyons comment fonctionne mon framework MVVM.
Tout d'abord, nous avons le concept du modèle :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
public
interface
IDomainModel :
IDisposable
{
void
RejectChanges
(
);
ISubmitOperation SaveChanges
(
);
ISubmitOperation SaveChanges
(
Action<
ISubmitOperation>
callback,
object
userState);
IDomainQuery<
T>
GetQuery<
T>(
);
void
Add<
T>(
T entity);
void
Remove<
T>(
T entity);
void
Attach<
T>(
T entity);
void
Detach<
T>(
T entity);
bool
HasChanges {
get
;
}
bool
IsSubmitting {
get
;
}
bool
IsLoading {
get
;
}
}
Si vous êtes habitué à l'API DomainContext, vous trouverez cette interface très facile à comprendre. Cette interface modèle est censée vous fournir des fonctionnalités de base pour extraire des données, les modifier et enregistrer les modifications. Pour des opérations plus spécifiques, vous pouvez étendre cette interface, ajouter des opérations spécifiques et faire en sorte que votre viewmodel en dépende.
L'interface IDomainQuery représente une requête sur un certain repository d'un type d'entité (quel qu'il soit). Comme une requête, elle vous permet de filtrer, trier et paginer les données.
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.
public
interface
IDomainQuery :
IDisposable
{
void
Cancel
(
);
void
Load
(
);
event
Action Loading;
event
Action<
ILoadOperation>
Loaded;
event
Action Canceled;
}
public
interface
IDomainQuery<
T>
:
IDomainQuery
{
///
<
summary
>
/// Delegate that returns a lambda expression to apply in a Where operation
///
<
/summary
>
Func<
Expression<
Func<
T,
bool
>>>
FilterExpression {
get
;
set
;
}
///
<
summary
>
/// SortDescription values that are applied in a OrderBy/ThenBy operation
///
<
/summary
>
Func<
SortDescriptionCollection>
SortDescriptions {
get
;
set
;
}
///
<
summary
>
/// Contains the required data that are applied in a Skip and Take operation
///
<
/summary
>
Func<
PageDescription>
PageDescription {
get
;
set
;
}
new
event
Action<
ILoadOperation<
T>>
Loaded;
}
public
sealed
class
PageDescription
{
public
int
PageSize {
get
;
set
;
}
public
int
PageIndex {
get
;
set
;
}
}
En outre, elle possède des événements pour vous permettre de savoir quand elle a commencé et terminé une opération de chargement. Il y a deux choses importantes à noter dans l'interface IDomainQuery. La première est le fait que la façon dont vous filtrez une IDomainQuery est la même que pour filtrer une RIA Services EntityQuery, ou tout autre objet IQueryable. Cela signifie qu'il est très facile de réaliser des implémentations IDomainQuery qui interrogent les repositories en mémoire ou des repositories côté serveur (notez qu'EntityQuery n'implémente pas IQueryable et qu'il n'est pas facile d'intégrer les deux concepts). L'autre chose importante est que le mécanisme de filtrage, de tri, de pagination est basé sur les délégués, ce qui signifie que chaque fois que la requête est exécutée/réexécutée ces délégués sont toujours réévalués et vont toujours refléter l'état actuel de votre viewmodel.
Dernier point, mais non des moindres, nous avons les opérations. J'ai créé des interfaces pour les opérations « Load » et « Submit ». J'ai implémenté toutes ces interfaces avec des classes qui s'appuient sur WCF RIA Services. Je ne vais pas poster le code de ces classes, mais ils sont disponibles dans le dépôt du code source, donc vous pouvez l'y consulter.
Si vous êtes habitué à DomainContext, vous êtes sans doute en train de vous demander où sont les EntitySet et où vous pouvez manipuler les collections. Eh bien, puisque mon implémentation IDomainModel s'appuie sur WCF RIA Services, vous pouvez étendre cette classe et manipuler votre DomainContext pour implémenter toute opération que vous souhaitez rendre disponible dans votre IDomainModel étendue. Mais avec ce framework elles ne sont pas censées être accessibles dans votre viewmodel. Au lieu de cela, j'utilise mon implémentation ICollectionView, que j'ai appelée DomainCollectionView.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
public
interface
IDomainCollectionView :
ICollectionView,
IPagedCollectionView,
INotifyPropertyChanged
{
void
Add
(
object
item);
void
Remove
(
object
item);
}
public
interface
IDomainCollectionView<
T>
:
IDomainCollectionView,
IEnumerable<
T>
{
void
Add
(
T item);
void
Remove
(
T item);
}
L'implémentation actuelle s'appuie sur une IDomainQuery et sera une vue pour les résultats qui sont récupérés au cours de chaque opération. Puisque vous pouvez implémenter une IDomainQuery pour toute IEnumerable vous pouvez utiliser cette implémentation pour n'importe quelle source de données.
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.
public
class
DomainCollectionView<
T>
:
ViewModelBase,
IDomainCollectionView<
T>
{
private
IDomainQuery<
T>
query;
private
ObservableCollection<
T>
sourceCollection;
#region DomainCollectionView Members
public
DomainCollectionView
(
IDomainQuery<
T>
query,
bool
usePaging =
true
)
{
this
.
query =
query;
this
.
sourceCollection =
this
.
CreateSourceCollection
(
);
this
.
CurrentItem =
null
;
this
.
CurrentPosition =
-
1
;
//Apply sort and paging
query.
SortDescriptions =
(
) =>
this
.
SortDescriptions;
if
(
usePaging)
{
this
.
canChangePage =
true
;
this
.
pageSize =
10
;
// default page size to 10
query.
PageDescription =
(
) =>
new
PageDescription {
PageSize =
this
.
PageSize,
PageIndex =
this
.
PageIndex };
}
query.
Loaded +=
this
.
OnDataRefreshed;
}
private
void
OnDataRefreshed
(
ILoadOperation<
T>
loadOp)
{
using
(
this
.
DeferRefresh
(
))
{
this
.
sourceCollection.
Clear
(
);
foreach
(
var
entity in
loadOp.
Entities)
this
.
sourceCollection.
Add
(
entity);
this
.
ItemCount =
loadOp.
TotalEntityCount;
this
.
TotalItemCount =
loadOp.
TotalEntityCount;
}
}
//...
}
Maintenant que j'ai introduit le modèle et l'IDomainCollectionView, il est temps que nous parlions un peu du viewmodel.
Contrairement au modèle, les viewmodels ne sont pas tellement réutilisables, car ils dépendent généralement des besoins de la vue. Cependant, plusieurs vues dans les applications métier ont des besoins communs : la recherche/énumération des entités, édition des entités, créer des associations entre entités. Nous pouvons créer des viewmodels pour ces concepts et les étendre uniquement pour implémenter la logique métier. Toute l'infrastructure et la plomberie peuvent être centralisées et réutilisées partout.
Avant de terminer, je vais déposer simplement le premier jet d'un viewmodel de base qui peut être utilisé pour rechercher/énumérer des entités.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
public
class
ListVM<
T>
:
ViewModelBase,
IListVM<
T>
{
private
IDomainModel model;
public
ListVM
(
) {
}
protected
virtual
IDomainModel CreateModel
(
)
{
return
ServiceLocator.
Current.
GetInstance<
IDomainModel>(
);
}
public
void
Initialize
(
IListVMInitializeArgs args)
{
this
.
model =
args.
ExistingModel ??
this
.
CreateModel
(
);
var
query =
model.
GetQuery<
T>(
);
this
.
Entities =
new
DomainCollectionView<
T>(
query,
args.
RequiresPaging);
query.
Load
(
);
}
public
IDomainCollectionView<
T>
Entities
{
get
;
private
set
;
}
}
Avec seulement quelques lignes de code, nous avons un viewmodel de base qui peut :
- afficher des résultats d'une requête qui peuvent être triés et paginés sur le serveur. Liez simplement une datagrid à la collection Entities et manipulez-la ;
- réutiliser un modèle existant ou en créer un nouveau. L'implémentation actuelle peut utiliser un modèle partagé pour toute l'application qui est enregistré dans le conteneur d'injection de dépendances actuel ou laisser un viewmodel dérivé décider quoi faire.
Je vous laisse y réfléchir.
Conclusion▲
Cette série est maintenant terminée. À l'avenir je pourrais creuser davantage pour voir comment nous pouvons créer des viewmodels de base pour tous les concepts cités plus haut et ajouter plus de fonctionnalités aux viewmodels.