
Einführung
Hallo Kollegen!
Heute möchte ich Ihnen meine Erfahrungen bei der Entwicklung der View Model- Architektur im Rahmen der Entwicklung von ASP.NET-Webanwendungen mithilfe der Razor Template Engine mitteilen .
Die in diesem Artikel beschriebenen technischen Implementierungen sind für alle aktuellen Versionen von ASP geeignet . NET ( MVC 5 , Core usw.). Der Artikel selbst richtet sich an Leser, die zumindest bereits Erfahrung mit diesem Stapel haben. Es ist auch erwähnenswert, dass wir im Rahmen dessen die Nützlichkeit des Ansichtsmodells und seine hypothetische Anwendung nicht berücksichtigen (es wird angenommen, dass der Leser bereits mit diesen Dingen vertraut ist), sondern die Implementierung direkt diskutieren.
Herausforderung
Für eine bequeme und rationale Assimilation des Materials schlage ich vor, sofort die Aufgabe zu berücksichtigen, die uns natürlich zu potenziellen Problemen und deren optimalen Lösungen führt.
Dies ist das Problem der banalen Hinzufügung beispielsweise eines neuen Autos zu einem bestimmten Fahrzeugkatalog . Um die abstrakte Aufgabe nicht zu komplizieren, werden die Details der verbleibenden Aspekte absichtlich übersehen. Es scheint jedoch, dass die elementare Aufgabe darin besteht, alles mit einem Fokus auf die weitere Skalierung des Systems zu tun (insbesondere das Erweitern von Modellen in Bezug auf die Anzahl der Eigenschaften und anderer definierender Komponenten), damit es später so angenehm wie möglich ist, zu arbeiten.
Implementierung
Lassen Sie das Modell wie folgt aussehen (der Einfachheit halber werden Dinge wie Navigationseigenschaften usw. nicht angegeben):
class Transport { public int Id { get; set; } public int TransportTypeId { get; set; } public string Number { get; set; } }
Natürlich ist TransportTypeId ein Fremdschlüssel für ein Objekt vom Typ TransportType :
class TransportType { public int Id { get; set; } public string Name { get; set; } }
Für die Verbindung zwischen Frontend und Backend verwenden wir die Vorlage Data Transfer Object . Dementsprechend sieht der DTO für das Hinzufügen eines Autos ungefähr so aus:
class TransportAddDTO { [Required] public int TransportTypeId { get; set; } [Required] [MaxLength(10)] public string Number { get; set; } }
* Verwendet Standardvalidierungsattribute aus System.ComponentModel.DataAnnotations
.
Es ist Zeit herauszufinden, wie das Ansichtsmodell für die Seite zum Hinzufügen von Autos aussehen wird. Einige Entwickler würden gerne erklären, dass TransportAddDTO selbst so wäre, dies ist jedoch grundsätzlich falsch, da nichts in diese Klasse "eingepfercht" werden kann, außer direkt die Backend-Informationen, die zum Hinzufügen eines neuen Elements (per Definition) erforderlich sind. Darüber hinaus können auf der Hinzufügungsseite weitere Daten erforderlich sein: beispielsweise ein Verzeichnis von Fahrzeugtypen (auf dessen Grundlage TransportTypeId anschließend ausgedrückt wird). In dieser Hinsicht bietet sich das folgende Ansichtsmodell an:
class TransportAddViewModel { public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Wobei TransportTypeDTO in diesem Fall eine direkte Zuordnung von TransportType ist (und dies ist bei weitem nicht immer der Fall - sowohl in Richtung der Kürzung als auch in Richtung der Erweiterung):
class TransportTypeDTO { public int Id { get; set; } public string Name { get; set; } }
In dieser Phase stellt sich die vernünftige Frage: In Razor kann nur ein Modell übertragen werden (und Gott sei Dank). Wie kann TransportAddDTO dann zum Generieren von HTML-Code auf dieser Seite verwendet werden?
Sehr einfach! Es reicht aus, insbesondere dieses DTO zum Ansichtsmodell hinzuzufügen, etwa so:
class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Nun beginnen die ersten Probleme. Versuchen wir, der Seite in unserer .cshtml-Datei eine Standard- TextBox für die "Fahrzeugnummer" hinzuzufügen (sei es TransportAddView.cshtml):
@model TransportAddViewModel @Html.TextBoxFor(m => m.AddDTO.Number)
Dies wird wie folgt in HTML-Code gerendert:
<input id="AddDTO_Number" name="AddDTO.Number" />
Stellen Sie sich vor, der Teil des Controllers mit der Methode zum Hinzufügen von Fahrzeugen sieht folgendermaßen aus (der Code gemäß MVC 5 ist für Core etwas anders, aber das Wesentliche ist dasselbe ):
[Route("add"), HttpPost] public ActionResult Add(TransportAddDTO transportAddDto) {
Hier sehen wir mindestens zwei Probleme:
- ID- und Name- Attributen wird AddDTO vorangestellt. Wenn die Methode zum Hinzufügen eines Transports im Controller mithilfe des Modellbindungsprinzips versucht, die vom Client an TransportAddDTO gelieferten Daten zu binden, besteht das darin enthaltene Objekt vollständig aus Nullen (Standardwerten). d.h. Es wird nur eine neue leere Instanz sein. Es ist logisch - der Ordner erwartet Namen der Formularnummer, nicht AddDTO_Number .
- Alle Metaattribute sind weg, d.h. Datenwert erforderlich und alle anderen, die wir in AddDTO so sorgfältig als Validierungsattribute beschrieben haben. Für diejenigen, die die volle Leistung von Razor nutzen, ist dies von entscheidender Bedeutung, da dies einen erheblichen Informationsverlust für das Frontend darstellt.
Wir haben Glück und sie haben entsprechende Entscheidungen.
Diese Dinge "funktionieren", wenn Sie beispielsweise einen Wrapper für die Kendo-Benutzeroberfläche verwenden (d. H. @Html.Kendo().TextBoxFor()
usw.).
Beginnen wir mit dem zweiten Problem: Der Grund dafür ist, dass im Ansichtsmodell die übertragene TransportAddDTO- Instanz null war . Und die Implementierung von Rendering-Mechanismen ist so, dass Attribute in diesem Fall zumindest nicht vollständig gelesen werden. Die Lösung liegt auf der Hand - zuerst im Ansichtsmodell, um die TransportAddDTO- Eigenschaft mit einer Instanz der Klasse unter Verwendung des Standardkonstruktors zu initialisieren. Es ist besser, dies in einem Dienst zu tun, der ein initialisiertes Ansichtsmodell zurückgibt. Als Teil des Beispiels wird jedoch dasselbe getan:
class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } = new TransportAddDTO(); public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Nach diesen Änderungen sieht das Ergebnis wie folgt aus:
<input data-val="true" id="AddDTO_Number" name="AddDTO.Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" />
Schon besser! Es bleibt das erste Problem zu lösen - damit ist übrigens alles etwas komplizierter.
Um dies zu verstehen, müssen Sie zunächst herausfinden, was Razor (impliziert WebViewPage, von dem eine Instanz in .cshtml als solche verfügbar ist) eine HTML- Eigenschaft ist, auf die wir verweisen, um TextBoxFor
.
Wenn Sie es betrachten, können Sie sofort verstehen, dass es vom Typ HtmlHelper<T>
, in unserem Fall HtmlHelper<TransportAddViewModel>
. Eine mögliche Lösung für das Problem ergibt sich: Erstellen Sie Ihren eigenen HtmlHelper im Inneren und übergeben Sie unser TransportAddDTO als Eingabe. Wir finden den kleinstmöglichen Konstruktor für eine Instanz dieser Klasse:
HtmlHelper<T>.HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer);
Wir können ViewContext direkt von unserer WebViewPage- Instanz über this.ViewContext
. Lassen Sie uns nun herausfinden, wo Sie eine Instanz einer Klasse erhalten, die die IViewDataContainer-Schnittstelle implementiert. Erstellen Sie beispielsweise Ihre Implementierung:
public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } }
Wie Sie sehen können, sind wir jetzt auf ein Objekt angewiesen, das zum Initialisieren des ViewDataDictionary an den Konstruktor übergeben wurde , da hier alles einfach ist - dies ist eine Instanz unseres TransportAddDTO aus dem View-Modell. Das heißt, Sie können die geschätzte Instanz folgendermaßen erhalten:
var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO);
Dementsprechend gibt es auch keine Probleme beim Erstellen eines neuen HtmlHelper:
var Helper = new HtmlHelper<T>(this.ViewContext, vdc);
Jetzt können Sie Folgendes verwenden:
@model TransportAddViewModel @{ var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO); var Helper = new HtmlHelper<T>(this.ViewContext, vdc); } @Helper.TextBoxFor(m => m.Number)
Dies wird wie folgt in HTML-Code gerendert:
<input data-val="true" id="Number" name="Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" />
Wie Sie sehen können, gibt es jetzt keine Probleme mit dem gerenderten Element und es ist voll einsatzbereit. Es bleibt nur, den Code zu "kämmen", damit er weniger sperrig aussieht. Zum Beispiel erweitern wir unseren ViewDataContainer wie folgt:
public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } public HtmlHelper<T> GetHtmlHelper(ViewContext context) { return new HtmlHelper<T>(context, this); } }
Dann können Sie von Razor aus wie folgt arbeiten:
@model TransportAddViewModel @{ var Helper = new ViewDataContainer<TransportAddDTO>(Model.AddDTO).GetHtmlHelper(ViewContext); } @Helper.TextBoxFor(m => m.Number)
Darüber hinaus stört es niemanden, die Standardimplementierung von WebViewPage so zu erweitern, dass sie die gewünschte Eigenschaft enthält (mit einem Setter für eine Instanz der DTO-Klasse).
Fazit
Dies löste das Problem und erhielt auch die View Model-Architektur für die Arbeit mit Razor, die möglicherweise alle erforderlichen Elemente enthalten könnte.
Es ist erwähnenswert, dass sich der resultierende ViewDataContainer als universell und für die Verwendung geeignet herausstellte.
Es müssen noch einige Schaltflächen zu unserer CSHTM-Datei hinzugefügt werden, und die Aufgabe wird abgeschlossen (ohne Berücksichtigung der Verarbeitung im Backend). Dies schlage ich selbst vor.
Wenn ein angesehener Leser Ideen hat, wie er das, was benötigt wird, optimaler umsetzen kann, werde ich gerne in den Kommentaren zuhören.
Mit freundlichen Grüßen,
Peter Osetrov