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.

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 die required-Klasse den Stern; die Fehlermeldung erscheint unter dem Editor im invalid-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.