Création dynamique de contrôles de validation

 

Callum Shillan
Service de conseil Microsoft

Octobre 2004

S’applique à :
   Microsoft Framework versions 1.0 et 1.1
   Microsoft Visual C#
   Microsoft ASP.NET

Résumé: Explique comment utiliser un fichier de configuration XML pour contrôler la création dynamique de contrôles de validation ASP.NET qui sont automatiquement appliqués aux contrôles d’entrée utilisateur. Cela permet au développeur de formulaires Web de se concentrer sur l’implémentation de la logique métier et fournit également une interface utilisateur plus cohérente. (22 pages imprimées)

Téléchargez le code source de cet article.

Contenu

Introduction et confession
Fichier de configuration de validation dynamique
Structures de données
Classe DynamicValidationManager
Utilisation de DynamicValidationManager
Conclusion
Ouvrages connexes

Introduction et confession

J’aimerais dire que les idées de cet article étaient basées sur quelque chose de noble, comme l’espoir de libérer le développeur web de se concentrer sur l’implémentation de la logique métier, ou peut-être le désir de créer un framework qui permet de fournir une interface utilisateur plus cohérente, mais ce n’était pas le cas. Le concept d’utilisation de contrôles de validation créés dynamiquement sur les formulaires Web est né de la frustration d’une équipe d’interface utilisateur.

Comme pour la plupart des projets d’application web bien exécutés, il y avait plusieurs équipes avec des responsabilités clairement définies : nous avions les suspects habituels de développement, de test, d’expérience utilisateur, etc. Sur ce projet, nous avons utilisé des scénarios de cas d’usage avec des conceptions d’écran associées, et il a très bien fonctionné. Il y avait même un seul document qui contenait la définition de (presque) chaque champ d’entrée utilisateur, les écrans sur lesquels il a été utilisé, s’il s’agissait d’un champ obligatoire, quels étaient les caractères valides et quels étaient les messages d’erreur de validation. Donc, nous avons tous travaillé, commencé à développer nos écrans, et tout se passait très bien. Et puis les problèmes ont commencé...

Lors de l’une des réunions du matin, l’équipe de l’interface utilisateur a dit qu’elle devait modifier la langue utilisée pour signaler les erreurs de validation. Je ne me souviens pas des détails: peut-être qu’ils étaient trop détaillés et qu’ils devaient être resserrés; C’était peut-être l’inverse. Quoi qu’il en soit, nous avons perdu beaucoup de temps lorsque nous sommes retournés sur tous nos écrans et que nous avons modifié la formulation sur les propriétés ErrorMessage et Text des contrôles de validation, mais nous l’avons fait. Une partie de l’équipe est que vous devez être prêt à accueillir d’autres personnes, même si cela signifie plus de travail pour vous-même.

Puis c’est arrivé à nouveau. Peut-être que cette fois-ci, il y avait eu d’autres consultations avec le client. Il y avait peut-être une autre bonne raison. Mais nous nous sommes retrouvés à implémenter des changements. Cette fois, cependant, les changements ont été plus étendus : la couleur des messages devait être modifiée; les champs qui étaient auparavant obligatoires étaient désormais facultatifs, et vice versa ; les jeux de caractères autorisés ont été modifiés ; et les changements se sont poursuivis.

Nous avons réalisé que nous avions besoin d’un moyen de nous protéger. D’une manière ou d’une autre, nous avons dû séparer le développement du formulaire de la validation des entrées. Nous avons réalisé que nous avions besoin d’un moyen de nous libérer pour nous concentrer sur l’implémentation de la logique métier sans nous soucier de la couleur ou du libellé d’un message d’erreur. En outre, nous avions besoin d’une infrastructure qui permettait de créer une validation d’entrée cohérente qui pouvait être facilement modifiée et modifiée sans avoir à modifier chaque page ASPX.

Après une session de tableau blanc concentrée, nous nous sommes retrouvés avec une version de l’implémentation présentée ci-dessous. En substance, chaque champ d’entrée a un contrôle PlaceHolder associé. La propriété ID de l’espace réservé s’indexe dans un fichier de configuration pour récupérer une collection de validateurs créés et ajoutés dynamiquement à l’espace réservé. Lorsque le formulaire Web est rendu au client, les contrôles de validation sont également inclus et l’entrée utilisateur est validée comme d’habitude. Très simple, vraiment.

Après avoir supprimé tous les contrôles de validateur des pages ASPX et inséré les contrôles PlaceHolder , nous pouvons remettre le remplissage du fichier de configuration à l’équipe de l’interface utilisateur, qui pourrait apporter des modifications en fonction des besoins.

Il s’agit d’un concept assez simple à expliquer, mais en cours (dans cet article), nous allons traiter de l’accès aux données contenues dans un fichier XML, nous allons mettre en cache les données de configuration dans une table de hachage qui contient arrayListdes dictionnaires string , et nous allons utiliser la réflexion pour accéder aux contrôles de validation créés dynamiquement et les définir. Je ne recommande pas l’utilisation de la réflexion dans un scénario réel, car elle aura un impact sur le débit et les performances, mais l’utilisation de la réflexion ici me permet de simplifier un peu le code, comme je l’aborderai ci-dessous.

Fichier de configuration de validation dynamique

La première chose que nous devons faire est de décider comment conserver les informations de configuration. Nous avons plusieurs façons de stocker ces informations : dans une base de données, dans un fichier de ressources, dans un fichier XML, etc. Nous allons stocker les informations dans un fichier XML pour la simple raison qu’elles sont faciles à modifier et à manipuler.

Le fichier de configuration comporte deux sections : une qui définit les valeurs de propriété commune et par défaut utilisées pour tous les validateurs, et une autre qui définit une collection de validateurs et de propriétés à appliquer à un champ d’entrée utilisateur donné.

Section Valeurs par défaut

