Partager via


Validation des entrées

Application de règles de données métier complexes avec WPF

Brian Noyes

Télécharger l'exemple de code

Microsoft Windows Presentation Foundation (WPF) contient un système de liaison de données riche. Non seulement le système de liaison de données est un élément clé du couplage faible de la définition de l'interface utilisateur à partir de la logique et des données associées via le Modèle-Vue-Modèle de vues (MVVM, Model-View-ViewModel), mais il offre également une prise en charge puissante et souple pour les scénarios de validation des données métier. Dans le cadre de WPF, les mécanismes de liaison des données comprennent plusieurs options permettant d'évaluer la validité des données entrées lorsque vous créez une vue modifiable. De plus, les fonctionnalités de création de modèle et de style pour les contrôles vous permettent de personnaliser la façon dont vous signalez les erreurs de validation à l'utilisateur.

Pour prendre en charge des règles complexes et afficher les erreurs de validation pour l'utilisateur, vous devez généralement utiliser une combinaison des mécanismes de validation disponibles. Même une forme d'entrée de données apparemment simple peut présenter des difficultés de validation lorsque les règles métier deviennent complexes. Les scénarios les plus courants impliquent à la fois des règles simples au niveau d'une propriété individuelle et des propriétés à couplage croisé où la validité d'une propriété dépend de la valeur d'une autre. Cependant, la prise en charge de la validation dans la liaison de données WPF facilite la résolution de ces problèmes.

Dans cet article, vous apprendrez à utiliser l'implémentation de l'interface IDataErrorInfo, les règles ValidationRule, les groupes BindingGroup, les exceptions, ainsi que les propriétés et les événements associés liés à la validation pour répondre à vos besoins en matière de validation de données. Vous apprendrez également à personnaliser l'affichage des erreurs de validation avec vos propres modèles d'erreur et info-bulles. En rédigeant cet article, je suppose que vous connaissez déjà les fonctionnalités de liaison des données de WPF. Pour plus d'informations sur ce sujet, consultez l'article de John Papa publié en décembre 2007 dans MSDN Magazine, « Data Binding in WPF ».

Présentation de la validation des données

Presque chaque fois que vous entrez ou modifiez des données dans une application, vous devez vous assurer que celles-ci sont valides avant qu'elles ne s'éloignent trop de la source de ces modifications, à savoir l'utilisateur dans ce cas. De plus, vous devez indiquer clairement aux utilisateurs si les données entrées ne sont pas valides et, avec un peu de chance, leur donner également quelques indications sur la façon dont ils peuvent les corriger. Ces choses sont relativement faciles à faire avec WPF tant que vous savez quelle fonctionnalité utiliser et quand l'utiliser.

Lorsque vous utilisez la liaison des données dans WPF pour présenter des données métier, vous utilisez généralement un objet Binding pour fournir un pipeline de données entre une propriété unique sur un contrôle cible et une propriété d'objet de source de données. Pour que la validation soit pertinente, vous effectuez généralement une liaison de données bidirectionnelle. Cela signifie qu'outre les données qui passent de la propriété source à la propriété cible pour l'affichage, les données modifiées transitent également de la cible vers la source, comme illustré à la figure 1.

Figure 1 Data Flow in TwoWay Data Binding
Figure 1 Flux de données dans la liaison de données bidirectionnelle

Trois mécanismes permettent de déterminer si les données saisies via le contrôle de liaison de données sont valides. Ils sont résumés à la figure 2.

Figure 2 Liaison des mécanismes de validation

Mécanisme de validation Description
Exceptions Lorsque la propriété ValidatesOnExceptions est définie sur un objet Binding, si une exception est levée lors de la tentative de définition de la valeur modifiée sur la propriété de l'objet source, une erreur de validation est définie pour cette liaison.
ValidationRules La classe Binding dispose d'une propriété permettant de fournir une collection d'instances de classe dérivées de ValidationRule. Ces règles ValidationRules doivent remplacer une méthode Validate qui sera appelée par la liaison dès que les données du contrôle lié seront modifiées. Si la méthode Validate renvoie un objet ValidationResult qui n'est pas valide, une erreur de validation est définie pour cette liaison.
IDataErrorInfo Si l'interface IDataErrorInfo est implémentée sur un objet de source de données lié et si la propriété ValidatesOnDataErrors est définie sur un objet Binding, la liaison effectue des appels auprès de l'API IDataErrorInfo exposée à partir de l'objet de source de données lié. Si des chaînes non null ou non vides sont renvoyées à partir de ces appels de propriété, une erreur de validation est définie pour cette liaison.

