ASP.NET MVC

Überschreiben der standardmäßigen Gerüstvorlagen

Jonathan Waldman

Codebeispiel herunterladen

Alltägliche Routineaufgaben wie Erstellen, Abrufen, Aktualisieren und Löschen im Datenspeicher wird passend mit dem geläufigen Akronym CRUD (create, retrieve, update, delete) umschrieben. Microsoft bietet eine nützliche Gerüstengine, die von T4-Vorlagen unterstützt wird und die Erstellung grundlegender CRUD-Controller und Ansichten für Modelle in ASP.NET MVC-Anwendungen automatisieren, die das Entity Framework verwenden. (Es sind aktuell auch WebAPI und MVC ohne Entity Framework Scaffolders verfügbar.)

Gerüste erzeugen Seiten, die navigierbar und verwendbar sind. Dadurch sorgen sie für weniger Monotonie bei der Erstellung von CRUD-Seiten. Allerdings bieten erstellte Ergebnisse nur begrenzte Funktionen, sodass Sie sich bald mit der Optimierung der erzeugten Controllerlogik und der Ansichten beschäftigen und sie an Ihre Anforderungen anpassen können.

Jedoch besteht dabei die Gefahr, dass die Gerüsterstellung zu einem Einwegprozess mutiert. Sie können für Ihren Controller und die Ansichten nicht erneut ein Gerüst erstellen und Änderungen an einem Modell sichtbar machen, ohne die Optimierungen zu überschreiben. Achten Sie daher darauf, welche Module Sie angepasst haben, damit Sie wissen, für welche Modelle problemlos neue Gerüste erstellt werden können und für welche nicht.

In einer Teamumgebung ist diese Nachverfolgung aber nicht so einfach durchzusetzen. Hinzu kommt, dass ein Bearbeitungscontroller die meisten Modelleigenschaften in der Ansicht "Bearbeiten" anzeigt und möglicherweise vertrauliche Informationen sichtbar macht. Er verwendet automatisch die Modellbindung und speichert alle durch die Ansicht übermittelten Eigenschaften, was das Risiko von Massenzuweisungen erhöht.

In diesem Artikel werde ich Ihnen erklären, wie Sie projektspezifische Anpassungen der T4-Vorlagen erstellen, die das Gerüstsubsystem für ASP.NET MVC, Entity Framework und CRUD unterstützen. Des Weiteren zeige ich Ihnen, wie Sie die Postbackhandler "Erstellen" und "Bearbeiten" des Controllers erweitern, um Ihren eigenen Code in die Postbackmodellbindung und die Datenspeicherpersistenz einbringen können.

Um das Massenzuweisungsproblem anzugehen, erstelle ich ein benutzerdefiniertes Attribut, mit dem die volle Kontrolle darüber erreicht werden kann, welche Modelleigenschaften gespeichert werden und welche nicht. Danach füge ich ein weiteres benutzerdefiniertes Attribut hinzu, mit dem Sie eine Eigenschaft als schreibgeschütztes Etikett in der Ansicht "Bearbeiten" anzeigen können.

Anschließend haben Sie eine bisher unerreichte Kontrolle über Ihre CRUD-Seiten sowie darüber, wie Ihre Modelle angezeigt und gespeichert werden, während Ihre Anwendung besser vor Angriffen geschützt ist. Das Beste daran: Sie werden diese Methoden für alle Modelle in Ihren ASP.NET MVC-Projekten nutzen und Controller und Ansichten sicher neu erstellen können, wenn Modelle geändert werden.

Projekteinstellungen

Ich habe diese Lösung mithilfe von Visual Studio 2013 Ultimate, ASP.NET MVC 5, Entity Framework 6 und C# erstellt (die besprochenen Methoden sind auch mit Visual Studio 2013 Professional, Premium und Express für das Web und Visual Basic .NET kompatibel). Für den Download habe ich zwei Lösungen erstellt: Die erste ist eine Basislösung, die Sie verwenden können, um mit einem Projekt anzufangen und diese Methoden manuell zu implementieren. Bei der zweiten handelt es sich um eine vollständige Lösung, die alle besprochenen Verbesserungen enthält.

Jede Lösung enthält drei Projekte: eines für die ASP.NET MVC-Website, eines für Entitätsmodelle und T4-Gerüstfunktionen sowie eines für den Datenkontext. Der Datenkontext der Lösungen deutet auf eine SQL Server Express-Datenbank hin. Zusätzlich zu den bereits erwähnten Abhängigkeiten fügte ich mithilfe von NuGet Bootstrap hinzu, um die Gerüstansichten thematisch zu gestalten.

