Individuelle DataAnnotations bei Entity Framework
Posted by sprinter | Filed under Coding, Programmierung, Tools, Web
Entity Framework (EF) ist an sich eine super Sache. Kein Linke-Hand-rechte-Hand-Code mehr. Die Daten fließen und man kümmert sich um die Details. Allerdings hat das mit dem automatisch generierten Code den Haken, dass man ihn nun nicht mehr ohne weiteres erweitern kann. codingfreaks zeigt, wie man DataAnnotations u.a. Attribute anwenden kann, ohne dass EF in seiner Arbeit gehindert wird.
Bevor es hier so richtig los geht, ein kleiner Hinweis. Ich gehe davon aus, dass der Leser bereits fit im Umgang mit dem EF selbst ist. Hier geht es darum, die Grenzen von Standard-EF zu erweitern.
Das Problem
Die Vorgehensweise ist immer die Gleiche. Man erstellt sich eine Datenbank (sollte man eigentlich immer zuerst machen, trotz code-first), benutzt den EF-Designer im VS und bindet dann das entstehende Modell. Ein erstes UI ist schnell gestrickt und alles funktioniert. Benutzt man hierzu z.B. ASP.NET MVC, braucht man fast schon gar nicht mehr zu tippen, um die üblichen CRUD-Operationen zu implementieren. Einige Entwickler glauben nun, den heiligen Gral gefunden zu haben.
Profis wissen, dass das erst der Anfang ist. Woher soll nun das liebe MVC eigentlich wissen, wie er ein Label beschriften soll, das für die Eigenschaft “Firstname” zuständig ist? Die Textbox wird artig implementiert und das Label zeigt störrisch “Firstname” an. Auch andere Probleme, wie die Validierung von Dateneingaben sind mit den Bordmitteln nur rudimentär umgesetzt.
Man braucht also eine Möglichkeit, die durch das EF generierten Klassen zu erweitern.
Ein Beispiel. Wir benutzen die gute alte AdventureWorks-Datenbank von MS und binden sie per EF in eines unserer Projekte ein. Ziel ist es nun, alle Departments zu verwalten oder vielleicht auch erstmal, sie anzuzeigen. Wählt man nur diese Tabelle im EF-Assistenten aus, sieht das Ergebnis im Designer wie folgt aus:
So weit, so gut. Sieht man sich nun die zugehörige *Designer.cs-Datei an, die EF als Code-Behind zur edmx erstellt hat, sieht man die Name-Eigenschaft inkl. der erweiterten notwendigen Elemente so:
/// <summary>
/// Keine Dokumentation für Metadaten verfügbar.
/// </summary>
[EdmScalarPropertyAttribute(EntityKeyProperty=false, IsNullable=false)]
[DataMemberAttribute()]
public global::System.String Name
{
get
{
return _Name;
}
set
{
OnNameChanging(value);
ReportPropertyChanging("Name");
_Name = StructuralObject.SetValidValue(value, false);
ReportPropertyChanged("Name");
OnNameChanged();
}
}
private global::System.String _Name;
partial void OnNameChanging(global::System.String value);
partial void OnNameChanged();
EF macht eine ganze Menge. Es zeigt evtl. WCF-Services an, dass die Eigenschaft zum Datenvertrag gehört (DataMemberAttribute), INotifyPropertyChanged wird eingebunden und alle dafür notwendigen Event-Handler implementiert. Super!
Jetzt möchten wir aber dafür sorgen, dass ein späreres User-Interface automatisch Masken erzeugt und dabei jeweils sinnvolle Bezeichnungen für Eingabe- und Anzeige-Felder verwendet. Und das bitte mehrsprachig!
Das UI
Ich verzichte in diesem Beispiel mal auf eine strikte Schichten-Trennung und füge einfach ein MVC-Projekt hinzu und setze dann den Link direkt auf das EF-Projekt:
Durch einen Rechtsklick auf den Controllers-Ordner im MVC-Projekt und Auswahl von “Hinzufügen” -> “Controller…” kommt ein Dialog auf, in dem ich folgende Auswahl treffe:
Nachdem man einen Namen vergibt, muss zwingend die Option “Controller with read/write…” ausgewählt sein. Dahinter steckt letztlich wieder ein T4-Mechanismus, der später alle benötigten Controller- und View-Programmierungen erledigt. Wichtig ist außerdem, dass als Model die Department-Klasse aus dem Entities-Projekt gewählt wird und der Context ebenfalls der aus diesem Projekt ist. Achtung: Damit dieser Schritt funktioniert, müssen alle Einstellungen der Web.config richtig gesetzt sein, d.h., die connectionStrings der Web.config müssen bereits richtig eingerichtet sein. Außerdem muss ein erfolgreicher Build der Web-Anwendung erfolgt sein. Sonst hagelt es Fehler.
Wenn alles geklappt hat, sollte der Projektbaum jetzt so aussehen:
Das Projekt kann man nun ausführen und die Bedienung klappt tatsächlich. NOT-NULL-Werte der Datenbank werden übrigens bereits unterstützt. Nach dem Start der Anwendung sieht man zunächst einen Browser mit einer URL á la “http://localhost:2331/”. Um nun in die Liste der Departments zu gelangen, gibt man hinter dem Slash einfach per Hand ein “Departments” ein, also:
und drückt Enter. Wer nun einen Serverfehler erhält, der hat wahrscheinlich das alte Entity-Referenz-Problem. Es hat mit dem Einbinden der Entity-Framework-DLL System.Data.Entity in die Web.config des MVC-Projektes zu tun. Also die web.config öffnen und unterhalb des Tags <compilation> die Liste der <add assembly…> um folgendes Element erweitern:
<code><add assembly=</code><code>"System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"</code> <code>/></code>
Eine weitere relativ unnötige Hürde der MS-Jungs. Wenn mans aber erstmal weiß, nervt es nur noch und führt nicht gleich zu Frustration :-). Hier noch einmal der Standard-<compilation>-Bereich komplett (befindet sich innerhalb der <system.web>-Tags!):
<compilation> <assemblies> <add assembly="System.Data.Entity, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" /> </assemblies> </compilation>
Jetzt sollte man bei Eingabe von “Departments” in der URL die Liste der Departments sehen. Ein Klick auf “Details” sollte nun etwa Folgendes ergeben:
Ich habe gleich mal grün markiert, was uns nun stört. Wir möchten also, dass die Labels automatisch die richtigen Werte anzeigen. Sieht man sich einmal den Inhalt der Datei Details.cshtml im MVC-Projekt im Ordner Departments an, so merkt man, dass eigentlich alles für diese Automatik vorhanden ist. Hier einmal das Razor, das letztlich verantwortlich für den HTML-Aufbau ist. Ich habe bereits einige Änderungen vorgenommen, sodass die Labels nicht fix codiert sind, sondern sich aus den Attributen der Eigenschaften ergeben:
<fieldset> <legend>Department</legend> <div>@Html.LabelFor(model => model.Name)</div> <div>@Html.DisplayFor(model => model.Name)</div> <div>@Html.LabelFor(model => model.GroupName)</div> <div>@Html.DisplayFor(model => model.GroupName)</div> <div>@Html.LabelFor(model => model.ModifiedDate)</div> <div>@Html.DisplayFor(model => model.ModifiedDate)</div> </fieldset>
@Html.LabelFor() prüft intern, ob eine Eigenschaft das DisplayAttribute aus den DataAnnotations besitzt und wertet es aus. Im Moment bringt das aber nichts, weil wir keine Möglichkeit haben, das DisplayAttribut über die Eigenschaft zu setzen, die EF für uns generiert hat.
Was können wir tun?
Zunächst einmal, ist zu klären, was wir nicht tun können! Wir können hier keinen Nutzen aus der Tatsache ziehen, dass EF immer partielle Klassen generiert. Wir können über diesen Mechanismus jetzt zwar weiteren Code zu einer EF-generierten Klasse hinzufügen. Wir können aber nicht einfach irgendwelche generierten Elemente “anreichern”.
Wir haben nun mehrere Möglichkeiten. Variante 1 ist, dass wir das T4 des EntityFrameworks komplett so anpassen, dass es uns aufgrund eigener Logik immer die richtigen Attribute mit den richtigen Werten über die Klassen und Eigenschaften schreibt. Das ist ziemlich komplex und fehleranfällig.
Variante 2 ist da schon bedeutend übersichtlicher. Sie basiert auf einem Attribut, das ebenfalls in den DataAnnotations vorgesehen ist: MetadataTypeAttribute.
MetadataTypeAttribute
Dieses Attribut ist genau für den Einsatzzweck gedacht, der uns hier vorschwebt. Man übergibt ihm einfach den Typ einer Klasse, in der unter Berücksichtigung gewisser Konventionen angegeben wird, wie die EF-generierten Eigenschaften mit Metadataten zu versehen sind.
Unsere Department-Klasse müsste dem Muster entsprechend so dekoriert werden:
[MetadataType(typeof(DepartmentMetadata))] public partial class Department : EntityObject
Was aber soll DeparmentMetadata sein? Ganz einfach: Eine Klasse, die wir schreiben müssen. Sie könnte z.B. wie folgt aussehen:
public class DepartmentMetadata
{
[Display(Name = "Bezeichnung")]
public string Label;
}
Wir erstellen also eine ganz einfache Klasse mit öffentlichen Member-Variablen. Der Member-Variablen verpassen wir nun das oder die Attribute, die wir eigentlich der EF-generierten Klasse geben wollten. MetadataType verlinkt nun beide Klassen so, dass zum Schluss das gewünschte Ergebnis erzeugt wird.
EF muss angepasst werden
Die Erkenntnis ist schon mal ein wichtiger Schritt. Wir müssen aber immer noch dafür sorgen, dass uns der EF-Assistent bei jedem neuen Generierungs-Lauf die MetadataType-Attribute über die Klassen schreibt. Das bedeutet, wir müssen ein wenig T4 anpassen.
Um dies bequem tun zu können, überreden wir als erstes den EF-Designer dazu, die T4-Datei nicht mehr im Projekt zu verstecken, sondern sie uns zur Verfügung zu stellen. Das geht so:
- Doppelklick auf die edmx-Datei im Entities-Projekt.
- Rechtsklick auf einen freien Bereich im EF-Designer.
- Klick auf “Neues Codegenerierungselement hinzufügen…”.
- Auswahl von “ADO.NET EntityObject Generator”.
- Als Dateiname den gleichen verwenden, der bereits beim ersten Assistentenlauf des EF verwendet wurde.
- Klick auf OK. (Sollte VS fragen, ob es überschreiben soll, dann mit Ja antworten).
- (*.tt-Datei wird nun angezeigt).
- Anklicken der edmx-Datei im Projektmappen-Explorer.
- In den Eigenschaften (F4) unter “Benutzerdefiniertes Tool” den Eintrag löschen.
Das Ergebnis sollte im Projektmappen-Explorer wie folgt aussehen:
Unser Standard-EDMX generiert also keine Klassen mehr, sondern überlässt dies dem neuen T4-Skript. Wenn man alles richtig gemacht hat, sollte das Projekt immer noch komplett builden und alles läuft, wie bisher auch.
Nun geht es ans Eingemachte! Wir fügen dem Entities-Projekt einen Verweis auf die System.ComponentModel.DataAnnotations.dll hinzu.
Ein Doppelklick auf die tt-Datei ermöglicht uns das Ändern des Skripts. Als erstes müssen wir den Teil finden, in dem die Namespaces in die cs-Datei geschrieben werden. Er endet bei der Standard-T4-Vorlage in Zeile 67. Hier fügen wir nun ein weiteres using ein:
using System.ComponentModel.DataAnnotations;
Weiter geht es auf Zeile 319 des T4. In der Nähe müsste folgender Block stehen:
<#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity)#> : <#=BaseTypeName(entity, code)#>
Wir müssen nun eine Zeile darüber einfügen, sodass Folgendes stehen bleibt:
[MetadataType(typeof(<#=code.Escape(entity)#>Metadata))] <#=Accessibility.ForType(entity)#> <#=code.SpaceAfter(code.AbstractOption(entity))#>partial class <#=code.Escape(entity)#> : <#=BaseTypeName(entity, code)#>
Wenn wir nun die T4 speichern und das Entities-Projekt erstellen, bekommen wir den Compiler-Fehler, dass der Typ “DepartmentMetadata nicht gefunden wurde. Das ist gut, denn es zeigt uns, dass die Änderung am T4 auf die Ergebnis-Klasse Department durchgeschlagen hat. Sie hat nun dieses Attribut, nur dass wir ihm die Metadaten-Klasse halt nicht gegeben haben. Das kommt jetzt zum Schluss.
Metadaten-Klasse hinzufügen
Dem Entites-Projekt wird eine neue Klasse mit dem Namen DepartmentMetadata hinzugefügt. Diese wird wie folgt angepasst:
public class DepartmentMetadata
{
[Display(Name = "Bezeichnung")]
public string Name;
}
Das Ergebnis beim nächsten Testlauf im Browser sieht im Details-Bereich wie folgt aus:
Genau das wollten wir erreichen! Ohne dass man an allzuviele Änderungen am Original-T4 machen muss, hat man nun immer einen Link zu einer Klasse, die die Anreicherung mit den erforderlichen Attributen vornimmt.
Der Pferdefuss an der Sache ist natürlich, dass man nun jeder durch EF generierten Klasse eine entsprechende Gegenklasse geben muss, damit das Projekt kompiliert. Das ist jedoch zum einen zu verschmerzen, da man in solchen Projekten tatsächlich immer oder nie eine Attributierung braucht. Zum anderen könnte man durch etwas fleißigeres Anpassen des T4 immer eine solche Klasse generieren lassen.
Das T4 kann man sich nach erfolgter Änderung irgendwo abspeichern und es immer wieder verwenden. Ganz Fleißige Zeitgenossen werden es womöglich sogar als Extension für den Neu-Dialog im VS entwerfen.
Im Webcast zu diesem Artikel wird die gesamte Prozedur einmal komplett durchgespielt.
Hier noch das Projekt im VS2010-Format zum Download (bitte daran denken, die web.config anzupassen!): Download EfAnnotationsSample.zip.
