Ärgernisse mit MVC Validierungs-Attributen

Ich habe jetzt ca. 2 Stunden mit den Attributen aus System.ComponentModel.DataAnnotations und System.Web.Mvc und bin froh. Ganz einfach deshalb, weil auch Microsoft und die eigentlich coolen Typen bei MVC auch mal Schwachsinn verzapfen. Macht sie irgendwie menschlich!

Worum geht’s?

In ASP.NET MVC kann man seit jeher Attribute benutzen, um Models, die wiederum an Views gebunden werden mitzugeben, wie sie zu validieren sind. Wer jetzt ausgestiegen ist, der ist entweder kein MVC-Entwickler oder ist über die Einsteiger-Tutorials noch nicht hinweg gekommen. In beiden Fällen rate ich vom Weiterlesen ab.

Alle anderen wissen, was ich meine. In diesem Beitrag möchte ich nun eine Entdeckung kundtun, die wahrscheinlich schon für alle ein alter Hut ist. Mir war sie neu und ich möchte meine Leser daran teilhaben lassen. Na denn.

Was ich versuche

Mein Plan klang irgendwie zunächst ganz einfach. Ziel war es, eine sauber lokalisierte Eingabevalidierung in ASP.NET MVC 4 vorzunehmen. Als .NET-Version kommt natürlich 4.5 zum Einsatz. Angesichts meiner bereits längeren Erfahrung mit dem Thema sollte man meinen, ich hätte sowas schonmal gemacht. Stimmt auch. Nur, dass ja mit jeder MVC-Version alles besser wird und man sich daher immer mal wieder in neue Themen begibt, selbst da, wo man sich bereits sicher fühlte.

Die alte Welt

„Früher war alles besser“, hat mein Opa immer gesagt. Stimmt und stimmt auch wieder nicht. Erstmal der Code, wie ich ihn kannte:

[RegularExpression(@"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}",
    ErrorMessageResourceType = typeof(UiValidation),
    ErrorMessageResourceName = "RegisterModel_EMail_RegularExpression")]
public string EMail { get; set; }

Diesen Code fand ich schon immer blöd, weil ich hier irgendwie Business-Logic mit UI und Model vermengele. Ich konnte z.B. nicht wirklich gut testen, ob denn wohl die Regular Expression die richtige ist, oder ich mich vertippt habe. Indirekt kann man das natürlich über den Controller machen, eine wirklich saubere Vorgehensweise ist es aber nicht.

Die Frage nach ErrorMessageResourceType und ErrorMessageResourceName möchte ich vorwegnehmend auch gleich beantworten. Diese beiden Eigenschaften kommen aus der Klasse System.ComponentModel.DataAnnotations.ValidationAttribute. Diese Klasse bietet außerdem noch eine Eigenschaft ErrorMessageString. Man kann nun entweder ErrorMessageString einen festen Wert zuweisen, oder eben den in Listing 1 gewählten Weg gehen. UiValidation ist hierbei übrigens einfach eine Resource-Datei, die genau so heißt. Abb. 1 zeigt die Datei(en) im Solution Explorer:

Abb. 1: Die Ressourcendatei aus Listing 1
Abb. 1: Die Ressourcendatei aus Listing 1

Somit mach dann auch ErrorMessageResourceName aus Listing 1 Sinn. Es ist einfach der Key einer der String-Resourcen aus dieser Datei.

Die neue Welt – Der Irrweg

Kommt man nun in ASP.NET 4 an, begegnet einem sehr schnell ein neues Attribut. Es heißt DataType und nimmt als einzigen Parameter eine DataTypeEnum-Variable auf. Diese Enum kennt derzeit 17 verschiedene Typen (z.B. Time, Date, Html, Currency und eben auch EmailAddress). Der unbedarfte Entwickler (wie ich auch) wird nun so etwas erzeugen:

[DataType(DataType.EmailAddress,
    ErrorMessageResourceType = typeof(UiValidation),
    ErrorMessageResourceName = "RegisterModel_EMail_EmailAddress")]
public string EMail { get; set; }

Führt man das nun aus, wird man folgendes statt der eigenen Fehlermeldung aus der Ressourcendatei erhalten:

Abb. 2: Hmmm
Abb. 2: Hmmm

Das ist tatsächlich auch so gewollt. Das DataType-Attribute soll nämlich nicht wirklich zur Validierung genutzt werden, sondern wurde für die Kompatibilität mit HTML 5 eingefügt. Sieht man sich den View im Firebug an, erkennt man die Intention:

Abb. 3: Aha!
Abb. 3: Aha!

Durch das in Abb. 3 hervorgehobene type-Attribute wissen nun z.B. iPhones, dass sie eine E-Mail-Tastatur einblenden sollen (um es mal vereinfacht auszudrücken). Dadurch wird nun quasi eine browserseitige Validierung angestoßen. Das haben wir nun also von den mega-schlauen Browsern! Aber für uns heißt das erstmal: Hände weg von DataType für Validierung!

