7. Razor

Razor ist die Auszeichnungssprache für Views. Sie integriert C# serverseitig in die HTML-Seite. Die Views heißen folgerichtig .cshtml. Razor-Views sind technisch Klassen, die auf der Basisklasse RazorPage aufsetzen. Das optional für die View vereinbarte View-Model ist ein generischer Parameter der Seite.

Abbildung: Basisklasse der Views
Abbildung: Basisklasse der Views

Die Möglichkeit, Code in die Seite zu schreiben, sollte freilich eine Ausnahme sein bzw. nur dazu dienen, die passenden Übersetzungsanweisungen auszulösen. Um Daten in die View zu bekommen, gibt es spezielle Techniken. Schreiben Sie Code immer in den Controller, nie in die View.

7.1 Was ist neu?

Bisher sah eine typische View folgendermaßen aus:

 1 @Html.ValidationSummary(true, "", new { @class = "text-danger" })
 2 <div class="form-group">
 3     @Html.LabelFor(m => m.UserName, new { @class = "col-md-2 control-label\
 4 " })
 5     <div class="col-md-10">
 6         @Html.TextBoxFor(m => m.UserName, new { @class = "form-control" })
 7         @Html.ValidationMessageFor(m => m.UserName, "", new { @class = "te\
 8 xt-danger" })
 9     </div>
10 </div>

Die Wechsel mit @-Zeichen – typisch für Razor – zerreißen das Bild der Seite. Der Code wirkt funktional, aber nicht ästhetisch. In MVC 6 sieht dies nun folgendermaßen aus:

1 <div asp-validation-summary="ModelOnly" class="text-danger"></div>
2 <div class="form-group">
3     <label asp-for="UserName" class="col-md-2 control-label"></label>
4     <div class="col-md-10">
5         <input asp-for="UserName" class="form-control" />
6         <span asp-validation-for="UserName" class="text-danger"></span>
7     </div>
8 </div>

Der Schlüssel zu dieser Schreibweise sind sogenannte Tag Helper. Entwickler, die clientseitig entwickeln können die wenigen Befehle lernen, ohne zugleich auch noch eine neue Syntax studieren zu müssen.

Tag Helper

Neu in MVC 6 sind Tag Helper. Diese ersetzen die bisherigen Helper und dienen wie diese dazu, den serverseitigen Code zu modularisieren. Die Erstellung ist stärker auf HTML ausgelegt und Markup steht im Vordergrund. Geschrieben werden sie jedoch in reinem C#.

Ein typisches Beispiel für einen Link in MVC 5:

1 @Html.ActionLink("About me", "About", "Home")

Hier wurde eine Hilfsfunktion (Html-Helper) benutzt, um einen Link mittels Parametern zu erzeugen.

In MVC sieht dies folgendermaßen aus:

1 <a asp-controller="Home" asp-action="About">About me</a>

In beiden Fällen entsteht daraus dasselbe HTML:

1 <a href="http://localhost:12345/Home/About">About me</a>
Eingebaute Helper

Tag Helper reagieren in erster Linie auf Tags. Dass sie scheinbar auf Attribute reagieren, liegt daran, dass es viele integrierte Tag Helper gibt, die die Standard-HTML-Elemente behandeln. Zuerst ein einfaches Formular, basierend auf einem View-Model:

 1 @model NextWebApp.Models.About
 2 @{
 3     ViewData["Title"] = "About";
 4 }
 5 <h2>@ViewData["Title"].</h2>
 6 <h3>@ViewData["Message"]</h3>
 7 
 8 <p>Use this area to provide additional information.</p>
 9 
10 <form>
11   <label asp-for="Email"></label>
12   <input asp-for="Email"/>
13 </form>

Das Model enthält eine passende Eigenschaft, Email:

1 namespace NextWebApp.Models {
2   public class About {
3 
4     public string Name { get; set; }
5 
6     public string Email { get; set; }
7 
8   }
9 }

Benutzt werden hier implizit der LabelTagHelper und der InputTagHelper. Das führt zu folgendem HTML im Browser:

1 <form>
2   <label for="Email">Email</label>
3   <input type="text" id="Email" name="Email" 
4          value="joerg@krause.net" />
5 </form>

Dabei sind zwei wichtige Eigenschaften zu sehen:

  1. Metadaten werden gelesen und nach HTML transportiert
  2. Es wird kein statisches HTMl erzeugt, dass die Ausgabe beschränkt

Punkt 1 war bisher auch schon verfügbar. Punkt 2 ist entscheidend, denn bisher wurden einige Attribute fest kodiert. Dies war hinderlich, wenn andere clientseitige Bausteine benutzt werden sollten, also vom Helper vorgesehen.

Direktiven für Tag Helper

Durch Direktiven werden Tag Helper sichtbar oder unsichtbar gemacht. Verfügbar sind folgende Direktiven:

  • @addTagHelper: Macht einen Tag Helper verfügbar
  • @removeTagHelper: Entfernt den Zugriff auf einen Tag Helper
  • ! wird zum Umkehren der Auswahl benutzt
Listing: Views/_ViewImports.cshtml des Beispielprojekts
1 @using NextWebApp
2 @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"

Die Direktive erwartet einen voll qualifizierten Namen. Es kann der Platzhalter “*” benutzt werden. Der erste Teile ist der Namensraum bzw. Klassenname, der zweite Teil die Assembly. Das Beispiel zeigt den Import der eingebauten Tag Helper. Das folgende Listing zeigt die Anpassung derselben Datei mit einer weiteren Direktive, um alle selbst entwickelten Tag Helper zu importieren. Der Namensraum des Beispielprojeckts ist NextWebApp:

Listing: Views/_ViewImports.cshtml des Beispielprojekts für eigene Tag Helper
1 @using NextWebApp
2 @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
3 @addTagHelper "*, NextWebApp"

Der Import kann auf ein Tag Helper oder eine Gruppe beschränkt werden:

1 @using NextWebApp
2 @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
3 @addTagHelper "NextWebApp.TagHelpers.EmailTagHelper, AuthoringTagHelpers"

Der Platzhalter kann beliebig angeordnert werden:

1 @addTagHelper "NextWebApp.TagHelpers.E*, NextWebApp"
2 @addTagHelper "NextWebApp.TagHelpers.Email*, NextWebApp"

@removeTagHelper entfernt Tag Helper. Dies ist sinnvoll, wenn Sie einen Tag Helper auf einer Seite nicht benutzen möchten, dieser aber in der zentralen Datei _ViewImports.cshtml bereits per Platzhalter inkludiert wurde. Die Parameter entsprechen genau der Direktive @addTagHelper.

Es kann passieren, dass Tag Helper stören. In solchen Fällen kann die Benutzung in einem Tag mit dem “!”-Zeichen unterdrückt werden.

1 <!span asp-validation-for="Email" class="text-danger"></!span>

Beachten Sie hier, dass das “!”-Zeichen auch im schließenden Tag benötigt wird. Der Umgang mit Tags im Editor kann schwierig werden, wenn HTML und Tag Helper stark gemischt werden. Das Verhalten ist zwar besser als im bisher benutzten @Razor-Stil, aber eine explizite Hervorhebung erscheint manchmal sinnvoll. In solchen Fällen kann ein Namensraum-Alias vereinbart werden:

1 @tagHelperPrefix "th:"

Alle Tags, die die Helper nutzen werden dann mit <th:tag></th:tag> bezeichnet. Der Doppelpunkt muss mit angegeben werden. Alle Tags, die auf Helpern basieren, tragen den Alias. Es ist nur ein Alias pro View erlaubt.

1 <th:LargeButton Text="Anzeigen"></th:LargeButton>

Eingebaute Tag Helper

