November 2017

Band 33, Nummer 11

Cutting Edge: Richtlinien für ASP.NET MVC Core-Ansichten

Von Dino Esposito | November 2017

Dino EspositoAuch wenn ASP.NET Core auf den ersten Blick dem klassischen ASP.NET MVC sehr ähnlich zu sein scheint, gibt es im Detail viele Unterschiede. Controller, Razor-Ansichten und sogar Modellklassen lassen sich oft mit minimalem Aufwand migrieren, aber architektonisch gesehen unterscheidet sich ASP.NET Core deutlich von früheren Nicht-Core-Versionen von ASP.NET.

Der Hauptgrund dafür ist die neu geschriebene Pipeline, die einer ASP.NET Core-Anwendung mindestens zwei Möglichkeiten bietet, eine HTML-basierte Antwort zu generieren. Wie erwartet, kann eine Anwendung das MVC-Programmiermodell übernehmen und HTML aus Razor-Ansichten generieren, die über Controlleraktionen aufgerufen werden. Alternativ kann eine Anwendung auch als ein sehr schlanker Webserver fungieren, der um eine abschließende Middleware herum aufgebaut ist, und Ihr Code in dieser abschließenden Middleware kann beliebige Aktionen ausführen, z. B. auch eine Zeichenfolge zurückgeben, die der Browser als HTML behandelt. Schließlich können Sie in ASP.NET Core 2.0 einen völlig neuen Ansatz verwenden: Razor-Seiten. Razor-Seiten sind Razor-Ansichten, die die MVC-Infrastruktur nicht benötigen, sondern HTML direkt aus Razor-Dateien generieren und bereitstellen können.

In diesem Artikel werde ich (nach einer kurzen Vorstellung von Miniservern, abschließender Middleware und Razor-Seiten) die Ansätze gegenüberstellen und vergleichen, die im MVC-Programmiermodell zu Erstellen von Ansichten verfügbar sind. Insbesondere werde ich mich auf die Neuerungen in ASP.NET Core konzentrieren, z. B. auf Taghilfsprogramme, Ansichtskomponenten und Abhängigkeitsinjektion (Dependency Injection, DI), sowie auf deren Auswirkungen auf die tatsächlichen Codierungspraktiken.

HTML aus der abschließenden Middleware

Die abschließende Middleware ist der letzte Codeblock in der ASP.NET Core-Pipeline, der eine Anforderung verarbeitet. Im Wesentlichen handelt es sich um eine direkte Funktion (Lambdafunktion), in der Sie die HTTP-Anforderung verarbeiten, um eine beliebige erkennbare Antwort zu generieren, sei es Nur-Text, JSON, XML, Binärdaten oder HTML. Die folgende Startbeispielklasse kann zu diesem Zweck verwendet werden:

public class Startup
{
  public void Configure(IApplicationBuilder app)
  {
    app.Run(async context =>
    {
    var html = BuildHtmlFromRequest(context);
    await context.Response.WriteAsync(html);
    });
  }
}

Durch das Schreiben von HTML-formatiertem Text im Ausgabedatenstrom der Antwort und das Festlegen des entsprechenden MIME-Typs können Sie HTML-Inhalte an den Browser übermitteln. Dies alles geschieht auf eine sehr direkte Art und Weise, ohne Filter und ohne Vermittlung, aber es funktioniert definitiv und ist schneller als alle Möglichkeiten in früheren Versionen von ASP.NET. Die abschließende Middleware verleiht Ihnen früher als jede andere Option die Kontrolle über den Datenfluss. Natürlich ist die Programmierung einer HTML-Factory direkt in der abschließenden Middleware weit davon entfernt, eine verwaltbare und flexible Lösung zu sein, aber sie funktioniert.

Razor-Seiten

In ASP.NET Core 2.0 bieten Razor-Seiten eine weitere Möglichkeit, HTML-Inhalte zu verwalten, indem sie direkt eine Razor-Vorlagendatei aufrufen, ohne einen Controller und eine Aktion durchlaufen zu müssen. Solange sich die Datei der Razor-Seiten im Ordner „Pages“ befindet und ihr relativer Pfad und Name mit der angeforderten URL übereinstimmen, verarbeitet das Ansichtsmodul den Inhalt und generiert HTML.

Der wirkliche Unterschied zwischen Razor-Seiten und Razor-Ansichten besteht darin, dass eine Razor-Seite eine einzelne Datei sein kann (ähnlich einer ASPX-Seite), die Code und Markup enthält. Eine Razor-Seite ist eine CSHTML-Datei, die mit einer @page-Direktive gekennzeichnet ist und an ein Ansichtsmodell gebunden werden kann, das von der vom System bereitgestellten PageModel-Klasse erbt. Wie ich bereits erwähnt habe, werden alle Razor-Seiten unter dem neuen Stammordner „Pages“ des Projekts gespeichert, und das Routing erfolgt nach einem einfachen Muster. Die URL verwendet den Ordner „Pages“ als Stamm, und die CSHTML-Erweiterung wird aus dem eigentlichen Dateinamen entfernt. Weitere Informationen zu Razor-Seiten finden Sie unter bit.ly/2wpOdUE. Außerdem finden Sie ein komplexeres Beispiel unter msdn.com/magazine/mt842512.