Lorsqu'un utilisateur entre ou modifie des données dans la liaison de données bidirectionnelle, un workflow est lancé :

  • Les données sont entrées ou modifiées par l'utilisateur via des frappes, ou une interaction avec l'élément effectuée tactilement, à l'aide de la souris ou d'un stylo, ce qui provoque la modification d'une propriété sur l'élément.
  • Les données sont converties dans le type de propriété de la source de données, si nécessaire.
  • La valeur de la propriété source est définie.
  • L'événement associé Binding.SourceUpdated est déclenché.
  • Les exceptions sont attrapées par la liaison à condition qu'elles aient été lancées par l'accesseur sur la propriété de la source de données. De plus, elles peuvent être utilisées pour indiquer une erreur de validation.
  • Les propriétés IDataErrorInfo sont appelées sur l'objet de source de données, s'il est implémenté.
  • Les indications d'erreur de validation sont présentées à l'utilisateur et l'événement associé Validation.Error est déclenché.

Comme vous pouvez le constater, il peut se produire des erreurs de validation à différents moments du processus, selon le mécanisme que vous sélectionnez. Il n'est pas indiqué dans la liste quand les règles ValidationRules sont déclenchées. En effet, ces règles peuvent être déclenchées à divers moments du processus selon la valeur définie pour la propriété ValidationStep dans ValidationRule. Elles peuvent ainsi être appliquées avant la conversion de type, après la conversion, après la mise à jour de la propriété ou lorsque la valeur modifiée est engagée (si l'objet de données implémente IEditableObject). La valeur par défaut est RawProposedValue, qui se produit avant la conversion de type. Le moment où les données sont converties du type de propriété du contrôle cible vers le type de propriété de l'objet de source de données se produit généralement implicitement sans toucher au code, comme pour une entrée numérique dans le contrôle TextBox. Ce processus de conversion de type peut lancer des exceptions qui doivent être utilisées pour indiquer une erreur de validation à l'utilisateur.

Si la valeur ne peut même pas être écrite dans la propriété de l'objet source, il s'agit clairement d'une entrée non valide. Si vous choisissez d'ajouter des règles ValidationRules, elles sont appelées au moment du processus spécifié par la propriété ValidationStep et elles peuvent renvoyer des erreurs de validation en fonction de la logique intégrée en elles ou appelée à partir d'elles. Si l'accesseur de propriété de l'objet source lance une exception, celle-ci devrait presque toujours être traitée comme une erreur de validation, comme dans le cas de la conversion de type.

En dernier lieu, si vous implémentez IDataErrorInfo, la propriété d'indexeur que vous ajoutez à l'objet de source de données pour cette interface sera appelée pour la propriété qui était définie afin de vérifier s'il y a une erreur de validation en fonction de la chaîne renvoyée par cette interface. Je couvrirai chacun de ces mécanismes plus en détail par la suite.

Vous devrez également décider du moment où vous souhaitez voir la validation se produire. La validation a lieu lorsque la liaison écrit les données dans la propriété sous-jacente de l'objet source. Le moment de la validation est défini par la propriété UpdateSourceTrigger de la liaison, à savoir PropertyChanged pour la plupart des propriétés. Certaines propriétés, telles que TextBox.Text, remplacent la valeur par FocusChange, ce qui signifie que la validation se produit lorsque le contrôle utilisé pour modifier les données perd le focus. La valeur peut également être définie sur Explicit, ce qui signifie que la validation doit être appelée explicitement sur la liaison. Le groupe BindingGroup dont je parle plus loin dans cet article utilise le mode Explicit.

Dans les scénarios de validation, particulièrement avec les contrôles TextBox, il est généralement préférable de faire un retour immédiat à l'utilisateur. Dans cette optique, définissez la propriété UpdateSourceTrigger de la liaison sur PropertyChanged :

Text="{Binding Path=Activity.Description, UpdateSourceTrigger=PropertyChanged}

Il s'avère que pour de nombreux scénarios de validation réels, vous devrez exploiter plusieurs de ces mécanismes. Chacun a ses avantages et ses inconvénients en fonction du type d'erreur de validation qui vous intéresse et de l'emplacement de la logique de validation.

Scénario de validation professionnel

Pour rendre tout cela plus concret, examinons un scénario de modification avec un contexte professionnel semi-réel et vous verrez comment ces mécanismes peuvent entrer en jeu. Ce scénario et les règles de validation reposent sur une application réelle que j'ai écrite pour un client. Dans ce cas de figure, un formulaire relativement simple demandait l'utilisation de presque chaque mécanisme de validation en raison des règles métier de prise en charge pour la validation. Pour l'application plus simple utilisée dans cet article, je me servirai de chacun des mécanismes afin d'illustrer son utilisation, même s'ils ne sont pas tous explicitement nécessaires.

