MultiselectList

Das MultiselectList-EditorTemplate rendert ein Auswahlfeld mit Mehrfachauswahl. Der Wert ist ein string[]; die Auswahlliste kommt über [AdditionalMetadata("Selection", ...)]; Einbindung über [UIHint(UiEditor.Basis.MultiselectList)]. Ob ein leerer Eintrag in der Liste steht, steuert [EmptyEntry(false)].

MultiselectList (Grundfall)

Auswahlfeld mit Mehrfachauswahl; die Liste kommt aus einer Eigenschaft des ViewModels, zwei Einträge sind vorausgewählt.

@model MultiselectListBeispielViewModel

<form class="fmh-demo-form">
    @Html.EditorFor(m => m.Laenderauswahl)
</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: Mehrfachauswahl mit Liste aus einer zweiten Eigenschaft des ViewModels.
    /// Zwei Einträge sind vorausgewählt, das ist schlicht der Wert der Eigenschaft.
    /// </summary>
    public class MultiselectListBeispielViewModel
    {
        public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
                                                              {
                                                                  new ("Schweiz", "CH"),
                                                                  new ("Deutschland", "DE"),
                                                                  new ("Österreich", "AT"),
                                                              };

        [Display(Name = "Länder (Mehrfachauswahl)")]
        [UIHint(UiEditor.Basis.MultiselectList)]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        public string[] Laenderauswahl { get; set; } = { "CH", "AT" };
    }
}

Darstellung

Das MultiselectList-EditorTemplate rendert ein Auswahlfeld mit Mehrfachauswahl (<select multiple>). Der Wert ist ein Array der gewählten Werte, die Optionen liegen als IEnumerable<SelectListItem> in einer zweiten Eigenschaft des ViewModels; welche das ist, sagt UiMetadata.Basis.DropdownListSelection.

[Display(Name = "Länder (Mehrfachauswahl)")]
[UIHint(UiEditor.Basis.MultiselectList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
public string[] Laenderauswahl { get; set; } = { "CH", "AT" };

public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
{
    new("Schweiz", "CH"), new("Deutschland", "DE"), new("Österreich", "AT"),
};
@Html.EditorFor(m => m.Laenderauswahl)

Die Vorauswahl ist schlicht der Wert der Eigenschaft. Was im Array steht, wird mit dem Value der Einträge verglichen und als gewählt gerendert; ein leeres Array oder null heisst «nichts gewählt». Der Typ des Arrays ist frei, string[], int[] oder int?[] funktionieren gleichermassen, solange die Werte zu den Value-Angaben passen.

Die Attribute im Überblick

Attribut Wirkung
[UIHint(UiEditor.Basis.MultiselectList)] 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 «MultiselectList ohne View-Model»
[AdditionalMetadata(UiMetadata.Basis.FavoritenSelection, nameof(…))] Zweite Liste, die vor der regulären steht, siehe «MultiselectList mit Favoriten»
[Required] Pflichtauswahl, siehe «MultiselectList mit Pflichtauswahl»
[EmptyEntry(false)] Unterdrückt den leeren Eintrag, siehe «MultiselectList 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. Es sind dieselben Konstanten und dieselbe Mechanik wie bei DropdownList, die Vorlage unterscheidet sich nur in der Mehrfachauswahl.

MultiselectList mit data-Attributen

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

@model MultiselectListAttributeBeispielViewModel

<form class="fmh-demo-form">
    @Html.EditorFor(m => m.LaenderMitAttributen)
</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 MultiselectListAttributeBeispielViewModel
    {
        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 = "Länder mit data-Attributen")]
        [UIHint(UiEditor.Basis.MultiselectList)]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        public string[] LaenderMitAttributen { 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), an der Angabe der Liste ändert sich nichts.

MultiselectList mit Favoriten

Eine zweite Liste steht vor der regulären, getrennt durch eine Linie.

@model MultiselectListFavoritenBeispielViewModel

<form class="fmh-demo-form">
    @Html.EditorFor(m => m.LaenderMitFavoriten)
</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 MultiselectListFavoritenBeispielViewModel
    {
        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 = "Länder mit Favoriten")]
        [UIHint(UiEditor.Basis.MultiselectList)]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        [AdditionalMetadata(UiMetadata.Basis.FavoritenSelection, nameof(Favoriten))]
        public string[] LaenderMitFavoriten { 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 = "Länder mit Favoriten")]
