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.
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:
- Metadaten werden gelesen und nach HTML transportiert
- 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
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:
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 EnumerationCachePreservationPriority
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:
| .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.
| .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" />
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>
Link und Script
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&&e.defau\
6 ltView.getComputedStyle?e.defaultView.getComputedStyle(g):g.currentStyle;i\
7 f(h&&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 EnumerationValidationSummary. 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:
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:
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.
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:
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:
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:
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.
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.
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:
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.
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.
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.
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:
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:
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.
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 }
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>
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.
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.
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.
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>© 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.
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.
Beim Start des Debuggers wird dann das passende Profil zum Testen geladen. Dies kann im Startmenü des Debuggers gewählt werden:
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:
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.