Supposons que vous ayez besoin d'écrire une application pour aider les techniciens qui effectuent du support technique à domicile (imaginez le personnel du câble qui tente également de vendre des fonctionnalités et des services supérieurs supplémentaires). Pour chaque activité effectuée par le technicien sur le terrain, celui-ci doit entrer un rapport d'activité indiquant ce qu'il a fait et associant les opérations à plusieurs données différentes. Le modèle d'objet est illustré à la figure 3.

Figure 3 Object Model for the Sample Application
Figure 3 Modèle d'objet pour l'exemple d'application

La donnée la plus importante que les utilisateurs doivent remplir est un objet Activity, qui comprend un titre Title, une date d'activité ActivityDate, un type d'activité ActivityType (une liste de sélection déroulante comportant des types d'activité prédéfinis) et une Description. Ils doivent également associer leur activité à l'une des trois possibilités suivantes. Ils doivent sélectionner soit un client pour lequel l'activité a été effectuée dans une liste de clients qui leur est attribuée, soit un objectif de l'entreprise auquel l'activité était associée dans une liste d'objectifs de la société. La troisième possibilité consiste à entrer manuellement un motif si ni le client, ni l'objectif ne s'appliquent à cette activité.

Vous trouverez ci-après les règles de validation que l'application doit faire appliquer :

  • Title et Description sont des champs obligatoires.
  • Le champ ActivityDate doit contenir une date qui ne doit pas être sept jours avant ou après la date du jour.
  • Si l'option Install est sélectionnée pour ActivityType, le champ de stock Inventory est obligatoire et doit indiquer l'équipement utilisé par le technicien. Les articles en stock doivent être entrés sous forme de liste contenant des éléments séparés par une virgule, avec une structure de numéro de modèle définie pour les articles saisis.
  • Au moins l'un des trois champs Customer, Objective ou Reason doit être rempli.

Ces conditions peuvent sembler simples, mais les deux dernières en particulier ne sont pas si faciles à gérer dans la mesure où elles indiquent un couplage croisé entre les propriétés. L'application exécutée avec des données non valides, indiquées par la zone rouge, est illustrée à la figure 4.

Figure 4 A Dialog Showing ToolTips and Invalid Data
Figure 4 Boîte de dialogue illustrant les info-bulles et les données non valides

Validation d'exception

La forme la plus simple de validation consiste à traiter comme erreur de validation une exception levée au cours du processus de définition de la propriété cible. L'exception peut provenir du processus de conversion de type avant que la liaison ne définisse la propriété cible, elle peut provenir du lancement explicite d'une exception dans l'accesseur de propriété ou encore d'un appel d'un objet métier à partir de l'accesseur où l'exception est lancée plus bas dans la pile.

Pour utiliser ce mécanisme, il suffit de définir la propriété ValidatesOnExceptions sur true pour l'objet Binding :

Text="{Binding Path=Activity.Title, ValidatesOnExceptions=True}"

Lorsqu'une exception est lancée pendant une tentative de définition de la propriété d'objet source (Activity.Title dans le cas présent), une erreur de validation est définie sur le contrôle. L'erreur de validation par défaut est indiquée par une bordure rouge située autour du contrôle, comme illustré à la figure 5.

Figure 5 A Validation Error
Figure 5 Une erreur de validation

Dans la mesure où les exceptions peuvent se produire au cours du processus de conversion de type, il est conseillé de définir cette propriété sur les liaisons d'entrée dès qu'il y a un risque d'échec de la conversion de type, même si la propriété de stockage ne fait que définir la valeur sur une variable de membre sans aucune chance d'exception.

Par exemple, supposons que vous souhaitiez utiliser TextBox comme contrôle d'entrée pour une propriété DateTime. Si un utilisateur entre une chaîne qui ne peut pas être convertie, ValidatesOnExceptions est la seule façon pour votre liaison d'indiquer une erreur car la propriété d'objet source ne sera jamais appelée.

Si vous avez besoin de faire quelque chose de spécifique dans la vue en cas de données non valides, par exemple désactiver une commande, vous pouvez associer l'événement Validation.Error correspondant au contrôle. Vous devrez également définir la propriété NotifyOnValidationError sur true pour la liaison.

<TextBox Name="ageTextBox" 
  Text ="{Binding Path=Age, 
    ValidatesOnExceptions=True, 
    NotifyOnValidationError=True}" 
    Validation.Error="OnValidationError".../>

