Wizard

Zustände und gesperrte Navigation

Die vier Werte von WizardStepState nebeneinander – erledigt, fehlerhaft, aktuell, offen. Dazu die beiden Sperren: StepNavigationDisabled schaltet den Direktsprung dauerhaft ab, WizardNavigationDefaultState lässt die Schaltflächen gesperrt starten. Das Kontrollkästchen gibt sie über die jQuery-Funktion wieder frei.

@model WizardBeispielViewModel
@{
    var wizard = ViewBag.Wizard as WizardSetup;
}

@* Zwei Dinge auf einmal: die vier Zustände eines Schritts (WizardStepState) und die gesperrte
   Navigation. Die Schritt-Navigation ist über StepNavigationDisabled dauerhaft gesperrt, die
   Schaltflächen unten starten über WizardNavigationDefaultState gesperrt und lassen sich über die
   jQuery-Funktionen wieder freigeben. *@
<form method="post" asp-area="Wizard" asp-controller="WizardBeispiel" asp-action="Schritt">
    @Html.Fmh(Fmh.Wizard()
        .Id("wz-zustaende")
        .Add(wizard))

    <div class="my-4" style="max-width: 40rem;">
        @Html.HiddenFor(m => m.Nr)
        @Html.HiddenFor(m => m.Anzahl)

        @* Der IdPrefix am Behälter sagt dem Skript, welche Navigationsleiste gemeint ist. *@
        <div data-wizard-navigation-schalter="@wizard.IdPrefix" class="mb-3">
            @Html.EditorFor(m => m.NavigationFreigeben)
        </div>

        @Html.EditorFor(m => m.Bemerkung)
    </div>

    @Html.Fmh(Fmh.WizardNavigation()
        .Add(wizard))
</form>

<script src="~/js/demos/wizard-navigation-steuern.js" asp-append-version="true"></script>
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;
        }
    }
}
// Navigationsleiste des Wizards zur Laufzeit sperren und freigeben.
//
// Die Funktionen sind jQuery-Plugins der Basis (jquery-fmh-sitesetup.js) — auch diese Fassung ruft
// sie deshalb über $ auf. Unterschiedlich ist nur, wie das Ereignis angebunden wird.
//
// Angesprochen werden die Schaltflächen über den IdPrefix aus dem WizardSetup; ihre Ids lauten
// btn-<IdPrefix>-Back, -Save und -Next. Den Prefix trägt der Behälter um das Kontrollkästchen,
// weil das Kontrollkästchen selbst aus dem Checkbox-EditorTemplate kommt.

// Delegation an document: Damit ist es gleichgültig, wann das Kontrollkästchen in die Seite kommt.
document.addEventListener("change", (event) => {
    if (event.target.type !== "checkbox") {
        return;
    }

    const behaelter = event.target.closest("[data-wizard-navigation-schalter]");
    if (!behaelter) {
        return;
    }

    const idPrefix = behaelter.dataset.wizardNavigationSchalter;

    if (event.target.checked) {
        $(document).wizardNavbarEnabled(idPrefix);
    } else {
        $(document).wizardNavbarDisabled(idPrefix);
    }
});

// Einzelne Schaltflächen gehen auch:
// $(document).wizardNavbarButtonEnabled(idPrefix, NavbarButton.Next);
// $(document).wizardNavbarButtonDisabled(idPrefix, NavbarButton.Save);
// jQuery-Fassung von wizard-navigation-steuern.js.
//
// Diese Datei wird NICHT geladen – sie zeigt nur dieselbe Umsetzung mit jQuery. Würde sie
// zusätzlich eingebunden, liefe die Umschaltung zweimal.

