Label aus dem ViewModel

DisplayFromViewModelAttribute nennt die Eigenschaft, aus der die Beschriftung eines Eingabefelds kommt – statt sie als Zeichenkette in [Display] zu schreiben. Gedacht für Formulare, deren Texte aus einer Konfiguration oder der Datenbank stammen und je Datensatz wechseln.

Erkennt ein Template das Attribut, rendert es die Beschriftung zusätzlich als verstecktes Feld. Damit kommt sie beim Absenden zurück und muss für ein erneutes Anzeigen nicht noch einmal geladen werden. Die Angabe gilt für alle EditorTemplates und steht deshalb über den Rubriken.

Beschriftung aus einer Eigenschaft

Der Label-Text steht nicht im Attribut, sondern in einer zweiten Eigenschaft des ViewModels – und kommt über ein verstecktes Feld ohne zweiten Datenbankzugriff zurück.

@model DisplayFromViewModelBeispielViewModel

@* Am Feld steht kein [Display(Name = "…")] – die Beschriftung kommt über
   [DisplayFromViewModel(nameof(Bezeichnung))] aus der Eigenschaft daneben. In der View ändert
   sich dadurch nichts: EditorFor wie immer. *@
<form asp-area="Basis" asp-controller="DisplayFromViewModel" asp-action="Index" method="post">

    @Html.EditorFor(m => m.Text)

    @Html.Fmh(Fmh.Button()
        .Text("Absenden")
        .Size(ButtonSize.Sm)
        .Attributes(new { @class = "mt-2" }))
</form>
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;

namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
    /// <summary>
    /// Zeigt <see cref="DisplayFromViewModelAttribute" />: Die Beschriftung des Eingabefelds steht
    /// nicht im Attribut, sondern in einer zweiten Eigenschaft des ViewModels.
    /// </summary>
    /// <remarks>
    /// Der übliche Fall sind Formulare, deren Fragen aus einer Konfiguration oder der Datenbank
    /// kommen: Der Text steht dort und wechselt je Datensatz, ein <c>[Display(Name = "…")]</c> ist
    /// deshalb nicht möglich.
    /// </remarks>
    public class DisplayFromViewModelBeispielViewModel
    {
        /// <summary>
        /// Holt oder setzt den Schlüssel des Datensatzes, aus dem die Beschriftung stammt.
        /// </summary>
        public int Id { get; set; } = 42;

        /// <summary>
        /// Holt oder setzt die Beschriftung. In einem Projekt kommt sie aus der Datenbank; das
        /// Template rendert sie zusätzlich als verstecktes Feld, damit sie den POST übersteht.
        /// </summary>
        public string Bezeichnung { get; set; } = "Begründung der Abweichung";

        /// <summary>
        /// Holt oder setzt den erfassten Text. Das Attribut nennt die Eigenschaft, aus der die
        /// Beschriftung kommt – <c>[Display]</c> wird dadurch überflüssig.
        /// </summary>
        [DisplayFromViewModel(nameof(Bezeichnung))]
        [MaxLength(200)]
        public string Text { get; set; }
    }
}
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.Basis.Ui.Feedback.Alert;
using FMH.Komponente.WebUI.Areas.Basis.Models;
using Microsoft.AspNetCore.Mvc;

namespace FMH.Komponente.WebUI.Areas.Basis.Controllers.Beispiele
{
    /// <summary>
    /// Die Gegenstelle der beiden DisplayFromViewModel-Beispiele.
    /// </summary>
    /// <remarks>
    /// Zwei Varianten, deshalb je ein GET+POST-Paar: <c>Index</c> für das einzelne Feld,
    /// <c>Formular</c> für die Auflistung mit eigenem Zeilen-Template.
    /// </remarks>
    [Area("Basis")]
    public class DisplayFromViewModelController : Controller
    {
        /// <summary>
        /// Zeigt das einzelne Feld, dessen Beschriftung aus dem ViewModel kommt.
        /// </summary>
        /// <returns>Die Beispiel-Seite.</returns>
        [HttpGet]
        public IActionResult Index()
        {
            return this.View(new DisplayFromViewModelBeispielViewModel());
        }

