Validation dans un langage spécifique à un domaine

En tant qu'auteur d'un langage spécifique à un domaine (DSL), vous pouvez définir des contraintes de validation afin de vérifier que le modèle créé par l'utilisateur a un sens. Par exemple, si votre DSL permet aux utilisateurs de tracer l'arbre généalogique d'une famille et de ses ancêtres, vous pouvez écrire une contrainte qui garantit que les enfants ont des dates de naissance postérieures à celles de leurs parents.

Vous pouvez demander que les contraintes de validation s'exécutent quand le modèle est enregistré, quand il est ouvert ou quand l'utilisateur exécute explicitement la commande de menu Valider. Vous pouvez aussi exécuter la validation sous le contrôle du programme. Par exemple, vous pouvez exécuter la validation en réponse à la modification d'une valeur de propriété ou d'une relation.

La validation est particulièrement importante si vous écrivez des modèles de texte ou d’autres outils qui traitent les modèles de vos utilisateurs. La validation garantit que les modèles remplissent les conditions préalables assumées par ces outils.

Avertissement

Vous pouvez aussi autoriser que les contraintes de validation soient définies dans des extensions distinctes de votre DSL, en même temps que les gestionnaires de mouvements et les commandes de menu de l'extension. Les utilisateurs peuvent choisir d'installer ces extensions en plus de votre DSL. Pour plus d’informations, consultez l’article Extension de votre DSL à l’aide de MEF.

Exécution de la validation

Quand un utilisateur modifie un modèle, à savoir, une instance de votre langage spécifique à un domaine, les actions suivantes peuvent exécuter la validation :

  • Cliquez avec le bouton droit sur le diagramme et sélectionnez Valider tout.

  • Cliquez avec le bouton droit sur le nœud supérieur de l'Explorateur de votre DSL et sélectionnez Valider tout

  • Enregistrez le modèle.

  • Ouvrez le modèle.

  • En outre, vous pouvez écrire le code de programme qui exécute la validation comme partie d'une commande de menu ou en réponse à une modification, par exemple.

    Les erreurs de validation éventuelles apparaîtront dans la fenêtre Liste d'erreurs. L'utilisateur peut double-cliquer sur un message d'erreur pour sélectionner les éléments du modèle qui sont à l'origine de l'erreur.

Définition des contraintes de validation

Vous définissez les contraintes de validation en ajoutant les méthodes de validation aux classes de domaine ou aux relations de votre DSL. Quand la validation est exécutée, que ce soit par l'utilisateur ou sous le contrôle du programme, tout ou partie des méthodes de validation est exécuté. Chaque méthode est appliquée à chaque instance de sa classe et il peut y avoir plusieurs méthodes de validation dans chaque classe.

Chaque méthode de validation signale les erreurs éventuelles qu'elle détecte.

Notes

Les méthodes de validation signalent les erreurs, mais ne modifient pas le modèle. Si vous voulez régler ou empêcher certaines modifications, consultez la rubrique Alternatives à la validation.

Pour définir une contrainte de validation

  1. Activez la validation dans le nœud Éditeur\Validation :

    1. Ouvrez Dsl\DslDefinition.dsl.

    2. Dans l'Explorateur DSL, développez le nœud Éditeur et sélectionnez Validation.

    3. Dans la fenêtre Propriétés, définissez les propriétés Utilisations sur true. Il est plus pratique de définir toutes ces propriétés.

    4. Cliquez sur Transformer tous les modèles dans la barre d'outils de l'Explorateur de solutions.

  2. Écrivez les définitions de classe partielle pour une ou plusieurs de vos classes de domaine ou relations de domaine. Écrivez ces définitions dans un nouveau fichier de code du projet Dsl.

  3. Préfixez chaque classe avec l'attribut suivant :

    [ValidationState(ValidationState.Enabled)]
    
    • Par défaut, cet attribut activera aussi la validation pour les classes dérivées. Si vous voulez désactiver la validation pour une classe dérivée spécifique, vous pouvez utiliser ValidationState.Disabled.
  4. Ajoutez les méthodes de validation aux classes. Chaque méthode de validation peut avoir un nom quelconque, mais un seul paramètre de type ValidationContext.

    Elle doit être préfixée par un ou plusieurs attributs ValidationMethod :

    [ValidationMethod (ValidationCategories.Open | ValidationCategories.Save | ValidationCategories.Menu ) ]
    

    L'attribut ValidationCategories spécifie à quel moment la méthode est exécutée.

    Par exemple :