Die neue Welt – Eigentlich so

Das bring uns nun zur neuen Welt von MVC 4 und dem dort vorhandenen Attribut EmailAddress:

[EmailAddress]
public string EMail { get; set; }

Ich habe bewusst die Ressourcen-Dinge weggelassen, um erstmal den gewollten Aha-Effekt zu erzeugen:

Abb. 4: Geht erstmal
Abb. 4: Geht erstmal

Das ist ja einfach! Na denn fügen wir doch einfach die Ressourcen-Dinge hinzu! Gesagt getan:

[EmailAddress(ErrorMessageResourceType = typeof(UiValidation),
    ErrorMessageResourceName = "RegisterModel_EMail_EmailAddress")]
public string EMail { get; set; }

Wenn man das nun ausführt (und natürlich gesetzt den Fall, man hat eine entsprechende Ressource definiert), dann …

Booooom

Abb. 5: Exception im VS (View)
Abb. 5: Exception im VS (View)

und im Browser:

Abb. 6: Exception im Browser
Abb. 6: Exception im Browser

Nachdem der Herzschlag sich wieder normalisiert hat und nachdem dann die Phase der Ungläubigkeit der Was-ist-denn-nun-schon-wieder-Phase gewichen ist und wenn man dann endlich soweit ist, sich an die Fehlersuche zu machen, fällt erstmal die Diskrepanz zwischen der Meldung in Abb. 6 und dem Code in Listing 4 auf.

Ich habe doch ErrorMessageString gar nicht gesetzt! Wir kommt er drauf, ich hätte es getan? Erkenntnis: Das kommt nicht aus meinem Code! Irgendwas ist da in EmailAddressAttribute los. Also rein in den Quellcode:

namespace System.ComponentModel.DataAnnotations
{
  [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
  public sealed class EmailAddressAttribute : DataTypeAttribute
  {
    private static Regex _regex = new Regex("^((([a-z]| ... wow!!!", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled);

    static EmailAddressAttribute()
    {
    }

    public EmailAddressAttribute()
      : base(DataType.EmailAddress)
    {
      this.ErrorMessage = DataAnnotationsResources.EmailAddressAttribute_Invalid;
    }

    public override bool IsValid(object value)
    {
      if (value == null)
        return true;
      string input = value as string;
      if (input != null)
        return EmailAddressAttribute._regex.Match(input).Length > 0;
      else
        return false;
    }
  }
}

Schnell fällt hier Zeile 15 auf. Die EmailAddressAttribute-Klasse ist die einzige Klasse, die ich gefunden habe, die sowas im Konstruktor macht. Jetzt macht die Fehlermeldung auch wieder Sinn und eine Lösung ist schnell gefunden. Listing 4 wird einfach zu:

[EmailAddress(ErrorMessage = null,
    ErrorMessageResourceType = typeof(UiValidation),
    ErrorMessageResourceName = "RegisterModel_EMail_EmailAddress")]
public string EMail { get; set; }

und schon funktioniert es:

Abb. 7: Endlich meine Validierungs-Meldung
Abb. 7: Endlich meine Validierungs-Meldung

Mit anderen Worten: Die Klasse EmailAddressAttribute ist zumindest mal diskussionswürdig implementiert. Die MSDN hat an keiner von mir durchsuchten Stelle ein Wörtchen über dieses seltsame Verhalten verloren und von der Fehlermeldung ausgehend käme man nicht auf eine sinnvolle Lösung.

Leider kein Happy End

Ein Problem haben nun allerdings immer noch Leute, die sowohl schicken HTML-5-Code, als auch vernünftige Validierung haben wollen. Schauen wir uns mal das Ergebnis des folgenden Listings an:

[DataType(DataType.EmailAddress)]
[EmailAddress(ErrorMessage = null,
    ErrorMessageResourceType = typeof(UiValidation),
    ErrorMessageResourceName = "RegisterModel_EMail_EmailAddress")]
public string EMail { get; set; }

Das Ergebnis ist nun das gleiche, wie in Abb. 2 :-(. Ich kann zum jetzigen Zeitpunkt noch nicht sagen, ob es da einen Weg raus gibt, aber ich bleibe dran. Vielleicht kennt ja einer der Leser eine Varianten.

Fazit

JA, MVC wird immer cooler in seinen Features. Aber die Jungs sollten bitte aufpassen, dass die Qualität nicht auf der Strecke bleibt. Scott Hanselman wird nicht müde, zu betonen, wie leicht jetzt die Updates für MVC werden. Jeder, der aber eine Anwendung über nuget mit Updates versorgen wollte und dann die elenden nuget-Rollbacks erlebt hat, weil „jQuery >= 1.4.4“ bei nuget scheinbar bei „jQuery == 2.0x“ false ergibt, der wird mir wohl Recht geben, dass weniger oft mehr sein kann.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.