Wenn Sie an die Arbeit mit MVC-Controllern gewöhnt sind, gehe ich davon aus, dass Sie Razor-Seiten im Grunde sinnlos und vielleicht nur minimal hilfreich in den seltenen Szenarien finden werden, in denen Sie eine Controllermethode verwenden, die eine Ansicht ohne jegliche Geschäftslogik rendert. Wenn das MVC-Anwendungsmodell aber neu für Sie ist, bieten Razor-Seiten eine weitere Option zum Arbeiten mit dem ASP. NET Core-Framework. Für einige Benutzer stellen Razor-Seiten eine niedrigere Barriere dar, um mit dem Framework vertraut zu werden.

Taghilfsprogramme

Die Razor-Syntax war immer im Wesentlichen eine mit C#-Codeausschnitten versehene HTML-Vorlage. Das @-Symbol wird verwendet, um dem Razor-Parser mitzuteilen, wo ein Übergang zwischen statischem HTML-Inhalt und einem Codeausschnitt stattfindet. Beliebiger Text, der auf das @-Symbol folgt, wird nach den Syntaxregeln der Sprache C# analysiert. Die beim Parsen von Texten erkannten Elemente werden in einer dynamisch erstellten C#-Klasse verkettet, die direkt mit der .NET-Compilerplattform („Rosly“) kompiliert wird. Durch das Ausführen der C#-Klasse sammelt einfach HTML-Text im Antwortdatenstrom an, indem statische Inhalte und dynamisch berechnete Inhalte kombiniert werden. Letztendlich beschränken sich die Ausdrucksmöglichkeiten der Razor-Sprache auf die Ausdrucksmöglichkeiten von HTML5.

Mit Razor hat das ASP. NET-Team auch ein Artefakt eingeführt, das als HTML-Hilfsprogramm bezeichnet wird. Ein HTML-Hilfsprogramm ist eine Art kleine HTML-Factory, die einige Eingabedaten erhält und dann einfach das gewünschte HTML ausgibt. Allerdings haben HTML-Hilfsprogramme Entwickler nie überzeugen können, sodass in ASP.NET Core ein viel besseres Tool hinzugefügt wurde: Taghilfsprogramme. Taghilfsprogramme spielen die gleiche Rolle wie HTML-Hilfsprogramme (sie fungieren als HTML-Factorys), bieten aber eine viel präzisere und natürlichere Syntax. Insbesondere benötigen Sie keinen C#-Code, um Taghilfsprogramme mit dem Razor-Vorlagencode zu verknüpfen. Stattdessen sehen sie wie Elemente einer erweiterten HTML-Syntax aus. Das folgende Beispiel zeigt ein Taghilfsprogramm:

<environment names="Development">
  <script src="~/content/scripts/yourapp.dev.js" />
</environment>
<environment names="Staging, Production">
  <script src="~/content/scripts/yourapp.min.js" 
    asp-append-version="true" />
</environment>

Das Umgebungsmarkupelement wird nicht unverändert an den Browser ausgegeben. Stattdessen wird seine gesamte Teilstruktur serverseitig von einer Taghilfsprogramm-Komponente analysiert, die in der Lage ist, Attribute zu lesen und den aktuellen HTML-Baum zu untersuchen und zu ändern. In diesem Beispiel ordnet das für das Umgebungselement verantwortliche Taghilfsprogramm die aktuelle ASP.NET Core-Umgebung dem Inhalt des Attributs „names“ zu und gibt nur die zutreffenden Skriptelemente aus. Außerdem wird das Skriptelement von einem anderen Taghilfsprogramm verarbeitet, das den Baum auf der Suche nach einem benutzerdefinierten asp-append-version-Attribut untersucht. Wenn ein solches Attribut gefunden wird, wird an die tatsächlich generierte URL ein Zeitstempel angefügt, um sicherzustellen, dass die verknüpfte Ressource nie zwischengespeichert wird.