Das Gerüstsubsystem wird beim Setup installiert, wenn Sie die Setupoption für Microsoft Web Developer Tools aktivieren. Die nachfolgenden Service Packs für Visual Studio werden die Gerüstdateien dann automatisch aktualisieren. Sie erhalten sämtliche Aktualisierungen für das Gerüstsubsystem, das zwischen Service Packs von Visual Studio im aktuellen Microsoft Web Platform Installer veröffentlicht wurde. Laden Sie diese unter bit.ly/1g42AhP herunter.

Sollte es beim Download mit dem in diesem Artikel enthaltenen Code Probleme geben, stellen Sie sicher, dass Sie über die aktuelle Version verfügen und die ReadMe.txt-Datei sorgfältig gelesen haben. Ich werde diese bei Bedarf aktualisieren.

Festlegen der Geschäftsregeln

Um den gesamten Workflow für die Erstellung von CRUD-Ansichten zu illustrieren und Ablenkungen zu reduzieren, werde ich mit einem sehr einfachen Entitätsmodell namens "Produkt" arbeiten.

public class Product { public int ProductId { get; set; } public string Description { get; set; } public DateTime? CreatedDate { get; set; } public DateTime? ModifiedDate { get; set; } }

Grundsätzlich erkennt MVC, dass ProductId der Primärschlüssel ist. Jedoch ist unklar, dass ich bezüglich der CreatedDate- und ModifiedDate-Eigenschaften besondere Anforderungen habe. Wie der Name schon vermuten lässt, möchte ich, dass CreatedDate mitteilt, wenn das betreffende Produkt (durch ProductId dargestellt) in die Datenbank eingefügt wurde. Ebenso möchte ich, dass ModifiedDate mitteilt, wann es zuletzt geändert wurde (ich verwende hierfür UTC-Werte für Datum und Uhrzeit).

Ich möchte den ModifiedDate-Wert in der Ansicht "Bearbeiten" als schreibgeschützten Text anzeigen (falls der Datensatz nie geändert wurde, sind ModifiedDate und CreatedDate gleich). Ich möchte aber CreatedDate in keiner der Ansichten anzeigen. Ich möchte auch nicht, dass Benutzer diese Datumsangaben bereitstellen oder ändern können, deshalb werde ich keine Formularsteuerelemente wiedergeben, die hierzu Eingaben in den Ansichten "Erstellen" oder "Bearbeiten" erfasst.

