Richtext
Das EditorTemplate Richtext bindet den Quill-Editor an eine
string-Eigenschaft. Der Editor schreibt sein Markup vor dem Absenden in ein
verstecktes Feld; validiert und übertragen wird dieses Feld, nicht der sichtbare Editor.
Jedes Beispiel zeigt in Tabs die cshtml, das ViewModel und den
Controller (GET/POST).
Grundfall
Eine string-Eigenschaft mit [UIHint("Richtext")] – mehr braucht es nicht. Der Editor schreibt sein Markup vor dem Absenden in ein verstecktes Feld.
@model RichtextStandardBeispielViewModel
<form asp-area="RichText" asp-controller="Richtext" asp-action="Standard" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
@Html.EditorFor(m => m.Beschreibung)
@Html.Fmh(Fmh.Button()
.Text("Absenden")
.Size(ButtonSize.Sm)
.Attributes(new { @class = "mt-2" }))
</form>
using System.ComponentModel.DataAnnotations;
namespace FMH.Komponente.WebUI.Areas.RichText.Models
{
public class RichtextStandardBeispielViewModel
{
// UIHint wählt das EditorTemplate des Pakets FMH.Komponente.RichText. Der Wert ist HTML,
// deshalb string — der Editor schreibt sein Markup vor dem Absenden in das versteckte Feld.
[Display(Name = "Beschreibung")]
[UIHint("Richtext")]
public string Beschreibung { get; set; }
}
}
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.Basis.Ui.Feedback.Alert;
using FMH.Komponente.WebUI.Areas.RichText.Models;
using Microsoft.AspNetCore.Mvc;
namespace FMH.Komponente.WebUI.Areas.RichText.Controllers.Beispiele
{
// Ein Controller je EditorTemplate, je Variante ein GET+POST-Paar. Die ModelState-Verzweigung
// steht bewusst inline in den POST-Actions — sie ist der Lerninhalt der Beispiele.
[Area("RichText")]
public class RichtextController : Controller
{
[HttpGet]
public IActionResult Pflicht()
{
return this.View(new RichtextPflichtBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Pflicht(RichtextPflichtBeispielViewModel modell)
{
if (this.ModelState.IsValid)
{
// Gültig: Meldung merken und per Redirect auf die GET-Action (PRG verhindert
// doppeltes Absenden bei erneutem Laden).
this.AddAlert("Formular ist gültig – Begründung erfasst.", AlertType.Success, autoHide: true);
return this.RedirectToAction(nameof(this.Pflicht));
}
// Ungültig: dieselbe View mit den Validierungsfehlern erneut anzeigen.
return this.View(modell);
}
[HttpGet]
public IActionResult Standard()
{
return this.View(new RichtextStandardBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Standard(RichtextStandardBeispielViewModel modell)
{
// Ohne Pflichtangabe ist das Formular immer gültig. Die Meldung zeigt das übertragene
// Markup, damit sichtbar wird, was der Editor liefert und was der Sanitizer davon
// übrig lässt.
var inhalt = string.IsNullOrWhiteSpace(modell.Beschreibung)
? "leer"
: modell.Beschreibung;
this.AddAlert($"Formular übertragen – Inhalt: {inhalt}", AlertType.Success, autoHide: true);
return this.RedirectToAction(nameof(this.Standard));
}
}
}
Darstellung
Das EditorTemplate bindet den Quill-Editor an eine string-Eigenschaft:
[Display(Name = "Beschreibung")]
[UIHint("Richtext")]
public string Beschreibung { get; set; }
Gerendert wird ein verstecktes Feld mit dem Wert und daneben der Editor. Übertragen und validiert
wird das versteckte Feld, nicht der sichtbare Editor: Vor dem Absenden liest die
Validierungs-Routine der Basis (jquery-fmh-validation.js) das Markup aus dem Editor, räumt leere
Absätze und Quill-Listen auf und schreibt es in das Feld.
Der Link-Button hängt am HtmlSanitizer
Die Werkzeugleiste zeigt Fett, Kursiv, Unterstrichen, Überschriften, Listen — und den
Link-Button nur dann, wenn das Tag a erlaubt ist:
@if (HtmlSanitizerRichtextDefauls.AllowedTags.Contains("a"))
{
<text>['link'],</text>
}
Das ist kein Schönheitsentscheid, sondern verhindert eine stille Enttäuschung: Wäre der Button ohne Freigabe sichtbar, könnte man einen Link setzen — der Sanitizer entfernte ihn beim Absenden zusammen mit seinem Text, und die Eingabe wäre verloren.
Freigegeben wird in der Program.cs der Anwendung, vor der ersten Verwendung:
HtmlSanitizerRichtextDefauls.AddAllowedTags("a");
HtmlSanitizerRichtextDefauls.AddAllowedAttributes("href", "target", "rel");
Ohne Attributfreigabe bliebe das Tag ohne href übrig — also ein Link, der nirgends hinführt.
Beide Seiten arbeiten gegen dieselbe Liste: Serverseitig liest HtmlWhitelistSanitizer über
HtmlSanitizerRichtextDefauls.Options(), clientseitig meldet der Endpunkt
/api/widgets/HtmlSanitizer/settings dieselben Werte an DOMPurify. Wer die Freigabe ändert, ändert
damit Werkzeugleiste, Browser-Bereinigung und Server-Bereinigung in einem Zug.
| Erlaubt | Wirkung |
|---|---|
Voreinstellung ohne a |
kein Link-Button; ein eingefügter Link wird samt Text entfernt |
a ohne Attribute |
Link-Button erscheint, href wird beim Absenden entfernt |
a mit href, target, rel |
Link bleibt vollständig erhalten |
target="_blank" sollte immer mit rel="noopener" einhergehen — die Freigabe des Attributs
erzwingt das aber nicht, Quill setzt es von sich aus nicht.
Pflichtfeld
Mit [Required] muss Inhalt erfasst sein. Ein Editor, in dem nur ein leerer Absatz steht, gilt als leer – darum kümmert sich die Validierungs-Routine der Basis.
@model RichtextPflichtBeispielViewModel
<form asp-area="RichText" asp-controller="Richtext" asp-action="Pflicht" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
@Html.EditorFor(m => m.Begruendung)
@Html.Fmh(Fmh.Button()
.Text("Absenden")
.Size(ButtonSize.Sm)
.Attributes(new { @class = "mt-2" }))
</form>
using System.ComponentModel.DataAnnotations;
namespace FMH.Komponente.WebUI.Areas.RichText.Models
{
public class RichtextPflichtBeispielViewModel
{
// [Required] wirkt auf das versteckte Feld hinter dem Editor. Ein leerer Editor liefert
// "<p><br></p>"; die Validierungs-Routine der Basis räumt das vor dem Absenden zu einer
// leeren Zeichenkette auf, damit die Pflichtprüfung greift.
[Display(Name = "Begründung")]
[UIHint("Richtext")]
[Required(ErrorMessage = "Bitte eine Begründung erfassen.")]
public string Begruendung { get; set; }
}
}
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.Basis.Ui.Feedback.Alert;
using FMH.Komponente.WebUI.Areas.RichText.Models;
using Microsoft.AspNetCore.Mvc;
namespace FMH.Komponente.WebUI.Areas.RichText.Controllers.Beispiele
{
// Ein Controller je EditorTemplate, je Variante ein GET+POST-Paar. Die ModelState-Verzweigung
// steht bewusst inline in den POST-Actions — sie ist der Lerninhalt der Beispiele.
[Area("RichText")]
public class RichtextController : Controller
{
[HttpGet]
public IActionResult Pflicht()
{
return this.View(new RichtextPflichtBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Pflicht(RichtextPflichtBeispielViewModel modell)
{
if (this.ModelState.IsValid)
{
// Gültig: Meldung merken und per Redirect auf die GET-Action (PRG verhindert
// doppeltes Absenden bei erneutem Laden).
this.AddAlert("Formular ist gültig – Begründung erfasst.", AlertType.Success, autoHide: true);
return this.RedirectToAction(nameof(this.Pflicht));
}
// Ungültig: dieselbe View mit den Validierungsfehlern erneut anzeigen.
return this.View(modell);
}
[HttpGet]
public IActionResult Standard()
{
return this.View(new RichtextStandardBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Standard(RichtextStandardBeispielViewModel modell)
{
// Ohne Pflichtangabe ist das Formular immer gültig. Die Meldung zeigt das übertragene
// Markup, damit sichtbar wird, was der Editor liefert und was der Sanitizer davon
// übrig lässt.
var inhalt = string.IsNullOrWhiteSpace(modell.Beschreibung)
? "leer"
: modell.Beschreibung;
this.AddAlert($"Formular übertragen – Inhalt: {inhalt}", AlertType.Success, autoHide: true);
return this.RedirectToAction(nameof(this.Standard));
}
}
}
Darstellung
[Required] wirkt auf das versteckte Feld hinter dem Editor:
[Display(Name = "Begründung")]
[UIHint("Richtext")]
[Required(ErrorMessage = "Bitte eine Begründung erfassen.")]
public string Begruendung { get; set; }
Der entscheidende Punkt ist der leere Editor: Quill liefert dafür nicht eine leere Zeichenkette,
sondern <p><br></p>. Ohne Aufräumen wäre das ein Wert und die Pflichtprüfung liefe ins Leere.
setQuillElement in jquery-fmh-validation.js fängt genau diesen Fall ab und setzt das Feld auf
leer, bevor validiert wird.
Was zu beachten ist
- Die Beschriftung bekommt über
[Display]und dierequired-Klasse den Stern; die Fehlermeldung erscheint unter dem Editor iminvalid-feedback-Bereich. - Serverseitig gilt
[Required]unabhängig vom Browser erneut — die Aufräumroutine läuft im Browser und ist damit kein Ersatz für die Prüfung am Server. - Zusätzliche Längenprüfungen (
[StringLength]) zählen Markup mit, nicht den sichtbaren Text. Für eine Begrenzung nach Zeichen des reinen Texts braucht es eine eigene Prüfung.