Valider : Quoi ?
C'est un point de vue personnel et de ce fait sujet à discussion, mais le rôle de l'interface utilisateur
en terme de validation des données devrait rester le plus réduit possible.
Prenons un exemple bien tordu :
La valeur de TextBox1 doit être numérique et inférieure à celle de TextBox2 si, et seulement si la valeur
de DropDownList1 est égale à 4. Si DropDownList1 est égale à 3 ou 2, TextBox1 peut être égale à TextBox2...etc...
Il est possible d'aller très loin dans la complexité et l'on va très rapidement dépasser les
responsabilités d'une interface utilisateur (UI) pour empiéter sur celles de la couche métier.
Alors que peut-on laisser comme validation à la charge de l'UI ? Deux ou trois choses permettant "d'alléger" le
travail de la couche logique, et surtout d'éviter des aller-retours à coup d'exceptions, sans pour autant placer
des règles métier au mauvais endroit.
Pour rester simple, nous allons donc donner deux missions à notre contrôle :
- Vérifier qu'un champ obligatoire soit renseigné.
- Vérifier qu'une entrée soit du type attendu (currency, date, double, integer, string.).
Au passage, et parce que ça ne coûte pas grand chose, on en profitera pour parer à l'absence de la propriété
"MaxLenght" lorsque le TextBox est en mode "Multiline"...
Valider : Comment ?
Pour commencer, nous allons nous servir de l'explorateur d'objets de Visual Studio afin de décortiquer
le fonctionnement des validateurs existants.
Nous pouvons constater les choses suivantes :
- Les validateurs héritent tous
de System.Web.UI.WebControls.BaseValidator
- BaseValidator hérite de Label
(et donc de WebControl).
- BaseValidator implémente l'interface IValidator.
- BaseValidator "override" les méthodes OnInit() et OnUnload()
- La classe System.Web.UI.Page possède une propriété du type
System.Web.UI.ValidatorCollection.
- ValidatorCollection possède une méthode Add() qui attend un IValidator
Nous avons tous les éléments requis pour commencer à construire notre contrôle.
Etudions en détails ces différents points avant de nous attaquer au code.
L'interface IValidator
Donc, pour qu'un contrôle puisse être reconnu comme un validateur de page, il lui suffit
d'implémenter l'interface System.Web.UI.IValidator.
Cette dernière n'est pas bien lourde; elle définit une méthode et deux propriétés :
public interface IValidator
{
void Validate();
string ErrorMessage { get; set; }
bool IsValid { get; set; }
}
Lors du déclenchement de processus de validation, la page va appeler les méthodes Validate() de tous les IValidator qu'elle contient.
S'enregistrer comme validateur
Nous avons vu que BaseValidator surchargeait OnInit() et UnLoad().
On pouvait le deviner (et un petit coup de Reflector nous en apporte la confirmation), c'est dans ces méthodes que
le contrôle s'enregistre et se retire de la collection de validateurs de la page.
Voici donc le code que devra contenir notre TextBox pour réaliser cette opération :
protected override void OnInit(EventArgs e)
{
base.OnInit (e);
this.Page.Validators.Add(this);
}
protected override void OnUnload(EventArgs e)
{
if (Page != null)
{
Page.Validators.Remove(this);
}
base.OnUnload(e);
}
Maintenant que nous savons comment faire passer un contrôle comme un validateur, penchons-nous sur les missions
que nous avons définies.
Champ requis
Cette validation est évidemment la plus simple à mettre en place...
Une simple propriété binaire fera l'affaire :
private bool _Required;
public bool Required
{
get { return _Required; }
set { _Required = value; }
}
Valider le type de donnée
C'est un légèrement plus compliqué, mais évitons de réinventer la roue et cherchons dans les différentes classes de validation.
Une classe devrait rapidement vous sauter aux yeux : System.Web.UI.WebControls.BaseCompareValidator.
Cette classe possède une méthode protégée et statique Compare() qui permet
de comparer deux chaînes en spécifiant un opérateur et un type de donnée.
Les opérateurs disponibles sont :
- ValidationCompareOperator.DataTypeCheck
- ValidationCompareOperator.Equal
- ValidationCompareOperator.GreaterThan
- ValidationCompareOperator.GreaterThanEqual
- ValidationCompareOperator.LessThan
- ValidationCompareOperator.LessThanEqual
- ValidationCompareOperator.NotEqual
Dans notre cas, ValidationCompareOperator.DataTypeCheck est exactement
l'opérateur qu'il nous faut; le seul problème est que la méthode dont nous avons besoin est protégée.
Pour pouvoir y accéder, notre contrôle devrait hériter de BaseCompareValidator,
ce qui n'est bien sûr pas possible (surtout peu pratique) puisque nous voulons conserver l'héritage de
TextBox.
N'ayant pas l'héritage multiple à disposition, nous n'avons pas tellement le choix.
Il va falloir confier ce travail à une nouvelle classe, héritant de BaseCompareValidator,
que nous ajouterons comme membre à notre TextBox et qui se chargera de nous fournir cette méthode.
Voici son code :
internal class TypeValidator : BaseCompareValidator
{
internal TypeValidator() : base() {}
protected override bool EvaluateIsValid()
{
return true;
}
protected ValidationDataType GetValidationDataType(string textType)
{
if (textType == null || textType.Trim() == string.Empty)
{
return ValidationDataType.String;
}
switch (textType.ToLower())
{
case "string" :
return ValidationDataType.String;
case "currency" :
return ValidationDataType.Currency;
case "date" :
return ValidationDataType.Date;
case "double" :
return ValidationDataType.Double;
case "integer" :
return ValidationDataType.Integer;
}
return ValidationDataType.String;
}
internal bool CompareType(string textValue, string textType)
{
return BaseCompareValidator.Compare(textValue,
string.Empty,
ValidationCompareOperator.DataTypeCheck,
this.GetValidationDataType(textType));
}
}
TypeValidator comporte une méthode publique CompareType() qui
va se charger d'appeler BaseCompareValidator.Compare() et de retourner son résultat.
Notez au passage que CompareType n'attend que 2 paramètres et appelle Compare() en lui passant string.Empty comme valeur pour le second paramètre et fixe l'opérateur
de comparaison sur DataTypeCheck.
Nous n'avons pas besoin de plus pour cet exemple, mais si vous reprenez le code, vous aurez peut être à redéfinir CompareType afin de profiter de toute les fonctions
offertes par BaseCompareValidator.Compare().
La méthode Validate()
Une ultime étape est nécessaire avant d'assembler notre TextBox : la méthode Validate() requise par IValidator.
Celle-ci va effectuer les tests suivants :
- Si les propriétés Visible et Enabled du TextBox ne sont pas à true, la validation n'a pas lieu d'être et le contrôle est valide.
- Si la propriété Required est à true, on vérifie que sa propriété Text n'est pas vide.
- A l'aide du TypeValidator que nous venons de voir, on vérifie le type de la valeur.
- En dernier lieu, si la propriété TextBoxMode est égale à Multiline, on fait un test sur MaxLenght.
Et voici donc le code complet de notre contrôle :
using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Text.RegularExpressions;
using System.ComponentModel;
using System.Reflection;
using System.Drawing;
namespace nx.Web.UI.Controls
{
public class SelfValidatingTextBox : TextBox, IValidator
{
private bool _Required;
private string _RequiredType = "string";
private string _ErrorMessage;
private bool _IsValid;
private TypeValidator _TypeValidator = new TypeValidator();
public bool Required
{
get { return _Required; }
set { _Required = value; }
}
public string RequiredType
{
get { return _RequiredType; }
set { _RequiredType = value; }
}
public string ErrorMessage
{
get { return _ErrorMessage; }
set { _ErrorMessage = value; }
}
public bool IsValid
{
get { return _IsValid; }
set { _IsValid = value; }
}
public SelfValidatingTextBox() : base()
{
}
public virtual void Validate()
{
if ( !base.Visible || !base.Enabled )
{
this.IsValid = true;
return;
}
if ( this.Required && this.Text.Trim() == string.Empty )
{
this.ErrorMessage = String.Format("'{0}' is required.", this.ID);
this.IsValid = false;
return;
}
if ( this._TypeValidator.CompareType(this.Text,this.RequiredType) )
{
this.ErrorMessage = String.Format("'{0}' data type is not valid.", this.ID);
this.IsValid = false;
return;
}
if ( this.TextMode == TextBoxMode.MultiLine && this.MaxLength != 0 )
{
if ( this.Text.Length > this.MaxLength )
{
this.ErrorMessage = String.Format("'{0}' is limited to {1} characters", this.ID, this.MaxLength);
this.IsValid = false;
return;
}
}
}
protected override void OnInit(EventArgs e)
{
base.OnInit (e);
this.Page.Validators.Add(this);
}
protected override void OnUnload(EventArgs e)
{
if (this.Page != null)
{
this.Page.Validators.Remove(this);
}
base.OnUnload(e);
}
}
}
Conclusion
Très simple d'utilisation, ce contrôle est susceptible de vous faire gagner pas mal de temps dans le développement de vos pages.
De plus, et ce n'est pas négligeable, il vous assure contre les inévitables oublis qui surviennent lorsque vous devez manuellement
enregistrer vos contrôles dans un validateur existant.