MVC 6 enthält eine ganze Reihe von Tag Helpern, die standardmäßig vorhanden sind. Sie ersetzen die bisher von der Erweiterungsklasse HtmlHelper über die Eigenschaft Html. Sie können aber weiter benutzt werden. Die Tag Helper bieten lediglich eine weitere Option. Folgende Tag Helper stehen zur Verfügung:

  • Anchor (<a/>): erzeugt Hyperlinks
  • Cache (<cache>): Verwaltet partielle Cache-Funktionen
  • Environment (<environment>): Zugriff auf Umgebungsbedingungen und bedingte Ausführung
  • Form (<form/>): Form-Elemente erzeugen
  • Input (<input>): Input-Elemente erzeugen
  • Label (<label/>): Label erstellen
  • Link (<link/>): Link-Elemente erzeugen
  • Select und Option (<select/><option/>): Klappmenüs und deren Optionen
  • Script (<script/>): Für Script-Tags
  • TextArea (<textarea/>): Das Textarea-Element
  • ValidationMessage (asp-validation-for): Ausgabe von Validierungsmeldungen
  • ValidationSummary (asp-validation-summary): Zusammenfassung von Validierungsmeldungen

Diese Tag Helper werden folgendermaßen bekannt gemacht:

1 @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"

Die Tag Helper reagieren auf bestimmte Tags wie in der Liste gezeigt. Manchmal kann der Tag Helper störend sein. Dann kann er temporär mit “!” im Tag unterdrückt werden (auch im schließenden Tag):

1 <!a href="http://www.google.de">Google</!a>
AnchorTagHelper

Der AnchorTagHelper ist eine Alternative zu @Html.ActionLink oder @Url.Action. Unterstützt werden folgende Attribute:

  • asp-controller: Name des Controllers
  • asp-action: Name der Action
  • asp-route-<parameter>: Spezifikation eines Parameters im Platzhalter <parameter>, z.B. asp-route-id="@value"
  • asp-route: Name einer benannten Route
  • asp-protocol: “http” oder “https”
  • asp-host: Name des Servers
  • asp-fragment: Client-Teil des URL nach (und inklusive) des #-Zeichens

Folgende Konstruktion ist damit möglich:

1 <a asp-controller="Product" 
2    asp-action="Display" 
3    asp-route-id="@ViewBag.ProductId">
4    View Details
5 </a>

Bei einer benannten Route wird es noch einfacher. Nehmen Sie zuerst folgende Routendefinition an:

1 routes.MapRoute(
2     name: "login",
3     template: "login",
4     defaults: new { controller = "Account", action = "Login" });

Dann reicht zum Erzeugen des Links folgendes Tag:

1 <a asp-route="login">Login</a>

Der gesamte URL kann folgendermaßen bestimmt werden:

1 <a asp-controller="Account" 
2    asp-action="Register" 
3    asp-protocol="http" 
4    asp-host="meinserver.de" 
5    asp-fragment="logon">Anmelden</a>

Dies erzeugt folgende Ausgabe:

1 <a href="http://meinserver.de/Account/Register#logon">Anmelden</a>
CacheTagHelper

Der CacheTagHelper ermöglicht Cache-Funktionen in Teilen einer Ansicht.

1 <cache expires-after="@TimeSpan.FromMinutes(10)">
2     @Html.Partial("_WhatsNew")
3     *last updated  @DateTime.Now.ToLongTimeString()
4 </cache>

Die möglichen Attribute decken das übliche Spektrum eines Caches ab und entsprechen dem in älteren Versionen bereits verfügbaren [OutputCache]-Attribut auf der Controller-Methode.

Zuerst wird der Verfallsalgorithmus für den Cache festgelegt:

  • expires-after: Nach einer Zeit
  • expires-on: Zu einer Zeit
  • expires-sliding: Nach einem inaktiven Zeitraum

Dann werden Bedingungen festgelegt, wann der Cache ungültig wird:

  • vary-by-user: Neuer Benutzer (Boolesch)
  • vary-by-query: Anderer QueryString (Boolesch)
  • vary-by-cookie: Geändertes Cookie (Name angeben)
  • vary-by-route: Andere Route (Name des Route-Parameters)
  • vary-by-header: Anderes Kopffeld (Name angeben)
  • vary-by: Beliebiger Wert, der direkt angegeben wird

Dazu kommt noch die Priorität:

  • priority: Ein Wert aus der Enumeration CachePreservationPriority
1 <cache vary-by-user="true" vary-by-route="id">
2     <!--View Component or something that gets data from the database-->
3     *last updated  @DateTime.Now.ToLongTimeString()
4 </cache>
Environment

Mit dem Tag Helper <environment> erhalten Sie Zugriff auf Umgebungsbedingungen und damit eine bedingte Ausführung von Teilen der View.

Form

Das <form>-Tag kennt einige Attribute, die das Senden von Formularen steuern. Es ersetzt den bisherigen Helper Html.BeginForm.

Zur Verfügung stehen folgende Attribute:

  • asp-controller: Name des Controllers
  • asp-action: Name der Action
  • asp-route: Name einer benannten Route
  • asp-route-returnurl: URL zum Zurücksenden
  • method: HTTP-Verb (“GET”, “POST”)
  • asp-anti-forgery: Boolescher Wert für das Anti-Forgery-Token zum Schutz des Formulars. Die Controller-Methode muss zusätzlich das Attribut [ValidateAntiForgeryToken] tragen, damit das Token ausgewertet wird.
1 <form asp-controller="Account" 
2       asp-action="Login" 
3       asp-route-returnurl="@ViewBag.ReturnUrl" 
4       method="post" >
5 </form>
Input

Das Tag <input> erzeugt fast alle Input-Elemente. Die Validierungsinformationen entsprechen der Syntax für jQuery-Validation. Dies war auch bei den bisherigen Versionen der Fall. Wenn Sie AngularJS im Client einsetzen, müssen Sie entweder eigene Tag Helper schreiben oder die Anpassung in AngularJS mittels Direktiven vornehmen.

Zur Verfügung stehen folgende Attribute:

  • asp-for: Bindung an eine Eigenschaft des Models. Damit wird auch das zugehörige for-Attribut in HTML erzeugt, dass auf das korrespondierende Input-Element verweist.
  • asp-format: Eine Formatierungsanweisung im bekannten ToString-Stil, beispielsweise asp-format=”{0:N4}”.

Das type-Attribute ist ein reguläres Attribut in HTML, dass hier zusätzliche Typprüfungen übernimmt. Es wird automatisch erzeugt:

Tabelle: .NET-Typen und das type-Attribut
.NET type=””
String type=”text”
DateTime type=”datetime”
Byte type=”number”
Int16,Int32 type=”number”
Single,Double type=”number”
Boolean type=”checkbox”

Natürlich kann das type-Attribut auch explizit angegeben werden, beispielsweise für type=”radio”.

Der Tag Helper erzeugt Validierungs-Attribute. Stellen Sie sich folgendes Model vor:

1 public class SimpleViewModel
2 {
3   [Required]
4   public string Email { get; set; }
5 }

Im Formular wird folgendes Tag benutzt:

1 <input asp-for="Email" />

Daraus entsteht folgendes HTML:

1 <input type="text" data-val="true" 
2        data-val-required="The Email field is required." 
3        id="Email" 
4        name="Email" 
5        value="" />

Neben der Reaktion auf .NET-Typen reagiert der Helper auch auf Attribute aus dem folgenden Namensraum:

  • System.ComponentModel.DataAnnotations.
Tabelle: .NET-Typen und das type-Attribut
.NET-Attribut type=””
[EmailAddress] type=”email”
[Url] type=”url”
[HiddenInput] type=”hidden”
[Phone] type=”tel”
[DataType(DataType.Password)] type=”password”
[DataType(DataType.Date)] type=”date”
[DataType(DataType.Time)] type=”time”

