Anmerkung: im weiteren Verlauf des Threads gibt es dann Infos zu Aktualisierungen und Beispiel für deren Verwendung.
Wer mit Daten arbeitet muss diese (früher od. später) auch validieren, sprich prüfen ob sie dem erwarteten Format entsprechen, innerhalb des erwarteten Wertebereichs liegen, etc. Hierzu bietet das .net Framework die IDataErrorInfo-Schnittstelle die vorzugsweise in Datenbindungs-Szenarien verwendet wird.
Jeder der damit schon mal gearbeitet hat und den trivialen Ansatz verfolgte wird sich vllt. auch überlegt haben ob es nicht doch eine elegantere Möglichkeit als im jeweiligen Eigenschafts-Setter eine Prüfung durchzuführen wie es die MSDN im IDataErrorInfo Beispiel von Silverlight zeigt oder durch ein if/switch auf dem Eigenschaftsnamen im Indexer von IDataErrorInfo wie es WPF IDataErrorInfo and Databinding zeigt. (Beide Links sind zwar für WPF bzw. Silverlight, für WinForms gilt das aber analog).
In [Artikel] Attribute zur Prüfung von Properties verwenden hat herbivore schon eine Möglichkeit gezeigt wie das Problem eleganter und v.a. deklarativer gelöst werden kann. Hier stelle ich eine Komponente vor, die ebenfalls mit Attributen zur Validierung arbeitet, im Hintegrund aber ganz anders aufgebaut ist.
Die zentrale Klasse für die Verwendung ist DomainObject von dem, wie der Name suggerieren mag, jedes Domänen-Objekt ableiten muss. Eine Beispiel-Klasse schaut dann wie folgt aus:
public class Customer : DomainObject
{
[StringNotBlank]
public string Name { get; set; }
//---------------------------------------------------------------------
[Email]
public string Email { get; set; }
}
Vordefiniert sind ein paar Attribute:
- StringLengthAttribute -> Zeichenfolgenlänge innerhalb min/max
- StringNotNullAttribute -> Zeichenfolge darf nicht null sein
- StringNotBlankAttribute -> Zeichenfolge darf nicht null od. leer sein
- RangeAttribute -> int muss innerhalb eines bestimmten Intervalls (inklusiv) sein
- GreaterOrEqualZeroAttribute -> int muss positiv inkl. 0 sein
- RegexAttribute -> validierte eine Zeichenfolge gegen das angegeben Muster
- EmailAttribute -> validiert ob eine Zeichenfolge einer Email-Adresse entspricht
Im Hintergrund sieht das Ganze so aus wie im angehängten Bild. Für die Verwendung mit den Attributen gibts die Klassen die von ValidationAttribute erben. Die Klassen, die von ValidationRule erben, erledigen die eigentliche Arbeit und können auch selbt, also ohne die Verwendung von Attributen, verwendet werden. Dies kann z.B. angewandt werden wenn die ValidierungsRegeln aus der Konfigurations-Datei od. einer Datenbank geladen werden (das sich aber auch mit eigenen Attributen erreichen lässt).
Eine Wiedergabe des Codes erspare ich mir hier, da zum einem der Code angehängt ist und zum anderen zeige ich lieber wie die bestehenden Klassen verwendet und erweitert werden können (um eigene Attribute und Regeln erstellen zu können).
Verwenden von Validierungsregeln außerhalb von DomainObject:
Die Validierungsregeln, jene Klassen die von ValidationRule erben, können auch ganz ohne DomainObject verwendet werden. Nachfolgend ein Beispiel das die Verwendung der konkreten Klasse DelegateValidationRule<T> zeigt, wobei T der Typparameter für die zu validierende Eigenschaft ist.
Für das Beispiel wird folgende Klasse angenommen:
public class Person
{
public int Age { get; set; }
}
[Test]
[TestCase(true, ExpectedException = typeof(Exception))]
[TestCase(false)]
public void IsValid_ThrowOnInvalid_ThrowsExceptionWhenSet(bool throwOnException)
{
Person p = new Person { Age = -1 };
DelegateValidationRule<int> rule = new DelegateValidationRule<int>(
this.IntValidate,
() => p.Age);
rule.ThrowOnInvalid = throwOnException;
Assert.IsFalse(rule.IsValid("Age"));
}
//---------------------------------------------------------------------
private bool IntValidate(int value, out string msg)
{
msg = "Wert muss ≥ 0 sein.";
return value ≥ 0;
}
Verwenden von Validierungsregeln mit DomainObject:
Die Valdierungsregeln können mit einer von DomainObject abgeleiteten Klasse verwendet werden indem die CreateRules-Methode überschrieben wird.
public class User : DomainObject
{
public string Email { get; set; }
//---------------------------------------------------------------------
protected override List<ValidationRule> CreateRules()
{
EmailValidationRule emailRule = new EmailValidationRule(() => this.Email);
List<ValidationRule> rules = base.CreateRules();
rules.Add(emailRule);
return rules;
}
}
Üblicherweise braucht man CreateRules gar nicht zu überschreiben, sondern wird alle Regelen einfach deklarativ durch die Attribute vorgeben. Mit dem Überschreiben von CreareRules wird es jedoch möglich, die Regeln stattdessen oder ergänzend zur Laufzeit z.B. aus der Konfiguration oder einer Datenbank laden.
Verwenden von Attributen für die Validierung:
Das ist sicherlich die eleganteste und einfachste Art und Weise eine Validierung für eine Eigenschaft zu erstellen.
public class User : DomainObject
{
[StringLength(1, 100)]
public string FullName { get; set; }
[StringLength(1, 100)]
public string UserName { get; set; }
[Range(1, 150)]
public int Age { get; set; }
[Email]
public string Email { get; set; }
}
Als Fehler-Meldungen, wenn die Validierung einer Eigenschaft fehlschlägt, werden je nach Eingabe passende Texte lokalisiert (derzeit Deutsch und Englisch) ausgegeben. Beispielsweise wenn der Wert der Age-Eigenschaft 160 ist dann wird angepasst "Der Wert 160 ist größer als der Maximalwert 150." vom IDataErrorInfo.Indexer["Age"] zurückgegeben.
Es ist auch möglich einen eigenen konstanten Standardtext für Validierung bereit zustellen, der dann verwendet wird wenn die Validierung fehlschlägt.
public class User : DomainObject
{
[Range(1, 150, Message = "Die eingegeben Zahl liegt nicht im Bereich von 1-150.")]
public int Age { get; set; }
}
Wenn gewünscht ist dass ein Laufzeitfehler erzeugt wird so kann dies ebenfalls in der Deklaration des Attributes angegeben werden.
public class User : DomainObject
{
[Range(1, 150, ThrowOnInvalid = true)]
public int Age { get; set; }
}
Die Message- und ThrowOnInvalid-Eigenschaft können auch kombiniert werden und gelten für alle Attribute die von ValidationAttribute erben, also auch für alle die standardmäßig dabei sind.
Erstellen eines eigenen Attributes für die Validierung:
Auf eine Einführung in Attribute wird verzichtet und stattdessen auf [Artikel] Attributbasierte Programmierung verwiesen.
Als Beispiel zeige ich wie ein Attribut für Regex-Validierung implementiert werden kann.
[AttributeUsage(AttributeTargets.Property)]
public class RegexAttribute : ValidationAttribute
{
private readonly string _pattern;
private readonly RegexOptions _regexOptions;
//---------------------------------------------------------------------
public RegexAttribute(string pattern, RegexOptions regexOptions)
{
_pattern = pattern;
_regexOptions = regexOptions;
}
//---------------------------------------------------------------------
protected override ValidationRule CreateRule(
string propertyName,
Delegate propertySelector)
{
return new RegexValidationRule(
_pattern,
propertyName,
(Func<string>)propertySelector)
{
RegexOptions = _regexOptions,
Message = this.Message,
ThrowOnInvalid = this.ThrowOnInvalid
};
}
}
Basierend vom RegexAttribute ist es dann sehr einfach weitere Regex-basierte Regeln zu erstellen. Z.B. für die Email-Validierung:
[AttributeUsage(AttributeTargets.Property)]
public sealed class EmailAttribute : RegexAttribute
{
public EmailAttribute()
: base(@"[A-Z0-9._%+-][email protected][A-Z0-9.-]+\.[A-Z]{2,4}", RegexOptions.IgnoreCase)
{
this.Message = Strings.EmailIsNotValid;
}
}
Über den Pattern für die Email könnte man diskutieren, aber bitte nicht hier in diesem Thema.
Erstellen einer eigenen ValidationRule:
Abschließend zeige ich noch wie eigene Validierungsregelen durch Ableiten von ValidationRule od. besser von ValidationRule<T> erstellt werden können. Gezeigt wird dies anhand der RegexValidationRule.
public class RegexValidationRule : ValidationRule<string>
{
public RegexOptions RegexOptions { get; set; }
protected string Pattern { get; private set; }
public string Message { get; set; }
//---------------------------------------------------------------------
public RegexValidationRule(
string pattern,
string propertyName,
Func<string> propertySelector)
: base(propertyName, propertySelector)
{
this.Pattern = "^" + pattern + "$";
}
//---------------------------------------------------------------------
protected override bool Validate(string value, out string message)
{
message = this.Message ?? Strings.RegexNoMatch;
return value != null && Regex.IsMatch(value, this.Pattern, this.RegexOptions);
}
}
Für weitere Beispiele verweise ich auf die Unit-Tests (im Anhang des nächsten Beitrags).
Ganz besonders bedanke ich mich bei herbivore für das Begutachten des ersten Entwurfs der Komponente.
Wenn ihr eigene Attribute erstellt, würde ich mich freuen, wenn diese hier gepostet werden können - vllt. kann jemand anders diese dann ja auch gebrauchen.
mfG Gü
Schlagwörter: Validierung, validieren, IDataErrorInfo, Attribute, Speedski, DomainObject