Views mit Razor
Kurz gesagt: Razor ist die Template-Sprache der Views in ASP.NET Core: HTML mit C# darin, mit einem Model, Schleifen, einem gemeinsamen Layout, Partial Views und Formularen mit Validierung.
Problem
HTML, aus Strings zusammengeklebt
In der vorigen Lektion hat Details eine View mit drei Zeilen zurückgegeben. Der Katalog ist aber eine Tabelle mit vier Produkten, jede Seite hat dasselbe Menü, und das Bestellformular darf keinen leeren Namen und keine Menge von −3 annehmen.
Das alles mit "<tr><td>" + product.Name + ... zusammenzukleben ist langsam, und man vertut sich leicht. Es ist auch gefährlich: Wenn jemand einem Produkt den Namen <script>…</script> gibt, setzt der String ihn als echten Code in die Seite, und er läuft im Browser jeder Kundin und jedes Kunden. Dieser Angriff heißt XSS (Cross-Site Scripting).
Sieh es dir an
Razor: HTML mit C# darin
Eine View ist eine Datei .cshtml. Sie ist HTML, und @ schaltet auf C# um. Der Controller übergibt die Liste und einen Titel:
public class ProductsController : Controller{ private readonly IProductService products;
public ProductsController(IProductService products) => this.products = products;
public IActionResult Index() { ViewData["Title"] = "Katalog"; return View(products.All()); }}Die View Views/Products/Index.cshtml:
@model List<Product><h1>@ViewData["Title"]</h1><table> <tr><th>Produkt</th><th>Preis</th><th></th></tr> @foreach (Product product in Model) { <tr> <td>@product.Name</td> <td>@product.Price €</td> @if (product.Price > 10) { <td>empfohlen</td> } else { <td></td> } </tr> }</table><p>Produkte: @Model.Count</p><h1>Katalog</h1><table> <tr><th>Produkt</th><th>Preis</th><th></th></tr> <tr> <td>Ethiopia Yirgacheffe</td> <td>14.90 €</td> <td>empfohlen</td> </tr> <tr> <td>Colombia Supremo</td> <td>12.50 €</td> <td>empfohlen</td> </tr> <tr> <td>Sencha</td> <td>8.40 €</td> <td></td> </tr> <tr> <td>Earl Grey</td> <td>6.90 €</td> <td></td> </tr></table><p>Produkte: 4</p>@model List<Product>sagt, welche Art von Objekt der Controller übergibt. In der View ist esModel. Dadurch ist die View stark typisiert: Ein Fehler wie@Model.Contfällt schon beidotnet buildauf.@product.Namesetzt einen Wert ein.@foreachund@ifsind gewöhnliches C#, und das HTML darin wird wiederholt oder je nach Bedingung gezeigt.ViewData["Title"]ist ein Dictionary, mit dem der Controller neben dem Model kleine Dinge an die View schickt.- Jeder Wert wird kodiert:
<wird zu<. Ein Name wie<script>wird als Text gezeigt und läuft nicht. Das ist der Schutz vor XSS, ohne dass du etwas tun musst.
Die Template-Sprache der View heißt Razor. Für das Ende des C# gibt es keine besondere Syntax: Razor erkennt selbst, wann ein HTML-Tag beginnt.
Dasselbe in einer kürzeren View, Produkt für Produkt:
@model List<Product><h1>Katalog</h1><ul>@foreach (Product product in Model){ <li>@product.Name: @product.Price €</li>}</ul><p>Produkte: @Model.Count</p>Schritt für Schritt, in Worten
- Reines HTML kommt so, wie es ist, in die Antwort.
- Der Controller übergibt eine Liste mit 4 Produkten. In der View ist sie Model.
- @foreach nimmt das erste Produkt. @product.Name wird zu „Ethiopia Yirgacheffe“ und @product.Price zu 14.90.
- Das nächste Produkt: dieselbe Zeile <li> mit „Colombia Supremo“ und 12.50.
- Das nächste Produkt: dieselbe Zeile <li> mit „Sencha“ und 8.40.
- Das nächste Produkt: dieselbe Zeile <li> mit „Earl Grey“ und 6.90.
- Die Schleife ist zu Ende. @Model.Count setzt 4 ein. Der Browser bekommt fertiges HTML, ohne ein einziges @.
Genauer, für Neugierige
- Ein Ausdruck mit Leerzeichen oder Operatoren kommt in Klammern:
@(product.Price * 2). Ein Block C# ohne Ausgabe ist@{ ... }, zum Beispiel@{ ViewData["Title"] = "Katalog"; }am Anfang der View. - Wenn du wirklich fertiges HTML ohne Kodierung einsetzen willst, gibt es
@Html.Raw(text). Nimm es nur für Text, den du selbst geschrieben hast, nie für Text, den ein Nutzer eingegeben hat. @DateTime.Now.Yearsetzt das aktuelle Jahr ein: praktisch für das „© 2026“ unten, das du nicht jedes Jahr ändern willst.- Warum
AddWebEncodersmitUnicodeRanges.AllinProgram.cssteht, erklärt die MVC-Lektion: Ohne die Zeile kommen Buchstaben außerhalb des einfachen lateinischen Alphabets als Codes wieüheraus. Der Browser zeigt sie so oder so richtig an.
Probier es aus
Was angezeigt wird
Die Action Cheap übergibt alle vier Produkte. Was gibt die View zurück?
public class ProductsController : Controller{ private readonly IProductService products;
public ProductsController(IProductService products) => this.products = products;
public IActionResult Cheap() => View(products.All());}@model List<Product><ul>@foreach (Product product in Model){ if (product.Price < 10) { <li>@product.Name: @product.Price €</li> }}</ul><p>@Model.Count(p => p.Price < 10) von @Model.Count</p>In @foreach bist du schon in C#, also hat das if kein @. Gezeigt werden nur die Produkte unter 10 €. Am Ende ist @Model.Count(p => p.Price < 10) LINQ, direkt in der View aufgerufen: 2 von 4.
Die Idee
Ein gemeinsames Layout
Menü und Fußzeile stehen auf jeder Seite. Statt sie zu kopieren, legst du sie in ein Layout: einen Rahmen, in den die Seite kommt. Es liegt in Views/Shared/_Layout.cshtml:
<!DOCTYPE html><html lang="de"><head> <title>@ViewData["Title"] | Kaffee und Tee</title></head><body> <nav> <a asp-controller="Home" asp-action="Index">Startseite</a> <a asp-controller="Products" asp-action="Index">Produkte</a> </nav> <main> @RenderBody() </main> <footer>Kaffee und Tee, Hauptstraße 12</footer></body></html>@RenderBody() ist die Stelle für die Seite. Welches Layout verwendet wird, steht in Views/_ViewStart.cshtml, das vor jeder View läuft:
@{ Layout = "_Layout";}<a asp-controller="Products" asp-action="Index"> ist ein Tag-Helper: Die Attribute asp-… laufen auf dem Server und bauen die Adresse aus der Route. In der Antwort wird daraus <a href="/Products">. Die Helper schaltest du einmal für alle Views in Views/_ViewImports.cshtml ein:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpersGenauer, für Neugierige
- Der Titel der Seite kommt aus
ViewData["Title"], das die View oder der Controller füllt. So ist das Layout gemeinsam, aber der Titel verschieden. - Eine View kommt mit
@{ Layout = null; }ohne Layout aus oder wählt ein anderes, etwa"_AdminLayout". - Ein Layout kann auch Sections haben:
@RenderSection("Scripts", required: false)ist eine Stelle, die die Seite mit@section Scripts { ... }füllt, zum Beispiel mit einem Skript nur für diese Seite.
Sieh es dir an
Partial Views, ViewData und ViewBag
Die Zeile eines Produkts wird im Katalog, im Warenkorb und in der Bestellung gebraucht. Also kommt sie in eine Partial View, Views/Shared/_ProductRow.cshtml:
@model Product<tr><td>@Model.Name</td><td>@Model.Price €</td></tr>Der Katalog ruft sie mit <partial> für jedes Produkt auf:
@model List<Product><h1>@ViewData["Title"]</h1><p>@ViewBag.Message</p><table> @foreach (Product product in Model) { <partial name="_ProductRow" model="product" /> }</table>Die Controller legen den Titel in ViewData und die Meldung in ViewBag:
public class HomeController : Controller{ public IActionResult Index() { ViewData["Title"] = "Startseite"; return View(); }}
public class ProductsController : Controller{ private readonly IProductService products;
public ProductsController(IProductService products) => this.products = products;
public IActionResult Index() { ViewData["Title"] = "Produkte"; ViewBag.Message = "Kostenlose Lieferung ab 50 €"; return View(products.All()); }}Die Antwort für /Products: das Layout und in <main> die Seite mit den vier Zeilen:
<!DOCTYPE html><html lang="de"><head> <title>Produkte | Kaffee und Tee</title></head><body> <nav> <a href="/">Startseite</a> <a href="/Products">Produkte</a> </nav> <main> <h1>Produkte</h1><p>Kostenlose Lieferung ab 50 €</p><table> <tr><td>Ethiopia Yirgacheffe</td><td>14.90 €</td></tr>
<tr><td>Colombia Supremo</td><td>12.50 €</td></tr>
<tr><td>Sencha</td><td>8.40 €</td></tr>
<tr><td>Earl Grey</td><td>6.90 €</td></tr>
</table>
</main> <footer>Kaffee und Tee, Hauptstraße 12</footer></body></html>ViewData["Message"]undViewBag.Messagesind derselbe Speicher.ViewDataist ein Dictionary mit String-Schlüsseln, undViewBagist dasselbe, aber mit Punkt. Keins von beiden wird beim Kompilieren geprüft: Ein Tippfehler im Schlüssel ergibt eine leere Stelle. Für die Hauptdaten nimm das Model.- Nach Konvention beginnt der Name einer Partial View mit
_, damit man sieht, dass sie keine ganze Seite ist.
Genauer, für Neugierige
- In älteren Projekten wird eine Partial View mit
@Html.Partial("_ProductRow", product)aufgerufen. Das funktioniert auch heute, wartet aber synchron, und der Compiler warnt davor. Der neue Weg ist<partial>oder@await Html.PartialAsync(...). - Eine Partial View mit eigener Logik (zum Beispiel der Warenkorb im Menü, der seine Produkte selbst zählt) ist eine View Component: eine Klasse plus eine View, aus dem Layout aufgerufen.
- Die Leerzeilen zwischen den
<tr>kommen daher, dass<partial>in der Schleife in einer eigenen Zeile steht. Der Browser ignoriert sie.
Probier es aus
Wohin die Seite kommt
Was ruft das Layout dort auf, wo die Seite hineinkommt?
<body>
<nav>…</nav>
<main>
@()
</main>
</body>@RenderBody() setzt die View der Action ein. Alles darum herum, Menü und Fußzeile, teilen sich die Seiten.
Die Idee
Formulare, Binding und Validierung
Eine Bestellung ist eine Klasse. Die Attribute über den Eigenschaften sagen, was erlaubt ist:
public class Order{ public int Id { get; set; }
[Required(ErrorMessage = "Gib einen Namen ein.")] public string Name { get; set; }
[Required(ErrorMessage = "Gib eine E-Mail-Adresse ein.")] [EmailAddress(ErrorMessage = "Das ist keine E-Mail-Adresse.")] public string Email { get; set; }
[Range(1, 20, ErrorMessage = "Die Menge muss zwischen 1 und 20 liegen.")] public int Quantity { get; set; }}Das Formular ist eine View mit dem Model Order. asp-for macht für die Eigenschaft ein Feld mit dem richtigen name, id und type, und asp-validation-for ist ein Platz für ihren Fehler:
@model Order
<form asp-action="Create" method="post"> <label asp-for="Name">Name</label> <input asp-for="Name" /> <span asp-validation-for="Name"></span>
<label asp-for="Email">E-Mail</label> <input asp-for="Email" /> <span asp-validation-for="Email"></span>
<label asp-for="Quantity">Menge</label> <input asp-for="Quantity" /> <span asp-validation-for="Quantity"></span>
<button>Order</button></form>Der Controller hat zwei Actions Create: Mit GET zeigt er das leere Formular, mit POST nimmt er es entgegen:
public class OrdersController : Controller{ private readonly OrderBook book;
public OrdersController(OrderBook book) => this.book = book;
[HttpGet] public IActionResult Create() => View(new Order { Quantity = 1 });
[HttpPost] public IActionResult Create(Order order) { if (!ModelState.IsValid) return View(order); int id = book.Add(order); return RedirectToAction("Thanks", new { id }); }
public IActionResult Thanks(int id) { Order order = book.Find(id); if (order == null) return NotFound(); return View(order); }}Create(Order order)bekommt ein fertiges Objekt. ASP.NET Core nimmt die Felder des Formulars nach ihren Namen (Name,Email,Quantity) und setzt sie in die Eigenschaften. Das heißt Model Binding.- Beim Binding prüft es die Attribute. Das Ergebnis steht in
ModelState:IsValidistfalse, wenn eine Regel verletzt ist. - Bei einem Fehler gibst du mit
View(order)dasselbe Formular zurück: Was getippt wurde, bleibt, und jedes Feld hat seinen Fehler daneben. - Bei Erfolg speicherst du und leitest weiter.
new { id }ist ein Objekt mit einer Eigenschaft,id, aus der die Adresse/Orders/Thanks/1gebaut wird. Das Muster heißt POST, Redirect, GET (PRG): Wer die Seite „Danke“ neu lädt, schickt die Bestellung nicht ein zweites Mal.
Sieh es dir an
Das Formular in Aktion
Der Client öffnet das leere Formular, schickt ein falsches und dann ein richtiges:
var http = new HttpClient(new HttpClientHandler { AllowAutoRedirect = false });string site = "http://localhost:7244";
string empty = await http.GetStringAsync(site + "/Orders/Create");Console.WriteLine(empty.Split('\n').First(line => line.Contains("id=\"Name\"")).Trim());
var bad = new FormUrlEncodedContent(new Dictionary<string, string> { ["Name"] = "", ["Email"] = "anna@", ["Quantity"] = "0" });HttpResponseMessage rejected = await http.PostAsync(site + "/Orders/Create", bad);Console.WriteLine("Ungültiges Formular: " + (int)rejected.StatusCode);string page = await rejected.Content.ReadAsStringAsync();foreach (string line in page.Split('\n').Where(line => line.Contains("field-validation-error") || line.Contains("value=\"anna@\""))){ Console.WriteLine(line.Trim());}
var good = new FormUrlEncodedContent(new Dictionary<string, string> { ["Name"] = "Anna", ["Email"] = "anna@example.bg", ["Quantity"] = "2" });HttpResponseMessage accepted = await http.PostAsync(site + "/Orders/Create", good);Console.WriteLine("Gültiges Formular: " + (int)accepted.StatusCode + ", Location: " + accepted.Headers.Location);Console.Write(await http.GetStringAsync(site + accepted.Headers.Location));<input type="text" data-val="true" data-val-required="Gib einen Namen ein." id="Name" name="Name" value="" />Ungültiges Formular: 200<span class="field-validation-error" data-valmsg-for="Name" data-valmsg-replace="true">Gib einen Namen ein.</span><input type="email" class="input-validation-error" data-val="true" data-val-email="Das ist keine E-Mail-Adresse." data-val-required="Gib eine E-Mail-Adresse ein." id="Email" name="Email" value="anna@" /><span class="field-validation-error" data-valmsg-for="Email" data-valmsg-replace="true">Das ist keine E-Mail-Adresse.</span><span class="field-validation-error" data-valmsg-for="Quantity" data-valmsg-replace="true">Die Menge muss zwischen 1 und 20 liegen.</span>Gültiges Formular: 302, Location: /Orders/Thanks/1<p>Danke, Anna! Die Bestellung Nr. 1 über 2 Artikel ist eingegangen.</p><p>Du bekommst eine Bestätigung an anna@example.bg.</p>- Die erste Zeile ist das Feld, das
asp-forausNamegemacht hat.data-val-requiredträgt die Meldung zum Browser: Mit der JavaScript-Bibliothek für Validierung zeigt er den Fehler schon, bevor das Formular geschickt wird. - Das falsche Formular gibt 200 zurück: dieselbe Seite, mit der Klasse
field-validation-errorund den Meldungen. Die E-Mail behält, was getippt wurde,anna@. - Das richtige Formular ist eine 302 auf
/Orders/Thanks/1, und dort steht die Bestätigung aus der ViewThanks.cshtml:
@model Order<p>Danke, @Model.Name! Die Bestellung Nr. @Model.Id über @Model.Quantity Artikel ist eingegangen.</p><p>Du bekommst eine Bestätigung an @Model.Email.</p>Genauer, für Neugierige
- Die Prüfung im Browser ist Komfort. Die Prüfung, auf die du dich verlässt, ist
ModelState.IsValidauf dem Server: Jeder kann ein POST ohne Browser schicken, genau wie dieses Programm. - Weitere Attribute:
[StringLength(50)],[Phone],[Compare("Password")](zwei Felder müssen übereinstimmen),[RegularExpression(...)].[DataType(DataType.Password)]prüft nichts; es sagtasp-for, dass estype="password"machen soll. <form asp-action="Create" method="post">fügt außerdem ein verstecktes Feld__RequestVerificationTokenhinzu. Mit[ValidateAntiForgeryToken]über der POST-Action nimmt der Server nur Formulare an, die er selbst ausgegeben hat. So kann eine andere Website keine Bestellung im Namen des angemeldeten Nutzers schicken (der Angriff heißt CSRF).Quantityist einint, also ist es auch ohne[Required]Pflicht: Aus einem leeren Feld wird keine Zahl.
Probier es aus
Die Bestellung geht ohne Namen durch
Ein Formular mit leerem Namen wird gespeichert, und der Nutzer sieht „Danke, !“. Und ein richtiges Formular kommt mit Fehlern zurück. In welcher Zeile liegt das Problem?
Wähle die Zeile mit dem Fehler.
Die Bedingung ist umgedreht. View(order) soll zurückgegeben werden, wenn das Model nicht gültig ist: if (!ModelState.IsValid) return View(order);. Jetzt werden die falschen Bestellungen gespeichert, und die richtigen gehen zurück zum Formular.
Übung
Die Seiten des Shops
Löse die Aufgaben in einem Projekt aus dotnet new mvc. Die Produkte und IProductService sind aus der MVC-Lektion.
1. Eine Produkttabelle. Index übergibt die Produkte nach Preis sortiert. Die View ist eine Tabelle mit Spalten für Produkt, Kategorie und Preis, und die letzte Zeile ist die Summe mit @Model.Sum(...):
<table> <tr><th>Produkt</th><th>Kategorie</th><th>Preis</th></tr> <tr><td>Earl Grey</td><td>tee</td><td>6.90 €</td></tr> <tr><td>Sencha</td><td>tee</td><td>8.40 €</td></tr> <tr><td>Colombia Supremo</td><td>kaffee</td><td>12.50 €</td></tr> <tr><td>Ethiopia Yirgacheffe</td><td>kaffee</td><td>14.90 €</td></tr> <tr><td colspan="2">Summe</td><td>42.70 €</td></tr></table>2. Ein gemeinsames Layout mit Menü. HomeController hat Index und Contacts. Das Layout hat ein Menü aus Tag-Helpern und einen Titel aus ViewData["Title"]. Contacts schickt die Telefonnummer mit ViewBag.Phone:
<!DOCTYPE html><html lang="de"><head> <title>Kontakt | Kaffee und Tee</title></head><body> <nav> <a href="/">Startseite</a> <a href="/Home/Contacts">Kontakt</a> </nav> <h1>Kontakt</h1><p>Telefon: 030 123 4567</p>
</body></html>3. Eine Verkostung buchen. Das Formular Booking hat einen Namen ([Required]), eine E-Mail ([Required], [EmailAddress]) und eine Zahl von Plätzen von 1 bis 8 ([Range]). Bei einem Fehler kommt das Formular zurück, bei Erfolg leitet es auf Done weiter, das sagt, wie viele Plätze gebucht wurden und wie viele es bisher insgesamt sind. Der Client schickt drei Formulare: ein falsches (eine E-Mail ohne @ und 12 Plätze) und zwei richtige. Für ein falsches gibt er die Zahl der Meldungen aus, für ein richtiges die Seite, auf der er nach der Weiterleitung landet:
Fehler: 2Gebuchte Plätze: 3. Insgesamt für die Verkostung: 3.Gebuchte Plätze: 2. Insgesamt für die Verkostung: 5.Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: async und await · Wie das Internet funktioniert · Ein Webserver mit Minimal API · MVC: Model, View, Controller
- Geht weiter in: WebAPI: Daten für andere Programme · Nicht vertrauenswürdige Eingaben
- Aufgaben zum Thema: Web und Desktop