[UIHint(UiEditor.Basis.MultiselectList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
[AdditionalMetadata(UiMetadata.Basis.FavoritenSelection, nameof(Favoriten))]
public string[] LaenderMitFavoriten { get; set; }

Die Favoriten sind eine eigene Liste, keine Markierung an den regulären Einträgen: Ein Eintrag, der in beiden steht, erscheint zweimal und lässt sich zweimal auswählen.

MultiselectList mit Gruppen

Tragen die Einträge eine SelectListGroup, entsteht je Gruppe ein optgroup mit Zwischentitel.

@model MultiselectListGruppenBeispielViewModel

<form class="fmh-demo-form">
    @Html.EditorFor(m => m.LaenderNachRegion)
</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 Mehrfachauswahl: Tragen die Einträge eine SelectListGroup, rendert das Template
    /// je Gruppe ein optgroup.
    /// </summary>
    public class MultiselectListGruppenBeispielViewModel
    {
        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 = "Länder nach Region")]
        [UIHint(UiEditor.Basis.MultiselectList)]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        public string[] LaenderNachRegion { get; set; }
    }
}

Darstellung

Trägt ein SelectListItem eine SelectListGroup, entsteht je Gruppe ein <optgroup>:

private static readonly SelectListGroup Dach = new() { Name = "D-A-CH" };

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

Zwei Unterschiede zu DropdownList, die man kennen sollte:

  • Gruppiert wird über die Instanz, nicht über den Namen. Zwei SelectListGroup-Objekte mit gleichem Namen ergeben hier zwei Blöcke mit gleichem Titel; deshalb die Gruppen einmal anlegen und wiederverwenden, wie oben mit dem statischen Feld.
  • Einträge ohne Gruppe fallen weg. Sobald ein Eintrag eine Gruppe trägt, geht das Template in den Gruppen-Modus und überspringt die übrigen kommentarlos. Entweder alle Einträge bekommen eine Gruppe oder keiner.

MultiselectList mit Pflichtauswahl

[Required] verlangt mindestens einen Eintrag. Ohne Auswahl schickt der Browser für das Feld nichts mit, die Prüfung greift also.

@model MultiselectListPflichtBeispielViewModel