using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Validation;

// Allow validation methods in this class:
[ValidationState(ValidationState.Enabled)]
// In this DSL, ParentsHaveChildren is a domain relationship
// from Person to Person:
public partial class ParentsHaveChildren
{
  // Identify the method as a validation method:
  [ValidationMethod
  ( // Specify which events cause the method to be invoked:
    ValidationCategories.Open // On file load.
  | ValidationCategories.Save // On save to file.
  | ValidationCategories.Menu // On user menu command.
  )]
  // This method is applied to each instance of the
  // type (and its subtypes) in a model:
  private void ValidateParentBirth(ValidationContext context)
  {
    // In this DSL, the role names of this relationship
    // are "Child" and "Parent":
     if (this.Child.BirthYear < this.Parent.BirthYear
        // Allow user to leave the year unset:
        && this.Child.BirthYear != 0)
      {
        context.LogError(
             // Description:
                       "Child must be born after Parent",
             // Unique code for this error:
                       "FAB001ParentBirthError",
              // Objects to select when user double-clicks error:
                       this.Child,
                       this.Parent);
    }
  }

Notez les points suivants relatifs au code :

  • Vous pouvez ajouter des méthodes de validation aux classes de domaine ou relations de domaine. Le code de ces types se trouve dans Dsl\Generated Code\Domain*.cs.

  • Chaque méthode de validation s'applique à chaque instance de sa classe et de ses sous-classes. Dans le cas d'une relation de domaine, chaque instance est un lien entre deux éléments de modèle.

  • Les méthodes de validation ne s'appliquent pas selon un ordre spécifié et chaque méthode ne s'applique pas aux instances de sa classe dans un ordre prévisible.

  • Il n'est généralement pas recommandé qu'une méthode de validation mette à jour le contenu du magasin, car cela conduirait à des résultats incohérents. La méthode doit à la place signaler une erreur en appelant context.LogError, LogWarning ou LogInfo.

  • Dans l'appel de LogError, vous pouvez fournir une liste d'éléments de modèle ou de liens de relation qui seront sélectionnés quand l'utilisateur double-clique sur le message d'erreur.

  • Pour plus d’informations sur la lecture du modèle dans le code du programme, consultez l’article Navigation et mise à jour d’un modèle dans le code du programme.

    L'exemple s'applique au modèle de domaine suivant. La relation ParentsHaveChildren possède des rôles nommés Child et Parent.