Validation à l'aide d'une règle de validation ValidationRule

Dans certains scénarios, vous choisirez d'insérer la validation au niveau de l'interface utilisateur et vous aurez alors besoin d'une logique plus compliquée pour déterminer si les entrées sont valides. Pour l'exemple d'application, observez la règle de validation du champ Inventory. Le cas échéant, les données doivent être entrées sous forme d'une liste de numéros de modèle, suivant une structure spécifique, et séparés par des virgules. Une ValidationRule s'adapte facilement à cette condition parce qu'elle dépend totalement de la valeur définie. Cette règle peut utiliser un appel string.Split pour transformer les entrées en tableau de chaîne, puis se servir d'une expression régulière pour vérifier si les parties individuelles répondent à un modèle particulier. Pour cela, vous pouvez définir une ValidationRule, comme illustré à la figure 6.

Figure 6 ValidationRule pour valider un tableau de chaîne

public class InventoryValidationRule : ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    if (InventoryPattern == null)
      return ValidationResult.ValidResult;

    if (!(value is string))
      return new ValidationResult(false, 
     "Inventory should be a comma separated list of model numbers as a string");

    string[] pieces = value.ToString().Split(‘,’);
    Regex m_RegEx = new Regex(InventoryPattern);

    foreach (string item in pieces) {
      Match match = m_RegEx.Match(item);
      if (match == null || match == Match.Empty)
        return new ValidationResult(
          false, "Invalid input format");
    }

    return ValidationResult.ValidResult;
  }

  public string InventoryPattern { get; set; }
}

Les propriétés exposées dans une ValidationRule peuvent être définies à partir du XAML au point d'utilisation, ce qui leur donne légèrement plus de souplesse. Cette règle de validation ignore les valeurs qui ne peuvent pas être converties en tableau de chaîne. Mais lorsque la règle peut exécuter string.Split, elle utilise ensuite une expression régulière RegEx pour s'assurer que chaque chaîne de la liste de valeurs séparées par des virgules est conforme au modèle défini via la propriété InventoryPattern.

Lorsque vous renvoyez un résultat ValidationResult avec un indicateur valide défini sur false, le message d'erreur que vous fournissez peut être utilisé dans l'interface utilisateur pour présenter l'erreur à l'utilisateur, comme je le démontrerai plus tard. L'un des inconvénients des règles de validation est que vous avez besoin d'un élément Binding développé dans le XAML pour les intégrer, comme illustré dans le code suivant :

<TextBox Name="inventoryTextBox"...>
  <TextBox.Text>
    <Binding Path="Activity.Inventory" 
             ValidatesOnExceptions="True" 
             UpdateSourceTrigger="PropertyChanged" 
             ValidatesOnDataErrors="True">
      <Binding.ValidationRules>
        <local:InventoryValidationRule 
          InventoryPattern="^\D?(\d{3})\D?\D?(\d{3})\D?(\d{4})$"/>
      </Binding.ValidationRules>
    </Binding>
  </TextBox.Text>
</TextBox>

Dans cet exemple, la liaison continuera à générer des erreurs de validation si une exception se produit à cause de la propriété ValidatesOnExceptions définie sur true. Je défends également la validation IDataErrorInfo reposant sur ValidatesOnDataErrors défini sur true, ce dont je parlerai ensuite.

Si plusieurs règles de validation sont associées à la même propriété, chacune d'entre elles peut avoir une valeur différente pour la propriété ValidationStep ou elles peuvent toutes avoir la même valeur. Les règles d'une même étape ValidationStep sont évaluées dans leur ordre de déclaration. Les règles des étapes de validation précédentes sont bien évidemment exécutées avant celles des étapes de validation ultérieures. Ce qui n'est peut-être pas évident, c'est que si une règle ValidationRule renvoie une erreur, aucune des règles suivantes n'est évaluée. Par conséquent, la première erreur de validation sera la seule indiquée si les erreurs proviennent des règles de validation.

Validation IDataErrorInfo

Avec l'interface IDataErrorInfo, le responsable de l'implémentation doit exposer une propriété et un indexeur :

public interface IDataErrorInfo {
  string Error { get; }
  string this[string propertyName] { get; }
}

La propriété Error permet d'indiquer une erreur pour l'objet dans son ensemble et l'indexeur est utilisé afin d'indiquer les erreurs au niveau de la propriété individuelle. Ils fonctionnent tous les deux de la même façon : le renvoi d'une chaîne non NULL ou non-vide indique une erreur de validation. De plus, la chaîne que vous renvoyez peut être utilisée pour afficher l'erreur aux yeux de l'utilisateur, comme je le démontrerai plus tard.