<form asp-area="Basis" asp-controller="MultiselectList" asp-action="Index" method="post">
    @Html.EditorFor(m => m.LaenderPflicht)
    @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: Ohne Auswahl schickt der Browser für das Feld nichts mit, die Eigenschaft
    /// bleibt null und [Required] schlägt an.
    /// </summary>
    public class MultiselectListPflichtBeispielViewModel
    {
        public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
                                                              {
                                                                  new ("Schweiz", "CH"),
                                                                  new ("Deutschland", "DE"),
                                                                  new ("Österreich", "AT"),
                                                              };

        [Display(Name = "Länder (mindestens eines)")]
        [UIHint(UiEditor.Basis.MultiselectList)]
        [Required(ErrorMessage = "Bitte mindestens ein Land auswählen.")]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        public string[] LaenderPflicht { 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 MultiselectList. Nur die Pflichtauswahl braucht
    /// einen Rücklauf; die übrigen Varianten der Demo-Seite zeigen allein die Darstellung.
    /// </summary>
    [Area("Basis")]
    public class MultiselectListController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return this.View(new MultiselectListPflichtBeispielViewModel());
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(MultiselectListPflichtBeispielViewModel 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] verlangt mindestens einen gewählten Eintrag:

[Display(Name = "Länder (mindestens eines)")]
[UIHint(UiEditor.Basis.MultiselectList)]
[Required(ErrorMessage = "Bitte mindestens ein Land auswählen.")]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
public string[] LaenderPflicht { get; set; }

Warum das genügt: Ist nichts gewählt, schickt der Browser für das Feld gar nichts mit, die Eigenschaft bleibt null, und [Required] schlägt an. Ein Array mit Elementen ist dagegen immer gültig, eine Mindestanzahl grösser eins prüft [Required] also nicht; dafür braucht es ein eigenes Attribut oder eine Prüfung in der Action.

Das Feld wird wie bei den übrigen Templates 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 dahinter.

MultiselectList ohne leeren Eintrag

[EmptyEntry(false)] unterdrückt den leeren Eintrag; die Liste enthält nur die echten Optionen.

@model MultiselectListOhneLeereintragBeispielViewModel

<form class="fmh-demo-form">
    @Html.EditorFor(m => m.LaenderOhneLeereintrag)
</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 Vorauswahl und ohne leeren Eintrag: Die Liste enthält nur die echten Optionen.
    /// </summary>
    public class MultiselectListOhneLeereintragBeispielViewModel
    {
        public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
                                                              {
                                                                  new ("Schweiz", "CH"),
                                                                  new ("Deutschland", "DE"),
                                                                  new ("Österreich", "AT"),
                                                              };

        [Display(Name = "Länder (Mehrfachauswahl)")]
        [UIHint(UiEditor.Basis.MultiselectList)]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        [EmptyEntry(false)]
        public string[] LaenderOhneLeereintrag { get; set; }
    }
}

Darstellung

Ohne weitere Angabe enthält die Liste einen leeren Eintrag (<option value="" class="fmh-empty-item">). [EmptyEntry(false)] unterdrückt ihn:

[Display(Name = "Länder (Mehrfachauswahl)")]
[UIHint(UiEditor.Basis.MultiselectList)]
[AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
[EmptyEntry(false)]
public string[] LaenderOhneLeereintrag { get; set; }
Angabe Wirkung
kein Attribut leerer Eintrag wird gerendert (Standard)
[EmptyEntry] / [EmptyEntry(true)] leerer Eintrag wird gerendert
[EmptyEntry(false)] kein leerer Eintrag

Bei der Mehrfachauswahl ist der leere Eintrag weniger wichtig als beim einfachen Auswahlfeld: «nichts gewählt» ist hier ohnehin möglich, indem kein Eintrag markiert ist. Dasselbe Attribut wirkt gleich bei DropdownList und ComboboxList.

MultiselectList 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 MultiselectListBeispielViewModel();
}

<form class="fmh-demo-form">
    @Html.EditorFor(m => auswahl.Laenderauswahl, 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: Mehrfachauswahl mit Liste aus einer zweiten Eigenschaft des ViewModels.
    /// Zwei Einträge sind vorausgewählt, das ist schlicht der Wert der Eigenschaft.
    /// </summary>
    public class MultiselectListBeispielViewModel
    {
        public IEnumerable<SelectListItem> Laender { get; } = new List<SelectListItem>
                                                              {
                                                                  new ("Schweiz", "CH"),
                                                                  new ("Deutschland", "DE"),
                                                                  new ("Österreich", "AT"),
                                                              };

        [Display(Name = "Länder (Mehrfachauswahl)")]
        [UIHint(UiEditor.Basis.MultiselectList)]
        [AdditionalMetadata(UiMetadata.Basis.DropdownListSelection, nameof(Laender))]
        public string[] Laenderauswahl { get; set; } = { "CH", "AT" };
    }
}

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

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

Die Vorauswahl kommt weiterhin aus dem Wert der Eigenschaft, hier also CH und AT aus auswahl.Laenderauswahl.

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