DataAnnotations sind eigentlich eine nette Sache. Implementiert wurden sie für die RIA Services, können jedoch auch ohne verwendet werden. Die Klassen befinden sich im Namespace System.ComponentModel.DataAnnotations. Bereit gestellt werden folgende Klassen:
| Validator | Kann über ValidateProperty, ValidateObject etc. Eigenschaften und Objektinstanzen validieren. |
| ValidatorContext | Beschreibt den Kontext, der für die Validierung Gültigkeit hat. Beispielsweise kann hier der Name des Members mitgegeben werden, der zu validieren ist. |
| ValidatorAttribute | Davon abgeleitet existieren zahlreiche Klassen, welche über eine Eigenschaft IsValid verfügen und den neuen Wert gegen bestehende Werte, welche über den ValidatorContext geliefert werden prüft. |
| ValidationResult | Liefert das Ergebnis zurück. Entweder ein leeres ValidationResult-Objekt (ValidationResult.Success) oder gefüllt mit einer ErrorMessage. |
| ValidationException | Diese wird ausgelöst, wenn die Validierung fehl schlägt und kann in den Bindings per ValidatesOnExceptions abgefragt werden. |
Der Vorteil dieser Variante ist, dass sämtliche Validierungslogik in eigenen Klassen implementiert und per Attribute eingebunden wird. Einfache Pflege, keine ellenlangen Validierungslogiken in den Settern sind die Vorteile.
Ohne RIA-Services sieht das dann in etwa so aus:
[Required(ErrorMessage="Name is Required")]
[StringLength(20, MinimumLength=5, ErrorMessage="Name must have between 5 to 20 characters")]
public string Name
{
get { return name; }
set
{
Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = "Name" });
name = value;
}
}
Und hier das XAML dazu:
<TextBox Style="{StaticResource SimpleTextBoxStyle}"
Text="{Binding Path=StartDate,
Mode=TwoWay,
ValidatesOnExceptions=True,
NotifyOnValidationError=True}"/>
Problemfall
Die Validierung selbst muss nun nicht zwangsweise am Model statt finden, sondern kann auch im ViewModel geschehen, vor allem dann, wenn die gebundenen Werte nicht sofort in das Model durchgeschrieben werden. Hier kommt es in der Praxis nun dazu, dass Basisklassen von mehreren Ableitungen benötigte Eigenschaften etc. zur Verfügung stellen und die entsprechenden Attribute für die Validierung darauf gesetzt werden. Beispiel:
public class BudgetViewModel
{
private decimal budgetLimit;
[Required]
public decimal BudgetLimit
{
get { return budgetLimit; }
set
{
Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = "BudgetLimit" });
budgetLimit = value;
}
}
}
public class SpecialBudgetViewModel : BudgetViewModel
{
private string specialProperty;
[Required]
public string SpecialProperty
{
get { return specialProperty; }
set
{
Validator.ValidateProperty(value, new ValidationContext(this, null, null) { MemberName = "BudgetLimit" });
budgetLimit = value;
}
}
}
Bei der Ausführung und Test der Validierung schlagen aber sämtliche – in der Basis gesetzte Attribute - nicht an. D.h. die Validierung verläuft immer positiv.
Nach einer kurzen Recherche und zumeist nur sehr einfachen Beispielen blieb nur mehr der Weg, per .NET Reflector die in Frage kommenden Klassen zu betrachten.
Der implementierte Validator verwendet nun eine interne Klasse ValidationAttributeStore, welche die zu validierenden Typen und die zugehörigen Attribute verwaltet. Die Methode GetTypeStoreItem liefert nun im Falle einer Validierung die gewünschten Informationen zu einem bestimmten Typ zurück. Werden diese nicht gefunden, wird ein entsprechender Eintrag für diesen Store erstellt. Dabei werden die benutzerdefinierten Attribute per
Type.GetCustomAttributes(bool inherit)
ausgelesen. Und genau hierin liegt das Problem. Die hauseigene Dokumentation sagt dazu folgendes:
This method ignores the inherit parameter for properties and events. To search the inheritance chain for attributes on properties and events, use the appropriate overloads of the Attribute.GetCustomAttributes method.
Fazit
Der Ansatz ist nett, funktioniert aber nur, wenn keine Basisklassen/Vererbung im Spiel sind/ist. Andernfalls muss man sich nach einer anderen Lösung umsehen und gegebenenfalls eine eigene Implementierung starten.
