Datum

Das DateTime-EditorTemplate rendert ein DateTime? als Datumsfeld. Einbindung über @Html.EditorFor(...) mit [UIHint(UiEditor.Basis.DateTime)].

Datum (Grundfall)

Ein DateTime? als Datumsfeld. Das Feld ist ein input vom Typ date, den Kalender bringt der Browser mit.

@model DatumBeispielViewModel

<form asp-area="Basis" asp-controller="Datum" asp-action="Index" method="post">
    @Html.EditorFor(m => m.Datum)
    @Html.Fmh(Fmh.Button()
        .Text("Absenden")
        .Size(ButtonSize.Sm)
        .Attributes(new { @class = "mt-2" }))
</form>
using System;
using System.ComponentModel.DataAnnotations;

namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
    /// <summary>
    /// Der Grundfall: ein Datumsfeld ohne Pflichtangabe und ohne Bereichsgrenzen.
    /// </summary>
    public class DatumBeispielViewModel
    {
        [Display(Name = "Datum")]
        [UIHint(UiEditor.Basis.DateTime)]
        public DateTime? Datum { 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>
    /// Ein Controller pro Template (DateTime); je eine Action/View pro Variante.
    /// Die POST-Actions zeigen, was der Server aus der Eingabe macht: ein leeres Feld ist ohne
    /// Pflichtangabe gültig, mit [Required] nicht, und ein unlesbares Datum scheitert schon beim
    /// Binden.
    /// </summary>
    [Area("Basis")]
    public class DatumController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return this.View(new DatumBeispielViewModel());
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(DatumBeispielViewModel modell)
        {
            return this.PruefeUndZeige(modell, nameof(this.Index));
        }

        [HttpGet]
        public IActionResult Pflicht()
        {
            return this.View(new DatumPflichtBeispielViewModel());
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Pflicht(DatumPflichtBeispielViewModel modell)
        {
            return this.PruefeUndZeige(modell, nameof(this.Pflicht));
        }

        /// <summary>
        /// Gemeinsame PRG-Logik der Varianten: Bei gültigem Modell eine Meldung setzen und per
        /// Redirect auf die GET-Action, sonst dieselbe View mit den Fehlern erneut anzeigen.
        /// </summary>
        /// <param name="modell">Das abgeschickte Modell.</param>
        /// <param name="action">Der Name der GET-Action dieser Variante.</param>
        /// <returns>Die Weiterleitung oder die erneut gerenderte View.</returns>
        private IActionResult PruefeUndZeige(object modell, string action)
        {
            if (this.ModelState.IsValid)
            {
                // Erfolgsfall: hier würden die eigentlichen Aktionen ausgeführt (z. B. Speichern).
                this.AddAlert("Formular ist gültig – Datum übernommen.", AlertType.Success, autoHide: true);

                // PRG-Pattern: nach erfolgreichem POST per Redirect auf die GET-Action.
                return this.RedirectToAction(action);
            }

            // Ungültige Eingabe: dieselbe Seite erneut anzeigen, damit die Meldungen erscheinen.
            return this.View(action, modell);
        }
    }
}

Darstellung

Das DateTime-EditorTemplate rendert ein DateTime? als Datumsfeld. Eingebunden wird es über den ASP.NET-Core-Standardmechanismus, also [UIHint(UiEditor.Basis.DateTime)] am Modell und Html.EditorFor(...) in der View, innerhalb eines Formulars.

[Display(Name = "Datum")]
[UIHint(UiEditor.Basis.DateTime)]
public DateTime? Datum { get; set; }
@Html.EditorFor(m => m.Datum)

Der Kalender kommt vom Browser. Gerendert wird ein <input type="date">; Aussehen und Bedienung des Auswahlfelds bestimmt also der Browser, ebenso das angezeigte Format. Über die Leitung geht der Wert immer als yyyy-MM-dd. Ein Platzhalter mit dem heutigen Datum in dieser Schreibweise steht am Feld, und autocomplete ist abgeschaltet.

DateTime? und nicht DateTime: Nur mit dem nullbaren Typ gibt es ein leeres Feld; ein DateTime käme mit dem 01.01.0001 an.

Die Attribute im Überblick

Attribut Wirkung
[UIHint(UiEditor.Basis.DateTime)] Wählt die Vorlage; Standard-MVC-Mechanik, die Konstante kommt aus UiEditor.Basis (FMH.Komponente.Basis.Validierung)
[Display(Name = "…")] Beschriftung des Felds
[Required] Pflichtangabe, siehe Beispiel «Datum mit Pflichtangabe»
[DatumBereich] Grenzt den erlaubten Bereich ein; ohne Argumente gilt 01.01.1970 bis 31.12. des Jahres «heute + 10 Jahre». Eigene Seite: Basis.Validierung › Validierung › DatumBereich
[HelpText(Name = "…")] Hinweis unter dem Feld; er weicht der Fehlermeldung, sobald eine erscheint
[DisplayFromViewModel(nameof(…))] Beschriftung aus einer zweiten Eigenschaft des ViewModels
[UniqueId] Hängt ein Zufalls-Suffix an die Id, wenn dasselbe Formular mehrfach auf der Seite steht

Dazu kommen die üblichen Attribute aus System.ComponentModel.DataAnnotations, sie gelten hier wie an jeder anderen Eigenschaft.

Was zu beachten ist

  • min und max setzt das Template nicht. Es liest die Grenzen aus einem vorhandenen [DatumBereich] zwar aus, schreibt sie aber nicht an das <input>. Der Browser lässt also jedes Datum zu; geprüft wird über die unaufdringliche Validierung des Attributs und beim Absenden auf dem Server.
  • Eine unlesbare Eingabe scheitert vor der Prüfung. Was der Modell-Binder nicht in ein DateTime umwandeln kann, landet als Bindungsfehler im ModelState; die Meldung dafür liefert data-val-date, entweder aus der eigenen [Required]-Meldung oder aus _FmhWebMvc.ValidationDateTime.
  • Das Template bringt ein eigenes Skript mit, das die Prüfung an das change-Ereignis hängt (fmhMDCvalidiere). Ohne das Skript-Bundle der Basis meldet das Feld erst beim Absenden.

Datum mit Pflichtangabe

[Required] macht das Feld zur Pflicht, [HelpText] ergänzt einen Hinweis darunter. Absenden mit leerem Feld zeigt die Meldung.

@model DatumPflichtBeispielViewModel

<form asp-area="Basis" asp-controller="Datum" asp-action="Pflicht" method="post">
    @Html.EditorFor(m => m.Geburtstag)
    @Html.Fmh(Fmh.Button()
        .Text("Absenden")
        .Size(ButtonSize.Sm)
        .Attributes(new { @class = "mt-2" }))
</form>
using System;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;

namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
    /// <summary>
    /// Pflichtangabe mit Hilfetext: Beides sind Standard-Bausteine, die jedes Eingabe-Template
    /// dieser Sammlung versteht.
    /// </summary>
    public class DatumPflichtBeispielViewModel
    {
        [Display(Name = "Geburtstag")]
        [UIHint(UiEditor.Basis.DateTime)]
        [Required(ErrorMessage = "Bitte den Geburtstag angeben.")]
        [HelpText(Name = "Der Browser zeigt seinen eigenen Kalender; die Eingabe über die Tastatur geht ebenso.")]
        public DateTime? Geburtstag { 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>
    /// Ein Controller pro Template (DateTime); je eine Action/View pro Variante.
    /// Die POST-Actions zeigen, was der Server aus der Eingabe macht: ein leeres Feld ist ohne
    /// Pflichtangabe gültig, mit [Required] nicht, und ein unlesbares Datum scheitert schon beim
    /// Binden.
    /// </summary>
    [Area("Basis")]
    public class DatumController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return this.View(new DatumBeispielViewModel());
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(DatumBeispielViewModel modell)
        {
            return this.PruefeUndZeige(modell, nameof(this.Index));
        }

        [HttpGet]
        public IActionResult Pflicht()
        {
            return this.View(new DatumPflichtBeispielViewModel());
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Pflicht(DatumPflichtBeispielViewModel modell)
        {
            return this.PruefeUndZeige(modell, nameof(this.Pflicht));
        }

        /// <summary>
        /// Gemeinsame PRG-Logik der Varianten: Bei gültigem Modell eine Meldung setzen und per
        /// Redirect auf die GET-Action, sonst dieselbe View mit den Fehlern erneut anzeigen.
        /// </summary>
        /// <param name="modell">Das abgeschickte Modell.</param>
        /// <param name="action">Der Name der GET-Action dieser Variante.</param>
        /// <returns>Die Weiterleitung oder die erneut gerenderte View.</returns>
        private IActionResult PruefeUndZeige(object modell, string action)
        {
            if (this.ModelState.IsValid)
            {
                // Erfolgsfall: hier würden die eigentlichen Aktionen ausgeführt (z. B. Speichern).
                this.AddAlert("Formular ist gültig – Datum übernommen.", AlertType.Success, autoHide: true);

                // PRG-Pattern: nach erfolgreichem POST per Redirect auf die GET-Action.
                return this.RedirectToAction(action);
            }

            // Ungültige Eingabe: dieselbe Seite erneut anzeigen, damit die Meldungen erscheinen.
            return this.View(action, modell);
        }
    }
}

Darstellung

Der Browser zeigt seinen eigenen Kalender; die Eingabe über die Tastatur geht ebenso.

[Required] macht die Angabe zur Pflicht, [HelpText] ergänzt einen Hinweis unter dem Feld:

[Display(Name = "Geburtstag")]
[UIHint(UiEditor.Basis.DateTime)]
[Required(ErrorMessage = "Bitte den Geburtstag angeben.")]
[HelpText(Name = "Der Browser zeigt seinen eigenen Kalender; die Eingabe über die Tastatur geht ebenso.")]
public DateTime? Geburtstag { get; set; }

Was dabei zu sehen ist:

  • Die Beschriftung wird ausgezeichnet. Bei Pflichtangabe hängt das Template die Klasse required an das Label und required an das Eingabefeld selbst, die Kennzeichnung kommt also ohne Zutun der Seite.
  • Der Hilfetext tritt zurück, sobald eine Fehlermeldung erscheint, und kommt danach wieder.
  • Geprüft wird server- und clientseitig. Der Tab «Controller (GET/POST)» zeigt das PRG-Muster dahinter: gültig führt zu einem Redirect, ungültig rendert dieselbe Seite mit der Meldung.

Soll zusätzlich der Bereich eingegrenzt werden, etwa ein Geburtsdatum zwischen 1950 und heute, kommt [DatumBereich("01.01.1950", 0)] dazu. Der zweite Wert ist ein Jahres-Offset auf das heutige Datum, 0 heisst also «nicht in der Zukunft», -18 entsprechend «vor mindestens 18 Jahren». Alle drei Überladungen mit lauffähigen Beispielen stehen unter Basis.Validierung › Validierung › DatumBereich.