Beim Zugriff auf Models mit asp-for können komplexe Objekte mit der .-Syntax adressiert werden. Folgendes Model soll als Beispiel dienen:

 1 public class AddressViewModel
 2 {
 3     public string Street { get; set; }
 4 }
 5 
 6 public class RegisterViewModel
 7 {
 8     public string UserName { get; set;}
 9     public AddressViewModel Address { get; set; }
10 }

Die Benutzung erfolgt nun folgendermaßen:

1 <input asp-for="Address.Street" />
Abbildung: Intellisense in Visual Studio
Abbildung: Intellisense in Visual Studio
Label

Mit dem Tag <label> werden Label in Formularen erstellt.

Zur Verfügung stehen folgende Attribute:

  • asp-for: Bindung an eine Eigenschaft des Models
1 <label asp-for="Email"></label>

Je nach Modell – die Daten kommen hier aus den Attributen des Namensraums System.ComponentModel.DataAnnotations – entsteht daraus folgendes HTML:

1 <label for="Email">E-Mail Adresse</label>

Mittels <link> und <script></script> wird auf weitere Ressourcen verwiesen. Der Tag Helper versteht globale Platzhalter, auch auf Verzeichnisebene.

Zur Verfügung stehen folgende Attribute:

  • asp-src-include: Globaler Import
  • asp-src-exclude: Vom globalen Import ausschließen
  • asp-fallback-href: Rückfall, wenn der Zugriff auf den URL in href nicht gelingt
  • asp-fallback-test-class: “hidden”
  • asp-fallback-test-property: “visibility”
  • asp-fallback-test-value: “hidden”
  • asp-append-version: Schaltet den Cache aus, indem ein variabler Wert angehängt wird

Ein Beispiel für CSS zeigt die Benutzung:

1 <link rel="stylesheet" 
2       href="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/css/bootstrap.min.cs\
3 s"
4       asp-fallback-href="~/lib/bootstrap/css/bootstrap.min.css"
5       asp-fallback-test-class="hidden" 
6       asp-fallback-test-property="visibility" 
7       asp-fallback-test-value="hidden" />

Für die Steuerung der Rückfallebenen wird einiges an Code in der Seite erzeugt:

 1 <link rel="stylesheet" href="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/css\
 2 /bootstrap.min.css" />
 3 <meta name="x-stylesheet-fallback-test" class="hidden" />
 4 <script>!function(a,b,c){var d,e=document,f=e.getElementsByTagName("SCRIPT\
 5 "),g=f[f.length-1].previousElementSibling,h=e.defaultView&amp;&amp;e.defau\
 6 ltView.getComputedStyle?e.defaultView.getComputedStyle(g):g.currentStyle;i\
 7 f(h&amp;&amp;h[a]!==b)for(d=0;d<c.length;d++)e.write('<link rel="styleshee\
 8 t" href="'+c[d]+'"/>')}("visibility","hidden",["\/lib\/bootstrap\/css\/boo\
 9 tstrap.min.css"]);</script>
10 
11 <script src="//ajax.aspnetcdn.com/ajax/bootstrap/3.0.0/bootstrap.min.js">
12 </script>
13 <script>(typeof($.fn.modal) === 'undefined'||document.write("<script src=\\
14 "\/lib\/bootstrap\/js\/bootstrap.min.js\"><\/script>"));</script>

Ein Beispiel für JavaScript zeigt die Benutzung:

1 <script asp-src-include="~/app/**/*.js"></script>

Dieses Tag sucht im Ordner app nach allen Unterordnern und in jedem von diesen nach JavaScript-Dateien. Das führt zu folgender Ausgabe:

1 <script src="/app/controllers/controller1.js"></script>
2 <script src="/app/controllers/controller2.js"></script>
3 <script src="/app/controllers/controller3.js"></script>
4 <script src="/app/controllers/controller4.js"></script>
5 <script src="/app/services/service1.js"></script>
6 <script src="/app/services/service2.js"></script>
Select und Option

Mit den Elementen <select> und <option> werden Klappmenüs und deren Optionen gesteuert.

Zur Verfügung stehen folgende Attribute:

  • asp-for: Bindung an eine Eigenschaft des Models
  • asp-items: Bindung an ein Objekt, das die Optionen erzeugt (was zwangsläufig aufzählbar sein muss)
1 <select asp-for="CountryCode" 
2         asp-items="ViewBag.Countries">
3 </select>
TextArea

Das <textarea>-Tag erzeugt ein mehrzeiliges Text-Eingabefeld. Zur Verfügung stehen folgende Attribute:

  • asp-for: Bindung an eine Eigenschaft des Models

Angenommen Sie haben folgendes Model:

1 public class SimpleViewModel
2 {
3     [Required]
4     [MaxLength(500)]
5     public string Description { get; set; }
6 }

Gestalten Sie nun die View folgendermaßen:

1 <textarea asp-for="Description"></textarea>

Dann wird daraus folgendes HTML:

1 <textarea name="Description" id="Description" 
2           data-val-required="The Description field is required." 
3           data-val-maxlength-max="500" 
4           data-val-maxlength="The field Description must be a string or ar\
5 ray type with a maximum length of '500'." 
6           data-val="true"></textarea>

Die Texte werden durch die Attribute in C# generiert und lassen sich dort vielfältig anpassen und lokalisieren.

TagHelper für die Validierung

Hier werden statt eines Tags zwei Attribute benutzt:

  • asp-validation-for: Ausgabe von Validierungsmeldungen für eine bestimmte Eigenschaft des Models
  • asp-validation-summary: Zusammenfassung von Validierungsmeldungen, angegeben wird ein Wert der Enumeration ValidationSummary. Zulässig Werte sind:
    • All: Alle
    • ModelOnly: Nur für das Model
    • None: Keine

Nutzen Sie folgende Form im HTML:

1 <span asp-validation-for="Email"></span>

Dies erzeugt folgende Ausgabe im HTML:

1 <span class="field-validation-error" 
2       data-valmsg-replace="true" 
3       data-valmsg-for="Email"> 
4    The Email field is required.</span>

Voraussetzung ist, dass die Eigenschaft Email des benutzen Viewmodels das Attribut [Required] trägt.

Sind andere Attribute im HTML im Einsatz, werden diese erhalten.

1 <span asp-validation-for="Email"
2       class="text-danger"></span>

In diesem Beispiel ist zusätzlich eine Klasse angegeben (aus Bootstrap). Das Ergebnis sieht nun folgendermaßen aus:

1 <span class="text-danger field-validation-error" 
2       data-valmsg-replace="true" 
3       data-valmsg-for="Email"> 
4    The Email field is required.</span>

Bei der Zusammenfassung wird ein Tag angegeben, an dessen Stelle die Sammlung aller Fehlermeldungen erscheint.

1 <div asp-validation-summary="ValidationSummary.All"></div>

Das HTML ohne Fehler sieht folgendermaßen aus:

1 <div class="validation-summary-valid" data-valmsg-summary="true">
2   <ul>
3     <li style="display: none;"></li>
4   </ul>
5 </div>

Mit Fehlern ist die Liste gefüllt:

1 <div class="validation-summary-errors" data-valmsg-summary="true">
2   <ul>
3     <li>The Email field is required.</li>
4     <li>The Password field is required.</li>
5   </ul>
6 </div>

Eigene Tag Helper

Eigene Tag Helper ersetzen die bisherigen Helper. Auf den ersten Blick erscheint die Vorgehensweise komplizierter. Allerdings haben sich Helper bisher eher wie speziell angeordnete Teilansichten (partial views) verhalten. Die Beschränkung auf App_Code und die Benennung der Helper nach dem Dateinamen waren ebenso eigenwillige Konzepte. Der neue Ansatz ist hier klarer und direkter.

