Wizard

Viele Schritte

Derselbe Aufbau mit zwölf Schritten – an der Konfiguration ändert sich nichts. Zu sehen ist, wie sich die Schritt-Navigation bei vielen Einträgen verhält.

@model WizardBeispielViewModel

@* Derselbe Aufbau mit zwölf Schritten. Interessant ist hier allein die Schritt-Navigation: Sie
   bricht um und bleibt bedienbar, ohne dass an der Konfiguration etwas anderes gesetzt wäre. *@
<form method="post" asp-area="Wizard" asp-controller="WizardBeispiel" asp-action="Schritt">
    @Html.Fmh(Fmh.Wizard()
        .Id("wz-viele")
        .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

Antrag einreichen

Step 1 of 12: Bemerkungen

Freitext zum Schritt. Für «Weiter» erforderlich, für «Entwurf speichern» nicht.0/1000
Loading... done highlight_off Cancel

Zwölf statt fünf Schritte — an der Konfiguration ändert sich nichts, die Schritte kommen aus derselben Liste. Sichtbar wird nur, wie sich die Schritt-Navigation bei vielen Einträgen verhält.

Zwei Dinge lohnen bei langen Wizards die Überlegung:

  • StepNavigationDisabled, wenn die Reihenfolge fachlich zwingend ist. Bei vielen Schritten ist der Direktsprung sonst der schnellste Weg in einen inkonsistenten Zustand.
  • IsDisabled je Schritt als feinere Variante: Die Navigation bleibt, einzelne Ziele sind aber gesperrt — hier der letzte Schritt «Antrag übermitteln».

Die Beschriftungen der Schaltflächen bleiben unverändert; bei vielen Schritten hilft ein sprechenderer Text über SetNextButtonText(...) eher als eine zusätzliche Erklärung auf der Seite.