DropdownList
Das DropdownList-EditorTemplate rendert ein Auswahlfeld. Die Auswahlliste kommt aus
einer Eigenschaft (IEnumerable<SelectListItem>), deren Name über
[AdditionalMetadata("Selection", ...)] angegeben wird; Einbindung über
[UIHint(UiEditor.Basis.DropdownList)]. Ob ein leerer Platzhalter-Eintrag gerendert wird, steuert
[EmptyEntry(false)].
DropdownList (Grundfall)
Auswahlfeld mit Liste aus einer Eigenschaft des ViewModels; ohne weitere Angabe steht ein leerer Eintrag zuoberst.
@model DropdownListBeispielViewModel
<form class="fmh-demo-form">
@Html.EditorFor(m => m.Land)
</form>
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.Infrastruktur;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
/// <summary>
/// Der Grundfall: Auswahlfeld mit Liste aus einer zweiten Eigenschaft des ViewModels und
/// leerem Eintrag zuoberst.
/// </summary>
public class DropdownListBeispielViewModel
{
/// <summary>
/// Der Name dieser Eigenschaft steckt in [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, …)];
/// das Template liest daraus die Auswahlliste.
/// </summary>
public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
{
new ("Schweiz", "CH"),
new ("Deutschland", "DE"),
new ("Österreich", "AT"),
};
[Display(Name = "Land")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
public string Land { get; set; } = "CH";
}
}
Darstellung
Das DropdownList-EditorTemplate rendert ein Auswahlfeld (eine Auswahl aus vielen). Die Optionen liegen als IEnumerable<SelectListItem> in einer zweiten Eigenschaft des ViewModels; welche das ist, sagt UiMetadata.Basis.DropdownListSelection.
[Display(Name = "Land")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
public string Land { get; set; } = "CH";
public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
{
new("Schweiz", "CH"), new("Deutschland", "DE"), new("Österreich", "AT"),
};
@Html.EditorFor(m => m.Land)
Der Aufruf gehört in ein Formular. Vorausgewählt ist der Eintrag, dessen Value dem Wert der Eigenschaft entspricht. Ohne weitere Angabe steht zuoberst ein leerer Eintrag als Platzhalter für «nichts gewählt», und das <select> bekommt dafür die Klasse fmh-select-empty.
Die Attribute im Überblick
| Attribut | Wirkung |
|---|---|
[UIHint(UiEditor.Basis.DropdownList)] |
Wählt die Vorlage; Standard-MVC-Mechanik, die Konstante kommt aus UiEditor.Basis (FMH.Komponente.Basis.Validierung) |
[Display(Name = "…")] |
Beschriftung des Felds |
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(…))] |
Eigenschaft mit den Optionen; ohne sie bleibt die Liste leer. Ohne View-Model kommt die Liste über EditorFor(…, additionalViewData), siehe «DropdownList ohne View-Model» |
[AdditionalMetadata(UiMetadata.Basis.FavoritenSelection, nameof(…))] |
Zweite Liste, die vor der regulären steht, siehe «DropdownList mit Favoriten» |
[Required] |
Pflichtauswahl, siehe «DropdownList mit Pflichtauswahl» |
[EmptyEntry(false)] |
Unterdrückt den leeren Eintrag, siehe «DropdownList ohne leeren Eintrag» |
[HelpText(Name = "…")] |
Hinweis unter dem Feld |
[DisplayFromViewModel(nameof(…))], [UniqueId] |
wie bei den übrigen Eingabe-Templates |
Die beiden Listen-Metadaten brauchen den AdditionalMetadataProvider, den AddFmhWidgetOptions() registriert: Das Template liest sie aus ModelMetadata.AdditionalValues. [EmptyEntry] dagegen wird reflektiv am Member gelesen und wirkt auch ohne Provider. Die Konstanten liegen in UiMetadata.Basis (Paket FMH.Komponente.Basis.Validierung), ihre Werte sind "Selection" und "FavSelection".
DropdownList mit data-Attributen
Einträge als SelectListItemWithAttributes: Was im Attributs-Objekt steht, landet als HTML-Attribut an der Option und ist im Skript auswertbar.
@model DropdownListAttributeBeispielViewModel
<form class="fmh-demo-form">
@Html.EditorFor(m => m.LandMitAttributen)
</form>
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Controls;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.Infrastruktur;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
/// <summary>
/// Einträge mit eigenen data-Attributen: SelectListItemWithAttributes trägt neben Wert und
/// Text ein Objekt, dessen Eigenschaften als HTML-Attribute an der Option landen.
/// </summary>
public class DropdownListAttributeBeispielViewModel
{
public IEnumerable<SelectListItemWithAttributes> Laender { get; } = new List<SelectListItemWithAttributes>
{
new () { Value = "CH", Text = "Schweiz", Attributes = new { data_vorwahl = "+41" } },
new () { Value = "DE", Text = "Deutschland", Attributes = new { data_vorwahl = "+49" } },
new () { Value = "AT", Text = "Österreich", Attributes = new { data_vorwahl = "+43" } },
};
[Display(Name = "Land mit data-Attributen")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
public string LandMitAttributen { get; set; }
}
}
Darstellung
Sollen Einträge Angaben mitführen, die ein Skript später braucht, treten SelectListItemWithAttributes an die Stelle von SelectListItem. Was im Attributs-Objekt steht, landet als HTML-Attribut an der Option:
public IEnumerable<SelectListItemWithAttributes> Laender { get; } = new List<SelectListItemWithAttributes>
{
new() { Value = "CH", Text = "Schweiz", Attributes = new { data_vorwahl = "+41" } },
new() { Value = "DE", Text = "Deutschland", Attributes = new { data_vorwahl = "+49" } },
};
[Display(Name = "Land mit data-Attributen")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
public string LandMitAttributen { get; set; }
Gerendert wird daraus <option value="CH" data-vorwahl="+41">Schweiz</option>. Unterstriche werden zu Bindestrichen, aus data_vorwahl wird also data-vorwahl; das ist dieselbe Regel wie bei den HTML-Attributen der übrigen Helfer. Der Typ liegt in FMH.Komponente.Basis.Ui.Controls (Paket FMH.Komponente.Basis.Validierung), die Angabe der Liste bleibt unverändert.
Ausgelesen wird das im Skript über die gewählte Option, etwa document.querySelector('#Land').selectedOptions[0].dataset.vorwahl.
DropdownList mit Favoriten
Eine zweite Liste steht vor der regulären, getrennt durch eine Linie. Für häufig gewählte Einträge.
@model DropdownListFavoritenBeispielViewModel
<form class="fmh-demo-form">
@Html.EditorFor(m => m.LandMitFavoriten)
</form>
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.Infrastruktur;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
/// <summary>
/// Häufig gewählte Einträge stehen zuoberst: Die Favoriten sind eine zweite Liste, das
/// Template setzt sie vor die reguläre und trennt beide mit einer Linie.
/// </summary>
public class DropdownListFavoritenBeispielViewModel
{
public IEnumerable<SelectListItem> Favoriten { get; } = new List<SelectListItem>
{
new ("Schweiz", "CH"),
new ("Liechtenstein", "LI"),
};
public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
{
new ("Deutschland", "DE"),
new ("Frankreich", "FR"),
new ("Italien", "IT"),
new ("Österreich", "AT"),
};
[Display(Name = "Land mit Favoriten")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
[AdditionalMetadata(UiMetadata.Basis.FavoritenSelection, nameof(Favoriten))]
public string LandMitFavoriten { get; set; }
}
}
Darstellung
Häufig gewählte Einträge gehören nach oben. UiMetadata.Basis.FavoritenSelection nennt eine zweite Liste, die das Template vor die reguläre setzt:
[Display(Name = "Land mit Favoriten")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
[AdditionalMetadata(UiMetadata.Basis.FavoritenSelection, nameof(Favoriten))]
public string LandMitFavoriten { get; set; }
Zwischen beiden Blöcken rendert das Template eine deaktivierte Trenn-Option. Zwei Punkte dazu:
- Die Favoriten sind eine eigene Liste, keine Markierung an den regulären Einträgen. Ein Eintrag, der in beiden steht, erscheint zweimal.
- Der vorausgewählte Text wird in beiden Listen gesucht, zuerst in den Favoriten.
DropdownList mit Gruppen
Tragen die Einträge eine SelectListGroup, entsteht je Gruppe ein optgroup mit Zwischentitel.
@model DropdownListGruppenBeispielViewModel
<form class="fmh-demo-form">
@Html.EditorFor(m => m.LandNachRegion)
</form>
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.Infrastruktur;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
/// <summary>
/// Gruppierte Auswahl: Tragen die Einträge eine SelectListGroup, rendert das Template je
/// Gruppe ein optgroup.
/// </summary>
public class DropdownListGruppenBeispielViewModel
{
private static readonly SelectListGroup Dach = new () { Name = "D-A-CH" };
private static readonly SelectListGroup Sued = new () { Name = "Südeuropa" };
public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
{
new ("Schweiz", "CH") { Group = Dach },
new ("Deutschland", "DE") { Group = Dach },
new ("Österreich", "AT") { Group = Dach },
new ("Italien", "IT") { Group = Sued },
new ("Spanien", "ES") { Group = Sued },
};
[Display(Name = "Land nach Region")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
public string LandNachRegion { get; set; }
}
}
Darstellung
Trägt ein SelectListItem eine SelectListGroup, entsteht je Gruppe ein <optgroup> mit Zwischentitel:
private static readonly SelectListGroup Dach = new() { Name = "D-A-CH" };
public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
{
new("Schweiz", "CH") { Group = Dach },
new("Deutschland", "DE") { Group = Dach },
new("Italien", "IT") { Group = Sued },
};
Gruppiert wird über den Namen der Gruppe, zwei Instanzen mit gleichem Namen landen also im selben Block. Die Reihenfolge der Blöcke folgt dem ersten Auftreten in der Liste.
Achtung bei gemischten Listen: Sobald ein Eintrag eine Gruppe trägt, geht das Template in den Gruppen-Modus, und Einträge ohne Gruppe fallen dabei kommentarlos aus der Liste. Entweder alle Einträge bekommen eine Gruppe oder keiner.
DropdownList mit Pflichtauswahl
[Required] auf einem int?: Der leere Eintrag bleibt stehen, damit «nichts gewählt» überhaupt möglich ist und die Prüfung greift.
@model DropdownListPflichtBeispielViewModel
<form asp-area="Basis" asp-controller="DropdownList" asp-action="Index" method="post">
@Html.EditorFor(m => m.AnredeId)
@Html.Fmh(Fmh.Button()
.Text("Absenden")
.Size(ButtonSize.Sm)
.Attributes(new { @class = "mt-2" }))
</form>
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.Infrastruktur;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
/// <summary>
/// Pflichtauswahl: Die Eigenschaft ist nullable, damit «nichts gewählt» überhaupt als Zustand
/// existiert und [Required] greifen kann.
/// </summary>
public class DropdownListPflichtBeispielViewModel
{
public IEnumerable<SelectListItem> Anreden { get; } = new List<SelectListItem>
{
new ("Frau", "1"),
new ("Herr", "2"),
new ("Keine Angabe", "3"),
};
[Display(Name = "Anrede")]
[UIHint(UiEditor.Basis.DropdownList)]
[Required(ErrorMessage = "Bitte treffen Sie eine Auswahl.")]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Anreden))]
public int? AnredeId { 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>
/// Der Beispiele-Controller des Templates DropdownList. Nur die Pflichtauswahl braucht einen
/// Rücklauf; die übrigen Varianten der Demo-Seite zeigen allein die Darstellung.
/// </summary>
[Area("Basis")]
public class DropdownListController : Controller
{
[HttpGet]
public IActionResult Index()
{
return this.View(new DropdownListPflichtBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(DropdownListPflichtBeispielViewModel modell)
{
if (this.ModelState.IsValid)
{
// Erfolgsfall: hier würden die eigentlichen Aktionen ausgeführt (z. B. Speichern).
this.AddAlert("Formular ist gültig – Auswahl übernommen.", AlertType.Success, autoHide: true);
// PRG-Pattern: nach erfolgreichem POST per Redirect auf die GET-Action.
return this.RedirectToAction(nameof(this.Index));
}
// Ungültige Eingabe: dieselbe Seite erneut anzeigen, damit die Meldung erscheint.
return this.View(modell);
}
}
}
Darstellung
[Required] macht die Auswahl zur Pflicht. Die Eigenschaft muss dafür nullable sein (int?, string), sonst gibt es keinen Zustand «nichts gewählt»:
[Display(Name = "Anrede")]
[UIHint(UiEditor.Basis.DropdownList)]
[Required(ErrorMessage = "Bitte treffen Sie eine Auswahl.")]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Anreden))]
public int? AnredeId { get; set; }
Was dabei zu sehen ist:
- Der leere Eintrag bleibt stehen. Er ist die Voraussetzung dafür, dass die Prüfung überhaupt anschlagen kann;
[EmptyEntry(false)]und[Required]zusammen ergeben ein Feld, das immer schon «ausgefüllt» ist. - Das Feld wird ausgezeichnet. Bei Pflichtauswahl hängt das Template die Klasse
requiredan das Label,requiredundaria-describedbyan das<select>und pflegtaria-invalidje nach Zustand. - Geprüft wird server- und clientseitig. Der Tab «Controller (GET/POST)» zeigt das PRG-Muster: gültig führt zu einem Redirect, ungültig rendert dieselbe Seite mit der Meldung.
DropdownList ohne leeren Eintrag
[EmptyEntry(false)] unterdrückt den leeren Platzhalter; die Liste beginnt beim ersten Eintrag.
@model DropdownListOhneLeereintragBeispielViewModel
<form class="fmh-demo-form">
@Html.EditorFor(m => m.LandOhneLeereintrag)
</form>
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.Infrastruktur;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
/// <summary>
/// Ohne Vorbelegung und ohne leeren Eintrag: Die Liste startet direkt beim ersten Eintrag.
/// </summary>
public class DropdownListOhneLeereintragBeispielViewModel
{
public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
{
new ("Schweiz", "CH"),
new ("Deutschland", "DE"),
new ("Österreich", "AT"),
};
[Display(Name = "Land")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
[EmptyEntry(false)]
public string LandOhneLeereintrag { get; set; }
}
}
Darstellung
Ohne weitere Angabe rendert das Template als erste Option einen leeren Eintrag. [EmptyEntry(false)] unterdrückt ihn, die Liste beginnt dann beim ersten echten Eintrag:
[Display(Name = "Land")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
[EmptyEntry(false)]
public string LandOhneLeereintrag { get; set; }
| Angabe | Wirkung |
|---|---|
| kein Attribut | leerer Eintrag wird gerendert (Standard) |
[EmptyEntry] / [EmptyEntry(true)] |
leerer Eintrag wird gerendert |
[EmptyEntry(false)] |
kein leerer Eintrag |
EmptyEntry liegt in FMH.Komponente.Basis.Ui.Validation.DataAnnotation und wird über ModelMetadata.HasEmptyEntry() reflektiv gelesen, also ohne AdditionalMetadataProvider.
Zu beachten: Ohne leeren Eintrag gibt es keinen «nichts gewählt»-Zustand. Ist der Wert nicht vorbelegt, zeigt der Browser den ersten Eintrag, und genau der wird beim Absenden übermittelt. Für Pflichtfelder den leeren Eintrag deshalb stehen lassen.
Dasselbe Attribut wirkt gleich bei ComboboxList und MultiselectList.
DropdownList ohne View-Model
Das ViewModel ist nur eine lokale Variable, etwa in einer Sidebar. Die Liste kommt dann über den zweiten Parameter von EditorFor ins Template.
@* Bewusst ohne @model: Das ViewModel ist eine lokale Variable, wie in einer Sidebar oder einem
Layout-Partial. Der Behälter im Template ist damit nicht dieses Objekt, darum reicht der zweite
Parameter von EditorFor das Objekt mit; seine Eigenschaften landen in der ViewData des Templates. *@
@{
var auswahl = new DropdownListBeispielViewModel();
}
<form class="fmh-demo-form">
@Html.EditorFor(m => auswahl.Land, auswahl)
</form>
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.Infrastruktur;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
/// <summary>
/// Der Grundfall: Auswahlfeld mit Liste aus einer zweiten Eigenschaft des ViewModels und
/// leerem Eintrag zuoberst.
/// </summary>
public class DropdownListBeispielViewModel
{
/// <summary>
/// Der Name dieser Eigenschaft steckt in [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, …)];
/// das Template liest daraus die Auswahlliste.
/// </summary>
public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
{
new ("Schweiz", "CH"),
new ("Deutschland", "DE"),
new ("Österreich", "AT"),
};
[Display(Name = "Land")]
[UIHint(UiEditor.Basis.DropdownList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
public string Land { get; set; } = "CH";
}
}
Darstellung
Manchmal hat die View kein Model, zu dem das Feld gehört: in einer Sidebar, einem Layout-Partial oder einer Seite, die das ViewModel nur als lokale Variable anlegt. Die Attribute stehen am ViewModel wie im Grundfall, nur der Aufruf ist ein anderer:
@{
var auswahl = new DropdownListBeispielViewModel();
}
@Html.EditorFor(m => auswahl.Land, auswahl)
Warum der zweite Parameter nötig ist. Das Template sucht die Liste am Behälter des Felds (ViewData.ModelExplorer.Container.Model). Bei m => auswahl.Land ist das nicht auswahl, sondern das Model der View, und das ist hier leer oder ein anderes Objekt. Die Metadaten stimmen trotzdem, weil sie vom Typ des Ausdrucks kommen; nur die Instanz fehlt dem Template. Der zweite Parameter von EditorFor (additionalViewData) liefert sie nach: ASP.NET legt jede öffentliche Eigenschaft des übergebenen Objekts als Eintrag in die ViewData des Templates, also auch ViewData["Laender"].
So sucht das Template die Liste, für DropdownListSelection und FavoritenSelection gleich:
| Reihenfolge | Quelle | Greift, wenn |
|---|---|---|
| 1 | Eigenschaft am Behälter des Felds | der Behälter eine Eigenschaft mit dem genannten Namen hat (Grundfall, auch m => m.Adresse.Land mit der Liste am Haupt-ViewModel) |
| 2 | ViewData["<Name>"] |
der Behälter die Eigenschaft nicht hat, etwa bei m => auswahl.Land |
| 3 | leere Liste | beides fehlt; das Feld erscheint ohne Einträge, es gibt keine Ausnahme |
Der Behälter hat damit Vorrang: Wo es bisher funktioniert hat, ändert der zweite Parameter nichts.
Statt des ganzen Objekts geht auch ein anonymes Objekt mit genau den benötigten Listen. Der Name muss dem in AdditionalMetadata entsprechen:
@Html.EditorFor(m => auswahl.Land, new { Laender = auswahl.Laender })
Das lohnt sich bei grossen ViewModels. Beim ganzen Objekt werden alle Getter ausgewertet, auch die, die das Template nicht braucht.
Zu beachten:
- Die Beschriftung aus dem Model (
[DisplayFromViewModel]) funktioniert in diesem Fall nicht. Sie liest am Behälter und findet dort die Eigenschaft nicht, das Feld zeigt dann die normale Beschriftung.[Display(Name = "…")]wirkt wie gewohnt. - Beim Absenden trägt das Feld den Namen der Variablen als Präfix.
m => auswahl.Landergibtname="auswahl.Land"undid="auswahl_Land". Die empfangende Action bindet es, wenn ihr Parameter gleich heisst (Index(DropdownListBeispielViewModel auswahl)) oder das Präfix ausdrücklich nennt ([Bind(Prefix = "auswahl")]). Einzige Ausnahme ist eine Variable namensmodel: Dieses Präfix entfernt ASP.NET, ausm => model.Landwirdname="Land". - Derselbe Weg gilt für
ComboboxList,MultiselectList,RadioListundCheckboxList.