La dernière chose que nous voulons faire est de devoir spécifier continuellement des propriétés répétées pour chaque contrôle de validateur. Nous utilisons donc une section Valeurs par défaut du fichier de configuration pour les spécifier une fois. Cela signifie que nous n’avons pas besoin de spécifier, par exemple, « ForeColor='RED » sur chaque contrôle de validation que nous définissons. Si nécessaire, ces valeurs de propriété par défaut peuvent être remplacées pour un validateur donné.

Il existe un ensemble de propriétés communes à chaque validateur, quel que soit leur type. Je ne vais pas les lister tous, mais ceux qui viennent à l’esprit sont CssClass, ForeColor et Visible. Pour l’ensemble des propriétés du validateur commun, nous aurons une sous-section du fichier de configuration qui contient les valeurs communes à appliquer à tous les validateurs créés dynamiquement, quel que soit leur type.

Chaque type de contrôle de validateur a également un ensemble unique de propriétés (par exemple, la propriété ShowMessageBox de ValidationSummary), nous avons donc besoin d’une sous-section qui contient les valeurs de propriété par défaut.

Enfin, il serait bon de pouvoir paramétrer ces valeurs. Cela nous permettrait de définir la valeur par défaut de la propriété ErrorMessage d’un champ obligatoire unique sur « Veuillez entrer quelque chose pour {FriendlyName} » et de définir le nom convivial ailleurs.

Il n’est pas surprenant que la section Valeurs par défaut du fichier de configuration soit un nœud XML avec des nœuds enfants pour les valeurs de propriété communes et spécifiques au validateur. Consultez l’illustration ci-dessous.

<!--
This section contains default values for validator controls
-->
   <Defaults>
      <!--
      These are default property values that are common to all validator controls
      -->
      <Common>
         <Property name="ForeColor" value="Red" />
         <Property name="Display" value="Dynamic" />
         <Property name="EnableViewState" value="False" />
      </Common>
      <!--
      These are default property values specific to the ValidationSummary controls
      -->
      <ValidationSummary>
         <Property name="EnableClientScript" value="True" />
         <Property name="Enabled" value="True" />
         <Property name="HeaderText" value="Please correct the following errors" />
         <Property name="ShowMessageBox" value="False" />
         <Property name="ShowSummary" value="True" />
         <Property name="DisplayMode" value="BulletList" />
      </ValidationSummary>
      <!--
      These are default property values specific to the Compare validator
      -->
      <Compare />
      <!--
      These are default property values specific to the RequiredField validator
      -->
      <RegularExpression>
         <Property name="Text" value="Allowed chracters are {LegalValues}" />
         <Property name="ErrorMessage" value="{FriendlyName} can only consist of {LegalValues}" />
      </RegularExpression>
      <!--
      These are default property values specific to the RequiredField validator
      -->
      <RequiredField>
         <Property name="InitialValue" value="" />
         <Property name="Text" value="This is a required field" />
         <Property name="ErrorMessage" value="You must enter something for the {FriendlyName}" />
      </RequiredField>
      <!--
      These are default property values specific to the Custom validator
      -->
      <Custom>
         <Property name="EnableClientScript" value="False" />
      </Custom>
   </Defaults>

Notez que la valeur par défaut de la propriété ForeColor de ValidationSummary doit remplacer la valeur contenue dans le nœud Commun, car il s’agit d’une compréhension intuitive. Ce comportement ne se produit pas par magie ; nous devrons l’implémenter dans notre code.

ValidatorSets Section

Tout champ d’entrée utilisateur a besoin d’une collection de contrôles de validation pour s’assurer que l’entrée est correctement validée. Par exemple, un champ de mot de passe peut avoir besoin d’un RequiredFieldValidator et d’un RegularExpressionValidator pour s’assurer que l’utilisateur a entré l’entrée et qu’elle provient d’un jeu de caractères défini.

Un ValidatorCollection aura un attribut ID et une collection de nœuds enfants Validator pour chaque validateur qui doit être créé et appliqué au champ d’entrée utilisateur. L’attribut ID de validatorCollection correspond à la propriété ID de PlaceHolder sur le formulaire Web afin de connecter le champ d’entrée utilisateur à la collection de validateurs qui doivent être créés et appliqués.

Par exemple, le ValidatorCollection pour le champ mot de passe peut avoir l’ID « Mot de passe » avec deux nœuds enfants validateurs : un pour le champ requis et l’autre pour l’expression régulière. Les nœuds enfants du validateur contiennent les valeurs de propriété qui remplacent celles définies dans la section Valeurs par défaut ou qui ne sont pas définies dans la section Valeurs par défaut .

Comme il y aura un certain nombre de champs d’entrée utilisateur, il y aura un ensemble de nœuds ValidatorCollection , qui seront conservés dans un seul nœud ValidatorSets du fichier de configuration.

Une partie du fichier de configuration pour les champs adresse de messagerie et mot de passe est indiquée ci-dessous.

<!--
This section defines the validator groups
A validator group defines a collection of validators and their properties
-->
<ValidatorSets>
   <!--
   This is the collection of validator controls to be used for the EmailAddress
   -->
   <ValidatorCollection id="EmailAddress" 
     FriendlyName="Email address" 
     ControlToValidate="TextBoxEmailAddress">
      <Validator type="RequiredField" />
   </ValidatorCollection>
   <!--
   This is the collection of validator controls to be used for the Passsword
   -->
   <ValidatorCollection id="Password" 
     FriendlyName="Password" 
     LegalValues="alphabetic characters and numbers" 
     ControlToValidate="TextBoxPassword">
      <Validator type="RequiredField" />
      <Validator type="RegularExpression">
         <Property name="ValidationExpression" value="[A-Za-z0-9]*" />
      </Validator>
   </ValidatorCollection>
</ValidatorSets>

