Dropdown-Button
Der DropdownButtonBuilder (Fmh.DropdownButton()) rendert eine
Auswahl-Schaltfläche: eine Schaltfläche, die ein Menü aufklappt. Der gewählte Eintrag landet in
einem versteckten Feld, danach sendet sich das umschliessende Formular selbst ab. Aufruf immer
über @Html.Fmh(Fmh.DropdownButton()...).
Dropdown-Button (Grundfall)
Vier Einträge aus einer SelectListItem-Liste. Ein Klick schreibt den Value in ein verstecktes Feld, das wie die Id der Schaltfläche heisst, und sendet das Formular an die Adresse aus .Action(...).
@using FMH.Komponente.Infrastruktur.Schnittstellen.Ui
@model DropdownButtonBeispielViewModel
@* Die Auswahl-Schaltfläche stellt mehrere Möglichkeiten zur Wahl. Ein Klick auf einen Eintrag
schreibt dessen Wert in ein verstecktes Feld und sendet das umschliessende Formular ab – an die
Adresse aus .Action(...). Ein <form> ist deshalb Pflicht.
Der Name des versteckten Feldes ist die Id der Schaltfläche: .Id("BtnAbteilung") bindet an die
gleichnamige Eigenschaft des ViewModels. *@
<form asp-area="Infrastruktur" asp-controller="DropdownButton" asp-action="Auswahl" method="post">
@Html.Fmh(
Fmh.DropdownButton()
.Icon(IconType.People)
.Text("Abteilung wählen")
.Action("Auswahl", "DropdownButton", new { area = "Infrastruktur" })
.Items(Model.Abteilungen.ToList())
.Id("BtnAbteilung"))
</form>
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace FMH.Komponente.WebUI.Areas.Infrastruktur.Models
{
/// <summary>
/// Ausgangslage für die Auswahl-Schaltfläche. Die Einträge kommen als
/// <c>SelectListItem</c>-Liste aus dem Modell, übertragen wird der <c>Value</c>.
/// </summary>
public class DropdownButtonBeispielViewModel
{
/// <summary>
/// Nimmt die Auswahl entgegen. Der Name ist bindend: Das versteckte Feld, das der Builder
/// rendert, heisst wie die Id der Schaltfläche, hier <c>BtnAbteilung</c>.
/// </summary>
public string BtnAbteilung { get; set; }
/// <summary>
/// Die Einträge des Menüs.
/// </summary>
public IEnumerable<SelectListItem> Abteilungen { get; } = new List<SelectListItem>
{
new ("Abteilung 2", "2"),
new ("Abteilung 4", "4"),
new ("Abteilung 6", "6"),
new ("Abteilung 8", "8"),
};
}
}
using System.Linq;
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.Basis.Ui.Feedback.Alert;
using FMH.Komponente.WebUI.Areas.Infrastruktur.Models;
using Microsoft.AspNetCore.Mvc;
namespace FMH.Komponente.WebUI.Areas.Infrastruktur.Controllers.Beispiele
{
/// <summary>
/// Die Gegenstelle der beiden Auswahl-Schaltflächen; je Variante ein GET+POST-Paar.
/// </summary>
/// <remarks>
/// Die Schaltfläche sendet das Formular selbst ab, sobald ein Eintrag gewählt wurde. Beim
/// Server kommt deshalb kein Klick an, sondern ein gewöhnlicher POST mit einem Feld, das wie
/// die Id der Schaltfläche heisst.
/// </remarks>
[Area("Infrastruktur")]
public class DropdownButtonController : Controller
{
[HttpGet]
public IActionResult Auswahl()
{
return this.View(new DropdownButtonBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Auswahl(DropdownButtonBeispielViewModel modell)
{
var eintrag = modell.Abteilungen.FirstOrDefault(x => x.Value == modell.BtnAbteilung);
return this.Bestaetige(eintrag?.Text, modell.BtnAbteilung, nameof(this.Auswahl));
}
[HttpGet]
public IActionResult Filter()
{
return this.View(new DropdownButtonFilterBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Filter(DropdownButtonFilterBeispielViewModel modell)
{
var eintrag = modell.Fachgebiete.FirstOrDefault(x => x.Value == modell.BtnFachgebiet);
return this.Bestaetige(eintrag?.Text, modell.BtnFachgebiet, nameof(this.Filter));
}
/// <summary>
/// Gemeinsame Rückmeldung der Varianten: meldet Text und Wert des gewählten Eintrags und
/// leitet per Redirect auf die GET-Action (PRG).
/// </summary>
/// <param name="text">Der angezeigte Text des gewählten Eintrags, sofern auflösbar.</param>
/// <param name="wert">Der übertragene Wert aus dem versteckten Feld.</param>
/// <param name="action">Der Name der GET-Action dieser Variante.</param>
/// <returns>Die Weiterleitung auf die GET-Action.</returns>
private IActionResult Bestaetige(string text, string wert, string action)
{
// Übertragen wird der Value, nicht der angezeigte Text. Wer den Text braucht, löst ihn
// über dieselbe Liste wieder auf, aus der das Menü gebaut wurde.
var meldung = string.IsNullOrEmpty(wert)
? "Formular übertragen – es wurde nichts gewählt."
: $"Gewählt: {text ?? "unbekannt"} (Wert {wert})";
this.AddAlert(meldung, AlertType.Success, autoHide: true);
return this.RedirectToAction(action);
}
}
}
Darstellung
Die Auswahl-Schaltfläche stellt mehrere Möglichkeiten zur Wahl, ohne dass daneben ein Auswahlfeld und eine zweite Schaltfläche stehen müssen:
@Html.Fmh(Fmh.DropdownButton()
.Icon(IconType.People)
.Text("Abteilung wählen")
.Action("Auswahl", "DropdownButton", new { area = "Infrastruktur" })
.Items(Model.Abteilungen.ToList())
.Id("BtnAbteilung"))
Der Ablauf in drei Schritten
- Die Schaltfläche klappt ein Bootstrap-Menü mit den Einträgen aus
.Items(...)auf. - Ein Klick auf einen Eintrag schreibt dessen
Valuein ein verstecktes Feld und löst daraufchangeaus. - Danach setzt das Skript die Adresse des umschliessenden Formulars auf die Adresse aus
.Action(...)und sendet das Formular ab.
Beim Server kommt also ein gewöhnlicher POST an, kein Klick-Ereignis. Das Feld heisst wie die
Id der Schaltfläche: .Id("BtnAbteilung") bindet an eine Eigenschaft BtnAbteilung.
public string BtnAbteilung { get; set; }
public IEnumerable<SelectListItem> Abteilungen { get; } = new List<SelectListItem>
{
new ("Abteilung 2", "2"),
new ("Abteilung 4", "4"),
};
Übertragen wird der Value, nicht der angezeigte Text. Wer den Text braucht, löst ihn über
dieselbe Liste wieder auf, aus der das Menü gebaut wurde.
Filter für lange Listen
.EnableFilter() setzt ein Eingabefeld über die Einträge. Getippt wird gefiltert, die Pfeiltasten
wählen, Enter übernimmt den hervorgehobenen Eintrag. Gefiltert wird im Browser über die bereits
gerenderten Einträge, es wird nichts nachgeladen; für eine Liste, die nicht vollständig gerendert
werden kann, ist der Dropdown-Button das falsche Mittel.
Die Filter- und Tastaturlogik liefert die Basis (jquery-fmh-sitesetup.js, Bundle fmh-scripts).
Optionen
| Methode | Wirkung |
|---|---|
.Items(IList<SelectListItem>) |
Die Einträge des Menüs; Text wird angezeigt, Value übertragen. |
.Id("…") |
Id der Schaltfläche und Name des versteckten Feldes. Ohne Angabe eine Guid, dann lässt sich der Wert serverseitig nicht binden. |
.Text("…") |
Beschriftung der Schaltfläche. |
.Icon(IconType) |
Symbol in der Schaltfläche. |
.IconPosition(ButtonIconPosition.Right) |
Symbol rechts statt links. |
.Action("Action", "Controller", routeValues) |
Ziel des Formulars. Alternativ .Action("https://…") für eine feste Adresse. |
.Color(ButtonColor) |
Farbe; Primary ist die Voreinstellung und rendert fmh-btn-primary. |
.EnableFilter() |
Suchfeld im Menü, siehe oben. |
.Attributes(new { … }) |
Weitere HTML-Attribute. Ein id darin gewinnt über .Id(...). |
.Disabled(true, "Titel") |
Deaktiviert die Schaltfläche. Achtung: Der zweite Parameter ersetzt die Beschriftung, er ist kein Tooltip. |
.Visible(false) |
Blendet die Schaltfläche über die Klasse fmh-button-invisible aus. |
Was zu beachten ist
- Ein Formular ist Pflicht. Das Skript sendet das nächstgelegene
<form>ab; ohne ein umschliessendes Formular passiert beim Klick auf einen Eintrag nichts. - Für Selenium zählt die Schaltfläche, nicht das Feld. Die Id aus
.Id(...)trägt das versteckte Feld, die Schaltfläche daneben bekommt den Zusatz-dd: ausBtnAbteilungwirdBtnAbteilung-dd. - Ohne
.Id(...)kommt beim Server nichts Verwertbares an. Der Standardwert istbtn-<Guid>, und der ändert sich bei jedem Rendern. - Mehrere Auswahl-Schaltflächen auf einer Seite sind unproblematisch, auch wenn der Builder
seinen Klick-Handler je Schaltfläche an alle Menüeinträge der Seite bindet: Jeder Eintrag trägt
sein Zielfeld als
data-forbei sich, ein mehrfach ausgeführter Handler schreibt deshalb denselben Wert in dasselbe Feld und sendet dasselbe Formular ab. - Kein Platz nach unten? Das Menü klappt dann nach oben auf und bekommt eine Höhe, die in das
Fenster passt; das erledigt
dropdownButtonOptionsTogglebeim Öffnen. - Das Absenden lässt sich nicht abwählen. Die Schaltfläche ist für «wählen und weiter»
gebaut. Wer den Wert nur im Formular halten will, ohne zu senden, nimmt eine Auswahlliste
(EditorTemplate
DropdownList).
Dropdown-Button mit Filter
Bei langen Listen blendet .EnableFilter() ein Suchfeld über den Einträgen ein; gefiltert wird im Browser, Pfeiltasten wählen und Enter übernimmt.
@using FMH.Komponente.Infrastruktur.Schnittstellen.Ui
@model DropdownButtonFilterBeispielViewModel
@* Dieselbe Schaltfläche mit .EnableFilter(): Das Menü bekommt zuoberst ein Eingabefeld, das die
Einträge beim Tippen filtert. Pfeiltasten wählen, Enter übernimmt.
Die Filterung läuft im Browser über die bereits gerenderten Einträge, es wird nichts nachgeladen. *@
<form asp-area="Infrastruktur" asp-controller="DropdownButton" asp-action="Filter" method="post">
@Html.Fmh(
Fmh.DropdownButton()
.Icon(IconType.Search)
.Text("Fachgebiet wählen")
.Action("Filter", "DropdownButton", new { area = "Infrastruktur" })
.Items(Model.Fachgebiete.ToList())
.Id("BtnFachgebiet")
.EnableFilter())
</form>
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace FMH.Komponente.WebUI.Areas.Infrastruktur.Models
{
/// <summary>
/// Ausgangslage für die Auswahl-Schaltfläche mit Filter. Bewusst eine lange Liste: Erst ab
/// einer Menge, die nicht mehr auf einen Blick überschaubar ist, verdient der Filter seinen
/// Platz im Menü.
/// </summary>
public class DropdownButtonFilterBeispielViewModel
{
private static readonly string[] Namen =
{
"Allgemeine Innere Medizin",
"Anästhesiologie",
"Chirurgie",
"Dermatologie",
"Endokrinologie",
"Gynäkologie",
"Hämatologie",
"Kardiologie",
"Nephrologie",
"Neurologie",
"Onkologie",
"Ophthalmologie",
"Pädiatrie",
"Psychiatrie",
"Radiologie",
"Rheumatologie",
};
/// <summary>
/// Nimmt die Auswahl entgegen, benannt wie die Id der Schaltfläche.
/// </summary>
public string BtnFachgebiet { get; set; }
/// <summary>
/// Die Einträge des Menüs; der Wert ist hier die Position in der Liste.
/// </summary>
public IEnumerable<SelectListItem> Fachgebiete { get; } = Namen
.Select((name, index) => new SelectListItem(name, (index + 1).ToString()))
.ToList();
}
}
using System.Linq;
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.Basis.Ui.Feedback.Alert;
using FMH.Komponente.WebUI.Areas.Infrastruktur.Models;
using Microsoft.AspNetCore.Mvc;
namespace FMH.Komponente.WebUI.Areas.Infrastruktur.Controllers.Beispiele
{
/// <summary>
/// Die Gegenstelle der beiden Auswahl-Schaltflächen; je Variante ein GET+POST-Paar.
/// </summary>
/// <remarks>
/// Die Schaltfläche sendet das Formular selbst ab, sobald ein Eintrag gewählt wurde. Beim
/// Server kommt deshalb kein Klick an, sondern ein gewöhnlicher POST mit einem Feld, das wie
/// die Id der Schaltfläche heisst.
/// </remarks>
[Area("Infrastruktur")]
public class DropdownButtonController : Controller
{
[HttpGet]
public IActionResult Auswahl()
{
return this.View(new DropdownButtonBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Auswahl(DropdownButtonBeispielViewModel modell)
{
var eintrag = modell.Abteilungen.FirstOrDefault(x => x.Value == modell.BtnAbteilung);
return this.Bestaetige(eintrag?.Text, modell.BtnAbteilung, nameof(this.Auswahl));
}
[HttpGet]
public IActionResult Filter()
{
return this.View(new DropdownButtonFilterBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Filter(DropdownButtonFilterBeispielViewModel modell)
{
var eintrag = modell.Fachgebiete.FirstOrDefault(x => x.Value == modell.BtnFachgebiet);
return this.Bestaetige(eintrag?.Text, modell.BtnFachgebiet, nameof(this.Filter));
}
/// <summary>
/// Gemeinsame Rückmeldung der Varianten: meldet Text und Wert des gewählten Eintrags und
/// leitet per Redirect auf die GET-Action (PRG).
/// </summary>
/// <param name="text">Der angezeigte Text des gewählten Eintrags, sofern auflösbar.</param>
/// <param name="wert">Der übertragene Wert aus dem versteckten Feld.</param>
/// <param name="action">Der Name der GET-Action dieser Variante.</param>
/// <returns>Die Weiterleitung auf die GET-Action.</returns>
private IActionResult Bestaetige(string text, string wert, string action)
{
// Übertragen wird der Value, nicht der angezeigte Text. Wer den Text braucht, löst ihn
// über dieselbe Liste wieder auf, aus der das Menü gebaut wurde.
var meldung = string.IsNullOrEmpty(wert)
? "Formular übertragen – es wurde nichts gewählt."
: $"Gewählt: {text ?? "unbekannt"} (Wert {wert})";
this.AddAlert(meldung, AlertType.Success, autoHide: true);
return this.RedirectToAction(action);
}
}
}
Darstellung
Die Auswahl-Schaltfläche stellt mehrere Möglichkeiten zur Wahl, ohne dass daneben ein Auswahlfeld und eine zweite Schaltfläche stehen müssen:
@Html.Fmh(Fmh.DropdownButton()
.Icon(IconType.People)
.Text("Abteilung wählen")
.Action("Auswahl", "DropdownButton", new { area = "Infrastruktur" })
.Items(Model.Abteilungen.ToList())
.Id("BtnAbteilung"))
Der Ablauf in drei Schritten
- Die Schaltfläche klappt ein Bootstrap-Menü mit den Einträgen aus
.Items(...)auf. - Ein Klick auf einen Eintrag schreibt dessen
Valuein ein verstecktes Feld und löst daraufchangeaus. - Danach setzt das Skript die Adresse des umschliessenden Formulars auf die Adresse aus
.Action(...)und sendet das Formular ab.
Beim Server kommt also ein gewöhnlicher POST an, kein Klick-Ereignis. Das Feld heisst wie die
Id der Schaltfläche: .Id("BtnAbteilung") bindet an eine Eigenschaft BtnAbteilung.
public string BtnAbteilung { get; set; }
public IEnumerable<SelectListItem> Abteilungen { get; } = new List<SelectListItem>
{
new ("Abteilung 2", "2"),
new ("Abteilung 4", "4"),
};
Übertragen wird der Value, nicht der angezeigte Text. Wer den Text braucht, löst ihn über
dieselbe Liste wieder auf, aus der das Menü gebaut wurde.
Filter für lange Listen
.EnableFilter() setzt ein Eingabefeld über die Einträge. Getippt wird gefiltert, die Pfeiltasten
wählen, Enter übernimmt den hervorgehobenen Eintrag. Gefiltert wird im Browser über die bereits
gerenderten Einträge, es wird nichts nachgeladen; für eine Liste, die nicht vollständig gerendert
werden kann, ist der Dropdown-Button das falsche Mittel.
Die Filter- und Tastaturlogik liefert die Basis (jquery-fmh-sitesetup.js, Bundle fmh-scripts).
Optionen
| Methode | Wirkung |
|---|---|
.Items(IList<SelectListItem>) |
Die Einträge des Menüs; Text wird angezeigt, Value übertragen. |
.Id("…") |
Id der Schaltfläche und Name des versteckten Feldes. Ohne Angabe eine Guid, dann lässt sich der Wert serverseitig nicht binden. |
.Text("…") |
Beschriftung der Schaltfläche. |
.Icon(IconType) |
Symbol in der Schaltfläche. |
.IconPosition(ButtonIconPosition.Right) |
Symbol rechts statt links. |
.Action("Action", "Controller", routeValues) |
Ziel des Formulars. Alternativ .Action("https://…") für eine feste Adresse. |
.Color(ButtonColor) |
Farbe; Primary ist die Voreinstellung und rendert fmh-btn-primary. |
.EnableFilter() |
Suchfeld im Menü, siehe oben. |
.Attributes(new { … }) |
Weitere HTML-Attribute. Ein id darin gewinnt über .Id(...). |
.Disabled(true, "Titel") |
Deaktiviert die Schaltfläche. Achtung: Der zweite Parameter ersetzt die Beschriftung, er ist kein Tooltip. |
.Visible(false) |
Blendet die Schaltfläche über die Klasse fmh-button-invisible aus. |
Was zu beachten ist
- Ein Formular ist Pflicht. Das Skript sendet das nächstgelegene
<form>ab; ohne ein umschliessendes Formular passiert beim Klick auf einen Eintrag nichts. - Für Selenium zählt die Schaltfläche, nicht das Feld. Die Id aus
.Id(...)trägt das versteckte Feld, die Schaltfläche daneben bekommt den Zusatz-dd: ausBtnAbteilungwirdBtnAbteilung-dd. - Ohne
.Id(...)kommt beim Server nichts Verwertbares an. Der Standardwert istbtn-<Guid>, und der ändert sich bei jedem Rendern. - Mehrere Auswahl-Schaltflächen auf einer Seite sind unproblematisch, auch wenn der Builder
seinen Klick-Handler je Schaltfläche an alle Menüeinträge der Seite bindet: Jeder Eintrag trägt
sein Zielfeld als
data-forbei sich, ein mehrfach ausgeführter Handler schreibt deshalb denselben Wert in dasselbe Feld und sendet dasselbe Formular ab. - Kein Platz nach unten? Das Menü klappt dann nach oben auf und bekommt eine Höhe, die in das
Fenster passt; das erledigt
dropdownButtonOptionsTogglebeim Öffnen. - Das Absenden lässt sich nicht abwählen. Die Schaltfläche ist für «wählen und weiter»
gebaut. Wer den Wert nur im Formular halten will, ohne zu senden, nimmt eine Auswahlliste
(EditorTemplate
DropdownList).