| 

.NET C# Java Javascript Exception

8
Hallo zusammen,
ich beschäftige mich seit einiger Zeit mit WPF und dem MVVM - Pattern. Dabei hänge ich im Moment am meisten bei den Fragen zur Validierung ...

Den Artikel von Josh Smith: Using a ViewModel to Provide Meaningful Validation Error Messages fand ich schon ganz gut. Er setzt dabei auf IDataErrorInfo und setzt z. B. für Zahlwerte im Model (z. B. int Alter) auf eine zweistufige Validierung. Im ViewModel wird zunächst geprüft, ob überhaupt eine Eingabe erfolgt und dann ob sich diese in den erforderlichen Zahlentyp parsen lässt. Danach übergibt er den Wert an das Model und dort erfolgen weitere Prüfungen, z. B. ob das Alter im erlaubten Bereich ist. Diese Überprüfung im Model führt er aber auch wieder per IDataErrorInfo durch:
public class Person : IDataErrorInfo
{
public Person(string name, int age)
{
this.Name = name;
this.Age = age;
}

public string Name { get; private set; }
public int Age { get; set; }

#region IDataErrorInfo Members

public string Error
{
get { return null; }
}

public string this[string propertyName]
{
get
{
if (propertyName == "Age")
{
if (this.Age < 0)
return "Age cannot be less than 0.";

if (120 < this.Age)
return "Age cannot be greater than 120.";
}

return null;
}
}

#endregion // IDataErrorInfo Members
}


Das was mich dabei massiv stört, ist die Verschiebung der Validierung in "this[string propertyName]". Bei mehreren Properties habe ich hier sehr unschöne if - Blöcke.

Ich habe das jetzt erst mal so geändert, dass ich bei meinen Exceptions in den Properties bleibe, denn nur so kann ich auch beim Deserialisieren eines gespeicherten Objektes sicherstellen, dass die Validierung greift:
public class Person
{
public string Name { get; set; }

private int m_Age = 0;

public int Age
{
get { return m_Age; }
set
{
if (m_Age != value)
{
if (value < 0)
{
throw new ArgumentException("Age cannot be less that 0!");
}
if (value > 120)
{
throw new ArgumentException("Age cannot be greater than 120!");
}
m_Age = value;
}
}
}

public Person(string name, int age)
{
Name = name;
Age = age;
}
}


Das ViewModel habe ich dann so abgeändert:
public string this[string propertyName]
{
get
{
if (propertyName == "Age")
{
int age;
string msg = this.ValidateAge(out age);
if (!String.IsNullOrEmpty(msg))
{
return msg;
}
try
{
m_Person.Age = age;
}
catch (ArgumentException ex)
{
return ex.Message;
}
}
return null;
}
}

private string ValidateAge(out int age)
{
age = -1;
string msg = null;

if (String.IsNullOrWhiteSpace(m_AgeText))
{
msg = "Age is missing";
}

if (!Int32.TryParse(m_AgeText, out age))
{
msg = "Age is not a whole number.";
}

return msg;
}


Wie geht Ihr mir dem Problem um? Wie sieht Eure Validierung der Benutzereingaben bei dem MVVM - Pattern aus?

Danke,
Mike