Ein Tag Helper ist eine Klasse, die die Schnittstelle ITagHelper implementiert. Diese Schnittstelle bietet eine Methode Process, in der die HTML-Ausgabe erzeugt wird. Im einfachsten Fall kann dies direkt erfolgen. Die Klasse kann im Projekt irgendwo platziert sein, der Namensraum sollte gegebenenfalls global bereitgestellt werden. Am einfachsten lässt sich ein Tag Helper erstellen, indem die abstrakte Basisklasse TagHelper benutzt wird.

Um die wiederholte Erstellung von bestimmten Schaltflächen mit Bootstrap-Klassen zu vereinfachen, wäre folgender Tag Helper denkbar:

Listing: Tag Helper für eine Schaltfläche (LargeButtonTagHelper.cs)
 1 [HtmlTargetElement("LargeButton", TagStructure = TagStructure.NormalOrSelf\
 2 Closing)]
 3 public class LargeButtonTagHelper : TagHelper {
 4 
 5   public string Text { get; set; }
 6 
 7   public override void Process(TagHelperContext context, TagHelperOutput o\
 8 utput) {
 9     output.TagName = "button";
10     output.Attributes["class"] = "btn btn-large btn-danger";
11     output.Content.SetContent(Text ?? "Senden");
12   }
13 }

Dieser Tag Helper kann nun in Views benutzt werden. Dazu wird die Direktive @addtaghelper benutzt:

1 @addTagHelper "NextWebApp.Helper.*, NextWebApp"
2 
3 <LargeButton Text="Anzeigen"></LargeButton>

Die Direktive verweist auf die Herkunft des Helpers mit einem vollqualifizierten Namen (FQN). Der erste Teil kann dabei zu * verkürzt werden, wenn alle Tag Helpers in einer Assembly importiert werden sollen. Im Beispiel ist NextWebApp.Helper der Namensraum, indem die Klasse LargeButtonTagHelper ist. Alternativ wäre es auch möglich, folgendes zu schreiben: @addTagHelper "*, NextWebApp". Alternativ kann auch eine @using-Direktive benutzt werden:

1 @using NextWebApp.Helper
2 @addTagHelper "*, NextWebApp"
3 
4 <LargeButton Text="Anzeigen"></LargeButton>

Das Attribut Text im Beispiel wird durch die gleichnamige Eigenschaft unterstützt. Groß- und Kleinschreibung wird hier nicht berücksichtigt. Allerdings wird Pascal-Case erkannt (Große Buchstaben relevanter Wortteile) und in HTML als Kebab-Case umgesetzt (mit Trennstrichen).

Komplexere Datentypen

Attribute sind keineswegs auf Zeichenketten beschränkt. Das folgende Beispiel zeigt die Benutzung von Enum-Typen:

Listing: Tag Helper für eine Schaltfläche (SmallButtonTagHelper.cs)
 1 using System.ComponentModel.DataAnnotations;
 2 using System.Reflection;
 3 using Microsoft.AspNet.Razor.TagHelpers;
 4 
 5 namespace NextWebApp.Helper {
 6 
 7   public enum BtnType {
 8     [Display(Name="btn-danger")]
 9     Danger,
10     [Display(Name = "btn-warning")]
11     Warning,
12     [Display(Name = "btn-primary")]
13     Primary,
14     [Display(Name = "btn-info")]
15     Info,
16     [Display(Name = "btn-default")]
17     Default
18   }
19 
20   [HtmlTargetElement("SmallButton", TagStructure = TagStructure.NormalOrSe\
21 lfClosing)]
22   public class SmallButtonTagHelper : TagHelper {
23 
24     public string Text { get; set; }
25 
26     public BtnType ButtonType { get; set; }
27 
28     public override void Process(TagHelperContext context, TagHelperOutput\
29  output)
30     {
31       var btClass =
32         ((DisplayAttribute)
33           typeof (BtnType)
34           .GetField(ButtonType.ToString())
35           .GetCustomAttribute(typeof (DisplayAttribute))).Name;
36       output.TagName = "button";
37       output.Attributes["class"] = "btn btn-sm " + btClass;
38       output.Content.SetContent(Text ?? "Senden");
39     }
40   }
41 }

Im Beispiel werden Data Annotations benutzt, um vom Enum-Wert auf die ensprechenden Bootstrap-Klassen für die Schaltfläche umzusetzen. Das Auslesen der Namen erfolgt über Reflection ab Zeile 28 (btClass enthält dann je nach Wert btn-danger usw.). Die Klassen btn und btn-sm erstellen eine typische Schaltfläche im Bootstrap-Stil. Die Anwendung ist sehr einfach:

1 @using NextWebApp.Helper
2 @addTagHelper "NextWebApp.Helper.*, NextWebApp"
3 
4 <SmallButton text="Löschen" button-type="BtnType.Info"></SmallButton>

Beachten Sie hier die Schreibweise des Attributs button-type mit Bindestrich, was zwingend erforderlich ist. Wenn das stört, wird entweder beim Namen der Eigenschaft auf das große “T” verzichtet oder das Attribut HtmlAttributName benutzt.

1 [HtmlAttributeName("buttontype")]
2 public BtnType ButtonType { get; set; }

Mit dieser Änderung sieht der korrekte Aufruf nun folgendermaßen aus (buttontype ohne Bindestrich):

1 @using NextWebApp.Helper
2 @addTagHelper "NextWebApp.Helper.*, NextWebApp"
3 
4 <SmallButton text="Löschen" buttontype="BtnType.Info"></SmallButton>

In allen Fällen muss in der View der Zugriff auf die Namensräume komplexer Klassen möglich sein.

Abbildung: Fehler, wenn ein komplexer Typ nicht gefunden wird
Abbildung: Fehler, wenn ein komplexer Typ nicht gefunden wird
Asynchrone Verarbeitung und Vorlagen

Tag Helper unterstützen neben der synchronen Methode Process auch die asynchrone Variante ProcessAsync. Dies ist sinnvoll, wenn komplexes HTML nicht direkt in der Klasse, sondern als externes Template bereitgestellt wird. Diese Variante ist überdies den bisherigen Helpern sehr viel ähnlicher, weil in den Dateien wieder Razor benutzt werden darf. Zuerst ein Beispiel:

Listing: Tag Helper für eine Schaltfläche (SendButtonTagHelper.cs)
 1 namespace NextWebApp.Helper
 2 {
 3 
 4   [HtmlTargetElement("Send", TagStructure = TagStructure.NormalOrSelfClosi\
 5 ng)]
 6   public class SendButtonTagHelper : TagHelper
 7     {
 8 
 9     private readonly HtmlHelper _htmlHelper;
10     private readonly IHtmlEncoder _htmlEncoder;
11 
12     public SendButtonTagHelper(IHtmlHelper htmlHelper, IHtmlEncoder htmlEn\
13 coder) {
14       _htmlHelper = htmlHelper as HtmlHelper;
15       _htmlEncoder = htmlEncoder;
16     }
17 
18     [ViewContext]
19     public ViewContext ViewContext
20     {
21       set
22       {
23         _htmlHelper.Contextualize(value);
24       }
25     }
26     
27     public string Value { get; set; }
28 
29     public string Id { get; set; }
30 
31     public override async Task ProcessAsync(TagHelperContext context, TagH\
32 elperOutput output) {
33       output.TagName = null;
34       output.SuppressOutput();
35  
36       var partial = await _htmlHelper.PartialAsync(
37           $"Helpers/Button",
38           new Button {
39             Value = Value,
40             Id = Id
41           }, null);
42 
43       var writer = new StringWriter();
44       partial.WriteTo(writer, _htmlEncoder);
45 
46       output.Content.AppendHtml(writer.ToString());
47 
48       
49     }
50   }
51 
52 }

Ab Zeile 33 ist hier zu sehen, dass auf eine Teilansicht (partial view) zugegriffen wird. Der Ladevorgang soll dabei asynchron erfolgen. Damit hier await benutzt werden kann, muss die Methode mit async dekoriert sein. Gesucht wird die View im Pfad Views/Shared. Dort wird die entsprechende Ansicht platziert:

