Share via


Modèles de conception

Problèmes et solutions relatifs à Model-View-ViewModel (Modèle-Vue-Modèle de vues)

Robert McCarter

Télécharger l'exemple de code

Windows Presentation Foundation (WPF) et Silverlight contiennent des API riches pour créer des applications évoluées, mais il peut s'avérer difficile de comprendre et d'appliquer toutes les fonctionnalités WPF harmonieusement pour créer des applications bien conçues et faciles à gérer. Par où commencer ? Comment composer correctement votre application ?

Le modèle de conception Model-View-ViewModel (MVVM) décrit l'approche courante utilisée pour créer des applications WPF ou Silverlight. Il s'agit à la fois d'un outil performant pour créer des applications et d'un langage courant pour les discussions relatives à la conception des applications avec les développeurs. MVVM est un modèle vraiment utile, mais il est assez récent et mal compris.

Quand le modèle de conception MVVM est-il applicable et quand est-il superflu ? Comment l'application devrait-elle être structurée ? Quelle est la quantité de travail nécessaire pour écrire et gérer la couche ViewModel et quelles sont les alternatives qui existent pour réduire la quantité de code dans cette couche ? Comment traiter de manière élégante les propriétés associées dans le Model ? Comment exposer des collections du modèle dans la vue ? Où les objets ViewModel devraient-ils être instanciés et associés aux objets Model ?

Dans cet article, j'expliquerai comment fonctionne le ViewModel et je décrirai certains avantages et problèmes liés à l'implémentation d'un ViewModel dans votre code. Je détaillerai également des exemples concrets illustrant l'utilisation du ViewModel comme gestionnaire de documents destiné à exposer des objets Model dans la couche View.

Modèle, ViewModel et Vue

Toutes les applications WPF et Silverlight sur lesquelles j'ai travaillé jusqu'à présent étaient conçues à partir de composants de haut niveau. Le modèle était la base de l'application et de grands efforts avaient été mis en œuvre pour le concevoir suivant les meilleures pratiques de conception et d'analyse orientées objet (OOAD).

Je considère que le modèle est le cœur de l'application, représentant l'actif commercial le plus important du fait qu'il capture toutes les entités métier complexes, leurs relations et leur fonctionnalité.

Au-dessus du modèle se trouve le ViewModel. Les deux principaux objectifs du ViewModel consistent à rendre le modèle facile à lire par la vue WPF/XAML et à séparer et encapsuler le modèle de la vue. Ce sont des objectifs remarquables, mais qui, pour des raisons pragmatiques, sont parfois délaissés.

Lorsque vous créez le ViewModel, vous savez comment les utilisateurs interagiront avec l'application à un haut niveau. Cependant, dans le cadre du modèle de conception MVVM, il est important que le ViewModel ne sache rien de la vue. Cela permet aux concepteurs et aux graphistes de créer des interfaces utilisateur agréables et fonctionnelles sur le ViewModel tout en travaillant étroitement avec les développeurs pour concevoir un ViewModel adapté pour soutenir leur action. Le découplage entre la vue et le ViewModel permet également de contrôler et de réutiliser le ViewModel plus facilement.

Pour contribuer à imposer une séparation stricte entre les couches Model, View et ViewModel, je préfère créer chaque couche comme un projet Visual Studio distinct. Si elles sont associées aux utilitaires réutilisables, à l'assembly exécutable principale et à un projet de test d'unités (vous en avez plein, non ?), vous obtenez de nombreux projets et assemblys, comme illustré à la figure 1.

Figure 1 The Components of an MVVM Application

Figure 1 Composants d'une application MVVM

Étant donné le grand nombre de projets, l'approche de séparation stricte est évidemment plus utile pour des grands projets. Pour les petites applications occupant seulement un ou deux développeurs, les avantages de la séparation stricte s'avèreront sans doute moindres par rapport aux contraintes liées à la création, à la configuration et à la gestion de différents projets. Séparer simplement le code en différents espaces de nom dans un même projet peut permettre d'obtenir une isolation plus que suffisante.

L'écriture et la gestion d'un ViewModel ne sont pas des opérations simples. Elles ne doivent pas être envisagées à la légère. Cependant, les réponses aux questions fondamentales (quand prendre en compte le modèle de conception MVVM ? et quand est-il inutile ?) se trouvent souvent dans votre modèle de domaine.