Lorsque vous travaillez avec des contrôles individuels liés aux propriétés individuelles sur un objet de source de données, l'indexeur est la partie la plus importante de l'interface. La propriété Error est uniquement utilisée dans des scénarios spécifiques, par exemple lorsque l'objet est affiché dans un contrôle DataGrid ou un groupe BindingGroup. La propriété Error permet d'indiquer une erreur au niveau de la ligne, tandis que l'indexeur sert à indiquer une erreur au niveau de la cellule.

L'implémentation de l'interface IDataErrorInfo présente un inconvénient majeur : l'implémentation de l'indexeur mène généralement à une instruction de cas switch, avec un cas pour chaque nom de propriété de l'objet et vous devez ensuite basculer et faire correspondre en fonction des chaînes, puis renvoyer des chaînes pour indiquer une erreur. De plus, cette implémentation n'est pas appelée tant que la valeur de propriété n'a pas été définie sur l'objet. Si d'autres objets sont abonnés à INotifyPropertyChanged.PropertyChanged sur votre objet, ils auront déjà été avertis de la modification et auront parfois commencé à travailler en fonction des données que l'implémentation d'IDataErrorInfo s'apprête à déclarer comme non valides. Si vous pensez que cela pourrait être un problème pour votre application, vous devrez lancer des exceptions à partir des accesseurs de propriété lorsque vous ne serez pas satisfait de la valeur définie.

IDataErrorInfo présente l'avantage de faciliter la gestion des propriétés en couplage croisé. Par exemple, outre l'utilisation de la règle ValidationRule pour valider le format d'entrée du champ Inventory, n'oubliez pas que ce même champ doit être rempli si Install a été sélectionné dans le champ ActivityType. La règle de validation elle-même n'a aucun accès aux autres propriétés de l'objet lié aux données. Elle reçoit simplement une valeur définie pour la propriété à laquelle la liaison est associée. Pour gérer cette condition, lorsque la propriété ActivityType est définie, vous devez faire en sorte que la validation se produise sur la propriété Inventory et renvoyer un résultat non valide lorsque le type ActivityType est défini sur Install, si la valeur du champ Inventory est vide.

Vous avez pour cela besoin de l'interface IDataErrorInfo afin d'inspecter les propriétés Inventory et ActivityType lors de l'évaluation du champ Inventory, comme illustré ici :

public string this[string propertyName] {
  get { return IsValid(propertyName); }
}