Taghilfsprogramme sind C#-Klassen, die konventionell von einer Basisklasse geerbt oder deklarativ an Markupelemente und -attribute gebunden werden. Jede Razor-Datei, die Taghilfsprogramme verwenden soll, muss diese mit der @addTagHelper-Direktive deklarieren. Die Direktive registriert einfach Taghilfsprogrammklassen aus einer bestimmten .NET Core-Assembly. Die @addTagHelper-Direktive kann in einzelnen Razor-Dateien auftreten, wird aber häufiger in der Datei „_ViewImports.cshtml“ verwendet und gilt global für alle Ansichten:

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Attribute und Elemente, die als Taghilfsprogramme erkannt werden, werden außerdem in Visual Studio durch eine besondere Farbe hervorgehoben. ASP.NET Core enthält eine vollständige Sammlung vordefinierter Taghilfsprogramme, die in wenigen Kategorien gruppiert werden können. Einige davon wirken sich auf bestimmte HTML-Elemente aus, die in einer Razor-Vorlage vorhanden sein können, z. B. „form“, „input“, „textarea“, „label“ und „select“. Andere Hilfsprogramme dienen stattdessen dazu, die Anzeige von Überprüfungsnachrichten in Formularen zu vereinfachen und zu automatisieren. Alle vordefinierten Taghilfsprogramme verwenden das Namenspräfix „asp-*“. Weitere Informationen finden Sie unter bit.ly/2w3BSS2.

Taghilfsprogramme unterstützen Sie dabei, den Razor-Quellcode gut lesbar und präzise zu gestalten. Es gibt daher keinen Grund, auf den Einsatz von Hilfsprogrammen zu verzichten. Insgesamt würde ich empfehlen, Taghilfsprogramme zu verwenden, um das Schreiben von langen, sich wiederholenden Blöcken von Markupcode zu automatisieren, anstatt eine ansichtsspezifische Sprache zu erstellen. Je mehr Taghilfsprogramme Sie verwenden, desto mehr entfernen Sie sich vom reinen HTML und desto teurer wird das Rendern von HTML-Ansichten. Taghilfsprogramme sind eine coole Technologie, sie stellen aber einen Kompromiss zwischen Ausdrucksmöglichkeit und Kosten für serverseitiges Rendern dar. Apropos Vorteile: Es ist auch erwähnenswert, dass Taghilfsprogramme die allgemeine Markupsyntax nicht verändern, sodass Sie eine Razor-Datei mit Taghilfsprogrammen in jedem HTML-Editor einfach öffnen können, ohne dass Probleme beim Analysieren auftreten. Das Gleiche gilt für HTML-Hilfsprogramme wohl kaum.

Ansichtskomponenten

Technisch gesehen sind Ansichtskomponenten in sich geschlossene Komponenten, die sowohl Programmlogik als auch Ansichten enthalten. In ASP. NET Core ersetzen sie untergeordnete Aktionen, die im klassischen ASP.NET MVC 5 verfügbar waren. Sie verweisen in Razor-Dateien über einen C#-Block auf Ansichtskomponenten und übergeben ihnen alle erforderlichen Eingabedaten:

@await Component.InvokeAsync("LatestNews", new { count = 4 })

Intern führt die Ansichtskomponente ihre eigene Logik aus, verarbeitet die von Ihnen übergebenen Daten und gibt eine zum Rendern bereite Ansicht zurück. Es gibt keine vordefinierten Ansichtskomponenten in ASP.NET Core. Dies bedeutet, dass sie auf einer strikten Anwendungsbasis erstellt werden. Die vorherige Codezeile stellt eine Ansichtskomponente „LatestNews“ dar (ein Paket eines Markupblocks, ähnlich wie Teilansichten). Der Unterschied zwischen einer Teilansicht und einer Ansichtskomponente besteht in der internen Implementierung. Eine Teilansicht ist eine einfache Razor-Vorlage, die optional Eingabedaten empfängt und diese in die HTML-Vorlage einbindet, aus der sie besteht. Von einer Teilansicht wird kein eigenes Verhalten mit Ausnahme von Formatierung und Rendering erwartet.

Eine Ansichtskomponente ist eine komplexere Form einer Teilsicht. Eine Ansichtskomponente erhält optional einfache Eingabeparameter, die sie normalerweise zum Abrufen und Verarbeiten ihrer Daten verwendet. Sobald die Daten verfügbar sind, werden sie in eine eingebettete Razor-Vorlage eingebunden. Dies ähnelt weitgehend dem Verhalten einer Teilansicht. Eine Ansichtskomponente ist jedoch schneller in der Implementierung, da sie nicht wie untergeordnete Aktionen die Controllerpipeline durchläuft. Das bedeutet, dass es keine Modellbindung und keine Aktionsfilter gibt.