// $(document).on(...) mit Selektor ist die jQuery-Entsprechung der Delegation an document. Der
// Selektor greift direkt das Kontrollkästchen im Behälter — das spart die Typprüfung.
$(document).on("change", "[data-wizard-navigation-schalter] input[type=checkbox]", function () {
    // Achtung: .data() wandelt data-wizard-navigation-schalter in camelCase
    // (wizardNavigationSchalter) – wie dataset im JavaScript-Pendant.
    var idPrefix = $(this).closest("[data-wizard-navigation-schalter]").data("wizardNavigationSchalter");

    // this.checked statt $(this).is(":checked") – beides geht, ersteres ist kürzer und schneller.
    if (this.checked) {
        $(document).wizardNavbarEnabled(idPrefix);
    } else {
        $(document).wizardNavbarDisabled(idPrefix);
    }
});

Darstellung

Antrag prüfen

Step 3 of 5: Angaben Teil 2

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

Zwei Themen, die in der Praxis zusammen auftreten: der Zustand je Schritt und das Sperren der Navigation.

WizardStepState

Jeder Schritt trägt einen fachlichen Zustand. Er entscheidet nur über die CSS-Klasse — die Darstellung kommt aus dem Stylesheet der Basis:

Wert CSS-Klasse Gedacht für
Filled item-done Schritt ist bearbeitet
Current item-current der laufende Schritt; steuert zugleich CurrentStep, NextStep und PreviousStep
ToDo (Standard) item-todo noch offen
Error item-error angefangen, aber unvollständig oder fehlerhaft

Error ist der Wert, der am häufigsten vergessen wird. Er ist gedacht für den Fall, dass ein Schritt zwar verlassen wurde, die Angaben aber nicht vollständig sind — typisch bei einer Entwurfserfassung, in der «Speichern» ohne Validierung erlaubt ist. Ohne diesen Zustand sieht ein lückenhafter Schritt aus wie ein erledigter.

Der Zustand wird beim Aufbau der Konfiguration gesetzt und kommt üblicherweise aus der Fachlogik:

new WizardStep
{
    Title = "Antrag ausfüllen",
    Action = new MvcAction("Schritt02", "Wizard", new { id }),
    State = antrag.IstVollstaendig ? WizardStepState.Filled : WizardStepState.Error,
}

Current genau einmal vergeben. Aus ihm leitet WizardSetup den aktuellen Schritt und damit Vor und Zurück ab; ohne ihn bleiben NextStep und PreviousStep leer und die Navigation läuft ins Leere.

Zwei Arten, die Navigation zu sperren

Einstellung Wirkt auf Dauerhaft?
StepNavigationDisabled = true die Schritt-Schaltflächen im Kopf ja, für die ganze Seite
WizardNavigationDefaultState = Disabled Zurück, Speichern, Weiter nein, nur der Startzustand

StepNavigationDisabled macht aus den Schritt-Schaltflächen gewöhnliche Buttons und hängt ihnen die Klasse wz-step-disabled an — sie posten nichts mehr. Dasselbe je Schritt erreicht IsDisabled.

WizardNavigationDefaultState setzt nur den Anfangszustand. Freigegeben wird clientseitig, über den IdPrefix der Konfiguration:

$(document).wizardNavbarEnabled('wzZustand');
$(document).wizardNavbarDisabled('wzZustand');
$(document).wizardNavbarButtonEnabled('wzZustand', NavbarButton.Next);

Das ist der Sinn der Einstellung: Der Anwender soll erst weiterkommen, wenn die Geschäftslogik im Browser das erlaubt — etwa nach einer Prüfung oder einer bestätigten Auswahl.

Die Zurück-Schaltfläche lässt sich nur sperren, wenn sie ein Button ist. Mit RenderBackButtonAsLink = true wird sie zum Link und ignoriert die jQuery-Funktionen. In diesem Beispiel bleibt sie deshalb bewusst ein Button.

Wer die Leiste gesperrt starten lässt, sollte sicherstellen, dass es einen Weg zur Freigabe gibt — sonst ist der Wizard nur noch über Abbrechen verlassbar.