Pour les projets plus importants, un domaine de modèle peut s'avérer complexe et comporter des centaines de classes soigneusement conçues pour travailler ensemble de manière élégante pour tous les types d'applications, y compris les services Web et les applications WPF ou ASP.NET. Le modèle peut comporter plusieurs assemblys fonctionnant ensemble et, dans les très grandes organisations, le modèle de domaine est parfois créé et géré par une équipe de développement spécialisée.

Dans le cas d'un modèle de domaine important et complexe, il est presque toujours avantageux d'introduire une couche ViewModel.

Par contre, dans le cas d'un modèle de domaine simple, une couche mince sur la base de données peut suffire. Les classes peuvent être générées automatiquement et elles implémentent fréquemment INotifyPropertyChanged. L'interface utilisateur est souvent un ensemble de listes ou de grilles avec des formulaires de modification permettant aux utilisateurs de manipuler les données sous-jacentes. Les outils Microsoft ont toujours été efficaces pour créer rapidement et facilement ces types d'applications.

Si votre modèle ou votre application correspond à cette catégorie, un ViewModel engendrerait sans doute une surcharge inacceptable sans dégager de bénéfice suffisant pour la conception de votre application.

Cela dit, le ViewModel peut quand même apporter de la valeur. C'est par exemple un excellent moyen d'implémenter la fonctionnalité d'annulation. Vous pouvez également choisir d'utiliser MVVM pour une partie de l'application (comme la gestion de documents, comme nous le verrons ultérieurement) et exposer de façon pragmatique votre modèle directement sur la vue.

Pourquoi utiliser un ViewModel ?

Si le ViewModel semble approprié à votre application, vous devez réfléchir à certaines questions avant de commencer le codage. L'une des premières questions est de savoir comment réduire le nombre de propriétés proxy.

La séparation de la vue et du modèle mise en avant par le modèle de conception MVVM est un aspect important et utile du modèle. Par conséquent, si une classe Model comporte 10 propriétés devant être exposées dans la vue, le ViewModel comporte généralement 10 propriétés identiques qui échangent simplement l'appel vers l'instance de modèle sous-jacent. Ces propriétés proxy déclenchent en général un événement property changed lorsqu'elles sont définies pour indiquer à la vue que la propriété a été modifiée.

Toutes les propriétés Model ne doivent pas avoir de propriété proxy ViewModel, mais chaque propriété Model qui doit être exposée dans la vue aura en général une propriété proxy. Ces dernières ressemblent généralement à celle-ci :

public string Description {
  get { 
    return this.UnderlyingModelInstance.Description; 
  }
  set {
    this.UnderlyingModelInstance.Description = value;
    this.RaisePropertyChangedEvent("Description");
  }
}

Toutes les applications non triviales comporteront des dizaines ou des centaines de classes Model devant être exposées aux utilisateurs de cette façon via le ViewModel. C'est intrinsèque à la séparation indiquée par MVVM.

L'écriture de ces propriétés proxy est une tâche ennuyeuse qui favorise les erreurs, en particulier du fait que le déclenchement de l'événement property changed nécessite une chaîne qui doit correspondre au nom de la propriété (et qui ne sera incluse dans aucune refactorisation de code automatique). Pour éliminer ces événements proxy, la solution courante consiste à exposer l'instance de modèle directement à partir du wrapper ViewModel, puis à implémenter l'interface INotifyPropertyChanged avec le modèle de domaine :