Insgesamt dienen Ansichtskomponenten dem Zweck, die Ansicht so in Komponenten aufzuteilen, dass sie sich aus der Zusammenstellung von eigenständigen und unabhängigen Widgets ergibt. Dieser Punkt ist ein zweischneidiges Schwert. Die Aufteilung der Ansicht in verschiedene und unabhängige Komponenten scheint eine bequeme Möglichkeit zu sein, die Arbeit zu organisieren und sie möglicherweise paralleler zu gestalten, indem verschiedene Entwickler sich um die verschiedenen Bestandteile kümmern. Ansichtskomponenten beschleunigen die Dinge jedoch nicht in allen Fällen. Alles hängt davon ab, wie jede Komponente intern agiert. Bei der Diskussion von Ansichtskomponenten und dem Muster der Kompositionsbenutzeroberfläche, das ihrer Darstellung zugrunde liegt, wird häufig als Beispiel die Architektur großer dienstorientierter Systeme angeführt, und das ist absolut richtig.

Wenn Sie Ansichtskomponenten und das Muster im Kontext eines monolithischen, kompakten Systems jedoch wahllos verwenden, kann schlecht optimierte Abfragelogik das Ergebnis sein. Jede Komponente kann ihre Daten abfragen, was zu wiederholten Abfragen und unnötigem Datenverkehr der Datenbank führen kann. Ansichtskomponenten können in diesem Szenario eingesetzt werden, aber Sie sollten Cacheschichten in Betracht ziehen, um schlechte Ergebnisse zu vermeiden.

Übergeben von Daten an Ansichten

Es gibt drei verschiedene Möglichkeiten, Daten an eine Razor-Ansicht zu übergeben (zusätzlich zu DI über die @inject-Direktive), die sich nicht gegenseitig ausschließen. Sie können eines der beiden integrierten Wörterbücher („ViewData“ oder „ViewBag“) verwenden, oder Sie können stark typisierte ViewModel-Klassen verwenden. Zwischen diesen Ansätzen besteht aus rein funktionaler Sicht kein Unterschied, und selbst aus Leistungssicht ist der Unterschied zu vernachlässigen.

Unter Entwurfsgesichtspunkten empfehle ich jedoch den ViewModel-Ansatz mit einem Kanal. Jede Razor-Ansicht sollte über eine einzige Datenquelle verfügen: das ViewModel-Objekt. Dieser Ansatz erfordert, dass Sie dem Entwurf der Basisklasse für Ihre Ansichtsmodelle große Aufmerksamkeit schenken und die Verwendung von Wörterbüchern und sogar der @inject-Direktive vermeiden.

Ich weiß, dass dies sehr rigide klingt. Ich spreche aber aus Erfahrung. Selbst der Aufruf von „DateTime.Now“ kann gefährlich sein, weil er Daten in die Ansicht injiziert, die möglicherweise nicht abstrakt genug für die Anwendung sind. „DateTime.Now“ zeigt die Uhrzeit des Servers an, nicht die Uhrzeit der Anwendung. Dies ist ein Problem für jede Anwendung, die die Uhrzeit für ihre Vorgänge benötigt. Um diese Art von Fußangel zu vermeiden, sollten Sie einige Zeit für den Entwurf der ViewModel-Klassen aufwenden, um sich die gemeinsamen Daten vor Augen zu führen, die in allen Ansichten benötigt werden. Achten Sie außerdem darauf, dass Sie Daten (alle Daten) immer über das Modell an die Ansichten übergeben, sodass Wörterbücher und DI so weit wie möglich vermieden werden. Es ist zwar schneller, Wörterbücher und DI zu verwenden, wird Sie aber später behindern, wenn Sie ein Refactoring für einige Domänenfunktionen in einer großen Anwendung ausführen müssen. DI in Ansichten ist eine coole Technologie, die aber mit Vorsicht verwendet werden sollte.

Zusammenfassung

Ansichten sind die Grundlage von Webanwendungen. In ASP.NET Core sind Ansichten das Ergebnis der Verarbeitung einer Vorlagendatei (normalerweise einer Razor-Vorlagendatei), die mit den von einem Aufrufer bereitgestellten Daten kombiniert wird, der meistens eine Controllermethode ist. In diesem Artikel habe ich versucht, die verschiedenen verfügbaren Ansätze zu vergleichen, um Ansichten zu erstellen und Daten an sie zu übergeben. Die Literatur zu ASP.NET Core betont größtenteils die Rolle von Taghilfsprogrammen und Ansichtskomponenten (und sogar Razor-Seiten und DI) in Ansichten. Dies alles sind überzeugende Ressourcen. Es ist aber wichtig, sie nicht wahllos zu verwenden, da sie natürliche Nachteile haben, die zu schlechten Ergebnissen führen können, wenn Sie nicht vorsichtig sind.


Dino Esposito ist Autor von „Microsoft .NET: Architecting Applications for the Enterprise“ (Microsoft Press, 2014) und „Programming ASP.NET Core“ (Microsoft Press, 2018). Als Pluralsight-Autor und Developer Advocate bei JetBrains teilt er seine Vision von Software auf Twitter: @despos.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: Steve Smith


Diesen Artikel im MSDN Magazine-Forum diskutieren