La collection d’adresses de messagerie a un seul nœud enfant validateur indiquant qu’un RequiredFieldValidator doit être créé et appliqué dynamiquement au contrôle d’entrée utilisateur TextBoxEmailAddress . Comme le nœud validateur n’a pas de nœuds enfants, toutes les valeurs de propriété sont dérivées de celles définies dans le nœud Valeurs par défaut . Là encore, cela ne se produit pas par magie et nous devrons implémenter ce comportement dans notre code.

La collection de mots de passe comporte deux nœuds enfants Validator qui indiquent qu’un RequiredFieldValidator et un RegularExpressionValidator doivent être créés et appliqués dynamiquement au contrôle d’entrée utilisateur TextBoxEmailPassword . La propriété ValidationExpressionValidator doit avoir la valeur « [A-Za-z0-9] ». Là encore, le validateur RequiredFieldValidator doit prendre ses valeurs de propriété à partir de celles définies dans le nœud Defaults .

Et c’est là que nous définissons les valeurs de nom de champ. Notez que nous avons un attribut appelé FriendlyName sur le nœud ValidatorCollection . Le texte interne de cet attribut est utilisé pour remplacer toutes les instances de {FriendlyName} trouvées dans les valeurs de propriété contenues dans le fichier de configuration. Cela signifie que de nombreux validatorCollectionpeuvent faire référence à la même définition de validateur de champ obligatoire par défaut, et qu’il leur suffit d’identifier le nom convivial pour personnaliser les messages affichés à l’utilisateur final.

Structures de données

Nous allons avoir besoin d’une forme de structure de données pour conserver toutes ces informations de configuration. Pour simplifier les choses, nous allons nous permettre de consacrer un certain temps à la création de cette structure de données, puis de la conserver dans le cache ASP.NET application.

Ce type de structure est un candidat valide à tenir dans le cache de l’application, car il est pertinent pour chaque page de l’application. Nous pouvons également définir une dépendance de cache sur le fichier de configuration, de sorte que si le fichier est modifié, l’entrée du cache est rendue nulle, puis recréée lors de son prochain accès.

La structure de données qui sera conservée dans le cache de l’application est illustrée ci-dessous.

Aa478956.datastructures_fig1(en-us,MSDN.10).gif

Figure 1. Structure de données conservées dans le cache d’application

La table de hachage aura une entrée pour chaque champ d’entrée utilisateur auquel des contrôles de validateur créés dynamiquement seront appliqués. La clé (pour instance, « email » et « psword » dans la figure 1, ci-dessus) correspond à la propriété ID de PlaceHolder, ce qui nous permet de déterminer la liste des validateurs à créer et ajouter dynamiquement.

Ainsi, quelque part sur le formulaire Web qui a un champ d’entrée utilisateur pour, par exemple, une adresse de messagerie, il y aura un contrôle Espace réservé avec un ID « e-mail ». Cela sera utilisé pour indexer dans la table de hachage et accéder à la ArrayList des validateurs qui doivent être créés dynamiquement et ajoutés à l’espace réservé. ArrayList sera itéré et les propriétés du validateur créé dynamiquement seront conservées dans le StringDictionary associé.

Classe DynamicValidationManager

Pour gérer ce processus, nous allons créer une classe simple avec deux méthodes publiques : une construction et une qui charge dynamiquement les contrôles de validation pour un formulaire Web ou un contrôle utilisateur donné.

Afin d’éviter que le gestionnaire ne soit créé à maintes reprises pour chaque espace réservé sur chaque formulaire Web, nous le plaçons dans le cache de l’application. Chaque accès case activée pour voir s’il doit être créé. Le besoin de création se produit dans deux situations : lors du premier accès et si l’élément a été vidé. Comme l’objet dépend du fichier de configuration associé, nous pouvons également définir une dépendance de cache afin que si le fichier est modifié, le gestionnaire soit automatiquement vidé du cache de l’application.

Constructeur du Gestionnaire de validation dynamique

Le constructeur doit créer les structures de données illustrées à la figure 1 ci-dessus. Pour ce faire, les étapes suivantes doivent être effectuées :

  1. Définissez les propriétés privées du gestionnaire et la signature du constructeur.
  2. Chargez le fichier de configuration dans un document XML.
  3. Chargez les valeurs de propriété commune et par défaut.
  4. Effectuez une boucle dans chaque collection de validateurs et
    Charger la collection de validateurs individuelle
    Boucle à travers chaque propriété définie pour le validateur
    Insérer la valeur de la propriété common/default, si elle est définie, dans un dictionnaire de chaînes
    Remplacer/insérer la valeur de propriété spécifique dans le dictionnaire de chaînes
    Ajouter le dictionnaire de chaînes à la liste matricielle des validateurs
    Ajoutez la liste de tableau dans la table de hachage des collections de validateurs.
  5. Le code permettant d’implémenter ces étapes est décrit ci-dessous.

Définir les propriétés privées et la signature du constructeur

Nous aurons besoin de deux propriétés qui sont privées au gestionnaire de validation dynamique : la liste des types de contrôle de validateurs que nous traiterons et la table de hachage des collections de validateurs. La signature du constructeur aura un paramètre définissant le chemin d’accès au fichier de configuration. Consultez l’illustration ci-dessous.

// These are the validator types that we will cater for
private enum ValidatorTypes {Common, Compare, 
  Custom, Range, RegularExpression, RequiredField, 
  ValidationSummary};

// Holds the collection of validator controls defined 
// in the <ValidatorCollections> node of the configuration file
// This will hold array lists of string dictionaries of property/values 
// for each defined validator
private Hashtable validatorCollections = new Hashtable();

