Wizard
Fünf Schritte
Der Regelfall. Der letzte Schritt ist über IsDisabled für den Direktsprung gesperrt, «Speichern» heisst «Entwurf speichern», und die Pflichtangabe blockiert nur «Weiter» – nicht Speichern und nicht die Schritt-Navigation.
@model WizardBeispielViewModel
@* Der Wizard ist ein Seiten-Steuerelement, kein Feld: Kopfbereich und Navigation umschliessen den
Inhalt der Seite. Die Konfiguration kommt wie im Projekt über ViewBag.Wizard, die Eingabefelder
dazwischen über die EditorTemplates der Komponenten. Nr und Anzahl sind reine Transportfelder für
den POST und deshalb HiddenFor — sie haben nichts anzuzeigen. «Weiter» führt in den Ablauf. *@
<form method="post" asp-area="Wizard" asp-controller="WizardBeispiel" asp-action="Schritt">
@Html.Fmh(Fmh.Wizard()
.Id("wz-fuenf")
.Add(ViewBag.Wizard as WizardSetup))
<div class="my-4" style="max-width: 40rem;">
@Html.HiddenFor(m => m.Nr)
@Html.HiddenFor(m => m.Anzahl)
@Html.EditorFor(m => m.Bemerkung)
</div>
@Html.Fmh(Fmh.WizardNavigation()
.Add(ViewBag.Wizard as WizardSetup))
</form>
using System.Collections.Generic;
using System.Linq;
using FMH.Komponente.Infrastruktur.Ui.Controls.Button;
using FMH.Komponente.WebUI.Areas.Wizard.Models;
using FMH.Komponente.Wizard.Ui.Controls.Wizard.Daten;
using FMH.Komponente.Wizard.Ui.Controls.Wizard.Erweiterungen;
using Microsoft.AspNetCore.Mvc;
namespace FMH.Komponente.WebUI.Areas.Wizard.Controllers.Beispiele
{
// Ein Endpunkt für alle Schritte: Welcher Schritt gerade läuft, sagt die Route (nr). In einem
// Projekt sind es üblicherweise benannte Actions je Schritt (Schritt01, Schritt02, …) — die
// Mechanik ist dieselbe, hier bleibt der Ablauf so aber in einer Datei nachlesbar.
[Area("Wizard")]
public class WizardBeispielController : Controller
{
[HttpGet]
public IActionResult Schritt(int nr = 1, int anzahl = 5)
{
this.ViewBag.Wizard = BuildWizard(nr, anzahl);
return this.View(new WizardBeispielViewModel { Nr = nr, Anzahl = anzahl });
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Schritt(WizardBeispielViewModel modell, string submitButton)
{
// Die Wizardkonfiguration wird für den POST neu aufgebaut — sie wird nicht mitgeführt.
var wz = BuildWizard(modell.Nr, modell.Anzahl);
this.ViewBag.Wizard = wz;
// Vorgelagerte Prüfung: IsWizardValid berücksichtigt die über DisableInputValidationFor
// abgeschalteten Bereiche, bevor es auf ModelState.IsValid zurückfällt. Ohne diesen
// Aufruf würde die Pflichtangabe auch «Speichern» und die Schritt-Navigation blockieren.
if (this.ModelState.IsWizardValid(wz, submitButton))
{
// PRG: Das Ziel ergibt sich aus der Wizardkonfiguration und der gedrückten
// Schaltfläche — der Controller muss die Schrittlogik nicht selbst kennen.
return this.RedirectWithWizardSetupAndNavigationValue(wz, submitButton);
}
return this.View(modell);
}
// Die Konfiguration liegt in einer eigenen Methode, damit GET und POST dieselbe erzeugen.
private static WizardSetup BuildWizard(int nr, int anzahl)
{
var ws = new WizardSetup
{
Title = "Antrag einreichen",
// Präfix der Button-Ids. Darüber sprechen die jQuery-Funktionen
// wizardNavbarDisabled/-Enabled die Schaltflächen an.
IdPrefix = "wz",
};
ws.Steps = Enumerable.Range(1, anzahl)
.Select(
i => new WizardStep
{
Title = SchrittTitel(i, anzahl),
Action = new MvcAction("Schritt", "WizardBeispiel", new { nr = i, anzahl }),
// Der letzte Schritt lässt sich nicht direkt anspringen — ein häufiger
// fachlicher Fall: Übermitteln erst nach dem Durchlaufen.
IsDisabled = i == anzahl,
State = ZuStatus(i, nr),
})
.ToList();
ws.SetCancelAction("Wizard", "Demo", new { area = "Wizard" });
ws.SetHelpAction("/Wizard/Demo/Wizard");
ws.SetSaveButtonText("Entwurf speichern");
// Speichern und die Schritt-Navigation sollen auch mit unvollständigen Angaben
// funktionieren; nur «Weiter» erzwingt die Pflichtangabe.
ws.DisableInputValidationFor(ValidationArea.NavigationWizard | ValidationArea.Save);
return ws;
}
private static string SchrittTitel(int nummer, int anzahl)
{
if (nummer == 1)
{
return "Bemerkungen";
}
if (nummer == anzahl)
{
return "Antrag übermitteln";
}
return $"Angaben Teil {nummer - 1}";
}
// Schritte vor dem aktuellen gelten als erledigt, der aktuelle ist Current, der Rest offen.
private static WizardStepState ZuStatus(int nummer, int aktuell)
{
if (nummer < aktuell)
{
return WizardStepState.Filled;
}
return nummer == aktuell ? WizardStepState.Current : WizardStepState.ToDo;
}
}
}
Darstellung
Der Wizard besteht aus zwei Buildern und einem Konfigurationsmodell:
| Baustein | Aufgabe |
|---|---|
Fmh.Wizard() |
Kopfbereich: Titel, «Schritt x von y», Hilfe-Schaltfläche und die Schritt-Navigation |
Fmh.WizardNavigation() |
Leiste mit Zurück, Abbrechen, Speichern und Weiter |
WizardSetup |
die gemeinsame Konfiguration beider Builder |
Konfiguration im Controller
Die Konfiguration gehört in eine eigene Methode — GET und POST müssen dieselbe erzeugen, weil sie nicht mitgeführt wird:
private WizardSetup BuildWizard(int id, int? stepIndex)
{
var ws = new WizardSetup();
ws.Title = "Antrag einreichen";
ws.Steps = new List<WizardStep>
{
new WizardStep { Title = "Bemerkungen", Action = new MvcAction("Schritt01", "Wizard", new { id }) },
new WizardStep { Title = "Antrag ausfüllen", Action = new MvcAction("Schritt02", "Wizard", new { id }) },
new WizardStep { Title = "Antrag übermitteln", Action = new MvcAction("Schritt05", "Wizard", new { id }), IsDisabled = true },
};
ws.SetCancelAction("Index", "Home", new { id });
ws.SetSaveButtonText("Entwurf speichern");
if (stepIndex.HasValue)
{
ws.Steps[stepIndex.Value].State = WizardStepState.Current;
}
ws.DisableInputValidationFor(ValidationArea.NavigationWizard | ValidationArea.Save);
return ws;
}
Im GET wird sie an die View gegeben, im POST wieder aufgebaut und ausgewertet:
[HttpPost]
public IActionResult Schritt01(Schritt01ViewModel model, string submitButton)
{
var wz = this.BuildWizard(model.Id, 0);
this.ViewBag.Wizard = wz;
if (this.ModelState.IsWizardValid(wz, submitButton))
{
return this.RedirectWithWizardSetupAndNavigationValue(wz, submitButton);
}
return this.View(model);
}
RedirectWithWizardSetupAndNavigationValue leitet nach dem PRG-Muster weiter und liest das Ziel aus
der Konfiguration — der Controller muss die Schrittfolge nicht selbst kennen.
In der View
Beide Builder gehören in dasselbe <form>: Die Schaltflächen sind Submit-Buttons mit
name="submitButton", deren Wert im Controller über das Ziel entscheidet.
<form id="fmh-form" method="post">
@Html.Fmh(Fmh.Wizard().Add(ViewBag.Wizard as WizardSetup))
@Html.HiddenFor(x => x.Id)
...
@Html.Fmh(Fmh.WizardNavigation().Add(ViewBag.Wizard as WizardSetup))
</form>
WizardSetup
| Eigenschaft / Methode | Beschreibung |
|---|---|
Title |
Die Überschrift des Wizards. |
AdditionalTitleText |
Zusatzinformation auf Ebene des Titels. Alternativ ViewBag.WzZusatzText — praktisch, wenn ein ActionFilter den Text setzt. |
Steps |
Liste der Schritte vom Typ WizardStep. |
StepNavigationDisabled |
Sperrt die Navigation über die Schritte; es bleiben Zurück, Abbrechen, Speichern und Weiter. |
SaveButtonDisabled |
Blendet die Speichern-Schaltfläche aus. Standard: sie wird angezeigt. |
RenderBackButtonAsLink |
Zurück als Link statt als Submit — dann wird nichts gepostet. Standard: Submit. |
IdPrefix |
Präfix der Button-Ids für die clientseitige Steuerung. Standard wz. |
WizardNavigationDefaultState |
Anfangszustand der Navigations-Schaltflächen. Standard Enabled. |
CurrentStepIndex, CurrentStep, NextStep, PreviousStep |
Abgeleitet aus dem Schritt mit State = Current. |
CancelStep, HelpStep |
Ergebnis von SetCancelAction(...) bzw. SetHelpAction(...). |
ValidationAreaDisabled |
Ergebnis von DisableInputValidationFor(...). |
SetCancelAction(action, controller, routeValues) |
Ziel der Abbrechen-Schaltfläche. |
SetHelpAction(action, controller, routeValues) / SetHelpAction(link) |
Verweis auf die Hilfe. Erst damit erscheint die Hilfe-Schaltfläche im Kopf. |
SetSaveButtonText(text), SetNextButtonText(text), SetBackButtonText(text) |
Beschriftungen anpassen, etwa «Entwurf speichern». |
DisableInputValidationFor(area) |
Schaltet die Validierung je Bereich ab (siehe unten). |
WizardStep
| Eigenschaft | Beschreibung |
|---|---|
Title |
Titel des Schritts. |
Action |
Route des Ziels als MvcAction, wie beim Button. |
State |
ToDo (Standard), Filled, Current, Error. Ergibt die CSS-Klassen item-todo, item-done, item-current, item-error. |
IsDisabled |
Schritt lässt sich in der Navigation nicht direkt anspringen — etwa der Übermittlungsschritt. |
Validierung: ValidationArea
ValidationArea ist ein Flags-Enum, die Werte lassen sich mit | kombinieren: None, All,
NavigationWizard, Save, Next, Back.
Wer DisableInputValidationFor(...) nutzt, muss im Controller ModelState.IsWizardValid(wz, submitButton)
statt ModelState.IsValid verwenden — sonst greift die Abschaltung nicht.
In diesem Beispiel ist NavigationWizard | Save abgeschaltet: Die Bemerkung ist Pflicht, blockiert
aber nur «Weiter». Speichern und der Sprung auf einen anderen Schritt funktionieren auch mit leerem
Feld — genau das braucht eine Entwurfserfassung.
Clientseitig: die Navigation steuern
Über IdPrefix lassen sich die Schaltflächen aus der Geschäftslogik heraus sperren:
| Funktion | Wirkung |
|---|---|
$(document).wizardNavbarDisabled(idPrefix) |
deaktiviert die Schaltflächen der Navigationsleiste |
$(document).wizardNavbarEnabled(idPrefix) |
aktiviert sie wieder |
$(document).wizardNavbarButtonDisabled(idPrefix, NavbarButton.Next) |
deaktiviert eine einzelne Schaltfläche |
$(document).wizardNavbarButtonEnabled(idPrefix, NavbarButton.Next) |
aktiviert eine einzelne Schaltfläche |
NavbarButton kennt Next, Back und Save. Die Zurück-Schaltfläche lässt sich nur deaktivieren,
wenn sie vom Typ Button ist — also nicht bei RenderBackButtonAsLink = true.
Soll der Wizard mit gesperrter Navigation starten, WizardNavigationDefaultState = Disabled setzen
und im richtigen Moment wizardNavbarEnabled aufrufen.