public class SomeViewModel {
  public SomeViewModel( DomainObject domainObject ) {
    Contract.Requires(domainObject!=null, 
      "The domain object to wrap must not be null");
    this.WrappedDomainObject = domainObject;
  }
  public DomainObject WrappedDomainObject { 
    get; private set; 
  }
...

Le ViewModel peut ainsi continuer à exposer les commandes et les propriétés supplémentaires nécessaires sans dupliquer les propriétés Model ni créer un nombre important de propriétés proxy. Cette approche est certainement très tentante, surtout si les classes Model implémentent déjà l'interface INotifyPropertyChanged. Le fait que le modèle implémente cette interface n'est pas nécessairement une mauvaise chose. C'était d'ailleurs souvent le cas avec les applications Microsoft .NET Framework 2.0 et les applications Windows Forms. Cela encombre cependant le modèle de domaine et ne serait utile ni pour les applications ASP.NET ni pour les services de domaine.

Avec cette approche, la vue a une dépendance sur le modèle, mais il s'agit seulement d'une dépendance indirecte via la liaison de données, ce qui ne nécessite pas de référence du projet View au projet Model. D'un point de vue purement pragmatique, cette approche peut être utile.

Par contre, elle ne respecte pas l'esprit du modèle de conception MVVM et réduit les possibilités d'introduire ultérieurement de nouvelles fonctionnalités spécifiques au ViewModel (comme la fonctionnalité d'annulation). Il m'est arrivé de rencontrer des scénarios utilisant cette approche qui avaient engendré une grande quantité de remaniements. Imaginons la situation, assez courante, dans laquelle des données sont liées à une propriété fortement imbriquée. Si le ViewModel Personne est le contexte de données actuel, et que Personne comporte une Adresse, la liaison des données risque de ressembler à cela :

{Binding WrappedDomainObject.Address.Country}

Si vous avez besoin d'introduire la fonctionnalité ViewModel sur l'objet Address, vous devez supprimer les références de liaison de WrappedDomainObject.Address et utiliser les propriétés ViewModel à la place. Cela pose problème car les mises à jour de la liaison de données XAML (et, probablement, les contextes de données également) sont difficiles à tester. La vue est le seul élément qui ne comporte pas de tests de régression automatisés et complets.

Propriétés dynamiques

La solution que je propose pour éviter la prolifération des propriétés proxy consiste à utiliser la nouvelle prise en charge .NET Framework 4 et WPF pour les objets dynamiques et la distribution des méthodes dynamique. Cette dernière vous permet de déterminer au moment de l'exécution comment gérer la lecture ou l'écriture sur une propriété qui n'existe pas réellement sur la classe. Cela signifie que vous pouvez éliminer les propriétés de proxy manuscrites du ViewModel, tout en continuant l'encapsulage du modèle sous-jacent. Notez cependant que Silverlight 4 ne prend pas en charge la liaison vers des propriétés dynamiques.

Le moyen le plus simple d'implémenter cette fonctionnalité est que la classe de base ViewModel étende la nouvelle classe System.Dynamic.DynamicObject et remplace les méthodes TryGetMember et TrySetMember. Le Dynamic Language Runtime (DLR) appelle ces deux méthodes lorsque la propriété référencée n'existe pas sur la classe, permettant ainsi de déterminer, au moment de l'exécution, comment implémenter les propriétés manquantes. La classe ViewModel, lorsqu'elle est associée à un peu de réflexion, peut remplacer dynamiquement l'accès de propriété sur l'instance de modèle sous-jacente en quelques lignes de code seulement :

public override bool TryGetMember(
  GetMemberBinder binder, out object result) {

  string propertyName = binder.Name;
  PropertyInfo property = 
    this.WrappedDomainObject.GetType().GetProperty(propertyName);

  if( property==null || property.CanRead==false ) {
    result = null;
    return false;
  }

  result = property.GetValue(this.WrappedDomainObject, null);
  return true;
}

La méthode commence par utiliser la réflexion pour trouver la propriété dans l'instance de modèle sous-jacente. (Pour plus de détails, consultez la rubrique Les coulisses du CLR du numéro de juin 2007 « Réflexions sur la réflexion ».) Si le modèle ne dispose pas de cette propriété, la méthode échoue et renvoie false, et la liaison des données échoue. Si la propriété existe, la méthode utilise les informations de la propriété pour récupérer et renvoyer la valeur de la propriété Model. Cette méthode est plus fastidieuse que la méthode get de la propriété proxy classique, mais elle constitue la seule implémentation que vous devrez écrire pour tous les modèles et toutes les propriétés.

Les accesseurs Set de propriété représentent le véritable avantage de l'approche de propriété proxy dynamique. Dans TrySetMember, vous pouvez inclure une logique commune, comme le déclenchement des événements property changed. Le code ressemble à celui-ci :

public override bool TrySetMember(
  SetMemberBinder binder, object value) {

  string propertyName = binder.Name;
  PropertyInfo property = 
    this.WrappedDomainObject.GetType().GetProperty(propertyName);

  if( property==null || property.CanWrite==false )
    return false;

  property.SetValue(this.WrappedDomainObject, value, null);

  this.RaisePropertyChanged(propertyName);
  return true;
}

La méthode commence aussi par utiliser la réflexion pour récupérer la propriété de l'instance de modèle sous-jacente. Si la propriété n'existe pas ou s'il s'agit d'une propriété en lecture seule, la méthode échoue et renvoie false. Si la propriété existe sur l'objet de domaine, les informations relatives à la propriété sont utilisées pour définir la propriété Model. Vous pouvez ensuite inclure une logique commune à tous les accesseurs Set de propriétés. Dans le présent exemple de code, je déclenche simplement l'événement property changed pour la propriété que je viens de définir, mais vous pouvez facilement aller plus loin.

L'un des défis soulevés par l'encapsulage d'un modèle est que ce dernier comporte souvent ce que le langage UML (Unified Modeling Language) appelle des propriétés dérivées. Par exemple, une classe Person comporte probablement une propriété BirthDate et une propriété Age dérivée. La propriété Age est en lecture seule et calcule automatiquement l'âge en fonction de la date de naissance et de la date actuelle :

public class Person : DomainObject {
  public DateTime BirthDate { 
    get; set; 
  }

  public int Age {
    get {
      var today = DateTime.Now;
      // Simplified demo code!
      int age = today.Year - this.BirthDate.Year;
      return age;
    }
  }
...

Lorsque la propriété BirthDate est modifiée, la propriété Age change aussi implicitement étant donné que l'âge est calculé mathématiquement à partir de la date de naissance. Ainsi, lorsque la propriété BirthDate est définie, la classe ViewModel doit déclencher un événement property changed pour la propriété BirthDate et pour la propriété Age. L'approche ViewModel dynamique vous permet de le faire de façon automatique en rendant cette relation inter-propriété explicite dans le modèle.

Pour commencer, vous avez besoin d'un attribut personnalisé pour capturer la relation de la propriété :

[AttributeUsage(AttributeTargets.Property, AllowMultiple=true)]
public sealed class AffectsOtherPropertyAttribute : Attribute {
  public AffectsOtherPropertyAttribute(
    string otherPropertyName) {
    this.AffectsProperty = otherPropertyName;
  }

  public string AffectsProperty { 
    get; 
    private set; 
  }
}

Je définis AllowMultiple sur true pour prendre en charge des scénarios dans lesquels une propriété peut affecter plusieurs autres propriétés. Il est très simple d'appliquer cet attribut pour codifier la relation entre BirthDate et Age directement dans le modèle :

[AffectsOtherProperty("Age")]
public DateTime BirthDate { get; set; }

Pour utiliser les métadonnées du nouveau modèle dans la classe ViewModel dynamique, je peux à présent mettre à jour la méthode TrySetMember à l'aide de trois lignes de code supplémentaires, pour qu'elle ressemble à celle-ci :

public override bool TrySetMember(
  SetMemberBinder binder, object value) {
...
  var affectsProps = property.GetCustomAttributes(
    typeof(AffectsOtherPropertyAttribute), true);
  foreach(AffectsOtherPropertyAttribute otherPropertyAttr 
    in affectsProps)
    this.RaisePropertyChanged(
      otherPropertyAttr.AffectsProperty);
}

Les informations de la propriété reflétée étant connues, la méthode GetCustomAttributes peut renvoyer n'importe quel attribut AffectsOtherProperty sur la propriété du modèle. Le code diffuse ensuite simplement les attributs en boucle, déclenchant des événements property changed pour chaque attribut. Ainsi, les modifications apportées à la propriété BirthDate via le ViewModel déclenchent automatiquement les événements property changed pour la propriété BirthDate et pour la propriété Age.

Il est important de comprendre que si vous programmez explicitement une propriété sur la classe ViewModel dynamique (ou, plus probablement, sur les classes ViewModel dérivées spécifiques au modèle), le DLR n'appellera pas les méthodes TryGetMember et TrySetMember mais il appellera directement les propriétés. Dans ce cas, le comportement automatique ne fonctionne plus. Cependant, le code pourrait facilement être refactorisé pour que les propriétés personnalisées puissent également utiliser cette fonctionnalité.

Revenons au problème de la liaison des données sur une propriété fortement imbriquée, dans laquelle le ViewModel est le contexte de données WPF, qui ressemble à cela :

{Binding WrappedDomainObject.Address.Country}

L'utilisation de propriétés proxy dynamiques signifie que l'objet de domaine sous-jacent encapsulé n'est plus exposé. La liaison de données ressemblera alors à ceci :

{Binding Address.Country}

Dans ce cas, la propriété Address continuerait à accéder directement à l'instance Address du modèle sous-jacent. À présent, lorsque vous souhaitez introduire un ViewModel sur l'Address, il vous suffit d'ajouter une nouvelle propriété sur la classe ViewModel Personne. La nouvelle propriété Address est très simple :

public DynamicViewModel Address {
  get {
    if( addressViewModel==null )
      addressViewModel = 
        new DynamicViewModel(this.Person.Address);
    return addressViewModel;
  }
}

private DynamicViewModel addressViewModel;

Aucune liaison de données XAML ne doit être modifiée étant donné que la propriété s'appelle toujours Address, mais le DLR appelle maintenant la nouvelle propriété concrète au lieu de la méthode TryGetMember dynamique. (Notez que l'instantiacion différée de cette propriété Address n'est pas thread-safe. Cependant, seule la vue devrait accéder au ViewModel et étant donné que la vue WPF/Silverlight est monothread, ce n'est pas un problème.)

Cette approche peut être utilisée même lorsque le modèle implémente INotifyPropertyChanged. Le ViewModel peut remarquer cela et décider de ne pas générer d'événements property changed. Dans ce cas, il les écoute dans l'instance de modèle sous-jacente et les re-déclenche. Je procède à la vérification dans le constructeur de la classe ViewModel dynamique et je note le résultat :

public DynamicViewModel(DomainObject model) {
  Contract.Requires(model != null, 
    "Cannot encapsulate a null model");
  this.ModelInstance = model;

  // Raises its own property changed events
  if( model is INotifyPropertyChanged ) {
    this.ModelRaisesPropertyChangedEvents = true;
    var raisesPropChangedEvents = 
      model as INotifyPropertyChanged;
    raisesPropChangedEvents.PropertyChanged +=
      (sender,args) => 
      this.RaisePropertyChanged(args.PropertyName);
  }
}

Pour éviter de dupliquer les événements property changed, je dois également procéder à une modification mineure de la méthode TrySetMember.

if( this.ModelRaisesPropertyChangedEvents==false )
  this.RaisePropertyChanged(property.Name);

Vous pouvez ainsi utiliser une propriété proxy dynamique pour simplifier considérablement la couche ViewModel en éliminant les propriétés proxy standard. Cela permet de réduire de façon significative le codage, la vérification, la documentation et la maintenance à long terme. Il n'est plus nécessaire de mettre à jour la couche ViewModel pour ajouter de nouvelles propriétés au modèle, à moins qu'il y ait une logique de vue très spéciale pour la propriété. Cette approche peut également résoudre des problèmes difficiles comme les propriétés associées. La méthode courante TrySetMember vous permet aussi d'implémenter une fonctionnalité d'annulation étant donné que les propriétés commandées par l'utilisateur modifient le flux de cette méthode.

Avantages et inconvénients

De nombreux développeurs se méfient de la réflexion (et du DLR) en raison de problèmes de performances. Dans mon travail, cela ne m'a jamais posé de problème. Lorsqu'une seule propriété est définie dans l'interface utilisateur, il est probable que la perte de performances pour l'utilisateur passe inaperçue. Ce ne sera peut être pas le cas dans les interfaces utilisateur extrêmement interactives, comme les surfaces tactiles multipoints.

Le seul problème de performances important est le remplissage initial de la vue lorsque le nombre de champs est important. Les préoccupations relatives à la convivialité devraient naturellement limiter le nombre de champs que vous affichez sur un écran, pour que les performances des liaisons de données initiales à l'aide de cette approche DLR passent inaperçues.

Les performances devraient cependant toujours être contrôlées attentivement et assimilées étant donné qu'elles concernent l'expérience utilisateur. L'approche simple décrite précédemment pourrait être réécrite à l'aide de la mise en cache de la réflexion. Pour plus d'informations, consultez l'article de Joel Pobar paru dans le numéro de juillet 2005 de MSDN Magazine.

L'argument selon lequel l'impact de cette approche est négatif sur la lisibilité et la gestion du code est fondé, étant donné que la couche View semble référencer sur le ViewModel des propriétés qui n'existent pas. Je pense cependant que les avantages que procure la suppression de la plupart des propriétés proxy codées manuellement sont beaucoup plus importants que les problèmes générés, surtout si vous disposez de la documentation correcte à propos du ViewModel.

L'approche de propriété proxy dynamique diminue ou supprime la capacité d'obfusquer la couche Model car les propriétés du modèle sont maintenant référencées par noms dans le XAML. L'utilisation des propriétés proxy classiques ne limite pas la possibilité d'obfusquer le modèle car elles sont référencées directement et seraient obfusquées avec le reste de l'application. Cela n'est cependant pas pertinent car la plupart des outils d'obfuscation ne fonctionnent pas encore avec XAML/BAML. Un cracker de code peut partir de XAML/BAML et s'introduire dans la couche Model dans les deux cas.

Enfin, cette approche pourrait être employée abusivement si des propriétés comportant des métadonnées liées à la sécurité étaient attribuées et si le ViewModel était responsable d'appliquer la sécurité. La sécurité ne semble pas être une responsabilité spécifique à la vue et je considère que cela revient à attribuer trop de responsabilités au ViewModel. Dans ce cas, une approche orientée objet appliquée dans le modèle serait plus adaptée.

Collections

Les collections représentent les aspects les plus compliqués et les moins satisfaisants du modèle de conception MVVM. Si une collection du modèle sous-jacent est modifiée par le modèle, il est de la responsabilité du ViewModel d'exposer la modification d'une manière ou d'une autre pour que la vue puisse se mettre à jour de manière appropriée.

Malheureusement, le modèle n'expose probablement pas les collections qui implémentent l'interface INotifyCollectionChanged. Dans le .NET Framework 3.5, cette interface est System.Windows.dll, qui décourage fortement son utilisation dans le modèle. Heureusement, dans le .NET Framework 4, cette interface a migré vers System.dll, rendant l'utilisation des collections observables du modèle beaucoup plus naturelles.

Les collections observables du modèle débouchent sur de nouvelles possibilités pour développer ce dernier et pourraient être utilisées dans les applications Windows Forms et Silverlight. Il s'agit à l'heure actuelle de mon approche préférée car c'est la méthode la plus simple. Je suis aussi heureux que l'interface INotifyCollectionChanged adopte une assembly plus courante.

Si le modèle ne comporte pas de collections observables, l'idéal est d'exposer d'autres mécanismes (principalement des événements personnalisés) sur le modèle pour indiquer le moment où la collection a été modifiée. Cette opération doit être effectuée d'une façon spécifique au modèle. Par exemple, si la classe Person comporte une collection d'adresses, elle peut exposer des événements tels que :

public event EventHandler<AddressesChangedEventArgs> 
  NewAddressAdded;
public event EventHandler<AddressesChangedEventArgs> 
  AddressRemoved;

Ceci est préférable plutôt que de déclencher un événement de collection personnalisée conçu spécifiquement pour le ViewModel WPF. Il est cependant difficile d'exposer des modifications de collection dans le ViewModel. Le seul recours est de déclencher un événement property changed sur l'ensemble de la propriété de collection de ViewModel. Au mieux, c'est une solution peu satisfaisante.

Un autre problème relatif aux collections est de déterminer quand ou si chaque instance de modèle doit être encapsulée dans la collection d'une instance ViewModel. Pour les collections plus petites, le ViewModel peut exposer une nouvelle collection observable et copier tous les éléments de la collection modèle sous-jacente dans la collection observable ViewModel, chaque élément Model étant encapsulé dans la collection de l'instance ViewModel correspondante. Le ViewModel pourra avoir besoin d'écouter des événements CollectionChanged pour transmettre les modifications des utilisateurs au modèle sous-jacent.

Par contre, pour les grandes collections qui seront exposées dans une certaine forme de panneau de virtualisation, l'approche la plus simple et la plus pragmatique consiste simplement à exposer les objets Model directement.

Instanciation du ViewModel

Un autre problème relatif au modèle de conception MVVM est rarement abordé. Il s'agit de savoir où et quand les instances ViewModel devraient être instanciées. Ce problème est également souvent négligé dans les discussions relatives aux modèles de conception similaires, comme MVC.

Personnellement, je préfère écrire un singleton de ViewModel qui fournit les principaux objets ViewModel à partir desquels la vue peut facilement récupérer tous les autres objets ViewModel demandés. Cet objet ViewModel principal fournit souvent les implémentations de commandes permettant à la vue de prendre en charge l'ouverture de documents.

Cependant, la plupart des applications avec lesquelles j'ai travaillé fournissent une interface centrée sur les documents. Elle utilise en général un espace de travail avec onglets similaire à Visual Studio. Ainsi, dans la couche ViewModel, je veux raisonner en termes de documents et les documents exposent un ou plusieurs objets ViewModel encapsulant des objets Model particuliers. Les commandes WPF standard de la couche ViewModel peuvent ensuite utiliser la couche de persistance pour récupérer les objets nécessaires, les encapsuler dans des instances ViewModel et créer des gestionnaires de documents ViewModel pour les afficher.

Dans l'exemple d'application exposé dans le présent article, la commande ViewModel pour créer une personne est :

internal class OpenNewPersonCommand : ICommand {
...
  // Open a new person in a new window.
  public void Execute(object parameter) {
    var person = new MvvmDemo.Model.Person();
    var document = new PersonDocument(person);
    DocumentManager.Instance.ActiveDocument = document;
  }
}

Le gestionnaire de documents ViewModel référencé dans la dernière ligne est un singleton qui gère tous les documents ViewModel ouverts. La question est de savoir comment la collection de documents ViewModel est exposée dans la vue.

Le contrôle onglet WPF intégré ne fournit pas la puissante interface multidocument que les utilisateurs attendent. Heureusement, ces derniers peuvent bénéficier de stations d'accueil et d'espaces de travail avec onglets tiers. La plupart des produits tiers disponibles s'efforcent d'émuler l'apparence du système d'onglets de Visual Studio, notamment les fenêtres ancrables, les affichages fractionnés, les fenêtres pop-up Ctrl+Tab (avec affichage de documents miniatures) et plus encore.

Malheureusement, la plupart de ces composants ne fournissent pas de prise en charge intégrée pour le modèle de conception MVVM. En fait, peu importe. Vous pouvez en effet facilement appliquer le modèle de conception Adapter pour lier le gestionnaire de documents ViewModel au composant d'affichage tiers.

Adaptateur du gestionnaire de documents

La conception de l'adaptateur illustrée à la figure 2 permet de garantir que le ViewModel ne nécessite aucune référence à la vue et qu'il respecte les objectifs principaux du modèle de conception MVVM. (Cependant, dans ce cas, le concept d'un document est défini dans la couche ViewModel plutôt que dans la couche Model étant donné que le concept est uniquement lié à l'interface utilisateur.)

Figure 2 Document Manager View Adapter

Figure 2 Adaptateur de la vue du gestionnaire de documents

Le gestionnaire de documents ViewModel est chargé de gérer la collection des documents ViewModel ouverts et d'identifier les documents actuellement actifs. Cette conception permet à la couche ViewModel d'ouvrir et de fermer des documents à l'aide du gestionnaire de documents, et de modifier le document actif sans connaître la vue. Côté ViewModel, cette approche est assez simple. Les classes ViewModel de l'exemple d'application sont illustrées à la figure 3.

Figure 3 The ViewModel Layer’s Document Manager and Document Classes

Figure 3 Gestionnaire de documents et classes de documents de la couche ViewModel

La classe Document de base expose plusieurs méthodes de cycle de vie interne (Activated, LostActivation et DocumentClosed) qui sont appelées par le gestionnaire de documents pour que le document soit à jour. Le document implémente également une interface INotifyPropertyChanged pour pouvoir prendre en charge la liaison de données. Par exemple, les données de l'adaptateur établissent une liaison entre la propriété Title du document vue et la propriété DocumentTitle du ViewModel.

L'élément le plus complexe de cette approche est la classe d'adaptateur. Le projet joint à cet article comporte une copie de travail. L'adaptateur souscrit aux événements du gestionnaire de documents et les utilise pour que le contrôle de l'espace de travail avec onglets reste à jour. Par exemple, lorsque le gestionnaire de documents indique que le nouveau document a été ouvert, l'adaptateur reçoit un événement, encapsule le document ViewModel dans le contrôle WPF qui convient et expose ce contrôle dans l'espace de travail avec onglets.

L'adaptateur doit également garantir la synchronisation du gestionnaire de documents ViewModel avec les actions de l'utilisateur. C'est pourquoi il doit également écouter les événements du contrôle de l'espace de travail avec onglets, afin de pouvoir notifier le gestionnaire de documents lorsque l'utilisateur modifie le document actif ou ferme un document.

Cette logique n'est pas complexe, mais il est essentiel de prendre en compte un certain nombre d'éléments. Dans certains scénarios, le code devient réentrant, ce qu'il faut gérer avec élégance. Par exemple, si le ViewModel utilise le gestionnaire de documents pour fermer un document, l'adaptateur reçoit un événement du gestionnaire et ferme la fenêtre du document dans la vue. Le contrôle de l'espace de travail avec onglets déclenche alors un événement DocumentClosing, que l'adaptateur reçoit également, et le gestionnaire d'événements de l'adaptateur notifie le gestionnaire de documents que le document doit être fermé. Le document étant déjà fermé, le gestionnaire de documents doit être suffisamment compréhensif pour le permettre.

L'autre difficulté à surmonter est que l'adaptateur de la vue doit être capable de lier le contrôle d'un document vue avec onglets et l'objet Document du ViewModel. La solution la plus fiable consiste à utiliser une propriété de dépendance attachée WPF. L'adaptateur déclare une propriété de dépendance attachée privée qui est utilisée pour lier le contrôle de fenêtre vue et son instance de document ViewModel.

Dans l'exemple de projet de cet article, j'utilise un espace de travail open source avec onglets appelé AvalonDock. Ma propriété de dépendance attachée ressemble donc au code de la figure 4.

Figure 4 Liaison du contrôle de la vue avec le document ViewModel

private static readonly DependencyProperty 
  ViewModelDocumentProperty =
  DependencyProperty.RegisterAttached(
  "ViewModelDocument", typeof(Document),
  typeof(DocumentManagerAdapter), null);

private static Document GetViewModelDocument(
  AvalonDock.ManagedContent viewDoc) {

  return viewDoc.GetValue(ViewModelDocumentProperty) 
    as Document;
}

private static void SetViewModelDocument(
  AvalonDock.ManagedContent viewDoc, Document document) {

  viewDoc.SetValue(ViewModelDocumentProperty, document);
}

Lorsque l'adaptateur créé un nouveau contrôle de la fenêtre vue, il définit la propriété attachée au nouveau contrôle de la fenêtre sur le document ViewModel sous-jacent (voir la figure 5). Vous pouvez également accéder à la configuration de la liaison des données du titre et voir la façon dont l'adaptateur configure le contexte de données et le contenu du contrôle du document vue.

Figure 5 Définition de la propriété attachée

private AvalonDock.DocumentContent CreateNewViewDocument(
  Document viewModelDocument) {

  var viewDoc = new AvalonDock.DocumentContent();
  viewDoc.DataContext = viewModelDocument;
  viewDoc.Content = viewModelDocument;

  Binding titleBinding = new Binding("DocumentTitle") { 
    Source = viewModelDocument };

  viewDoc.SetBinding(AvalonDock.ManagedContent.TitleProperty, 
    titleBinding);
  viewDoc.Closing += OnUserClosingDocument;
  DocumentManagerAdapter.SetViewModelDocument(viewDoc, 
    viewModelDocument);

  return viewDoc;
}

En définissant le contenu du contrôle du document vue, je laisse au WPF le soin de faire l'essentiel du travail, qui consiste à déterminer comment afficher ce type de document ViewModel particulier. Les modèles de données réels pour les documents ViewModel figurent dans un dictionnaire de ressources intégré à la fenêtre XAML principale.

J'ai utilisé avec succès cette approche de gestionnaire de documents ViewModel avec WPF et Silverlight. Le seul code de la couche View est l'adaptateur. Vous pouvez facilement le tester, puis ne plus y toucher. Cette approche permet de conserver l'indépendance totale du ViewModel par rapport à la vue. Par ailleurs, j'ai changé une fois de fournisseur pour mon composant d'espace de travail avec onglets et cela a entraîné peu de modifications au niveau de la classe de l'adaptateur et aucune modification au niveau du ViewModel ou du modèle.

Il est agréable de pouvoir travailler avec des documents dans la couche ViewModel et facile d'implémenter des commandes ViewModel comme celles démontrées ici. Les classes de document ViewModel deviennent également des endroits appropriés pour exposer les instances ICommand relatives au document.

La vue s'intègre à ces commandes et la beauté du modèle de conception MVVM rayonne. En outre, l'approche de gestion des documents ViewModel fonctionne aussi avec l'approche singleton si vous avez besoin d'exposer des données avant que l'utilisateur ait créé des documents (peut-être dans une fenêtre d'outils réductible).

Conclusion

Le modèle de conception MVVM est un modèle efficace et utile, mais aucun modèle de conception ne peut résoudre tous les problèmes. Comme je l'ai démontré dans cet article, combiner le modèle et les objectifs MVVM avec d'autres modèles, comme les adaptateurs et les singletons, tout en exploitant les nouvelles fonctionnalités .NET Framework 4, comme la distribution dynamique, peut permettre de résoudre des problèmes courants concernant l'implémentation du modèle de conception MVVM. Lorsque vous utilisez MVVM correctement, vos applications WPF et Silverlight sont beaucoup plus élégantes et faciles à gérer. Pour en savoir plus sur MVVM, consultez l'article de Josh Smith de numéro de février 2009 de MSDN Magazine.

Robert McCarter est un architecte, entrepreneur et développeur de logiciels canadien indépendant. Vous pouvez consulter son blog à l'adresse robertmccarter.wordpress.com.

Je remercie Josh Smith, expert technique, d'avoir relu cet article.