    DSL Definition diagram - family tree model

Catégories de validation

Dans l'attribut ValidationMethodAttribute, vous spécifiez à quel moment la méthode de validation doit s'exécuter.

Category Exécution
ValidationCategories Lorsque l'utilisateur appelle la commande de menu Valider.
ValidationCategories Lorsque le fichier de modèle est ouvert.
ValidationCategories Lorsque le fichier est enregistré. S'il y a des erreurs de validation, l'utilisateur se voit offrir la possibilité d'annuler l'opération d'enregistrement.
ValidationCategories Lorsque le fichier est enregistré. S'il y a des erreurs provenant des méthodes de la catégorie, l'utilisateur est prévenu qu'il peut ne pas être possible de rouvrir le fichier.

Utilisez cette catégorie pour les méthodes de validation qui testent la présence de noms ou ID dupliqués, ou autres conditions susceptibles de produire des erreurs de chargement.
ValidationCategories Lorsque la méthode ValidateCustom est appelée. Les validations de cette catégorie ne peuvent être appelées qu'à partir du code de programme.

Pour plus d'informations, consultez la rubrique Catégories de validation personnalisées.

Où placer les méthodes de validation

Vous pouvez souvent obtenir le même effet en plaçant une méthode de validation sur un type différent. Par exemple, vous pouvez ajouter une méthode à la classe Person au lieu de la relation ParentsHaveChildren et procéder à son itération à travers les liens :

[ValidationState(ValidationState.Enabled)]
public partial class Person
{[ValidationMethod
 ( ValidationCategories.Open
 | ValidationCategories.Save
 | ValidationCategories.Menu
 )
]
  private void ValidateParentBirth(ValidationContext context)
  {
    // Iterate through ParentHasChildren links:
    foreach (Person parent in this.Parents)
    {
        if (this.BirthYear <= parent.BirthYear)
        { ...

Regroupement des contraintes de validation. Pour appliquer la validation dans un ordre prévisible, définissez une méthode de validation unique sur une classe propriétaire, telle que l'élément racine de votre modèle. Cette technique permet aussi de regrouper plusieurs rapports d'erreur au sein d'un seul message.

L'inconvénient est que la méthode combinée est moins facile à gérer et que les contraintes doivent toutes avoir les mêmes ValidationCategories. Il est donc recommandé que vous conserviez chaque contrainte dans une méthode distincte si possible.

Passage des valeurs dans le cache du contexte. Le paramètre de contexte dispose d'un dictionnaire dans lequel vous pouvez placer des valeurs arbitraires. Le dictionnaire demeure pendant la durée de l'exécution de la validation. Une méthode de validation particulière peut, par exemple, conserver le nombre d'erreurs dans le contexte et l'utiliser pour éviter que la fenêtre d'erreurs ne déborde sous les messages répétés. Par exemple :

List<ParentsHaveChildren> erroneousLinks;
if (!context.TryGetCacheValue("erroneousLinks", out erroneousLinks))
erroneousLinks = new List<ParentsHaveChildren>();
erroneousLinks.Add(this);
context.SetCacheValue("erroneousLinks", erroneousLinks);
if (erroneousLinks.Count < 5) { context.LogError( ... ); }

Validation des multiplicités

Les méthodes de validation pour vérifier la multiplicité minimale sont automatiquement générées pour votre DSL. Le code est écrit dans Dsl\Generated Code\MultiplicityValidation.cs. Ces méthodes prennent effet lorsque vous activez la validation dans le nœud Éditeur\Validation de l'Explorateur DSL.

Si vous définissez la multiplicité d'un rôle d'une relation de domaine sur 1..* ou 1..1, mais que l'utilisateur ne crée pas le lien de cette relation, un message d'erreur de validation s'affiche.

Par exemple, si votre DSL possède les classes Person et Town, et une relation PersonLivesInTown avec une relation 1..\* dans le rôle Town, pour chaque Person qui n'a aucune Town, un message d'erreur s'affiche.

Exécution de la validation à partir du code de programme

Vous pouvez exécuter la validation en accédant à un ValidationController ou en en créant un. Si vous voulez que les erreurs s'affichent pour l'utilisateur dans la fenêtre des erreurs, utilisez le ValidationController attaché au DocData de votre diagramme. Par exemple, si vous écrivez une commande de menu, CurrentDocData.ValidationController est disponible dans la classe de l'ensemble de commandes :

using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Validation;
using Microsoft.VisualStudio.Modeling.Shell;
...
partial class MyLanguageCommandSet
{
  private void OnMenuMyContextMenuCommand(object sender, EventArgs e)
  {
   ValidationController controller = this.CurrentDocData.ValidationController;
...

Pour plus d’informations, consultez le Guide pratique pour ajouter une commande au menu contextuel.

Vous pouvez aussi créer un contrôleur de validation distinct et gérer les erreurs vous-même. Par exemple :

using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Validation;
using Microsoft.VisualStudio.Modeling.Shell;
...
Store store = ...;
VsValidationController validator = new VsValidationController(s);
// Validate all elements in the Store:
if (!validator.Validate(store, ValidationCategories.Save))
{
  // Deal with errors:
  foreach (ValidationMessage message in validator.ValidationMessages) { ... }
}

Exécution de la validation quand une modification intervient

Si vous voulez vous assurer que l'utilisateur est immédiatement averti si le modèle devient non valide, vous pouvez définir un événement de magasin qui exécute la validation. Pour plus d’informations sur les événements de stockage, consultez l’article Propagation de modifications en dehors du modèle par des gestionnaires d'événements.

En plus du code de validation, ajoutez un fichier de code personnalisé à votre projet DslPackage, avec un contenu similaire à l'exemple suivant. Ce code utilise le ValidationController attaché au document. Ce contrôleur affiche les erreurs de validation dans la liste d'erreurs de Visual Studio.

using System;
using System.Linq;
using Microsoft.VisualStudio.Modeling;
using Microsoft.VisualStudio.Modeling.Validation;
namespace Company.FamilyTree
{
  partial class FamilyTreeDocData // Change name to your DocData.
  {
    // Register the store event handler:
    protected override void OnDocumentLoaded()
    {
      base.OnDocumentLoaded();
      DomainClassInfo observedLinkInfo = this.Store.DomainDataDirectory
         .FindDomainClass(typeof(ParentsHaveChildren));
      DomainClassInfo observedClassInfo = this.Store.DomainDataDirectory
         .FindDomainClass(typeof(Person));
      EventManagerDirectory events = this.Store.EventManagerDirectory;
      events.ElementAdded
         .Add(observedLinkInfo, new EventHandler<ElementAddedEventArgs>(ParentLinkAddedHandler));
      events.ElementDeleted.Add(observedLinkInfo, new EventHandler<ElementDeletedEventArgs>(ParentLinkDeletedHandler));
      events.ElementPropertyChanged.Add(observedClassInfo, new EventHandler<ElementPropertyChangedEventArgs>(BirthDateChangedHandler));
    }
    // Handler will be called after transaction that creates a link:
    private void ParentLinkAddedHandler(object sender,
                                ElementAddedEventArgs e)
    {
      this.ValidationController.Validate(e.ModelElement,
           ValidationCategories.Save);
    }
    // Called when a link is deleted:
    private void ParentLinkDeletedHandler(object sender,
                                ElementDeletedEventArgs e)
    {
      // Don't apply validation to a deleted item!
      // - Validate store to refresh the error list.
      this.ValidationController.Validate(this.Store,
           ValidationCategories.Save);
    }
    // Called when any property of a Person element changes:
    private void BirthDateChangedHandler(object sender,
                      ElementPropertyChangedEventArgs e)
    {
      Person person = e.ModelElement as Person;
      // Not interested in changes in other properties:
      if (e.DomainProperty.Id != Person.BirthYearDomainPropertyId)
          return;

      // Validate all parent links to and from the person:
      this.ValidationController.Validate(
        ParentsHaveChildren.GetLinksToParents(person)
        .Concat(ParentsHaveChildren.GetLinksToChildren(person))
        , ValidationCategories.Save);
    }
  }
}

Les gestionnaires sont aussi appelés après les opérations Annuler ou Rétablir qui affectent les liens ou les éléments.

Catégories de validation personnalisées

En plus des catégories de validation standard, telles que Menu ou Ouvrir, vous pouvez définir vos propres catégories. Vous pouvez invoquer ces catégories à partir du code de programme. L'utilisateur ne peut pas les appeler directement.

Une utilisation classique des catégories personnalisées consiste à définir une catégorie qui teste si le modèle satisfait aux conditions préalables d'un outil particulier.

Pour ajouter une méthode de validation à une catégorie particulière, préfixez-la à l'aide d'un attribut comme suit :

[ValidationMethod(CustomCategory = "PreconditionsForGeneratePartsList")]
[ValidationMethod(ValidationCategory.Menu)]
private void TestForCircularLinks(ValidationContext context)
{...}

Notes

Vous pouvez préfixer une méthode avec autant d'attributs [ValidationMethod()] que vous le souhaitez. Vous pouvez ajouter une méthode aussi bien aux catégories personnalisées qu'aux catégories standard.

Pour appeler une validation personnalisée :


// Invoke all validation methods in a custom category:
validationController.ValidateCustom
  (store, // or a list of model elements
   "PreconditionsForGeneratePartsList");

Alternatives à la validation

Les contraintes de validation signalent les erreurs, mais ne modifient pas le modèle. Si, à la place, vous voulez empêcher que le modèle ne devienne non valide, vous pouvez utiliser d'autres techniques.

Cependant, ces techniques ne sont pas recommandées. Il est généralement préférable de laisser l'utilisateur décider de la façon dont un modèle non valide doit être corrigé.

Ajustez la modification pour rétablir la validité du modèle. Par exemple, si l'utilisateur définit une propriété au-dessus du maximum autorisé, vous pouvez réinitialiser la propriété à sa valeur maximale. Pour ce faire, définissez une règle. Pour plus d’informations, consultez l’article Propagation de modifications dans le modèle par des règles.

Annulez la transaction en cas de modification non valide. Vous pouvez aussi définir une règle à cette fin, mais dans certains cas il est possible de remplacer un gestionnaire de propriétés OnValueChanging() ou de remplacer une méthode telle que OnDeleted().. Pour annuler une transaction, utilisez this.Store.TransactionManager.CurrentTransaction.Rollback().. Pour plus d'informations, consultez l’article Gestionnaires de modifications de valeur de propriété de domaine.

Avertissement

Assurez-vous que l'utilisateur sache que la modification a été ajustée ou annulée. Par exemple, utilisez System.Windows.Forms.MessageBox.Show("message").