/// <summary>
/// Builds the Dynamic Validation object
/// </summary>
/// <param name="validatorsConfigFile">The configuration file 
/// for the Dynamic Validator Controls</param>
public DynamicValidationManager( string validatorsConfigFile )
{

L’énumération ValidatorTypes sera utilisée car elle nous permet de traiter facilement une liste d’équivalents de chaîne ou d’accéder à un index entier. Nous pouvons obtenir la liste des valeurs de chaîne à l’aide de la méthode statique GetNames() de la classe Enum et nous pouvons simplement caster un instance de l’énumération en entier afin d’obtenir son index.

Charger le fichier de configuration dans un document XML

Le code pour charger le fichier de configuration est assez simple et est illustré ci-dessous.

// Load the configuration file into an XML document
XmlTextReader xmlTextReader = new XmlTextReader( validatorsConfigFile );
XmlDocument configurationDocument = new XmlDocument();
configurationDocument.Load( xmlTextReader );

Nous utilisons un XmlTextReader afin d’avoir un accès rapide, non mis en cache et vers l’avant uniquement aux données XML. Nous allons traiter ce fichier de configuration le plus rapidement possible ; il n’est pas nécessaire de le mettre en cache, car nous détenons les valeurs dans notre structure de données.

Charger les valeurs de propriété commune et par défaut

Nous allons utiliser un tableau temporaire de StringDictionaries pour stocker les valeurs communes et par défaut. Chaque StringDictionary contient toutes les valeurs de propriété commune et par défaut telles que définies dans le nœud Defaults du fichier de configuration. De cette façon, lorsque nous en venons à définir les valeurs spécifiques d’un validateur défini dans un nœud ValidatorCollections , nous pouvons rapidement accéder à la table de hachage « valeurs par défaut » correcte.

Le code pour charger les valeurs de propriété par défaut est illustré ci-dessous.

// Holds the default properties defined in the 
// <Defaults> node of the configuration file
// The array will hold one StringDictionary of 
// default properties and values for each type of validator
StringDictionary[] defaultProperties = 
new StringDictionary[Enum.GetNames(typeof(ValidatorTypes)).Length];

// Loop through each ValidatorType
int iCnt = 0;
foreach ( string validatorType in Enum.GetNames( typeof(ValidatorTypes) ) )
{
   // Create a new hashtable to hold the 
   // property/value pairs for the current validator type
   defaultProperties[iCnt] = new StringDictionary();

   // Load the default settings from the configuration document
   LoadDefaultProperties( configurationDocument, 
     validatorType, defaultProperties[iCnt] );

   // Increment the counter
   iCnt++;
}

Nous définissons le tableau defaultProperties de StringDictionaries et utilisons la longueur du tableau retourné par la méthode statique GetNames() de la classe Enum pour obtenir le nombre correct d’entrées.

Nous parcourons ensuite chacune des entrées du tableau de noms de l’énumération ValidatorTypes et chargeons les propriétés par défaut en appelant la méthode LoadDefaultProperties .

Le code de la méthode LoadDefaultProperties est illustré ci-dessous.

/// <summary>
/// Loads default settings from the configuration document into a property store
/// </summary>
/// <param name="configurationDocument">The XML document 
/// that holds the configuration information</param>
/// <param name="validatorType">The validator type to load</param>
/// <param name="propertyStore">The store to hold the 
/// retrieved default properties and values</param>
private void LoadDefaultProperties( XmlDocument configurationDocument, 
  string validatorType, StringDictionary defaultPropertiesStore )
{
   // Select the node that holds the default 
   // properties for the specified validator
   XmlNode defaultValidatorNode = 
     configurationDocument.SelectSingleNode( "//Defaults/" + 
     validatorType );

   // If there was a node containing default validator properties
   if ( defaultValidatorNode != null )
   {
      // For each validator property
      foreach( XmlNode defaultValidatorProperty in 
        defaultValidatorNode.ChildNodes )
      {
         // Only process XML elements and ignore comments, etc
         if ( defaultValidatorProperty is XmlElement )
         {
            // Insert the property name and the 
            // default value into the store of default 
            // properties store
            string propertyName = GetAttribute( defaultValidatorProperty, 
              "name" );
            string propertyValue = GetAttribute( defaultValidatorProperty, 
              "value" );
            defaultPropertiesStore[ propertyName ] = propertyValue;
         }
      }
   }
}

Là encore, ce code est assez simple. La méthode reçoit le document de configuration XML, le type de validateur à traiter et le StringDictionary dans lequel les valeurs doivent être chargées.

Nous obtenons le nœud du validateur par défaut dans le document de configuration en utilisant la méthode SelectSingleNode et en transmettant une expression XPath de forme appropriée. Si cela retourne un nœud de validateur par défaut, nous aurons quelque chose de la forme indiquée ci-dessous.

<RequiredField>
   <Property name="InitialValue" value="" />
   <Property name="Text" value="This is a required field" />
   <Property name="ErrorMessage" 
      value="You must enter something for the {FriendlyName}" />
</RequiredField>

L’exemple de nœud RequiredField par défaut indiqué ci-dessus montre qu’il existe un certain nombre de nœuds enfants qui contiennent le nom et la valeur de propriété réels, et ce sont eux qui représentent les valeurs de propriété qui nous intéressent.

Nous accédons à la collection de nœuds enfants en accédant à la propriété ChildNodes du defaultValidatorNode. Pour chacun des nœuds enfants, nous utilisons la méthode GetAttribute pour extraire les attributs nom/valeur et les insérer dans le StringDictionary en tant que paire clé/valeur.

Il est intéressant de noter à ce stade qu’il est également possible d’inclure des paramètres dans les valeurs de propriété, ce qui est visible dans la propriété ErrorMessage ; la valeur réelle du paramètre FriendlyName sera définie pour un validateur spécifique défini dans le nœud ValidatorSets du document de configuration.

Boucler chaque collection de validateurs

Maintenant que nous avons chargé toutes les valeurs communes et par défaut spécifiées dans le document de configuration, nous pouvons charger les jeux de collections de validateurs conservés dans le nœud ValidatorSets .

Pour faciliter la lisibilité du code, le constructeur appelle une méthode LoadAllValidatorCollections et transmet le document de configuration XML et le tableau de tables de hachage qui contiennent les valeurs par défaut.

La méthode LoadAllValidatorCollections est illustrée ci-dessous.

/// <summary>
/// Loads all of the validator collections
/// </summary>
/// <param name="configurationDocument">The XML document 
///  that holds the configuration information</param>
private void LoadAllValidatorCollections( XmlDocument 
  configurationDocument, StringDictionary[] defaultProperties )
{
   // Select the node that holds all of the 
   // validator collections for a given user input field
   XmlNode allValidatorCollections = 
     configurationDocument.SelectSingleNode( "//ValidatorSets" );

   // If we got the node that holds the validator collections
   if ( allValidatorCollections != null )
   {
      // Iterate through the validator collections
      foreach ( XmlNode validatorCollection in 
        allValidatorCollections.ChildNodes )
      {
         // Load the validator collection for the user input field
         if ( validatorCollection is XmlElement )
         {
            LoadIndividualValidatorCollection( validatorCollection, 
              defaultProperties );
         }
      }
   }
}

Cette méthode est remarquablement similaire à la méthode LoadDefaultProperties en ce qu’elle traite une série de nœuds enfants. La méthode LoadIndividualValidatorCollection est appelée afin de charger une collection de validateurs individuelle à partir du document de configuration XML.

Charger la collection de validateurs individuelle

Un exemple de collection de validateurs de documents de configuration pour un champ de mot de passe est indiqué ci-dessous.

<ValidatorCollection id="Password" FriendlyName="Password" 
  LegalValues="alphabetic characters and numbers" 
  ControlToValidate="TextBoxPassword">
   <Validator type="RequiredField" />
   <Validator type="RegularExpression">
      <Property name="ValidationExpression" value="[A-Za-z0-9]*" />
   </Validator>
</ValidatorCollection>

Il y a deux choses que nous devons comprendre à propos de ce nœud XML afin de le traiter correctement. Tout d’abord, l’attribut id correspond à la propriété ID d’un contrôle PlaceHolder sur un formulaire Web. Cela nous indique où nous devons ajouter nos contrôles de validation créés dynamiquement afin qu’ils puissent être affichés correctement et affichés dans le formulaire Web.

Deuxièmement, l’attribut ControlToValidate correspond à la propriété ID d’un contrôle d’entrée utilisateur auquel tous les contrôles de validateur créés dynamiquement identifiés dans la collection seront appliqués. Enfin, le nœud ValidatorCollection définit les paramètres en tant qu’attributs, comme indiqué ci-dessus pour les paramètres FriendlyName et LegalValues .

La méthode LoadIndividualValidatorCollection crée d’abord une arrayList utilisée pour contenir les données des nœuds enfants de Validateur individuels. Ensuite, pour chaque nœud enfant du validateur, il crée un StringDictionary pour contenir les noms et valeurs de propriété réels. Les attributs du nœud enfant sont accessibles afin de mémoriser des informations telles que l’identificateur et le contrôle à valider, etc. Consultez l’illustration ci-dessous.

/// <summary>
/// Load a collection of validators to be applied to a given user input field
/// </summary>
/// <param name="validatorCollection">The validator collection</param>
/// <param name="defaultProperties">The default property values</param>
private void LoadIndividualValidatorCollection( XmlNode 
  validatorCollection, StringDictionary[] defaultProperties )
{
   // The list of validators to be applied to the given field
   ArrayList validatorList = new ArrayList();

   // Remember the control to validate
   string controlToValidate = GetAttribute( validatorCollection, 
     "ControlToValidate" );

   // Iterate through each validator in the collection
   foreach( XmlNode validatorNode in validatorCollection.ChildNodes )
   {
      // Only process XML elements and ignore comments, etc
      if ( validatorNode is XmlElement )
      {
         // Use a new string dictionary to hold the validator's 
         // properties and values
         StringDictionary validatorProperties = new StringDictionary();

         // Remember which control this validator should validate
         validatorProperties[ "ControlToValidate" ] = controlToValidate;

         // Remember the type of validator
         string typeofValidator = GetAttribute( validatorNode, "type" );
         validatorProperties["ValidatorType"] = typeofValidator;

         // Add the ServerValidate event handler (only used on Custom validators)
         validatorProperties[ "ServerValidate" ] = 
           GetAttribute( validatorNode, "ServerValidate" );

À ce stade, nous avons enregistré certaines informations de contrôle utilisées pour définir des valeurs de propriété spécifiques à un validateur individuel. Par exemple, nous avons enregistré quel contrôle d’entrée utilisateur doit être validé, quel type de contrôle de validateur doit être créé et, le cas échéant, le nom de la méthode à appeler sur les événements ServerValidate . Il est maintenant temps de charger les noms de propriétés et les valeurs propres à ce validateur dans le stringDictionaryvalidatorProperties.

Nous chargeons d’abord les valeurs de propriété courantes, puis les valeurs de propriété par défaut dans le stringDictionaryvalidatorProperties avant de charger les valeurs spécifiques à partir du document de configuration. Nous gérons l’attribution de valeurs communes et par défaut en appelant une méthode AssignDefaultValues . La méthode privée accepte deux paramètres : le StringDictionary qui contiendra les noms de propriétés et les valeurs de ce validateur et un autre StringDictionary qui contient la propriété et les valeurs communes ou par défaut.

De cette façon, les valeurs de propriété par défaut remplacent et sont donc prioritaires sur les valeurs courantes. Consultez l’illustration ci-dessous.

// Assign the default property values common to all validators
AssignDefaultValues( validatorProperties, 
  defaultProperties[(int) ValidatorTypes.Common] );

// Assign the default property values specific to this type of validator
ValidatorTypes validatorType = (ValidatorTypes) Enum.Parse( 
  typeof(ValidatorTypes), typeofValidator );
AssignDefaultValues( validatorProperties, 
  defaultProperties[(int) validatorType] );

Nous avons utilisé une petite astuce pour déterminer lequel des stringDictionarys conservésdans le tableau defaultProperties doit être utilisé. Vous vous souvenez de la déclaration de l’énumération ValidatorTypes précédemment ?

// These are the validator types that we will cater for
private enum ValidatorTypes {Common, Compare, 
  Custom, Range, RegularExpression, RequiredField, 
  ValidationSummary};

La variable typeOfValidator est une chaîne récupérée à partir de l’attribut « type » du validateatorNode et elle aura l’une des valeurs : « Compare », « Custom », et ainsi de suite. Comme il s’agit d’une représentation sous forme de chaîne du nom de l’une des constantes énumérées, nous pouvons utiliser la méthode Parse statique de la classe Enum pour obtenir un objet énuméré de manière équivalente.

Comme nous n’avons pas spécifié de type sous-jacent pour l’énumération, le type Int32 par défaut est utilisé. Par conséquent, la conversion de l’énumération validatorType en Int32 nous donne la valeur entière de l’énumération. Nous pouvons ensuite indexer dans le tableau defaultProperties de StringDictionarypour les valeurs par défaut du type de validateur correct.

Il ne nous reste plus qu’à itérer à travers chaque nœud enfant du validateur pour accéder aux propriétés et aux valeurs spécifiquement définies pour ce validateur. Après cela, nous pouvons remplacer tous les paramètres de nom de champ par les valeurs dérivées des attributs nommés de manière équivalente du nœud ValidatorCollection . Enfin, nous pouvons ajouter le StringDictionary de noms et de valeurs de propriétés spécifiques à ArrayList.

Consultez l’illustration ci-dessous.

// Iterate through each property node
foreach ( XmlNode propertyNode in validatorNode.ChildNodes )
{
   // Only process XML elements and ignore comments, etc
   if ( propertyNode is XmlElement )
   {
      // Add property names/values explicitly given for this validator
      string propertyName = GetAttribute( propertyNode, "name" );
      string propertyValue = GetAttribute( propertyNode, "value" );
      validatorProperties[ propertyName ] = propertyValue;
   }
}

// Now we have the string dictionary, make any fieldname 
//  replacements that might have been specified
ReplaceFieldnamesWithValues( validatorProperties, validatorCollection );

// Finally, add the string dictionary containing the 
// validator property values to the list of validators 
// for this group
validatorList.Add( validatorProperties );

Je ne vais pas entrer dans le détail de la façon dont les noms de champ sont remplacés par les valeurs appropriées. Simplement, nous devons parcourir en boucle les valeurs de propriété contenues dans le dictionnaire de chaînes à la recherche d’une accolade ouverte. Lorsque nous en trouvons un, nous obtenons la chaîne jusqu’à l’accolade rapprochée, et il s’agit de notre nom de paramètre. Nous nous attendons ensuite à trouver un attribut de ce nom sur le nœud de collection du validateur, et nous prenons sa propriété InnerText comme valeur de paramètre. Il s’agit alors d’une simple question de remplacement de chaîne pour remplacer le nom du paramètre par la valeur du paramètre.

Charger dynamiquement les contrôles de validation

Le fichier de configuration est maintenant chargé dans une structure de données runtime, comme défini dans la figure 1 ci-dessus, et nous pouvons nous intéresser à la partie amusante : créer dynamiquement les contrôles du validateur et les ajouter à l’espace réservé.

La méthode LoadDynamicValidators prend un seul paramètre d’entrée : le UserControl qui héberge les espaces réservés. L’idée de base est que cette méthode itérera dans la collection de contrôles hébergés par le contrôle utilisateur. Si un contrôle PlaceHolder est trouvé, nous allons indexer dans la table de hachage validatorCollections pour voir s’il existe un ensemble de contrôles de validateur qui doivent être créés dynamiquement pour cet espace réservé. Si tel est le cas, nous allons simplement les itérer, créer le contrôle approprié, définir ses propriétés en fonction des valeurs trouvées dans stringDictionary et l’ajouter aux contrôles de l’espace réservé. Lorsque la page Web est rendue dans le navigateur client, les contrôles de validation que nous avons créés dynamiquement sont inclus et l’entrée de l’utilisateur est validée comme défini par le fichier de configuration.

Notez que pour cette implémentation, j’ai restreint les contrôles d’entrée utilisateur à être hébergés dans un contrôle utilisateur. Si vous avez des contrôles d’entrée utilisateur sur la page Web ASPX réelle, vous devez modifier ou surcharger cette méthode en conséquence.

La signature de la méthode LoadDynamicValidators, qui itère à travers les contrôles enfants du contrôle utilisateur et détecte si nous avons un contrôle PlaceHolder est illustré ci-dessous.

/// <summary>
/// Dynamically load validators into a placeholder
/// </summary>
/// <param name="placeHolder">The place holder to load the 
/// validators into</param>
/// <param name="userControl">The user control that 
/// hosts the user input fields and validation controls</param>
public void LoadDynamicValidators( UserControl userControl )
{
   foreach ( Control childControl in userControl.Controls )
   {
      if ( childControl is PlaceHolder )
      {
         // Assign a place holder control, purely for readability
         PlaceHolder placeHolderControl = (PlaceHolder) childControl;

Notez que nous utilisons une variable placeHolderControl . Ceci est purement pour la lisibilité, car nous aurions pu référencer directement l’identificateur childControl dans le corps de l’instruction foreach . Toutefois, l’utilisation de la variable placeHolderControl facilite la lecture et la compréhension du code au détriment d’une surcharge de quelques cycles d’horloge supplémentaires et d’une consommation de mémoire minuscule.

Nous devons maintenant déterminer si placeHolderControl a une entrée dans la table de hachage de la table de hachage validatorCollections . Consultez l’illustration ci-dessous.

// Get the list of validators to be dynamically 
// added to this userControlChildControl
ArrayList validatorList = (ArrayList) 
  validatorCollections[ placeholderControl.ID ];

// Only process controls that have been configured 
// to contain dynamically created validator controls
if ( validatorList != null )
{

Si validatorList n’est pas null, cela signifie que nous disposons d’une liste de validateurs qui doivent être appliqués. Nous devons créer dynamiquement chaque contrôle de validation spécifié et définir leurs propriétés en conséquence. Consultez l’illustration ci-dessous.

// Loop through each validator in the list
for ( int iCnt = 0; iCnt < validatorList.Count; iCnt++ )
{
   // Get the string dictionary of property name/values for the validator
   StringDictionary validatorProperties = 
    (StringDictionary) validatorList[iCnt];

   // Create and add a spacer to go between each 
   // dynamically created placeholderControl
   // Note that whether this is done (and what is added) 
   // could be driven from the configuration file
   Literal spacer = new Literal();
   spacer.Text = "&nbsp;";
   userControl.Controls.Add( spacer );

   // Dynamically create and populate the validator type 
   // based on configuration information held in the string 
   // dictionary
   switch( validatorProperties["ValidatorType"].ToLower() )
   {
      // Each case statement has the same form:
      //    (1) create the correct type of validator,
      //    (2) set the properties of the validator
      //    (3) add it to the placeholderControl placeholderControl
      case "range":
         RangeValidator rangeValidator = new RangeValidator();
         SetProperties( rangeValidator, validatorProperties );
         placeholderControl.Controls.Add( rangeValidator );
         break;

      // The requiredfield, regularexpression, compare, 
      // validationsummary are omitted from this code snippet 
      // in the interests of brevity.  However, 
      // they are similar to that for the range validator

      // Custom validators also need the event handler to be set
      case "custom":
         CustomValidator customValidator = new CustomValidator();
         SetProperties( (Control) customValidator, validatorProperties );
         SetEventHandler( (Control) customValidator, 
           validatorProperties, userControl );
         placeholderControl.Controls.Add( customValidator );
         break;
   }
}

Nous obtenons le dictionnaire de chaînes qui contient les noms de propriétés et les valeurs spécifiés dans le fichier de configuration en indexant dans le validateatorList et en effectuant une conversion appropriée. Dans le dictionnaire de chaînes, les clés sont mappées aux différents noms de propriétés et aux valeurs associées à une représentation sous forme de chaîne de la valeur de la propriété.

Dans le code ci-dessus, nous ajoutons automatiquement un littéral contenant un espace HTML non cassant. Cela peut (et devrait probablement) provenir du fichier de configuration. Il est facile d’imaginer des attributs supplémentaires, « HTMLPrefix », par exemple, sur les différents nœuds Defaults , ainsi que sur les nœuds ValidatorCollection et Validator qui permettraient d’ajouter du code HTML commun, par défaut et spécifique. Et il n’y a aucune raison pour laquelle nous ne pourrions pas également répondre à un attribut « HTMLPostfix ».

La clé ValidatorType du dictionnaire de chaînes nous indique quel type de contrôle validateur doit être créé dynamiquement. L’instruction switch est utilisée pour diriger le contrôle de flux vers l’instruction case appropriée, et nous pouvons enfin créer le contrôle de validateur et définir ses propriétés.

Outre les contrôles CustomValidator , chaque instruction case a le même modèle : créer le contrôle de validation, définir ses propriétés telles que définies par le dictionnaire de chaînes et l’ajouter au contrôle PlaceHolder .

Nous n’examinerons pas les détails de la méthode SetProperties privée qui est appelée, car il s’agit d’une routine assez simple. Il itère chaque clé dans le dictionnaire de chaînes et l’interprète comme un nom de propriété. À l’aide de la réflexion, nous obtenons un objet PropertyInfo qui nous permet de définir la valeur de la propriété du contrôle. Nous créons ensuite un objet du type correct et nous utilisons enfin la méthode SetValue de PropertyInfo pour définir sa valeur.

Bien que nous ayons utilisé la réflexion pour définir les propriétés du contrôle, cette option ne serait pas recommandée pour un scénario réel. Le coût de la réflexion du point de vue du débit et des performances est trop élevé. Dans la pratique, il existe une série d’instructions switch qui déterminent d’abord le type de validateur utilisé et la propriété définie. Ensuite, une autre instruction switch détermine et définit directement la propriété. Toutefois, afin de garder le code de démonstration relativement simple et facile à lire, j’ai opté pour la réflexion.

Dans l’extrait de code ci-dessus, nous devions prendre en charge la définition d’un gestionnaire d’événements pour les contrôles CustomValidator . Le contrôle CustomValidator est utilisé pour fournir une fonction de validation définie par l’utilisateur (c’est-à-dire définie par le développeur) pour un contrôle d’entrée. Les contrôles CustomValidator ont toujours une fonction de validation côté serveur et peuvent avoir une fonction de validation côté client. La fonction de validation côté client est facilement gérée dans notre code, car il s’agit simplement d’une propriété de chaîne et peut être spécifiée, et elle est accessible et définie comme n’importe quelle autre propriété de type de chaîne (pour instance, ErrorMessage ou Text).

Normalement, le développeur de formulaires Web crée une méthode dans le code-behind du formulaire web ou du contrôle utilisateur et spécifie qu’il doit être appelé en réponse à l’événement ServerValidate . Dans le cadre du processus de génération, Visual Studio se chargeait d’effectuer tout le travail difficile en arrière-plan afin de l’implémenter. Malheureusement, il n’y a pas un tel luxe pour nous, et nous devons utiliser la réflexion pour y parvenir.

La méthode SetEventHandler est utilisée pour définir un gestionnaire d’événements et est illustrée ci-dessous.

/// <summary>
/// Set an event handler on a validation control to 
/// invoke a emthod in the user control
/// </summary>
/// <param name="validationControl">The validation 
/// control that wil raise the event</param>
/// <param name="eventName">The name of the event</param>
/// <param name="methodName">The method to invoke</param>
/// <param name="userControl">The user control on 
/// which the method is declared</param>
private void SetEventHandler( Control validationControl, 
  string eventName, string methodName, UserControl userControl)
{
   if ( methodName != null && eventName != null )
   {
      // Get the type object for the validation control
      Type childControlType = validationControl.GetType();

      // Get information on the event
      EventInfo eventInfo = childControlType.GetEvent( eventName );

      // Create a delegate of the correct type that will 
      // invoke the specified method on the class instance 
      // of the user control
      Delegate delegateEventHandler = 
        (Delegate) Delegate.CreateDelegate( eventInfo.EventHandlerType, 
         userControl, methodName);

      // Add the delegate as the eventhandler for the child control
      eventInfo.AddEventHandler( validationControl, delegateEventHandler );
   }
}

En supposant que nous avons reçu un nom d’événement et un nom de méthode non null, la première chose à faire est d’obtenir l’objet Type du contrôle de validation. Cela constitue la racine de la fonctionnalité de réflexion et constitue le principal moyen d’accéder aux métadonnées, telles que les informations d’événement.

Le modèle d’événement de Microsoft .NET Framework repose sur la présence d’un délégué qui connecte un événement à son gestionnaire (et c’est ce que nous essayons de faire : connecter un événement à son gestionnaire).

La classe déléguée peut contenir une référence à une méthode. Contrairement à d’autres classes, une classe déléguée peut contenir uniquement des références aux méthodes qui correspondent à sa propre signature. Par conséquent, un délégué équivaut à un pointeur de fonction de type sécurisé.

Nous invoquons GetEvent sur l’objet Type du contrôle de validation pour obtenir des informations sur l’événement spécifié. Cela nous permet d’accéder à l’objet Type du gestionnaire associé à cet événement. À partir de là, nous sommes en mesure de créer dynamiquement un délégué du type correct.

Il existe un certain nombre de surcharges de la méthode CreateDelegate statique de la classe Delegate . Celui que nous utilisons nous permet de créer un délégué pour (ou vous pouvez le considérer comme un pointeur de fonction vers) une méthode instance spécifiée sur une classe donnée. Par conséquent, nous créons un délégué du type correct qui appellera la méthode spécifiée par le paramètre methodName implémenté dans l’objet de contrôle utilisateur.

Une fois que nous avons notre gestionnaire d’événements délégués, nous l’ajoutons simplement au contrôle de validation en appelant l’addEventHandlerd’eventInfo. Une fois cette opération terminée, chaque fois que l’événement est déclenché sur le contrôle de validation, il appelle la méthode spécifiée à partir du instance du contrôle utilisateur.

Utilisation de DynamicValidationManager

Par conséquent, nous avons maintenant une classe qui, avec un fichier de configuration, implémente un mécanisme pour créer dynamiquement des contrôles de validation et les appliquer à différents contrôles d’entrée utilisateur.

Mais comment utiliser cette classe dans une application web ? Ça ne pourrait pas être plus simple, vraiment. Dans le projet d’application web qui accompagne cet article, j’ai créé très peu de formulaires qui collectent des informations auprès de l’utilisateur. L’idée est qu’ils inscrivent des informations pour un compte de messagerie gratuit. Une fois ces informations entrées, l’utilisateur peut se connecter à son compte et, s’il s’agit d’une application réelle, être en mesure d’envoyer et de recevoir des e-mails.

J’ai implémenté une forme simple d'« application web d’une page ». Cette page unique charge différents contrôles utilisateur comme spécifié par le paramètre de requête « page ». Cela signifie que l’ensemble de la logique métier de l’application est en fait implémenté dans une série de contrôles utilisateur.

Le code-behind des contrôles utilisateur est modifié afin qu’ils dérivent d’une classe d’assistance appelée DVCUserControl (qui elle-même dérive du System.Web.UI.UserControl). Nous substituons la méthode OnInit afin de pouvoir effectuer les étapes supplémentaires nécessaires pour créer un instance du contrôle utilisateur. Comme nous voulons charger dynamiquement des contrôles de validateur dans les différents espaces réservés sur le contrôle utilisateur, nous invoquons simplement la méthode LoadDynamicValidators du gestionnaire de validation dynamique.

Consultez l’illustration ci-dessous.

/// <summary>
/// Used to perform any initialization steps required 
/// to create and set up this instance
/// </summary>
/// <param name="e">The event arguments</param>
protected override void OnInit( EventArgs e)
{
   // Load all dynamically created validators for this user control
   DynamicValidationManager.LoadDynamicValidators( this );
}

Conclusion

Cet article décrit un mécanisme permettant d’implémenter la création dynamique de contrôles de validation pour les applications web. Ce mécanisme est plus efficace lorsqu’il existe des contrôles d’entrée utilisateur pour des informations similaires réparties sur de nombreuses pages ou si les propriétés sous-jacentes des contrôles de validation sont fréquemment modifiées.

Le mécanisme libère les développeurs web d’avoir à se concentrer sur la validation des entrées utilisateur et leur permet de se concentrer sur l’implémentation de la logique métier. Par conséquent, ce mécanisme améliore simultanément la cohérence de l’expérience de l’utilisateur final tout en augmentant la productivité des développeurs.

 

À propos de l’auteur

Callum Shillan est consultant principal travaillant pour Microsoft au Royaume-Uni, spécialisé dans les affaires Internet. Depuis quelques années, il travaille avec C# et ASP.NET sur de grands sites Internet. Callum est accessible à l’adresse callums@microsoft.com.

© Microsoft Corporation. Tous droits réservés.