private string IsValid(string propertyName) {
  switch (propertyName) {
    ...
    case "Inventory":
      if (ActivityType != null && 
        ActivityType.Name == "Install" &&  
        string.IsNullOrWhiteSpace(Inventory))
        return "Inventory expended must be entered for installs";
      break;
}

De plus, vous avez besoin que la liaison du champ Inventory appelle la validation lorsque la propriété ActivityType change. En règle générale, une liaison n'effectue de requête que sur l'implémentation de l'interface IDataErrorInfo ou appelle ValidationRules si cette propriété a été modifiée dans l'interface utilisateur. Dans ce cas, vous devez déclencher la réévaluation de la validation de liaison même si la propriété Inventory n'a pas changé, contrairement au type ActivityType associé.

Deux méthodes permettent de provoquer l'actualisation de la liaison du champ Inventory lorsque la propriété ActivityType change. La première solution, et la plus simple, consiste à publier l'événement PropertyChanged pour Inventory lorsque vous définissez ActivityType :

ActivityType _ActivityType;
public ActivityType ActivityType {
  get { return _ActivityType; }
  set { 
    if (value != _ActivityType) {
      _ActivityType = value;
      PropertyChanged(this, 
        new PropertyChangedEventArgs("ActivityType"));
      PropertyChanged(this, 
        new PropertyChangedEventArgs("Inventory"));
    }
  }
}

La liaison est ainsi actualisée et sa validation est réévaluée.

La deuxième méthode consiste à lier l'événement correspondant Binding.SourceUpdated au contrôle ComboBox de la propriété ActivityType ou à l'un de ses éléments parents, et de déclencher une actualisation de la liaison à partir du gestionnaire de code-behind pour cet événement :

<ComboBox Name="activityTypeIdComboBox" 
  Binding.SourceUpdated="OnPropertySet"...

private void OnPropetySet(object sender, 
  DataTransferEventArgs e) {

  if (activityTypeIdComboBox == e.TargetObject) {
    inventoryTextBox.GetBindingExpression(
      TextBox.TextProperty).UpdateSource();
  }
}

Si vous appelez UpdateSource sur une liaison par programme, la valeur actuelle risque d'être écrite dans l'élément cible lié de la propriété source et de déclencher ainsi la chaîne de validation comme si l'utilisateur venait de modifier le contrôle.

Utilisation de BindingGroup pour les propriétés en couplage croisé

La fonctionnalité BindingGroup a été ajoutée à la plateforme Microsoft .NET Framework 3.5 SP1. Elle est spécialement conçue pour vous permettre d'évaluer la validation simultanée de toutes les liaisons d'un même groupe. Par exemple, vous pouvez autoriser un utilisateur à remplir tout un formulaire et attendre jusqu'à ce qu'il appuie sur le bouton Submit ou Save pour évaluer les règles de validation de ce formulaire, puis présenter toutes les erreurs de validation en même temps. Dans l'exemple d'application, la condition était qu'au moins l'un des trois champs Customer, Objective ou Reason soit rempli. Un BindingGroup peut également être utilisé pour évaluer un sous-ensemble de formulaire.

Pour utiliser un BindingGroup, vous avez besoin d'un ensemble de contrôle avec des liaisons normales associées qui partagent un élément ancêtre commun. Dans l'exemple d'application, le contrôle ComboBox du champ Customer, le contrôle ComboBox du champ Objective et le contrôle TextBox du champ Reason vivent tous dans la même grille pour la disposition. BindingGroup est une propriété sur FrameworkElement. Cette propriété a une propriété de collection ValidationRules que vous pouvez remplir avec un ou plusieurs objets ValidationRule. Le XAML suivant illustre l'association du BindingGroup pour l'exemple d'application :

<Grid>...
<Grid.BindingGroup>
  <BindingGroup>
    <BindingGroup.ValidationRules>
      <local:CustomerObjectiveOrReasonValidationRule 
        ValidationStep="UpdatedValue" 
        ValidatesOnTargetUpdated="True"/>
    </BindingGroup.ValidationRules>
  </BindingGroup>
</Grid.BindingGroup>
</Grid>

Dans cet exemple, j'ai ajouté une instance de la règle CustomerObjectiveOrReasonValidationRule à la collection. La propriété ValidationStep vous permet d'avoir une forme de contrôle sur la valeur passée à la règle. UpdatedValue implique l'utilisation de la valeur écrite dans l'objet de source de données après son écriture. Vous pouvez également choisir des valeurs pour ValidationStep qui vous permettent d'utiliser les entrées brutes de l'utilisateur, la valeur une fois la conversion de valeur et de type appliquée, ou la valeur « engagée », ce qui signifie implémenter l'interface IEditableObject pour les modifications transactionnelles aux propriétés de votre objet.

L'indicateur ValidatesOnTargetUpdated provoque l'évaluation de la règle chaque fois que la propriété cible est définie via les liaisons. Cela inclut la définition initiale, ce qui implique que vous avez des indications d'erreur de validation immédiates si les données initiales ne sont pas valides, ainsi que chaque fois que l'utilisateur change les valeurs des contrôles faisant partie du BindingGroup.

Une ValidationRule associée à un BindingGroup fonctionne légèrement différemment d'une ValidationRule associée à une liaison unique. La figure 7 illustre la règle ValidationRule associée au BindingGroup de l'exemple de code précédent.

Figure 7 ValidationRule d'un BindingGroup

public class CustomerObjectiveOrReasonValidationRule : 
  ValidationRule {

  public override ValidationResult Validate(
    object value, CultureInfo cultureInfo) {

    BindingGroup bindingGroup = value as BindingGroup;
    if (bindingGroup == null) 
      return new ValidationResult(false, 
        "CustomerObjectiveOrReasonValidationRule should only be used with a BindingGroup");

    if (bindingGroup.Items.Count == 1) {
      object item = bindingGroup.Items[0];
      ActivityEditorViewModel viewModel = 
        item as ActivityEditorViewModel;
      if (viewModel != null && viewModel.Activity != null && 
        !viewModel.Activity.CustomerObjectiveOrReasonEntered())
        return new ValidationResult(false, 
          "You must enter one of Customer, Objective, or Reason to a valid entry");
    }
    return ValidationResult.ValidResult;
  }
}

Dans une ValidationRule associée à une liaison unique, la valeur qui est passée correspond à la valeur unique de la propriété de source de données définie comme chemin de la liaison. S'il s'agit d'un BindingGroup, la valeur passée à la ValidationRule est le BindingGroup lui-même. Il contient une collection Items remplie par le DataContext de l'élément de conteneur, Grid dans le cas présent.

Dans le cadre de l'exemple d'application, j'utilise le modèle MVVM, c'est pourquoi le DataContext de la vue est le modèle ViewModel lui-même. La collection Items contient simplement une référence unique au ViewModel. À partir de ce dernier, je peux accéder à la propriété Activity. Dans ce cas, la classe Activity contient la méthode de validation qui détermine si au moins un des champs Customer, Objective ou Reason a été rempli pour m'éviter de dupliquer cette logique dans la ValidationRule.

Comme avec les autres ValidationRules décrites précédemment, vous renvoyez ValidationResult.ValidResult si vous êtes satisfait des valeurs des données passées. En revanche, si vous n'êtes pas satisfait, vous construisez un nouveau ValidationResult avec un faux indicateur valide et un message de chaîne indiquant le problème (il pourra être utilisé par la suite à des fins d'affichage).

