UniqueId
Das UniqueIdAttribute ist ein Marker ohne eigene Prüfung: Die EditorTemplates hängen
an die gerenderte HTML-Id einen Zufallswert an, damit mehrfach gerenderte Formulare
(Nachlade-Szenarien, Dialoge) kollisionsfrei bleiben. Der name des Feldes bleibt
unverändert – Model-Binding und Validierung laufen weiter über den Eigenschaftsnamen.
Dasselbe Feld zweimal gerendert
Der eigentliche Zweck: ohne das Attribut tragen beide Felder dieselbe HTML-Id – das zweite ist darüber nicht mehr erreichbar. Der Tab „JavaScript“ zeigt, wie die Ids ausgelesen werden.
@model UniqueIdMehrfachBeispielViewModel
@* Beide Blöcke rendern dasselbe Feld zweimal – wie ein Teilformular, das zusätzlich in einem
nachgeladenen Dialog steht. Die data-uniqueid-*-Attribute wertet das Skript im Tab "JavaScript"
aus (Ids anzeigen, zweites Feld über seine Id suchen). *@
<div class="mb-4" data-uniqueid-block>
<p class="fw-semibold mb-1">Ohne <code>[UniqueId]</code></p>
<p class="small text-muted mb-2">Feld im Formular auf der Seite</p>
@Html.EditorFor(m => m.Referenz)
<p class="small text-muted mb-2 mt-3">Dasselbe Feld im nachgeladenen Dialog</p>
@Html.EditorFor(m => m.Referenz)
<ul class="small mt-3 mb-2" data-uniqueid-ids></ul>
<button type="button" class="btn btn-sm btn-outline-secondary" data-uniqueid-suchen>
Zweites Feld über seine Id suchen
</button>
<p class="small fw-semibold mt-2 mb-0" data-uniqueid-ergebnis></p>
</div>
<div data-uniqueid-block>
<p class="fw-semibold mb-1">Mit <code>[UniqueId]</code></p>
<p class="small text-muted mb-2">Feld im Formular auf der Seite</p>
@Html.EditorFor(m => m.ReferenzEindeutig)
<p class="small text-muted mb-2 mt-3">Dasselbe Feld im nachgeladenen Dialog</p>
@Html.EditorFor(m => m.ReferenzEindeutig)
<ul class="small mt-3 mb-2" data-uniqueid-ids></ul>
<button type="button" class="btn btn-sm btn-outline-secondary" data-uniqueid-suchen>
Zweites Feld über seine Id suchen
</button>
<p class="small fw-semibold mt-2 mb-0" data-uniqueid-ergebnis></p>
</div>
<script src="~/js/demos/uniqueid-mehrfach.js" asp-append-version="true"></script>
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
public class UniqueIdMehrfachBeispielViewModel
{
// Zweimal dieselbe Fachlichkeit: gleicher Anzeigename, gleiches Template – einzig der
// Marker unterscheidet die beiden Eigenschaften. Im Beispiel wird jede von beiden
// zweimal gerendert, damit sich die entstehenden HTML-Ids vergleichen lassen.
[Display(Name = "Referenz")]
public string Referenz { get; set; }
[Display(Name = "Referenz")]
[UniqueId]
public string ReferenzEindeutig { get; set; }
}
}
// Demo "Basis › Validierung › UniqueId": macht die gerenderten HTML-Ids sichtbar und zeigt, welches
// Feld document.getElementById() für das zweite Feld findet.
//
// Der Klick hängt per Delegation an document – damit ist es gleichgültig, wann der Beispiel-Block
// im DOM erscheint.
function uniqueIdDemoFelder(block) {
return Array.prototype.slice.call(block.querySelectorAll('input.fmh-text-input'));
}
// Die Ids entstehen erst im EditorTemplate – hier werden sie ausgelesen und angezeigt.
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-uniqueid-block]').forEach(function (block) {
var liste = block.querySelector('[data-uniqueid-ids]');
if (liste === null) {
return;
}
liste.innerHTML = uniqueIdDemoFelder(block)
.map(function (feld, nr) {
return '<li>Feld ' + (nr + 1) + ': <code>id="' + feld.id + '"</code></li>';
})
.join('');
});
});
document.addEventListener('click', function (event) {
var knopf = event.target.closest('[data-uniqueid-suchen]');
if (knopf === null) {
return;
}
var block = knopf.closest('[data-uniqueid-block]');
var felder = uniqueIdDemoFelder(block);
if (felder.length < 2) {
return;
}
felder.forEach(function (feld) {
feld.classList.remove('bg-warning-subtle');
});
// Denselben Weg gehen der Browser beim Label-Klick und jedes Skript, das ein Feld über seine
// Id anspricht: Die erste Übereinstimmung im Dokument gewinnt.
var gefunden = document.getElementById(felder[1].id);
gefunden.classList.add('bg-warning-subtle');
block.querySelector('[data-uniqueid-ergebnis]').textContent = gefunden === felder[1]
? 'Markiert ist das zweite Feld – jede Id kommt nur einmal vor.'
: 'Markiert ist das erste Feld – beide Felder tragen dieselbe Id, das zweite ist über sie nicht erreichbar.';
});
// jQuery-Variante zum Beispiel «Dasselbe Feld zweimal gerendert» – dasselbe wie im Tab
// "JavaScript", nur mit jQuery geschrieben. Diese Datei wird von der Demo-Seite NICHT geladen;
// sonst liefen die Handler doppelt. Sie ist als Vorlage zum Übernehmen gedacht.
$(function () {
$('[data-uniqueid-block]').each(function () {
var $block = $(this);
var eintraege = $block.find('input.fmh-text-input')
.map(function (nr, feld) {
return '<li>Feld ' + (nr + 1) + ': <code>id="' + feld.id + '"</code></li>';
})
.get()
.join('');
$block.find('[data-uniqueid-ids]').html(eintraege);
});
});
$(document).on('click', '[data-uniqueid-suchen]', function () {
var $block = $(this).closest('[data-uniqueid-block]');
var $felder = $block.find('input.fmh-text-input');
if ($felder.length < 2) {
return;
}
$felder.removeClass('bg-warning-subtle');
// Der Id-Selektor von jQuery verhält sich wie document.getElementById: Bei doppelt vergebener
// Id liefert er das erste Element im Dokument – das zweite Feld ist über seine Id nicht
// erreichbar.
var $gefunden = $('#' + $felder.eq(1).attr('id'));
$gefunden.addClass('bg-warning-subtle');
$block.find('[data-uniqueid-ergebnis]').text(
$gefunden.is($felder.eq(1))
? 'Markiert ist das zweite Feld – jede Id kommt nur einmal vor.'
: 'Markiert ist das erste Feld – beide Felder tragen dieselbe Id, das zweite ist über sie nicht erreichbar.');
});
Darstellung
Ohne [UniqueId]
Feld im Formular auf der Seite
Dasselbe Feld im nachgeladenen Dialog
Mit [UniqueId]
Feld im Formular auf der Seite
Dasselbe Feld im nachgeladenen Dialog
[UniqueId] (FMH.Komponente.Basis.Ui.Validation.DataAnnotation) ist ein Marker-Attribut ohne eigene Prüfung. Es steuert allein, wie das EditorTemplate die HTML-id bildet: Statt des Eigenschaftsnamens entsteht der Name plus ein Zufallswert (Referenzmdc4711…). Damit bleiben Felder eindeutig, wenn dasselbe Formular mehrfach auf einer Seite landet — nachgeladene Teilformulare, Dialoge, wiederholte Blöcke.
[Display(Name = "Referenz")]
public string Referenz { get; set; }
[Display(Name = "Referenz")]
[UniqueId]
public string ReferenzEindeutig { get; set; }
Was das Beispiel zeigt
Beide Blöcke rendern dasselbe Feld zweimal — die Situation, für die es das Attribut gibt: ein Teilformular steht auf der Seite und zusätzlich in einem nachgeladenen Dialog. Unter jedem Block stehen die tatsächlich gerenderten Ids, der Knopf sucht das zweite Feld über seine Id und markiert, was gefunden wurde.
- Ohne
[UniqueId]: Beide Felder heissenid="Referenz". Die Suche liefert das erste Feld — das zweite ist über seine Id gar nicht erreichbar. Über die Id läuft aber alles:foram Label,#help_…,#msg_…, jedes eigene Skript. - Mit
[UniqueId]: Jedes Rendern erzeugtReferenzmdc…mit anderem Suffix, die Suche findet das zweite Feld.
Derselbe Effekt ohne Knopfdruck: Ein Klick auf das Label des zweiten Feldes setzt den Fokus ohne das Attribut ins erste Feld.
Das Skript hinter den beiden Knöpfen zeigt der Code-Tab JavaScript (Attribute data-uniqueid-block, -ids, -suchen, -ergebnis), der Tab jQuery dieselbe Umsetzung mit jQuery — auch dort liefert der Id-Selektor $('#…') bei doppelter Id nur das erste Element. Die Dateien liegen unter wwwroot/js/demos/; geladen wird per <script src> nur die JavaScript-Fassung — extern statt inline, damit sie unabhängig von der Content-Security-Policy für Inline-Skripte läuft.
Wie es technisch wirkt
| Baustein | Wirkung |
|---|---|
[UniqueId] |
Marker-Attribut, keine Validierung, keine Optionen |
ModelMetadata.HasUniqueIdRequirement() |
Liest den Marker per Reflection an der Eigenschaft |
Html.RandomId(hasUniqueId) |
Liefert das Suffix mdc<Zahl> (ohne Marker einen leeren String) |
| EditorTemplate | var id = Html.IdFor(c => c) + Html.RandomId(...) — setzt id, for, field_…, help_…, msg_… |
- Der
namebleibt unverändert der Eigenschaftsname. Model-Binding,asp-forund die Validierungsmeldungen (data-valmsg-for) laufen darüber und sind vom Attribut nicht betroffen. - Das Suffix wird bei jedem Rendern neu erzeugt; die Id ist also weder stabil noch vorhersagbar.
Wann nicht
- Automatisierte Tests (Selenium & Co.): Ein Zugriff über die Id fällt aus, weil sie sich mit jedem Aufruf ändert. Über den
nameoder ein eigenesdata--Attribut ansprechen. - Eigenes JavaScript mit fixem Id-Selektor:
$('#Referenz')greift ins Leere. Die Basis-Skripte sind vorbereitet, weil sie die Id vom Element selbst lesen. - Kommt ein Formular nur einmal auf der Seite vor, bringt das Attribut nichts und kostet nur Nachvollziehbarkeit.
Zusammenspiel mit der Validierung
Pflichtfeld mit zufälliger Id: die Client-Prüfung meldet am Feld, der Server-Round-Trip nur in der Zusammenfassung.
@model UniqueIdValidierungBeispielViewModel
<form asp-area="Basis" asp-controller="UniqueId" asp-action="Validierung" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
@Html.EditorFor(m => m.Stichwort)
@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
{
public class UniqueIdValidierungBeispielViewModel
{
[Display(Name = "Stichwort")]
[UniqueId]
[Required(ErrorMessage = "Bitte ein Stichwort erfassen")]
public string Stichwort { 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
{
[Area("Basis")]
public class UniqueIdController : Controller
{
[HttpGet]
public IActionResult Index()
{
return this.View(new UniqueIdBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(UniqueIdBeispielViewModel modell)
{
if (this.ModelState.IsValid)
{
// Erfolgsfall: hier würden die eigentlichen Aktionen ausgeführt (z. B. Speichern).
this.AddAlert("Formular ist gültig – Validierung erfolgreich.", AlertType.Success, autoHide: true);
// PRG-Pattern (Post-Redirect-Get): nach erfolgreichem POST per Redirect auf die
// GET-Action weiterleiten, damit ein erneutes Laden/Aktualisieren das Formular nicht
// nochmals abschickt (verhindert doppeltes Speichern).
return this.RedirectToAction(nameof(this.Index));
}
// Ungültige Eingabe: dieselbe Seite erneut anzeigen, damit die Validierungsfehler erscheinen.
return this.View(modell);
}
[HttpGet]
public IActionResult Validierung()
{
return this.View(new UniqueIdValidierungBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Validierung(UniqueIdValidierungBeispielViewModel modell)
{
if (this.ModelState.IsValid)
{
// Das Model-Binding läuft über den name des Feldes – der bleibt trotz [UniqueId]
// der Eigenschaftsname, nur die HTML-Id trägt den Zufallswert.
this.AddAlert("Formular ist gültig – Validierung erfolgreich.", AlertType.Success, autoHide: true);
return this.RedirectToAction(nameof(this.Validierung));
}
// Beim erneuten Rendern findet das EditorTemplate den ModelState-Eintrag nicht, weil es
// ihn über die (zufällige) Id sucht – die Meldung erscheint deshalb nur in der
// Validierungs-Zusammenfassung, nicht am Feld selbst.
return this.View(modell);
}
}
}
Darstellung
[UniqueId] lässt sich mit jeder Validierung kombinieren — es verändert nur die Id, nicht die Prüfung. Zwei Dinge sind aber unterschiedlich, je nachdem ob der Browser oder der Server meldet.
[Display(Name = "Stichwort")]
[UniqueId]
[Required(ErrorMessage = "Bitte ein Stichwort erfassen")]
public string Stichwort { get; set; }
Client- und Serverseite
- Client: Die FMH-Validierung liest die Id vom Element selbst (
ValidationCSSarbeitet mit#msg_<id>und#help_<id>). Die Meldung erscheint direkt am Feld, rot markiert — genau wie ohne das Attribut. - Server: Beim erneuten Rendern nach einem ungültigen POST sucht das EditorTemplate den Fehler über
Html.ViewData.ModelState[id]— also über die zufällige Id. Der ModelState ist aber nach demname(dem Eigenschaftsnamen) abgelegt, der Treffer bleibt aus: Die Feldmarkierung und die Meldung am Feld fehlen, weilmsg_…ohne den Treffer ausgeblendet bleibt.
Die Prüfung selbst greift trotzdem — ModelState.IsValid ist false, die Aktion wird nicht ausgeführt. Damit die Meldung sichtbar wird, gehört auf ein Formular mit [UniqueId]-Feldern eine Validierungs-Zusammenfassung:
<div asp-validation-summary="All" class="text-danger"></div>
Weitere Hinweise
- Betroffen ist nur die Anzeige nach einem Server-Round-Trip; die Zuordnung der Meldung über
Html.ValidationMessageFor(...)bleibt korrekt (sie läuft über denname). - Rein serverseitige Prüfungen ohne Client-Adapter (z. B.
CheckboxRequired) melden folglich ausschliesslich über die Zusammenfassung, sobald[UniqueId]gesetzt ist. - Die Zufalls-Id ändert sich mit jedem Rendern: Nach einem Round-Trip trägt dasselbe Feld eine andere Id als vorher.