Nachtrag: der MSDN Artikel "How to: Implement Validation Logic on Custom Objects" nutzt auch das Schema mit dem IDataErrorInfo im Model ... :(
News:
01.10.2012
Xantiva 2,3k 2 9
Xantiva 2,3k 2 9
Grummel, irgendwie habe ich die Tag zuerst gesetzt und bin dann auf Enter gekommen. Scheinbar hat er dann gleich meine leere Frage gespeichert. Sorry!
Xantiva 01.10.2012
Hm, war meine Frage so blöd, dass keiner antwortet? <grübel /> Ich habe leider immer noch keine Lösung, denn wenn ich statt einer ArgumentException dann mal eine ArgumentOutOfRangeException werfe, dann steht immer noch "Parametername: XYZ" bei der Fehlermeldung dabei :(
Xantiva 02.10.2012
4 Antworten
3
Hi,

wir nutzen zur Validierung von Business Entitäten den Validation Application Block der Enterprise Library.

Kiurz umrissen läuft das so ab:
Unsere Business Entitäten werden aus dem Entity Framework per T4 erzeugt. Das T4 Template haben wir so angepasst, dass jede erzeugte Entitätenklasse ein Attribut bekommt, welches eine Metadatenklasse definiert. In der Metadatenklasse führen wir nun alle Properties auf, zu denen wir über Attribute ein oder mehr Validierungsregeln hinterlegen. Die Validatoren kommen aus der Enterprise Library bzw. in Sonderfällen werden die selbst programmiert.

Im UI validiert die EntLib die Usereingaben pro Property noch bevor die Daten im DataContext landen. In der Business-Schicht rufen wir die Validierung für eine gesamte Entität zu Fuss auf.

So haben wir einen Satz an Regeln, getrennt von der Business-Entität, welche bereits bei der Usereingabe im UI, als auch in der Business-Schicht greifen. Und testen lässt sich das Konstrukt auch wunderbar.
05.10.2012
Andreas Richter 1,7k 1 2 8
Hallo Andreas,
hm, dass muss ich mir noch mal genauer ansehen. Mit den Validierungsregeln als Attribut werde ich bei einigen Objekten nicht arbeiten können, da die "Range" - Werte z. B. variabel sind. (Aber das kann man ja scheinbar mit ([SelfValidation]) realisieren. Ich konnte auf die Schnelle nichts zur Serialisierung finden. Hast Du Erfahrungen damit, solche Objekte dann zu (de-)serialisieren?
Xantiva 08.10.2012
Variable Validierungswerte hatten wir bisher noch nicht. Aber es gibt die Möglichkeit eigene Validierungsregeln zu schreiben. Die könnten dann u.U. mit Variablen gesteuert werden. Ist aber nur eine Vermutung

Zur (De-)Serialisierung: Sowohl server- als auch clientseitig verwenden wir die gleichen Entitätsklassen (Querschnittsfunktion), sodass auf beiden Seiten die Validierungsattribute vorhanden sind und somit nicht mit übertragen werden müssen. Macht ja auch Sinn und spart vor allem Traffic.
Andreas Richter 08.10.2012
1
Hi, die unschönen If Blöcke kannst du beseitigen indem du beim setzten der Properties die Validierung durchführst. Das Ergebnis der Validierung wird dann in einem Dictionary einfügt. IDataErrorInfo, public string this[string propertyName], greift dann auf dieses Dictionary zu.

Im Beispiel (ungetestet und an dein Beispiel angelehnt) kann es sein dass du noch das INotifyPropertyChanged Event implementieren und bei Age aufrufen musst.

public <ctor>()
{
errorList = new Dictionary<string,string>();
errorList.Add("Age", null);
}

Dictionary<string,string> errorList;
public string this[string propertyName]
{
get
{
if (!errorList.Contains(propertyName)) return "";
return errorList[propertyName];
}
}
public int Age
{
get { return m_Age; }
set
{
if (m_Age == value) return;
m_Age = value;
if (value < 0) errorList["Age"] = "Age cannot be less that 0!";
if (value > 120) errorList["Age"] = "Age cannot be greater than 120!";
// OnPropertyChanged("Age");
}
}
08.10.2012
smartic 510 1 8
smartic 510 1 8
Hallo smartic,
die Idee mit dem Dictionary lässt den Code schon mal deutlich besser aussehen. ;)
Allerdings benötige ich die Exceptions in den Settern weiterhin, denn das Model kann z. B. per Xml Serializer deserialisiert werden und dann muss auch sichergestellt werden, dass alles valide ist. Der Serializer kennt ja IDataErrorInfo nicht und würde somit alles zulassen (was vom Typ her passt).
Aber auch hier stört mich, dass ich die Kapselung meines Model für irgend eine Art von "View" aufbrechen muss ...
Xantiva 08.10.2012
* Lösung ist nur ein Vorschlag, der gerne ausgebaut werden mag. ;-)
* Wenn du beim Setter (Model) ein Exception wirfst, wird das View nie ein Error anzeigen. Grund: m_Age kann nie gesetzt werden, Validiert(Error) wird nur bei einem OnPropertyChanged ausgewertet (habe ich zumindest so in Erinnerung)
* Wenn du den Model Layer aber nicht "Aufbrechen" willst, hilft vielleicht auch ein ValidierungsLayer der zw. View und Model liegt, wahrscheinlich ähnlich wie im Post von Andreas Richter.
* Bin mir nicht sicher, aber es müsste auch noch ein Validierungs Konstrukt ähnlich Error geben.
smartic 08.10.2012
1
Eigene Erfahrungen kann ich leider nicht beisteuern - habe schon ewig kein Native UI mehr bauen dürfen :-(

Aber ein paar Gedanken möchte ich teilen...

Das was mich dabei massiv stört, ist die Verschiebung der Validierung in "this[string propertyName]". Bei mehreren Properties habe ich hier sehr unschöne if - Blöcke.

Ja, die würden mich auch stören. Es hindert Dich aber niemand, die einzelnen Validierungen in kleine, übersichtliche Methoden auszulagern und diese im Indexer nur noch der Reihe nach aufzurufen.

Ich habe das jetzt erst mal so geändert, dass ich bei meinen Exceptions in den Properties bleibe, denn nur so kann ich auch beim Deserialisieren eines gespeicherten Objektes sicherstellen, dass die Validierung greift.

Warum willst Du bei der Deserialisierung validieren? Wenn Du befürchten mußt, das jemand die serialisierten Daten manipuliert, greift eine fachliche Validierung einzelner Properties bei weitem zu kurz (wenn der Name von "Müller" zu "Meier" verändert wird, ist das vermutlich valide, aber trotzdem fatal...) Wenn es Dir darum geht, Objekte zu identifizieren, die früher mal valide waren, es nun aber aufgrund geänderter Regeln nicht mehr sind (weil vielleicht Personen nun mindestens 18 statt 0 Jahre alt sein müssen), dann würde ich mir das Werfen von Exceptions sehr gut überlegen. Oft will man mit den Daten ja trotzdem noch was anstellen - sie gar nicht mehr laden zu können, könnte sich bald als gezielter Schuß ins Knie herausstellen.

Zu Deinem ViewModel-Code: Du machst da zwei Dinge auf einmal: technische/syntaktische (TryParse: ist das überhaupt eine Zahl?) und fachliche/semantische Validierung (ist diese Zahl ein gültiges Alter?). Ersteres würde ich, wenn schon Datenbindung im Spiel ist, dem Framework überlassen. Im ViewModel sollte Age schon ein int sein.

Hast Du Dir mal Thinktecture.DataObjectModel angesehen? Wurde vor zwei Jahren in der dotnetpro vorgestellt. Ich hab's bislang noch nicht ausprobieren können, fand das Konzept aber durchdacht und überzeugend. Da ist auch ein wenig Unterstützung für Validierung dabei (kurz beschrieben in der dnp 4/2010). Jörg Neumann schrieb damals dazu:
Datenklassen sollten nach Möglichkeit keine Logik enthalten, besonders wenn sie zwischen den Schichten einer verteilten Anwendung transferiert werden. ... Als Alternative ... können Sie ... Validierungsregeln ... hinterlegen. ... Auf diese Weise können Sie ihre Validierungslogik in separate Klassen auslagern
Selbst wenn Du die Bibliothek nicht einsetzt - diese Idee solltest Du auf jeden Fall mal für Deinen Anwendung bewerten.
08.10.2012
Matthias Hlawatsch 13,2k 4 9
Hi Matthias,
Danke für die Antwort, das DataObjectModel werde ich mir anschauen.
Warum ich bei der Deserialisierung validieren muss? Ich verwende das Command - Pattern, um "Meßprotokolle" für ein Meßgeräte zu erstellen. Die einzelnen Commands können dann serialisiert in einer Xml Datei vorliegen. Zum Messen nehme ich dann die Datei, deserialisiere meinen einzelnen Commands und führe die dann aus. Unterschiedliche "Werte" für die Command - Objekte sind also völlig normal. Je nach angeschlossener Hardware können aber z. B. die erlaubten Bereiche variieren.
Xantiva 10.10.2012
0
sollte Kommentar werden. :-(
08.10.2012
smartic 510 1 8
smartic 510 1 8

Stelle deine .net-Frage jetzt!
TOP TECHNOLOGIES CONSULTING GmbH