Cependant, la définition de l'indicateur ValidatesOnTargetUpdated ne suffit pas pour que les règles de validation soient déclenchées automatiquement. Le BindingGroup a été conçu sur la base du concept de déclenchement explicite d'une validation pour tout un groupe de contrôles, généralement via quelque chose de similaire à un bouton Submit ou Save dans un formulaire. Dans certains scénarios, les utilisateurs ne veulent pas s'embêter avec les indications d'erreur de validation avant que le processus de modification ne soit terminé. Le BindingGroup est alors conçu en fonction de cette approche.

Dans l'exemple d'application, je souhaite fournir un retour d'erreur de validation immédiat à l'utilisateur chaque fois qu'il modifie quelque chose dans le formulaire. Pour effectuer cette implémentation avec un BindingGroup, vous devez associer l'événement de modification approprié aux contrôles d'entrée individuels qui font partie du groupe et demander au gestionnaire d'événements de ces événements de déclencher l'évaluation du BindingGroup. Dans l'exemple d'application, cela signifie associer l'événement ComboBox.SelectionChanged aux deux contrôles ComboBox, et l'événement TextBox.TextChanged au contrôle TextBox. Ils peuvent tous pointer vers une seule méthode de gestion dans le code-behind :

private void OnCommitBindingGroup(
  object sender, EventArgs e) {

  CrossCoupledPropsGrid.BindingGroup.CommitEdit();
}

Notez que pour l'affichage de la validation, la bordure rouge par défaut apparaît sur le FrameworkElement dans lequel réside le BindingGroup, par exemple la Grid dans l'exemple d'application, comme illustré à la figure 4. Vous pouvez également modifier l'emplacement d'affichage de l'indication de validation en utilisant les propriétés associées Validation.ValidationAdornerSite et Validation.ValidationAdornerSiteFor. Par défaut, les contrôles individuels afficheront également des bordures rouges pour leurs erreurs de validation individuelles. Dans l'exemple d'application, je désactive ces bordures en définissant le modèle ErrorTemplate sur NULL via les Styles.

Dans .NET Framework 3.5 SP1, vous rencontrerez peut-être des problèmes avec BindingGroup pour l'affichage correct des erreurs de validation lors du chargement du formulaire initial, même si vous définissez la propriété ValidatesOnTargetUpdated dans la ValidationRule. Pour remédier à ce problème, j'ai découvert que vous pouvez « agiter » l'une des propriétés liées du BindingGroup. Dans l'exemple d'application, vous pourriez ajouter ou supprimer un espace à la fin du texte initialement présenté dans le contrôle TextBox de l'événement Loaded de la vue, en procédant ainsi :

string originalText = m_ProductTextBox.Text;
m_ProductTextBox.Text += " ";
m_ProductTextBox.Text = originalText;

Avec ce code, les règles de validation du BindingGroup sont déclenchées dans la mesure où les propriétés Binding contenues ont été modifiées, ce qui provoque la validation de chaque liaison à appeler. Ce comportement est fixe dans la plateforme .NET Framework 4.0, la solution de contournement ne devrait donc pas avoir à afficher initialement les erreurs de validation. Il vous suffit de définir la propriété ValidatesOnTargetUpdated sur true pour les règles de validation.

Affichage des erreurs de validation

Comme mentionné précédemment, par défaut, WPF affiche les erreurs de validation en dessinant une bordure rouge autour du contrôle. Vous aurez souvent besoin de personnaliser cela pour afficher les erreurs d'une autre façon. De plus, le message d'erreur associé à l'erreur de validation ne s'affiche pas par défaut. Dans cette optique, il est courant d'afficher le message d'erreur dans une info-bulle uniquement lorsque l'erreur de validation existe. Il est relativement facile de personnaliser les affichages des erreurs de validation via une combinaison de styles et un ensemble de propriétés associées à la validation.