Listing: Tag Helper für eine Schaltfläche (SendButtonTagHelper.cs)
1 @model NextWebApp.Models.Button
2 <div>
3    <button id="@Model.Id">@Model.Value</button>
4 </div>

Die Übertragung mehrerer Daten gelingt am besten mit einem Objekt:

Listing: Model für den Tag Helper (Button.cs)
1 namespace NextWebApp.Models {
2   public class Button {
3     public string Value { get; set; }
4     public string Id { get; set; }
5   }
6 }

Die Benutzung ist dagegen sehr einfach:

1 @addTagHelper "NextWebApp.Helper.*, NextWebApp"
2 
3 <Send Value="Senden" Id="btn1"></Send>  

7.2 Dienste in Ansichten

Dienste in Ansichten weren in diese injiziert. Der Dienst selbst ist eine Klasse, deren Instanzen eine dedizierte Aufgabe übernehmen.

Listing: ServicesStatisticsService.cs
 1 using System.Linq;
 2 using System.Threading.Tasks;
 3 using TodoList.Models;
 4 
 5 namespace NextWebApp.Services
 6 {
 7   public class StatisticsService
 8   {
 9     private readonly ApplicationDbContext db;
10 
11     public StatisticsService(ApplicationDbContext context)
12     {
13       db = context;
14     }
15 
16     public async Task<int> GetCount()
17     {
18       return await Task.FromResult(db.TodoItems.Count());
19     }
20 
21     public async Task<int> GetCompletedCount()
22     {
23       return await Task.FromResult(
24         db.TodoItems.Count(x => x.IsDone == true));
25     }
26 
27     public async Task<double> GetAveragePriority()
28     {
29       if (db.TodoItems.Count() == 0)
30       {
31         return 0.0;
32       }
33 
34       return await Task.FromResult(
35         db.TodoItems.Average(x =>x.Priority));
36     }
37   }
38 }

In der View wird diese Dienstklasse nun bereitgestellt. Der Controller hat hier keine Funktion.

Listing: Dienst in einer Ansicht
 1 @inject TodoList.Services.StatisticsService Statistics
 2 
 3 
 4 <div>@Html.ActionLink("Create New Todo", "Create", "Todo") </div>
 5 </div>
 6   <div class="col-md-4">
 7     @await Component.InvokeAsync("PriorityList", 4, true)
 8     <h3>Stats</h3>
 9     <ul>
10       <li>Items: @await Statistics.GetCount()</li>
11       <li>Completed: @await Statistics.GetCompletedCount()</li>
12       <li>Average Priority: @await Statistics.GetAveragePriority()</li>
13     </ul>
14   </div>
15 </div>

Damit das funktioniert, muss die Klasse bekanntgegeben werden. Dies passiert in der Datei Startup.cs:

Listing: Startup.cs
1 public void ConfigureServices(IServiceCollection services)
2 {
3   services.AddMvc();
4   services.AddTransient<NextWebApp.Services.StatisticsService>();
5 }

7.3 Ansichtskomponenten (View Components)

Das Konzept der Ansichtskomponenten ist neu in ASP.NET Core. Es soll die teilweise unübersichtlichen und komplexen Strukturen aus Teilansichten (partial views) vereinfachen.

Übersicht

Ansichtskomponenten sind ähnlich wie Teilansichten. Sie ermöglichen eine Trennung von Zuständigkeiten und sind testbar. Sie können sich eine Ansichtskomponente ähnlich wie einen einfachen, separaten Controller vorstellen, der außerhalb der Grundstruktur existiert und wiederverwendbar ist. Generell erstellen Ansichtskomponenten nur Teil einer Seite. Diese Teile können jedoch in sich komplexer als einfache Teilansichten sein. Typische Beispiele sind:

  • Dynamische Menüs
  • Tag Clouds
  • Anmeldemasken
  • Warenkörbe
  • Nachrichtenlisten

Am Beispiel einer Anmeldemaske ist leicht zu erkennen, dass es hier nicht nur um zwei Textfelder geht. Eine solche Maske hat viele Funktionen:

  • Wenn der Benutzer nicht angemeldet ist, wird die Anmeldemaske angezeigt
  • Wenn der Benutzer angemeldet ist, wird der Anmeldename und ein Link zum abmelden angezeigt
  • Wenn der Benutzer Administrator ist, wird ein Link zur Administration angezeigt

Viele weitere situations- und datenabhängige Funktionen sind leicht vorstellbar. Vor allem im Zusammenhang mit Claims – den Profileigenschaften angemeldeter Benutzer – lassen sich hier umfassende Funktionen abbilden. Die Anwendung ist überall möglich und nicht auf eine bestimmte Controller-Struktur beschränkt.

Ansichtskomponenten bestehen aus zwei Teilen: Einer Klasse, die direkt oder indirekt von ViewComponent abstammt und einer Ansicht im typischen Razor-Stil. Aus Gründen der Testbarkeit komplexer Klassen kann es sinnvoll sein, nicht direkt von ViewComponent abzuleiten. In solchen Fällen wird eine einfache C#-Klasse erstellt und mit dem Attribut [ViewComponent] dekoriert. Die dritte Möglichkeit ist die Benutzung einer Klasse mit dem Suffix ViewComponent. Diese Klasse muss public, darf nicht verschachtelt und nicht abstrakt sein.

Listing: PriorityListViewComponent.cs
 1 using System.Linq;
 2 using Microsoft.AspNet.Mvc;
 3 using NextWebApp.Models;
 4 
 5 namespace NextWebApp.ViewComponents
 6 {
 7   public class PriorityListViewComponent : ViewComponent
 8   {
 9     private readonly ApplicationDbContext db;
10 
11     public PriorityListViewComponent(ApplicationDbContext context)
12     {
13       db = context;
14     }
15 
16     public IViewComponentResult Invoke(int maxPriority)
17     {
18       var items = db.TodoItems.Where(x => x.IsDone == false &&
19           x.Priority <= maxPriority);
20 
21       return View(items);
22     }
23   }
24 }

Die Klasse kann sich irgendwo im Projekt befinden. Der Suffix wird überall erkannt und der Name vor dem Suffix extrahiert. Falls das Attribut benutzt wird, kann der Name explizit festgelegt werden:

1 [ViewComponent(Name = "PriorityList")]
2 public class NameEgal : ViewComponent

Ergänzend zur Klassen ist nun noch der Ansichtscode zu schreiben. Dieser ist nicht frei platzierbar. Da Ansichtskomponenten nicht spezifisch zu einem Controller sind, werden Sie am besten unter View/Shared/Components platziert. Im letzten Beispiel war der Name der Komponente “PriorityList”. Der Name der Ansichtskomponente ist also “PriorityList.cshtml”.

Die Bildung der Instanz erfolgt durch das Dependency Injection Modul und pflanzt die verlangten Dienste in den Konstruktor ein. Dadurch wird in den Beispielen der Datenbankkontext bereitgestellt.

Die Abarbeitung der Komponente erfolgt in der Methode Invoke. Diese gibt es auch als asynchrone Variante InvokeAsync.

Listing: View-Datei der Ansichtskomponente
1 @model IEnumerable<TodoList.Models.TodoItem>
2 
3 <h3>Priority Items</h3>
4 <ul>
5   @foreach (var todo in Model)
6   {
7     <li>@todo.Title</li>
8   }
9 </ul>

Der Aufruf der Komponente erfolgt in jeder regulären Ansicht mit der Hilfsmethode Component.

Listing: Benutzung der Ansichtskomponente
1 @* Markup removed for brevity *@
2 <div>@Html.ActionLink("Create New Todo", "Create", "Todo") </div>
3 <div>
4   <div class="col-md-4">
5     @Component.Invoke("PriorityList", 1)
6   </div>
7 </div>