        /// <summary>
        /// Nimmt das einzelne Feld entgegen.
        /// </summary>
        /// <param name="modell">Das ausgefüllte Formular.</param>
        /// <returns>Die Umleitung nach erfolgreichem POST, sonst dieselbe Seite mit den Fehlern.</returns>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(DisplayFromViewModelBeispielViewModel modell)
        {
            if (this.ModelState.IsValid)
            {
                // Der Kern des Attributs: Die Beschriftung kam über ein verstecktes Feld zurück und
                // steht hier zur Verfügung, ohne sie erneut aus der Datenbank zu holen.
                this.AddAlert(
                    $"Gespeichert. Die Beschriftung «{modell.Bezeichnung}» kam ohne Datenbankzugriff mit zurück.",
                    AlertType.Success,
                    autoHide: true);

                return this.RedirectToAction(nameof(this.Index));
            }

            return this.View(modell);
        }

        /// <summary>
        /// Zeigt das Formular, dessen Felder aus der Konfiguration kommen.
        /// </summary>
        /// <returns>Die Beispiel-Seite.</returns>
        [HttpGet]
        public IActionResult Formular()
        {
            return this.View(FragebogenBeispielViewModel.Ausgangslage());
        }

        /// <summary>
        /// Nimmt das Formular entgegen.
        /// </summary>
        /// <param name="modell">Das ausgefüllte Formular.</param>
        /// <returns>Die Umleitung nach erfolgreichem POST, sonst dieselbe Seite mit den Fehlern.</returns>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Formular(FragebogenBeispielViewModel modell)
        {
            if (this.ModelState.IsValid)
            {
                this.AddAlert(
                    $"{modell.Fragen.Count} Zeilen gespeichert – Beschriftungen und Eingabearten kamen mit zurück.",
                    AlertType.Success,
                    autoHide: true);

                return this.RedirectToAction(nameof(this.Formular));
            }

            // Vor dem erneuten Rendern die Konfiguration ergänzen, die in keinem Feld steht:
            // Ohne die Auswahllisten wäre das Dropdown beim zweiten Aufbau leer.
            modell.ErgaenzeKonfiguration();

            return this.View(modell);
        }
    }
}

Darstellung

Ein Eingabefeld wird normalerweise über [Display(Name = "…")] beschriftet. Das setzt voraus, dass der Text beim Kompilieren feststeht. Kommt er aus einer Konfiguration oder der Datenbank, geht das nicht, dann nennt [DisplayFromViewModel(…)] die Eigenschaft, in der der Text steht:

public string Bezeichnung { get; set; }

[DisplayFromViewModel(nameof(Bezeichnung))]
public string Text { get; set; }

In der View ändert sich nichts:

@Html.EditorFor(m => m.Text)

Das versteckte Feld ist der eigentliche Gewinn

Erkennt ein Template das Attribut, rendert es die Beschriftung zusätzlich als verstecktes Feld:

<input type="hidden" name="Bezeichnung" value="Begründung der Abweichung" />

Damit kommt der Text beim Absenden zurück. Wer das Formular nach einem Validierungsfehler erneut anzeigt, braucht die Beschriftung nicht noch einmal aus der Datenbank zu holen, und genau dafür ist das Attribut gedacht.

Welche Templates es auswerten

Auswerten das Attribut Werten es nicht aus
Boolean, BooleanCombobox, Checkbox, CheckboxList, ComboboxList, DateTime, DropdownList, MultiselectList, Password, RadioList, String, Textarea Hidden, HiddenList
aus anderen Paketen: DokumentListe, IFormFile (FileUpload), Richtext (RichText) FrageAnwort (Kompetenzen)

Ein eigenes Template wertet es aus, sobald es ViewData.ModelMetadata.HasDisplayFromViewModel() abfragt und das versteckte Feld selbst rendert, siehe die Templates der Basis als Vorlage.

Was zu beachten ist

  • Leerer Wert heisst Rückfall, nicht leeres Label. Ist die genannte Eigenschaft null oder liefert sie nichts, greift wieder [Display] beziehungsweise der Eigenschaftsname.
  • Der Name muss eine Eigenschaft desselben Objekts sein. Gesucht wird über ContainerType.GetProperty(...); ein Pfad über mehrere Ebenen funktioniert nicht.
  • nameof(...) verwenden, nicht die Zeichenkette. Sonst fällt eine Umbenennung erst zur Laufzeit auf – und zwar still, weil dann der Rückfall greift.
  • Das Attribut liegt in FMH.Komponente.Basis.Ui.Validation.DataAnnotation, validiert aber nichts.