Um diese Werte vor Angriffen zu schützen, möchte ich sicherstellen, dass die Werte nicht für Postback bestehen bleiben, auch wenn ein findiger Hacker diese als Formularfelder oder Abfragezeichenfolge bereitstellen kann. Da ich diese Regeln für Geschäftslogikebenen berücksichtige, möchte ich erreichen, dass die Datenbank nicht für die Pflege dieser Spaltenwerte zuständig ist (z. B. möchte ich keine Auslöser erstellen oder eine Definitionslogik für Tabellenspalten einbetten.

Detaillierte Untersuchung des CRUD-Gerüstworkflows

Wir untersuchen zunächst die standardmäßige Funktion des Gerüsts. Um dem Ordner "Controller" des Webprojekts einen Controller hinzuzufügen, klicke ich mit der rechten Maustaste darauf und wähle "Controller hinzufügen". Dadurch wird das Dialogfeld "Gerüst hinzufügen" geöffnet (siehe Abbildung 1).

The MVC 5 Add Scaffold Dialog
Abbildung 1 Dialogfeld "Gerüst hinzufügen" des MVC 5

Ich werde den Eintrag "MVC 5 Controller with views, using Entity Framework” (MVC 5-Controller mit Ansichten und Entity Framework) verwenden, weil er das Gerüst für den CRUD-Controller und die Ansichten für ein Modell bietet. Wählen Sie den Eintrag und klicken Sie auf "Hinzufügen". Das nächste Dialogfeld enthält einige Optionen, die zu Parametern für die T4-Vorlagen werden, die sie anschließend transformiert (siehe Abbildung 2).

The Add Controller Dialog
Abbildung 2 Dialogfeld "Controller hinzufügen"

Geben Sie "ProductController" als Namen für den Controller ein. Lassen Sie das Kontrollkästchen "Use the async controller actions” (Verwenden Sie asynchrone Controlleraktionen) der Einfachheit halber deaktiviert (asynchrone Operationen sprengen nämlich den Rahmen dieses Artikels). Wählen Sie als Nächstes die Modellklasse des Produkts aus. Da Sie Entity Framework verwenden, benötigen Sie eine Datenkontextklasse. Klassen, die von System.Data.Entity.DbContext erben, werden in der Dropdownliste angezeigt. Wählen Sie daraus die richtige aus, wenn Ihre Lösung mehr als einen Datenbankkontext verwendet. Bezüglich der Ansichtsoptionen wählen Sie "Generate views" (Ansichten erzeugen) und "Use a layout page" (Layoutseite verwenden). Lassen Sie das Textfeld der Layoutseite leer.

Wenn Sie auf "Hinzufügen" klicken, werden einige T4-Vorlagen transformiert, um erstellte Ergebnisse anzuzeigen. Dieser Prozess erzeugt Code für einen Controller (ProductController.cs), der in den Ordner "Controller" des Webprojekts geschrieben wird, und fünf Ansichten (Create.cshtml, Delete.cshtml, Details.cshtml, Edit.cshtml, und Index.cshtml), die in den Ordner "Ansichten" des Webprojekts geschrieben werden. Sie verfügen jetzt über einen funktionierenden Controller und alle CRUD-Ansichten, die Sie für die Verwaltung von Daten in der Produktentität benötigen. Sie können die Webseiten sofort verwenden und mit der Indexansicht beginnen.

Wahrscheinlich möchten Sie, dass Ihre CRUD-Seiten für alle Modelle in Ihrem Projekt ähnlich aussehen und sich ähnlich verhalten. Die Verwendung von T4-Vorlagen für die Erstellung von CRUD-Seiten unterstützt diese Konsistenz. Das bedeutet, dass Sie Controller oder Ansichten besser nicht direkt ändern sollten. Ändern Sie stattdessen die T4-Vorlagen, aus denen sie erzeugt werden. Folgen Sie dieser Vorgehensweise, um sicherzustellen, dass die erstellten Dateien ohne weitere Änderungen verwendet werden können.

Mängel des Controllers

Sie können zwar mit dem Gerüstsubsystem schnell starten, aber der daraus generierte Controller weist einige Mängel auf. Ich werde Ihnen zeigen, wie Sie Verbesserungen vornehmen können. Sehen Sie sich in Abbildung 3 die Aktionsmethoden für den erstellten Controller an, die die Vorgänge "Erstellen" und "Bearbeiten" abwickeln.

Abbildung 3 Aktionsmethoden für den erstellten Controller zur Abwicklung der Vorgänge Erstellen und Bearbeiten

public ActionResult Create( [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] Product product) { if (ModelState.IsValid) { db.Products.Add(product); db.SaveChanges(); return RedirectToAction("Index"); } return View(product); } public ActionResult Edit( [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] Product product) { if (ModelState.IsValid) { db.Entry(product).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }

Das Bind-Attribut für jede Methode enthält explizit jede Eigenschaft im Produktmodell. Wenn ein MVC-Controllermodell alle Modelleigenschaften nach einem Postback bindet, wird das als Massenzuweisung bezeichnet. Dieser Vorgang wird auch "Overposting" genannt und stellt ein ernsthaftes Sicherheitsrisiko dar. Hacker können diese Sicherheitslücke ausnutzen, da der Vorgang einen SaveChanges-Aufruf im Datenbankkontext nach sich zieht. Dadurch wird sichergestellt, dass das Modell im Datenspeicher verbleibt. Die Controllervorlage des erstellten CRUD-Systems in MVC 5 verwendet standardmäßig generierten Massenzuweisungscode für die Postback-Aktionsmethoden Erstellen und Bearbeiten.

Eine weitere Folge der Massenzuweisung tritt auf, wenn Sie bestimmte Eigenschaften im Modell ausstatten, sodass sie in der Ansicht "Erstellen" oder "Bearbeiten" nicht mehr gerendert werden. Diese Eigenschaften werden nach der Modellbindung auf null gesetzt. (Siehe Abschnitt “Verwenden von Attributen zur Unterdrückung von Eigenschaften in CRUD-Ansichten”. Hier sind alle Attribute aufgeführt, mit denen Sie festlegen können, ob Sie erstellte Eigenschaften in generierte Ansichten rendern sollten.) Um dies zu veranschaulichen, werde ich zunächst zwei Attribute zum Produktmodell hinzuzufügen:

public class Product { public int ProductId { get; set; } public string Description { get; set; } [ScaffoldColumn(false)] public DateTime? CreatedDate { get; set; } [Editable(false)] public DateTime? ModifiedDate { get; set; } }

Wenn ich den Gerüstprozess über die Option "Controller hinzufügen" erneut ausführe, stellt das Attribut [Scaffold(false)] sicher, dass CreatedDate in keiner der Ansichten angezeigt wird. Das Attribut [Editable(false)] sorgt dafür, dass ModifiedDate in den Ansichten "Löschen", "Details" und "Index" aber nicht in den Ansichten "Erstellen" oder "Bearbeiten" angezeigt wird. Wenn Eigenschaften nicht für die Ansichten "Erstellen" oder "Bearbeiten" gerendert werden, werden sie nicht im HTTP-Anforderungsstream des Postback angezeigt.

Das ist ein Problem, da während des Postback die letzte Möglichkeit besteht, den Modelleigenschaften in diesen von MVC unterstützten CRUD-Seiten Werte zuzuweisen. Wenn also beim Postback der Eigenschaftswert gleich null ist, ist dieser Wert an das Modell gebunden. Das Modell wird im Datenspeicher verbleiben, wenn SaveChanges im Datenkontextobjekt ausgeführt wird. Geschieht dies in der Postbackaktionsmethode "Bearbeiten", wird die Eigenschaft durch den Wert Null ersetzt. Und dies wiederum führt zur Löschung des aktuellen Werts im Datenspeicher.

In meinem Beispiel würde der von CreatedDate im Datenspeicher gehaltene Wert verloren gehen. Tatsächlich wird jede Eigenschaft, die nicht in der Ansicht "Bearbeiten" gerendert wird, dazu führen, dass der Datenspeicherwert mit Null überschrieben wird. Wenn Modelleigenschaft oder Datenspeicher die Zuweisung eines Werts von null nicht zulassen, wird eine Fehlermeldung im Postback angezeigt. Um diese Mängel zu beseitigen, ändere ich die T4-Vorlage, die für die Generierung des Controllers zuständig ist. 

Überschreiben der erstellten Gerüstvorlagen

Um die Art der Erstellung von Controller und Ansichten zu ändern, müssen die T4-Vorlagen geändert werden, die sie generieren. Sie können die Originalvorlagen ändern, was sich auf den Erstellungsprozess für alle Visual Studio-Projekte global auswirkt. Sie könnten ebenso projektspezifische Kopien der T4-Vorlagen ändern, sodass nur das Projekt betroffen ist, in dem die Kopien liegen. Letzteres werde ich jetzt tun.

Die originalen T4-Erstellungsvorlagen befinden sich im Ordner %programfiles%\Microsoft Visual Studio 12.0\Common7\IDE\Extensions\Microsoft\Web\Mvc\Scaffolding\Templates. (Diese Vorlagen sind von verschiedenen .NET-Assemblys abhängig, die im Ordner %programfiles%\­Microsoft Visual Studio 12.0\Common7\IDE\Extensions\Microsoft\Web Tools\Scaffolding liegen. Ich werde mich auf die Vorlagen konzentrieren, die den CRUD-Controller und die Ansichten im Entity Framework erstellen. Diese sind in Abbildung 4 zusammengefasst.

Abbildung 4 Die T4-Vorlagen für die Erstellung von CRUD Controller und Ansichten im Entity Framework

Unterordnernamen der Gerüstvorlagen

Dateiname der Vorlage

(.cs für C#; .vb für Visual Basic .NET)

Erzeugt diese Datei

(.cs für C#; .vb für Visual Basic .NET)

MvcControllerWithContext

Controller.cs.t4

Controller.vb.t4

Controller.cs

Controller.vb

MvcView

Create.cs.t4

Create.vb.t4

Create.cshtml

Create.vbhtml

MvcView

Delete.cs.t4

Delete.vb.t4

Delete.cshtml

Delete.vbhtml

MvcView

Details.cs.t4

Details.vb.t4

Details.cshtml

Details.vbhtml

MvcView

Edit.cs.t4

Edit.vb.t4

Edit.cshtml

Edit.vbhtml

MvcView

Index.cshtml

Index.vbhtml

Index.cshtml

Index.vbhtml

Zur Erstellung projektspezifischer Vorlagen kopieren Sie die Dateien, die Sie überschreiben möchten, aus dem Ordner mit originalen T4-Erstellungsvorlagen in einen Ordner im ASP.NET MVC-Webprojekt namens CodeTemplates (Ordner muss exakt diesen Namen haben). Üblicherweise sucht das Gerüstsubsystem zuerst im Ordner CodeTemplates im MVC-Projekt nach einer passenden Vorlage.

Damit das auch funktioniert, müssen Sie die spezifischen Unterordner- und Dateinamen, die Sie im Originalvorlagenordner sehen, präzise replizieren. Ich habe die T4-Dateien kopiert, die ich vom CRUD-Vorgang für das Gerüstsubsystem des Entity Framework überschreiben möchte. Siehe meine CodeTemplates des Webprojekts in Abbildung 5.

The Web Project’s CodeTemplates
Abbildung 5 CodeTemplates des Webprojekts

Außerdem habe ich die Dateien Imports.include.t4 und ModelMetadataFunctions.cs.include.t4 kopiert. Das Projekt benötigt diese Dateien zur Erzeugung der Ansichten. Darüber hinaus habe ich nur die Dateien mit C# (.cs) kopiert (wenn Sie Visual Basic .NET verwenden, sollten Sie alle Dateien kopieren, deren Dateinamen .vb enthalten). Das Gerüstsubsystem transformiert diese projektspezifischen Dateien, nicht deren globalen Versionen.

Erweitern der Aktionsmethoden "Erstellen" und "Bearbeiten"

Da ich jetzt projektspezifische T4-Vorlagen habe, kann ich sie nach Bedarf ändern. Zunächst erweitere ich die Aktionsmethoden "Erstellen" und "Bearbeiten" des Controllers, damit ich das Modell überprüfen und ändern kann, bevor es dauerhaft gespeichert wird. Um den von der Vorlage so allgemein wie möglich generierten Code zu behalten, werde ich der Vorlage keine modellspezifische Logik hinzuzufügen. Stattdessen rufe ich eine externe Funktion auf, die an das Modell gebunden ist. Auf diese Weise werden die Vorgänge "Erstellen" und "Bearbeiten" des Controllers erweitert, während sie am Modell Polymorphie simulieren. Zu diesem Zwecke werde ich eine Oberfläche erstellen und sie IControllerHooks nennen.

namespace JW_ScaffoldEnhancement.Models { public interface IControllerHooks { void OnCreate(); void OnEdit(); } }

Als nächstes ändere ich die Vorlage Controller.cs.t4 (im Ordner CodeTemplates\­MVCControllerWithContext), damit ihre Postback-Aktionsmethoden "Erstellen" und "Bearbeiten" jeweils die Modellmethoden "OnCreate" und "OnEdit" aufrufen, wenn das Modell IControllerHooks implementiert hat. Die Methode zur Erstellung der Postbackaktion des Controllers wird in Abbildung 6 gezeigt, seine Methode zur Bearbeitung der Postbackaktion ist in Abbildung 7 zu sehen.

Abbildung 6 Erweiterte Version der Methode zur Erstellung der Postbackaktion des Controllers

public ActionResult Create( [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] Product product) { if (ModelState.IsValid) { if (product is IControllerHooks) { ((IControllerHooks)product).OnCreate(); } db.Products.Add(product); db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }

Abbildung 7 Erweiterte Version der Methode zur Erstellung der Postbackaktion des Controllers

public ActionResult Edit( [Bind(Include="ProductId,Description,CreatedDate,ModifiedDate")] Product product) { if (ModelState.IsValid) { if (product is IControllerHooks) { ((IControllerHooks)product).OnEdit(); } db.Entry(product).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(product); }

Jetzt werde ich die Produktklasse so ändern, dass sie IController­Hooks implementiert. Dann füge ich den Code hinzu, den ich ausführen möchte, wenn der Controller OnCreate und OnEdit aufruft. Die neue Produktmodellklasse wird in Abbildung 8 gezeigt.

Abbildung 8 Produktmodell, das IControllerHooks für die Controllererweiterung implementiert

public class Product : IControllerHooks { public int ProductId { get; set; } public string Description { get; set; } public DateTime? CreatedDate { get; set; } public DateTime? ModifiedDate { get; set; } public void OnCreate() { this.CreatedDate = DateTime.UtcNow; this.ModifiedDate = this.CreatedDate; } public void OnEdit() { this.ModifiedDate = DateTime.UtcNow; } }

Natürlich gibt es unterschiedliche Methoden, um diese "Erweiterungslogik" zu implementieren. Durch die Verwendung dieser einzeiligen Änderung der Erstellungs- und Bearbeitungsmethoden der Controllervorlage kann ich jetzt die Produktmodellinstanz nach der Modellbindung ändern, bevor sie dauerhaft gespeichert wird. Es ist sogar möglich, den Wert für Modelleigenschaften festzulegen, die nicht in den Bearbeitungs- und Änderungsansichten veröffentlicht werden.

Sie werden sehen, dass die Modellfunktion "OnEdit" keinen Wert für CreatedDate festlegt. Wenn die Eigenschaft CreatedDate nicht in der Ansicht "Bearbeiten" geändert wird, wird sie mit einem Nullwert überschrieben. Das passiert, nachdem die Aktionsmethode "Bearbeiten" des Controllers das Modell dauerhaft nach Aufruf von SaveChanges speichert. Damit dies nicht vorkommt, werde ich weitere Änderungen an der Controllervorlage vornehmen.

Verbessern der Aktionsmethode "Bearbeiten"

Ich habe bereits einige der Probleme im Zusammenhang mit Massenzuweisungen erwähnt. Eine Möglichkeit das Modellbindungsverhalten zu ändern, ist die Änderung des BIND-Attributs, damit es Eigenschaften ausschließt, die nicht gebunden werden sollen. In der Praxis kann dieser Ansatz jedoch dazu führen, dass Nullwerte in den Datenspeicher geschrieben werden. Eine bessere Strategie umfasst zusätzliches Programmieren, aber das Ergebnis ist die Mühe wert.

Ich werde die Entity Framework-Methode "Hinzufügen" verwenden, um das Modell dem Datenbankkontext hinzuzufügen. Im Anschluss kann ich den Entitätseintrag nachverfolgen und die Eigenschaft IsModified im Bedarfsfall festlegen. Um diese Logik zu unterstützen, werde ich ein neues Klassenmodul namens CustomAttributes.cs im Projekt JW_Scaffold­Enhancement.Models erstellen (siehe Abbildung 9).

Abbildung 9 Das neue Klassenmodul CustomAttributes.cs

using System; namespace JW_ScaffoldEnhancement.Models { public class PersistPropertyOnEdit : Attribute { public readonly bool PersistPostbackDataFlag; public PersistPropertyOnEdit(bool persistPostbackDataFlag) { this.PersistPostbackDataFlag = persistPostbackDataFlag; } } }

Ich werde dieses Attribut verwenden, um Eigenschaften anzugeben, die nicht im Datenspeicher der Ansicht "Bearbeiten" gespeichert werden sollen (nicht ergänzte Eigenschaften werden ein implizites [PersistPropertyOnEdit(true)]-Attribut aufweisen). Ich möchte verhindern, dass die Eigenschaft CreatedDate dauerhaft gespeichert wird, daher habe ich das neue Attribut nur der Eigenschaft CreatedDate meines Produktmodells hinzugefügt. Die gerade ergänzte Modellklasse ist hier zu sehen:

public class Product : IControllerHooks { public int ProductId { get; set; } public string Description { get; set; } [PersistPropertyOnEdit(false)] public DateTime? CreatedDate { get; set; } public DateTime? ModifiedDate { get; set; } }

Jetzt muss ich die Vorlage Controller.cs.t4 ändern, damit sie das neue Attribut berücksichtigt. Bei der Verbesserung einer T4-Vorlage haben Sie die Wahl, ob Sie Änderungen innerhalb oder außerhalb der Vorlage vornehmen möchten. Falls Sie keine Tools von Drittanbietern verwenden, um Vorlagen zu ändern, empfehle ich, so viel Code wie möglich in ein externes Codemodul einzufügen. Dadurch entsteht ein pures C#-Canvas (also keines, das T4-Markups enthält), in dem Sie sich auf Code konzentrieren können. Die Methode unterstützt Tests und gibt Ihnen die Möglichkeit, Ihre Funktionen in weiter gefasste Testumgebungen einzubinden. Obwohl es einige Mängel bei der Referenzierung von Assemblys in einem T4-Gerüst gibt, werden Sie schlussendlich weniger technische Probleme bei der Einrichtung haben.

Mein Modellprojekt enthält eine öffentliche Funktion namens GetPropertyIsModifiedList. Sie gibt ein Objekt List<String> zurück, das ich durchlaufen lassen kann, um die Einstellungen IsModified für die übergebene Assembly und den Typ zu erzeugen. Abbildung 10 zeigt diesen Code in der Vorlage Controller.cs.t4.

T4 Template Code Used To Generate an Improved Controller Edit Postback Handler
Abbildung 10 T4-Vorlagencode, der zur Generierung eines verbesserten Postbackhandler "Bearbeiten" des Controllers

In der Eigenschaft GetPropertyIsModifiedList, siehe Abbildung 11, verwende ich eine Spiegelungsmethode, um Zugriff auf die Eigenschaften des bereitgestellten Modells zu erhalten. Dann lasse ich diese durchlaufen, um zu bestimmen, welchen Eigenschaften das Attribut PersistPropertyOnEdit hinzugefügt wurde. Wahrscheinlich werden Sie die meisten Eigenschaften Ihrer Modelle dauerhaft speichern wollen, deshalb habe ich einen Vorlagencode entwickelt, mit dem der Wert IsModified der Eigenschaft standardmäßig auf "true" festgelegt werden kann. Dabei müssen Sie nur noch [PersistPropertyOnEdit(false)] den Eigenschaften hinzufügen, die Sie nicht dauerhaft speichern möchten.

Abbildung 11 Die statische Modellprojektfunktion ScaffoldFunctions.GetPropertyIsModifiedList

static public List<string> GetPropertyIsModifiedList(string ModelNamespace, string ModelTypeName, string ModelVariable) { List<string> OutputList = new List<string>(); // Get the properties of the model object string aqn = Assembly.CreateQualifiedName(ModelNamespace + ", Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", ModelNamespace + "." + ModelTypeName); // Get a Type object based on the Assembly Qualified Name Type typeModel = Type.GetType(aqn); // Get the properties of the type PropertyInfo[] typeModelProperties = typeModel.GetProperties(); PersistPropertyOnEdit persistPropertyOnEdit; foreach (PropertyInfo propertyInfo in typeModelProperties) { persistPropertyOnEdit = (PersistPropertyOnEdit)Attribute.GetCustomAttribute( typeModel.GetProperty(propertyInfo.Name), typeof(PersistPropertyOnEdit)); if (persistPropertyOnEdit == null) { OutputList.Add(ModelVariable + "Entry.Property(e => e." + propertyInfo.Name + ").IsModified = true;"); } else { OutputList.Add(ModelVariable + "Entry.Property(e => e." + propertyInfo.Name + ").IsModified = " + ((PersistPropertyOnEdit)persistPropertyOnEdit). PersistPostbackDataFlag.ToString().ToLower() + ";"); } } return OutputList; }

Die überarbeitete Controllervorlage erzeugt eine neu konzipierte Postback-Aktionsmethode "Bearbeiten", siehe Abbildung 12. Meine Funktion GetPropertyIsModifiedList erzeugt Teile dieses Quellcodes.

Abbildung 12 Der neu erstellte Controller Edit Handler

if (ModelState.IsValid) { if (product is IControllerHooks) { ((IControllerHooks)product).OnEdit(); } db.Products.Attach(product); var productEntry = db.Entry(product); productEntry.Property(e => e.ProductId).IsModified = true; productEntry.Property(e => e.Description).IsModified = true; productEntry.Property(e => e.CreatedDate).IsModified = false; productEntry.Property(e => e.ModifiedDate).IsModified = true; db.SaveChanges(); return RedirectToAction("Index"); }

Verwenden von Attributen zur Unterdrückung von Eigenschaften in CRUD-Ansichten

ASP.NET MVC bietet nur drei Attribute, die eine gewisse Kontrolle darüber verleihen, ob Eigenschaften eines Modells in den erstellten Ansichten gerendert werden (siehe Abbildung A). Die ersten beiden Attribute tun dasselbe (obwohl sie in unterschiedlichen Namespaces liegen): [Editable(false)] und [ReadOnly(true)]. Beide sorgen dafür, dass die ergänzte Eigenschaft nicht in den Ansichten "Erstellen" und "Bearbeiten" gerendert werden. Das dritte Attribut [ScaffoldColumn(false)] ist dafür verantwortlich, dass die ergänzte Eigenschaft in keiner der gerenderten Ansichten angezeigt wird.

Abbildung A Die drei Attribute, die das Rendern von Eigenschaften unterbinden

Attribut der Modellmetadaten Namespace des Attributs Betroffene Ansichten Was geschieht?
  Keine Keine Ohne zusätzliche Attribute werden nur normale Ergebnisse erzielt.

[Editable(false)]

[ReadOnly(true)]

Bearbeitbar:

System.ComponentModel.DataAnnotations

ReadOnly:

System.ComponentModel

Erstellen

Bearbeiten

Ergänzte Modelleigenschaften werden nicht gerendert.
[ScaffoldColumn(false)] System.ComponentModel.DataAnnotations

Erstellen

Löschen

Details

Bearbeiten

Index

Ergänzte Modelleigenschaften werden nicht gerendert.

Anpassen einer Ansicht

Manchmal möchten Sie einen Wert in der Bearbeitungsansicht anzeigen, ohne dass ihn Benutzer bearbeiten können. Die von ASP.NET MVC bereitgestellten Attribute unterstützen dieses nicht. Ich würde gerne die Eigenschaft ModifiedDate in der Ansicht "Bearbeiten" anzeigen, die Benutzer sollen Sie aber nicht für ein bearbeitbares Feld halten. Um dies zu implementieren, erstelle ich ein weiteres benutzerdefiniertes Attribut namens DisplayOnEditView im Klassenmodul CustomAttributes.cs, das hier gezeigt wird:

public class DisplayOnEditView : Attribute { public readonly bool DisplayFlag; public DisplayOnEditView(bool displayFlag) { this.DisplayFlag = displayFlag; } }

Dadurch kann ich eine Modelleigenschaft ausstatten, sodass sie in der Ansicht "Bearbeiten" als Etikett gerendert wird. Danach kann ich ModifiedDate in der Bearbeitungsansicht anzeigen, ohne mir Gedanken machen zu müssen, dass jemand den Wert beim Postback manipuliert.

Ab jetzt kann ich das Attribut dazu verwenden, das Produktmodell weiter zu ergänzen. Ich werde das neue Attribut der Eigenschaft ModifiedDate hinzufügen. Ich verwende das Attribut [Editable(false)], um sicherzustellen, dass es in der Ansicht "Erstellen" nicht angezeigt wird, und [DisplayOnEditView(true)], um sicherzustellen, dass es als Etikett in der Ansicht "Bearbeiten" angezeigt wird:

public class Product : IControllerHooks { public int ProductId { get; set; } public string Description { get; set; } [PersistPropertyOnEdit(false)] [ScaffoldColumn(false)] public DateTime? CreatedDate { get; set; } [Editable(false)] [DisplayOnEditView(true)] public DateTime? ModifiedDate { get; set; } }

Zuletzt ändere ich die T4-Vorlage, die die Ansicht "Bearbeiten" erzeugt, damit diese das Attribut DisplayOnEditView berücksichtigt.

HtmlForDisplayOnEditViewAttribute = JW_ScaffoldEnhancement.Models.ScaffoldFunctions. GetHtmlForDisplayOnEditViewAttribute( ViewDataTypeName, property.PropertyName, property.IsReadOnly);

Zusätzlich werde ich die Funktion GetHtmlForDisplayOnEditViewAttribute der Klasse ScaffoldFunctions hinzufügen, wie in Abbildung 13 zu sehen ist.

Die Funktion GetHtmlForDisplayOnEditViewAttribute gibt Html.EditorFor zurück, wenn das Attribut "false" ist, und Html.Display­TextFor, wenn es "true" ist. Die Ansicht "Bearbeiten" wird ModifiedDate als Etikett anzeigen und alle anderen Nichtschlüsselfelder als bearbeitbare Textfelder, siehe Abbildung 14.

Abbildung 13 Statische Funktion ScaffoldFunctions.GetHtmlForDisplayOnEditViewAttribute zur Unterstützung des benutzerdefinierten Attributs DisplayOnEditViewFlag

static public string GetHtmlForDisplayOnEditViewAttribute( string ViewDataTypeName, string PropertyName, bool IsReadOnly) { string returnValue = String.Empty; Attribute displayOnEditView = null; Type typeModel = Type.GetType(ViewDataTypeName); if (typeModel != null) { displayOnEditView = (DisplayOnEditView)Attribute.GetCustomAttribute(typeModel.GetProperty( PropertyName), typeof(DisplayOnEditView)); if (displayOnEditView == null) { if (IsReadOnly) { returnValue = String.Empty; } else { returnValue = "@Html.EditorFor(model => model." + PropertyName + ")"; } } else { if (((DisplayOnEditView)displayOnEditView).DisplayFlag == true) { returnValue = "@Html.DisplayTextFor(model => model." + PropertyName + ")"; } else { returnValue = "@Html.EditorFor(model => model." + PropertyName + ")"; } } } return returnValue; }

Abbildung 14 Ansicht "Bearbeiten", die ein schreibgeschütztes Feld ModifiedDate zeigt

Zusammenfassung

Ich habe hier nur sehr oberflächlich geschildert, was Sie mit dem Gerüstsubsystem erreichen können. Ich habe mich auf die Gerüste konzentriert, die eine Kontrolle über CRUD und Ansichten für Entity Framework bieten. Es sind aber andere Gerüste verfügbar, mit denen Code für andere Arten von Webseiten und Web-API-Aktionen erzeugt werden kann.

Wenn Sie noch nie mit T4-Vorlagen gearbeitet haben, ist die Anpassung vorhandener Vorlagen ein guter Einstieg. Obwohl die hier besprochenen Vorlagen mit Visual Studio IDE über Menüs gestartet werden, können Sie benutzerdefinierte T4-Vorlagen erstellen und sie bei Bedarf umwandeln. Microsoft bietet einen guten Ausgangspunkt unter bit.ly/1coB616. Wenn Sie Informationen für Fortgeschrittene suchen, empfehle ich Ihnen den Kurs von Dustin Davis bit.ly/1bNiVXU.

Aktuell verfügt Visual Studio 2013 über keinen robusten T4-Editor, und es bietet weder Syntaxhervorhebung oder IntelliSense. Glücklicherweise gibt es für diese Zwecke einige Add-Ons. Testen Sie den Devart T4-Editor (bit.ly/1cabzOE) und den Tangible Engineering T4-Editor (bit.ly/1fswFbo).

Jonathan Waldman ist ein erfahrener Softwareentwickler und Technologiearchitekt. Er arbeitet mit dem Technologiestapel von Microsoft seit dessen Einführung. Er war an diversen wichtigen Unternehmensprojekten beteiligt und befasste sich mit allen Aspekten des Softwareentwicklungszyklus. Er ist technischer Mitarbeiter von Pluralsight und arbeitet weiterhin an der Entwicklung von Softwarelösungen und Schulungsmaterialien. Sie erreichen Ihn unter jonathan.waldman@live.com.

Unser Dank gilt dem folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Joost de Nijs
Joost de Nijs ist Program Manager im Azure Developer Experience-Team bei Microsoft und arbeitet an Tools für Web Developer.  Aktuell beschäftigt er sich mit den Verwaltungsfeaturebereichen des Weberstellungs- und NuGet-Pakets. Davor arbeitete er an Java-Clientbibliotheken für Windows Azure und am Windows Azure-Entwicklercenter.