ComboboxList

Das ComboboxList-EditorTemplate rendert ein durchsuchbares Auswahlfeld (Single-Select). Die Auswahlliste kommt wie bei der DropdownList aus einer Eigenschaft, deren Name über [AdditionalMetadata("Selection", ...)] angegeben wird; Einbindung über [UIHint(UiEditor.Basis.ComboboxList)]. Ob ein leerer Platzhalter-Eintrag gerendert wird, steuert [EmptyEntry(false)].

ComboboxList (Grundfall)

Durchsuchbares Auswahlfeld: tippen filtert die Liste. Ohne weitere Angabe steht ein leerer Eintrag zuoberst.

@model ComboboxListBeispielViewModel

<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
{
    public class ComboboxListBeispielViewModel
    {
        public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
                                                              {
                                                                  new ("Schweiz", "CH"),
                                                                  new ("Deutschland", "DE"),
                                                                  new ("Österreich", "AT"),
                                                              };

        [Display(Name = "Land (durchsuchbar)")]
        [UIHint(UiEditor.Basis.ComboboxList)]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        [EmptyEntry(true)]
        public string Land { get; set; } = "CH";
    }
}

Darstellung

Das ComboboxList-EditorTemplate rendert ein durchsuchbares Auswahlfeld: eine Auswahl aus vielen, mit Eingabefeld zum Filtern. Die Optionen liegen als IEnumerable<SelectListItem> in einer zweiten Eigenschaft des ViewModels; welche das ist, sagt UiMetadata.Basis.DropdownListSelection.