Die Methode Invoke verarbeitet die Ansichtskomponente synchron. Es kann für die Leistung des Gesamtsystems sinnvoll sein, die Verarbeitung asynchron zu erlauben. Stehen genug Threads zur Verfügung, wird die Verarbeitung parallelisiert. Eine asynchrone Ansichtskomponente könnte etwa folgendermaßen aussehen:

Listing: Asynchrone Ansichtskomponente
 1 using System.Threading.Tasks;
 2 
 3 public class PriorityListViewComponent : ViewComponent
 4 {
 5   private readonly ApplicationDbContext db;
 6 
 7   public PriorityListViewComponent(ApplicationDbContext context)
 8   {
 9     db = context;
10   }
11 
12   // Synchronous Invoke removed.
13 
14   public async Task<IViewComponentResult> InvokeAsync(int maxPriority, boo\
15 l isDone)
16   {
17     var items = await GetItemsAsync(maxPriority, isDone);
18     return View(items);
19   }
20 
21   private Task<IQueryable<TodoItem>> GetItemsAsync(int maxPriority, bool i\
22 sDone)
23   {
24     return Task.FromResult(GetItems(maxPriority, isDone));
25   }
26   private IQueryable<TodoItem> GetItems(int maxPriority, bool isDone)
27   {
28     var items = db.TodoItems.Where(x => x.IsDone == isDone &&
29         x.Priority <= maxPriority);
30 
31     string msg = "Priority <= " + maxPriority.ToString() +
32            " && isDone == " + isDone.ToString();
33     ViewBag.PriorityMessage = msg;
34 
35     return items;
36   }
37 }
1 @model IEnumerable<TodoList.Models.TodoItem>
2 
3 <h4>@ViewBag.PriorityMessage</h4>
4 <ul>
5   @foreach (var todo in Model)
6   {
7     <li>@todo.Title</li>
8   }
9 </ul>

Der Aufruf benötigt die Nutzung de Schlüsselwortes @await:

Listing: Nutzung einer asynchronen Ansichtskomponente
1 <div class="col-md-4">
2     @await Component.InvokeAsync("PriorityList", 2, true)
3 </div>
Umgang mit dem Namen

Specifying a view name

A complex view component might need to specify a non-default view under some conditions. The following shows how to specify the “PVC” view from the InvokeAsync method: Update the InvokeAsync method in the PriorityListViewComponent class.

Listing: View-Datei der Ansichtskomponente
 1 public async Task<IViewComponentResult> InvokeAsync(int maxPriority, bool \
 2 isDone)
 3 {
 4   string MyView = "Default";
 5   // If asking for all completed tasks, render with the "PVC" view.
 6   if (maxPriority > 3 && isDone == true)
 7   {
 8     MyView = "PVC";
 9   }
10   var items = await GetItemsAsync(maxPriority, isDone);
11   return View(MyView, items);
12 }
Listing: View-Datei der Ansichtskomponente
 1 @model IEnumerable<TodoList.Models.TodoItem>
 2 
 3 <h2> PVC Named Priority Component View</h2>
 4 <h4>@ViewBag.PriorityMessage</h4>
 5 <ul>
 6   @foreach (var todo in Model)
 7   {
 8     <li>@todo.Title</li>
 9   }
10 </ul>
Listing: View-Datei der Ansichtskomponente
1 @await Component.InvokeAsync("PriorityList",  4, true)

7.4 Controller

Controller sind der Baustein der Applikation der für die Verarbeitung der Anfragen zuständig ist. Hier werden die Modelle beschafft und die Views aufgerufen. Der Aufruf der Methoden des Controllers – den Action-Methoden – erfolgt über das Routing.

Grundlagen

Wichtig ist es von vornherein, dem Controller die richtigen Aufgaben zu überlassen. Es kann sonst leicht passieren, dass die Struktur des MVC-Entwurfsmusters kaputt geht. Zur Wiederholung sei hier nochmals aufgeführt, wozu MVC dient. MVC trennt Aufgaben in drei Bereiche:

  • Eingabe-Logik
  • Geschäfts-Logik
  • UI-Logik

Controller sind für die Eingabe-Logik zuständig. Die UI-Logik gehört in die View. Die Geschäfts-Logik sollte in einer separaten Logikschicht und in den Datenmodellen platziert werden.