Il est simple d'ajouter une info-bulle qui affiche le texte d'erreur. Vous devez simplement définir un Style s'appliquant au contrôle d'entrée qui définit la propriété ToolTip du contrôle sur le texte de l'erreur de validation dès qu'une erreur de validation se produit. De plus, vous devrez utiliser deux propriétés associées : Validation.HasError et Validation.Errors. Vous trouverez ci-après un exemple de style ciblant le type TextBox qui définit l'info-bulle :

<Style TargetType="TextBox">
  <Style.Triggers>
    <Trigger Property="Validation.HasError" 
             Value="True">
      <Setter Property="ToolTip">
        <Setter.Value>
          <Binding 
            Path="(Validation.Errors).CurrentItem.ErrorContent"
            RelativeSource="{x:Static RelativeSource.Self}" />
        </Setter.Value>
      </Setter>
    </Trigger>
  </Style.Triggers>
</Style>

Vous pouvez voir que le style contient simplement un déclenchement de propriété pour la propriété Validation.HasError associée. La propriété HasError sera définie sur true lorsqu'une liaison mettra à jour sa propriété d'objet source et les mécanismes de validation généreront une erreur. Cela peut venir d'une exception, d'une ValidationRule ou d'un appel IDataErrorInfo. Le Style utilise ensuite la propriété Validation.Errors associée, qui contiendra une collection de chaînes d'erreur si une erreur de validation existe. Vous pouvez utiliser la propriété CurrentItem sur ce type de collection pour saisir simplement la première chaîne de la collection. Vous avez également la possibilité de concevoir quelque chose que les données lient à la collection et qui affiche la propriété ErrorContent pour chaque élément d'un contrôle orienté liste.

Pour remplacer l'affichage par défaut de l'erreur de validation d'un contrôle par quelque chose d'autre que la bordure rouge, vous devrez définir la propriété associée Validation.ErrorTemplate sur un nouveau modèle pour le contrôle à personnaliser. Dans l'exemple d'application, au lieu d'une bordure rouge, c'est un petit cercle rouge qui s'affiche à droite de chaque contrôle avec une erreur. Pour cela, vous devez définir un modèle de contrôle qui sera utilisé comme ErrorTemplate.

<ControlTemplate x:Key="InputErrorTemplate">
  <DockPanel>
    <Ellipse DockPanel.Dock="Right" Margin="2,0" 
             ToolTip="Contains invalid data"
             Width="10" Height="10">
      <Ellipse.Fill>
        <LinearGradientBrush>
          <GradientStop Color="#11FF1111" Offset="0" />
          <GradientStop Color="#FFFF0000" Offset="1" />
        </LinearGradientBrush>
      </Ellipse.Fill>
    </Ellipse>
    <AdornedElementPlaceholder />
  </DockPanel>
</ControlTemplate>

Pour associer ce modèle de contrôle à un contrôle, vous devez simplement définir la propriété Validation.ErrorTemplate pour le contrôle, ce que vous pouvez faire via un Style, comme je l'ai déjà mentionné :

<Style TargetType="TextBox">
  <Setter Property="Validation.ErrorTemplate" 
    Value="{StaticResource InputErrorTemplate}" />
  ...
</Style>

Conclusion

Dans cet article, j'ai expliqué comment utiliser les trois mécanismes de validation de la liaison de données WPF pour gérer un certain nombre de scénarios de validation des données métier. Vous avez vu comment utiliser les exceptions, les ValidationRules et l'interface IDataErrorInfo pour gérer une validation de propriété unique, ainsi que les propriétés dont les règles de validation dépendent des valeurs courantes d'autres propriétés du contrôle. Vous avez également vu comment utiliser les BindingGroups pour évaluer plusieurs liaisons simultanément et comment personnaliser l'affichage des erreurs au-delà des valeurs par défaut de WPF.

L'exemple d'application de cet article comporte l'ensemble de validation complet qui répond aux règles métier décrites dans une application simple utilisant MVVM pour associer la vue aux données qui l'accompagnent.         

Brian Noyes *est l'architecte principal d'IDesign (idesign.net), directeur régional Microsoft et Microsoft MVP. Auteur, il participe aussi fréquemment à des conférences Microsoft Tech Ed, DevConnections, DevTeach et autres dans le monde entier. Vous pouvez le contacter via son blog à l'adresse briannoyes.net. *

Je remercie l'expert technique suivant pour la relecture de cet article : Sam Bent