[Display(Name = "Land (durchsuchbar)")]
[UIHint(UiEditor.Basis.ComboboxList)]
[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 Unterschied zu DropdownList ist allein die Suche: gleiche Attribute, gleiche Metadaten, gleiche Auswahlliste, nur die Bedienung ist eine andere. Ab wie vielen Einträgen sich die Suche lohnt, entscheidet die Anwendung; bei drei Ländern wie hier ist sie Anschauung, bei einer Fachgebietsliste die bessere Wahl.

Beim Filtern gewinnt die genaue Übereinstimmung. Die Suche vergleicht ohne Rücksicht auf Gross- und Kleinschreibung, und ein Eintrag, der genau dem Suchbegriff entspricht, steht zuoberst.

Die Attribute im Überblick

Attribut Wirkung
[UIHint(UiEditor.Basis.ComboboxList)] 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 «ComboboxList ohne View-Model»
[AdditionalMetadata(UiMetadata.Basis.FavoritenSelection, nameof(…))] Zweite Liste, die vor der regulären steht, siehe «ComboboxList mit Favoriten»
[Required] Pflichtauswahl, siehe «ComboboxList mit Pflichtauswahl»
[EmptyEntry(false)] Unterdrückt den leeren Eintrag, siehe «ComboboxList 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; [EmptyEntry] wird reflektiv gelesen und wirkt auch ohne.

ComboboxList mit data-Attributen

Einträge als SelectListItemWithAttributes: Was im Attributs-Objekt steht, landet als HTML-Attribut an der Option.

@model ComboboxListAttributeBeispielViewModel

<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, damit ein Skript zur gewählten Option weitere Angaben
    /// findet.
    /// </summary>
    public class ComboboxListAttributeBeispielViewModel
    {
        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 (durchsuchbar)")]
        [UIHint(UiEditor.Basis.ComboboxList)]
        [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:

public IEnumerable<SelectListItemWithAttributes> Laender { get; } = new List<SelectListItemWithAttributes>
{
    new() { Value = "CH", Text = "Schweiz", Attributes = new { data_vorwahl = "+41" } },
};

Gerendert wird daraus <option value="CH" data-vorwahl="+41">Schweiz</option>. Unterstriche werden zu Bindestrichen, aus data_vorwahl wird also data-vorwahl. Der Typ liegt in FMH.Komponente.Basis.Ui.Controls (Paket FMH.Komponente.Basis.Validierung).

ComboboxList mit Favoriten

Eine zweite Liste steht vor der regulären, getrennt durch eine Linie. Die Suche filtert über beide.

@model ComboboxListFavoritenBeispielViewModel

<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 zuoberst: Die Favoriten sind eine zweite Liste, das Template setzt
    /// sie vor die reguläre und trennt beide mit einer Linie.
    /// </summary>
    public class ComboboxListFavoritenBeispielViewModel
    {
        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 (durchsuchbar)")]
        [UIHint(UiEditor.Basis.ComboboxList)]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        [AdditionalMetadata(UiMetadata.Basis.FavoritenSelection, nameof(Favoriten))]
        public string LandMitFavoriten { get; set; }
    }
}

Darstellung

UiMetadata.Basis.FavoritenSelection nennt eine zweite Liste, die das Template vor die reguläre setzt und mit einer Linie abtrennt:

[Display(Name = "Land mit Favoriten (durchsuchbar)")]
[UIHint(UiEditor.Basis.ComboboxList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
[AdditionalMetadata(UiMetadata.Basis.FavoritenSelection, nameof(Favoriten))]
public string LandMitFavoriten { get; set; }

Die Suche filtert über beide Listen. Ein Eintrag, der in beiden steht, erscheint zweimal, die Favoriten sind eine eigene Liste und keine Markierung.

ComboboxList mit Gruppen

Tragen die Einträge eine SelectListGroup, entsteht je Gruppe ein optgroup mit Zwischentitel. Die Suche filtert in jeder Gruppe: «schw» zeigt die Schweiz unter D-A-CH und unter Mehrsprachig.

@model ComboboxListGruppenBeispielViewModel

<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. Die Suche filtert die Einträge jeder Gruppe für sich, die Schweiz steht
    /// deshalb bei «schw» in beiden Gruppen, in denen sie vorkommt.
    /// </summary>
    public class ComboboxListGruppenBeispielViewModel
    {
        private static readonly SelectListGroup Dach = new () { Name = "D-A-CH" };

        private static readonly SelectListGroup Mehrsprachig = new () { Name = "Mehrsprachig" };

        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 ("Belgien", "BE") { Group = Mehrsprachig },
            new ("Schweiz", "CH") { Group = Mehrsprachig },
            new ("Italien", "IT") { Group = Sued },
            new ("Spanien", "ES") { Group = Sued },
        };

        [Display(Name = "Land nach Region (durchsuchbar)")]
        [UIHint(UiEditor.Basis.ComboboxList)]
        [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" };
private static readonly SelectListGroup Mehrsprachig = new() { Name = "Mehrsprachig" };

public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
{
    new("Schweiz", "CH") { Group = Dach },
    new("Belgien", "BE") { Group = Mehrsprachig },
    new("Schweiz", "CH") { Group = Mehrsprachig },
    new("Italien", "IT") { Group = Sued },
};

Die Suche filtert in jeder Gruppe für sich. Passt ein Eintrag, bleibt er mit dem Zwischentitel seiner Gruppe stehen; Gruppen ohne Treffer verschwinden. Steht ein Eintrag in mehreren Gruppen, erscheint er in jeder davon: «schw» zeigt die Schweiz unter D-A-CH und unter Mehrsprachig. Passt der Suchbegriff auf den Namen einer Gruppe («d-a»), bleibt die ganze Gruppe stehen. Die genaue Übereinstimmung steht auch innerhalb einer Gruppe zuoberst, und die Gruppe mit dem besten Treffer kommt zuerst.

Zwei Punkte wie bei MultiselectList:

  • Gruppiert wird über die Instanz, nicht über den Namen: Zwei gleichnamige SelectListGroup-Objekte ergeben zwei Blöcke. Die Gruppen deshalb einmal anlegen und wiederverwenden, wie oben mit dem statischen Feld. (DropdownList gruppiert dagegen über den Namen.)
  • Einträge ohne Gruppe fallen weg, sobald ein Eintrag eine trägt. Entweder alle oder keiner.

Gleicher Wert in mehreren Gruppen: Abgeschickt wird nur der Wert (CH), aus welcher Gruppe gewählt wurde, ist danach nicht mehr zu erkennen. Bei der erneuten Anzeige markiert das Template jede Option mit diesem Wert, der Browser wählt davon die letzte. Spielt die Gruppe eine Rolle, braucht jeder Eintrag einen eigenen Wert.

ComboboxList 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 ComboboxListPflichtBeispielViewModel

<form asp-area="Basis" asp-controller="ComboboxList" asp-action="Index" method="post">
    @Html.EditorFor(m => m.FachgebietId)
    @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 ComboboxListPflichtBeispielViewModel
    {
        public IEnumerable<SelectListItem> Fachgebiete { get; } = new List<SelectListItem>
                                                                  {
                                                                      new ("Allgemeine Innere Medizin", "1"),
                                                                      new ("Chirurgie", "2"),
                                                                      new ("Kinder- und Jugendmedizin", "3"),
                                                                  };

        [Display(Name = "Fachgebiet (durchsuchbar)")]
        [UIHint(UiEditor.Basis.ComboboxList)]
        [Required(ErrorMessage = "Bitte ein Fachgebiet auswählen.")]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Fachgebiete))]
        public int? FachgebietId { 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 ComboboxList. Nur die Pflichtauswahl braucht einen
    /// Rücklauf; die übrigen Varianten der Demo-Seite zeigen allein die Darstellung.
    /// </summary>
    [Area("Basis")]
    public class ComboboxListController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return this.View(new ComboboxListPflichtBeispielViewModel());
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(ComboboxListPflichtBeispielViewModel 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 nullable sein (int?, string), sonst gibt es keinen Zustand «nichts gewählt»:

[Display(Name = "Fachgebiet (durchsuchbar)")]
[UIHint(UiEditor.Basis.ComboboxList)]
[Required(ErrorMessage = "Bitte ein Fachgebiet auswählen.")]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Fachgebiete))]
public int? FachgebietId { get; set; }
  • Der leere Eintrag bleibt stehen. Er ist die Voraussetzung dafür, dass die Prüfung anschlagen kann; [EmptyEntry(false)] und [Required] zusammen ergeben ein Feld, das immer schon «ausgefüllt» ist.
  • Das Feld wird ausgezeichnet: Klasse required am Label, required und aria-describedby am <select>, aria-invalid je nach Zustand.
  • Der Tab «Controller (GET/POST)» zeigt das PRG-Muster: gültig führt zu einem Redirect, ungültig rendert dieselbe Seite mit der Meldung.

ComboboxList ohne leeren Eintrag

[EmptyEntry(false)] unterdrückt den leeren Platzhalter; die Liste beginnt beim ersten Eintrag.

@model ComboboxListOhneLeereintragBeispielViewModel

<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
{
    public class ComboboxListOhneLeereintragBeispielViewModel
    {
        public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
                                                              {
                                                                  new ("Schweiz", "CH"),
                                                                  new ("Deutschland", "DE"),
                                                                  new ("Österreich", "AT"),
                                                              };

        /// <summary>
        /// Ohne Vorbelegung: mit [EmptyEntry(false)] gibt es keinen leeren Eintrag,
        /// die Liste startet direkt beim ersten Eintrag.
        /// </summary>
        [Display(Name = "Land (durchsuchbar)")]
        [UIHint(UiEditor.Basis.ComboboxList)]
        [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:

[Display(Name = "Land (durchsuchbar)")]
[UIHint(UiEditor.Basis.ComboboxList)]
[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

Ohne leeren Eintrag gibt es keinen «nichts gewählt»-Zustand: Ist der Wert nicht vorbelegt, zeigt das Feld den ersten Eintrag, und genau der wird beim Absenden übermittelt. Dasselbe Attribut wirkt gleich bei DropdownList und MultiselectList.

ComboboxList 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 ComboboxListBeispielViewModel();
}

<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
{
    public class ComboboxListBeispielViewModel
    {
        public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
                                                              {
                                                                  new ("Schweiz", "CH"),
                                                                  new ("Deutschland", "DE"),
                                                                  new ("Österreich", "AT"),
                                                              };

        [Display(Name = "Land (durchsuchbar)")]
        [UIHint(UiEditor.Basis.ComboboxList)]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        [EmptyEntry(true)]
        public string Land { get; set; } = "CH";
    }
}

Darstellung

Ist das ViewModel nur eine lokale Variable, etwa in einer Sidebar oder einem Layout-Partial, gibt der zweite Parameter von EditorFor das Objekt mit. Das Template findet die Liste dann in der ViewData statt am Behälter:

@{
    var auswahl = new ComboboxListBeispielViewModel();
}

@Html.EditorFor(m => auswahl.Land, auswahl)

Liste und Favoriten kommen auf demselben Weg ins Template, die Suche im Feld funktioniert unverändert.

Warum das nötig ist, in welcher Reihenfolge das Template sucht, die Variante mit anonymem Objekt und wie das Feld beim Absenden heisst (auswahl.Land), steht ausführlich im Beispiel «DropdownList ohne View-Model» auf der Seite DropdownList.