Ein neuer Controller wird im Projekt über Add > New Item > MVC Controller hinzugefügt. Er muss im Ordner Controllers sein und den Suffix`Controller haben.

Listing: Vorlage für einen Controller
1 namespace NextWebApp.Controllers {
2   public class UserController : Controller {
3     // GET: /<controller>/
4     public IActionResult Index() {
5       return View();
6     }
7   }
8 }

Dies unterscheidet sich nur geringfügig von den bisherigen MVC-Versionen. Der Rückgabewert ist eine Schnittstelle mit dem Namen IActionResult. Aufrufbar von außen sind alle Methoden, die folgende Eigenschaften haben:

  • öffentlich
  • nicht abstrakt
  • keine Erweiterungsmethode

Wenn Sie eine solche Methode unzugänglich machen möchten, dann eignet sich dafür das Attribut [NonAction]. Das Attribut hat keine Parameter. Der Aufruf erfolgt zudem über die Methode (auch HTTP-Verb genannt) GET, die standardmäßig benutzt wird. Alle anderen HTTP-Verben müssen explizit angegeben werden.

Das Routing enthält eine Standardroute, die den folgenden Aufbau hat:

  • {controller}/{action}

Dabei ist für den Wert {action} als Standard der Name Index festgelegt. Der Aufruf der Methode Index des Controllers UserControllerist deshalb alleine mit folgender URL möglich (der Port wird in den Projekteigenschaften festgelegt):

  • http://localhost:1234/User

Die Festlegung des Routing erfolgte bereits in der Datei Startup.cs:

1 app.UseMvc(routes =>
2 {
3   routes.MapRoute(
4     name: "default",
5     template: "{controller=Home}/{action=Index}/{id?}");
6 });

Die Segmentierung des URL erfolgt standardmäßig über “/”-Literale.

Die Methode kann sowohl Ansichten als auch einfache Daten zurückgegeben. Wenn Sie einen Client mit JavaScript haben, wäre eine solche Methode denkbar:

1 public IActionResult Index() {
2   return Json(new { Name = "Joerg", City = "Berlin" });
3 }

Parameter

Controller-Methoden sind ziemlich intelligent bei der Übernahme von Parametern. Der Standardparamter, der in der Route vereinbart wurde, ist dabei aber nur eine Option. Generell werden alle Parameter des QueryStrings (bei GET) oder die Formulardaten (via POST) direkt erkannt. Dies gilt auch für Daten, die als JSON angeliefert werden. Sie können einzeln als Parameter der Methode benannt werden.

Freilich ist es besser, die Parameter in der Route zu vereinbaren. So hat die Infrastruktur die Chance, fehlerhafte Daten bereits frühzeitig abzufangen. Darüberhinaus sollten Sie als Zeichenkette (string) angelieferte Daten immer besondres behandeln, um beim Rückliefern an die View das Einschleusen von Code zu verhindern. Dues erfolgt beispielsweise mit folgender Methode:

HtmlEncoder.Default.HtmlEncode

Werden Daten per GET geliefert, kann eine URL nun folgendermaßen aussehen:

  • http://localhost:1234/User/Show?Name=Joerg&City=Berlin

Actions und Rückgabemethoden

In den vorangegangenen Beispielen wurden bereits zwei Methoden eingesetzt: View zum Aufrufen von Ansichten und Json für einfache Nutzlasten. Oft wird jedoch mehr Kontrolle über HTTP erwartet. Vor allem komplexere Client-Applikationen profitieren davon, bestimmte HTTP-Codes zu senden.

Umgang mit View-Daten

Den Views können Daten übergeben werden. Dies erfolgt einmal durch ein sogenanntes Viewmodel, einem Datenmodell, das speziell für eine Ansicht aufbereitet wurde. Das ist der ideale Weg, weil dadurch über die Direktive @model ein typsisierter Zugriff auf die Daten in der Ansicht besteht. Die Direktive leitet den Typ als Generic an die Basisklasse der View weiter.

Nun ist die Bereitstellung eines ViewModels nicht immer möglich oder sinnvoll oder wird unnötig komplex, wenn beispielsweise einge Meldungstexte transportiert werden sollen. In solchen Fällen ist es wünschenswert, einen weiteren Weg zur Ansicht zu haben. Dies wird durch ein Verzeichnis (ViewDataDictionary) mit dem Namen ViewData erreicht.

Im Controller oder im Code-Block einer View kann dann der Zugriff folgendermaßen erfolgen:

1 ViewData["Title"] = "Index";

ViewData ist eine Eigenschaft der Basisklasse des Controllers. Sie wird der Ansicht über eine Eigenschaft mit demselben Namen ViewData übergeben. Die Gültigkeit der Daten ist auf den Verarbeitungszyklus der Anforderung beschränkt. Bei einer Weiterleitung wird der Wert wieder null. Der Datentyp der Werte ist object und im Ziel muss eine Konvertierung (cast) erfolgen.

Alternativ besteht die Option, ein dynamisches Objekt mit dem Namen ViewBag zu benutzen. Durch den Datentyp dynamic können beliebige Eigenschaften hinzugefügt werden. Das Objekt verhält sich ansonsten wie ViewData. Durch die dynamische Natur – die Festlegung der Typen erfolgt erst zu Laufzeit, fehlt hier jegliche Unterstützung im Editor.

1 ViewBag.Title = "Index";

Der dritte Weg ist die Eigenschaft TempData, die den Typ TempDataDictionary hat. Auch dies ist ein Dictionary. Diese Daten überleben eine Weiterleitung zu einer anderen Ansicht. Danach werden die Daten ungültig. Dies ist ideal, wenn Fehlermeldungen oder andere Nachrichten nach dem Absenden einer Seite auf einer anderen Seite nochmals angezeigt werden sollen, danach aber keine Bedeutung mehr haben.

Abbildung: Vergleich der View-Daten
Abbildung: Vergleich der View-Daten

Alternativ steht für eine längerfristige Aufbewahrung der Daten das Session-Objekt zur Verfügung. Es wird über die Eigenschaft Session bereitgestellt und ist vom Typ HttpSessionStateBase.

Routing to Controller Actions

Error Handling

Filters

Dependency Injection and Controllers

Testing Controller Logic

7.5 Working with the Application Model

7.6 Razor

Razor wurde an einigen Stellen bereits benutzt. Es ist nicht neu in ASP.NET Core, steht aber nach wie vor an zentraler Stelle. Prinzipiell dient Razor dazu, C# in HTML einzubetten. Folgerichtig heißen die Views .cshtml. Beim Verarbeiten der Seite wird der Code ausgeführt. Wenn dabei Ausgaben erzeugt werden, so gelangt das Ergebnis als Text in die HTML-Seite. Dies kann dann alles mögliche sein, HTML, CSS, aber auch JavaScript.

Der Weg von HTML nach C# wird mit dem @-Zeichen angezeigt. Der Rückweg wird automatisch erkannt, wenn ein Zeichen in C# nicht mehr sinnvoll ist. Gibt es Zweifel, sind verschiedene Klammern einsetzbar. Nachfolgend werden die wichtigsten Elemente aufgezeigt.

Code Block

Ein Block

1 @{ 
2   int zahl = 42; 
3   string n = "Joerg";
4 }

Ausdruck

Html-Encoded sieht dies folgendermaßen aus:

1 <span>@model.Message</span>

Soll keine Kodierung erfolgen, wird Raw benutzt:

1 <span>
2 @Html.Raw(model.Message)
3 </span>

Befehle

Text und Markup kann eng kombiniert werden:

1 @foreach(var item in items) {
2   <span>@item.Prop</span> 
3 }

Wir einfach nur Text benötigt, kann beim Verarbeiten kein HTML erkannt werden. Dafür gibt es das Pseudo-Tag <text>:

1 @if (foo) {
2   <text>Plain Text</text> 
3 }

Ist der Text auf genau eine Zeile begrenzt, geht es auch einfacher:

1 @if (foo) {
2   @:Plain Text is @bar
3 }

Blöcke werden mit usingumschlossen:

1 @ using (Html.BeginForm()) {
2   <input type="text" value="input here">
3 }

Ausgabe des @-Zeichens

E-Mail-Adressen werden erkannt und müssen nicht maskiert werden.

1 Hi philha@example.com

Wenn aber ein Konstrukt entsteht, das als E-Mail erkannt werden könnt, aber keine ist, müssen Klammern benutzt werden:

1 <span>ISBN@(isbnNumber)</span>

Wird das @-Zeichen benötigt, soll aber nicht umschalten, wird es doppelt geschrieben:

1 <span>In Razor, you use the 
2 @@foo to display the value 
3 of foo</span>

”@@” erzeugt ein einzelnes “@” in der Ausgabe.

Kommentarre

1 @*
2 This is a server side 
3 multiline comment 
4 *@

Generische Ausdrücke

Generische Ausdrücke sind etwas schwer handhabbar, weil diese in C# spitze Klammern nutzen, was ziemlich nach HTML aussieht. Da kommt Razor schnell durcheinander. Deshalb sind hier die Klammern immer erforderlich:

1 @(MyClass.MyMethod<AType>())

Mit einer Funktion kann man sich das manchmal etwas erleichtern:

1 @{
2   Func<dynamic, object> b = @<strong>@item</strong>;
3 }
4 
5 @b("Bold this")

Dies erzeugt ein Objekt vom Typ Func<T, HelperResult>

Attribute

Der Einsatz innerhalb von Tags funktioniert intuitiv.

1 <div class="@className"></div>

Spannend ist daran, das Razor das gesamte Attribut nicht rendert, wenn der Wert null ist:

1 <div></div>

Ist className dagegen eine leere Zeichenkette, sieht die Ausgabe folgendermaßen aus:

1 <div class=""></div>

In Kombination mit statischen Werten wird auch das führende Leerzeichen entfernt:

1 <div class="@className foo bar">
2 </div>

Das Attribut wird so gerendert, wenn der Wert null ist:

1 <div class="foo bar"></div>

Dageben werden *data-**-Attribute immer gerendert, egal wie der Wert der Variablen ist.

1 <div data-x="@xpos"></div>

Mit oder ohne null wird daraus:

1 <div data-x=""></div>

Ebenso intelligent sind Boolesche Attribute.

1 <input type="checkbox" checked="@isChecked" />

Ist die Variable @isChecked true, dann entsteht folgendes:

1 <input type="checkbox"
2   checked="checked" />

Ist die Variable @isChecked false, dann entsteht folgendes:

1 <input type="checkbox" />

Auflösung der URL

Der Pfad zum Stamm-URL des Projekts erfolgt mit “~”. Die Auflösung findet überall statt, auch in reinem HTML:

1 <script src="~/myscript.js">
2 </script>

7.7 Gestaltungsvorlagen (Layouts)

Layouts sind Gestaltungsvorlagen für Ansichten. Sie liefern das Grundgesicht der Anwendung. Die Ansichten liefern lediglich den Inhalt und Fragmente, die der Inhalt braucht, z.B. spezielle Skripte.

Die Standardgestaltungsvorlage

In ASP.NET Core heißt die Standardlayoutseite _Layout.cshtml und liegt im Ordner /Views/Shared. Sie wird in den Ansichten über die Eigenschaft Layout eingebunden.

Standardgestaltungsvorlage
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4   <meta charset="utf-8" />
 5   <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 6   <title>@ViewData["Title"] - DemoWeb</title>
 7 
 8   <environment names="Development">
 9     <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
10     <link rel="stylesheet" href="~/css/site.css" />
11   </environment>
12   <environment names="Staging,Production">
13     <link rel="stylesheet" 
14           href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/css/bootst\
15 rap.min.css"
16           asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
17           asp-fallback-test-class="sr-only" 
18           asp-fallback-test-property="position" 
19           asp-fallback-test-value="absolute" />
20     <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="t\
21 rue" />
22   </environment>
23 </head>
24 <body>
25   <div class="navbar navbar-inverse navbar-fixed-top">
26     <div class="container">
27       <div class="navbar-header">
28         <button type="button" 
29                 class="navbar-toggle" 
30                 data-toggle="collapse" 
31                 data-target=".navbar-collapse">
32           <span class="sr-only">Toggle navigation</span>
33           <span class="icon-bar"></span>
34           <span class="icon-bar"></span>
35           <span class="icon-bar"></span>
36         </button>
37         <a asp-controller="Home" asp-action="Index" class="navbar-brand">D\
38 emoWeb</a>
39       </div>
40       <div class="navbar-collapse collapse">
41         <ul class="nav navbar-nav">
42           <li><a asp-controller="Home" asp-action="Index">Home</a></li>
43           <li><a asp-controller="Home" asp-action="About">About</a></li>
44           <li><a asp-controller="Home" asp-action="Contact">Contact</a></l\
45 i>
46         </ul>
47         @await Html.PartialAsync("_LoginPartial")
48       </div>
49     </div>
50   </div>
51   <div class="container body-content">
52     @RenderBody()
53     <hr />
54     <footer>
55       <p>&copy; 2016 - DemoWeb</p>
56     </footer>
57   </div>
58 
59   <environment names="Development">
60     <script src="~/lib/jquery/dist/jquery.js"></script>
61     <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
62     <script src="~/js/site.js" asp-append-version="true"></script>
63   </environment>
64   <environment names="Staging,Production">
65     <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-2.1.4.min.j\
66 s"
67             asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
68             asp-fallback-test="window.jQuery">
69     </script>
70     <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/bootstrap\
71 .min.js"
72             asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
73             asp-fallback-test="window.jQuery && window.jQuery.fn && window\
74 .jQuery.fn.modal">
75     </script>
76     <script src="~/js/site.min.js" asp-append-version="true"></script>
77   </environment>
78 
79   @RenderSection("scripts", required: false)
80 </body>
81 </html>

Weil es viele View-Dateien und nur eine oder wenige Gestaltungsvorlagen gibt, ist der sich ständig wiederholende Aufruf ziemlich lästig. Dies wird vermieden, indem eine Standarddatei jeder View vorangestellt wird: _ViewStart.

Views/_ViewStart.cshtml
1 @{
2     Layout = "_Layout";
3 }

Was hier steht, wird für jede View ausgeführt. Dies betrifft im Urzustand des Projekts lediglich die Zuweisung der Gestaltungsvorlage.

Verarbeitungsreihenfolge

Die Verarbeitungsreihenfolge der Gestaltungsvorlage ist möglicherweise etwas anders als erwartet. Der Inhalt der Seite wird quasi in die Vorlage eingebettet. Die Vorlage ist also der äußere Teil, der Inhalt der innere. Dennoch wird erst die Seite verarbeitet und dann die Gestaltungsvorlage aufgerufen. Die Abarbeitung erfolgt also von innen nach außen. Aus diesem Grund kann die Seite den Zustand und Aufbau der Gestaltungsvorlage bestimmen und ändern.

Inhaltsbereiche

Der Inhalt der View wird in sogenannten Sections eingeblendet. Über die Position entscheidet folgender Befehl: @RenderSection. Die Methode bekommt einen Namen für den Abschnitt und einen Hinweis, ob der Inhalt zwingend angeliefert werden muss oder optional ist.

1 @RenderSection("script", required: false)

Alle Teile der View, die nicht explizit einer Section zugeordnet wurden, werden an der Stelle @RenderBody ausgegeben.

Dynamische Zonen

Mit dem Tag Helper <environment> können Bereiche festgelegt werden, die auf bestimmte Umgebungsvariablen reagieren. Die Applikation kann so erkennen, ob sie auf einem Entwicklungs-, Test- oder Produktions-Server läuft. Dies sieht typischerweise folgendermaßen aus:

1 <environment names="Development">            
2     <link rel="stylesheet" href="~/css/site1.css" />
3     <link rel="stylesheet" href="~/css/site2.css" />
4 </environment>
5 <environment names="Staging,Production">
6     <link rel="stylesheet" href="~/css/site.min.css" asp-file-version="tru\
7 e"/>
8 </environment>

Der Name der Umgebungsvariablen lautet Hosting:Environment. Zum Testen können Profile hinterlegt werden, die die Werte dynamisch setzen. Dazu öffnen Sie die Eigenschaften des Projekts und dann den Tab Debug.

Abbildung: Einstellen der Umgebungsvariablen für ein Profil
Abbildung: Einstellen der Umgebungsvariablen für ein Profil

Beim Start des Debuggers wird dann das passende Profil zum Testen geladen. Dies kann im Startmenü des Debuggers gewählt werden:

Abbildung: Aufruf eines Profils beim Debuggen
Abbildung: Aufruf eines Profils beim Debuggen

7.8 Area

Umfangreiche ASP.NET-Applikationen können in Bereiche zerlegt werden – sogenannte Areas. Dies war bisher eine recht aufwändige Prozedur, die nur durch Funktionen in Visual Studio entschärft wurde. Die Unterstützung durch Visual Studio ist nun nicht mehr gegeben, dafür ist der Aufbau der Area vergleichsweise einfach.

Area anlegen

Das Anlegen einer Area erfolgt durch einen Ordner. Am besten alle Areas werden unter einem Ordner Areas gesammelt:

Abbildung: Areas
Abbildung: Areas

Die Controller in diesem Bereich erhalten nun ein spezielles Attribut: AreaAttribut.

1 [Area("Admin")]
2 public class HomeController : Controller
3 {
4     // GET: /<controller>/
5     public IActionResult Index()
6     {
7         return View();
8     }
9 }

Nun muss das Routing noch angepasst werden, damit der Controller erreichbar ist. Dies erfolgt in der globalen Datei Startup.cs:

 1 app.UseMvc(routes =>
 2 {
 3   routes.MapRoute(
 4       name: "AreaRoute",
 5       template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
 6 
 7   routes.MapRoute(
 8         name: "default",
 9         template: "{controller=Home}/{action=Index}/{id?}");
10 });

Dieser Code hat zwei Besonderheiten. Zum einen muss die spezialisiertere Route zuerst stehen, also vor der allgemeinen Route default. Zum anderen werden Areas nicht individuell definiert, sondern über den Platzhalter area. Hier wird außerdem als Bedingung exists benutzt. Die Route ist also gültig, wenn der URL den Bereich enthält. Die Area muss natürlich an der entsprechenden Stelle existieren.

Routen zu Areas

Um nun zu einer bestimmten Aktion in einer Area zu verweisen, kann der passende Tag Helper benutzt werden:

1 <a asp-route-area="Admin" 
2    asp-controller="Home" 
3    asp-action="Index">
4   Startseite Administration  
5 </a>

Das Attribut asp-route-area ist hier entscheidend. Wenn hier keine vollstädige Route erstellt wird, dann liegt das daran, dass die Standardpfade berücksichtigt werden und kein unnützer Code produziert wird. Das Beispiel oben erstellt folgendes Tag:

1 <a href="/Admin">
2   Startseite Administration  
3 </a>

Die Angaben Home und Index fehlen, weil dies die Rückfallwerte der Route sind.