Mehrfaches Absenden verhindern

Dauert die Antwort auf ein Formular länger, wird gern ein zweites Mal geklickt, bei einer Neuerfassung entstehen so doppelte Datensätze. Die Basis fängt das ab: Beim ersten gültigen Absenden merkt sie das Formular vor, jedes weitere Absenden bricht sie ab.

Die Prüfung ist im Standard aktiv und braucht keine Konfiguration. Abschalten lässt sie sich je Formular über disableSubmittingFormCheck.

Zweiter Klick während der Wartezeit

Zwei gleich aufgebaute Formulare: Links greift die Prüfung des Standards, rechts ist sie über disableSubmittingFormCheck abgeschaltet. Der POST wartet zwei Sekunden – lange genug, um den zweiten Klick auszuprobieren.

@model AbsendenCheckBeispielViewModel

@* Zwei gleich aufgebaute Formulare, ein Unterschied: Beim zweiten schaltet
   disableSubmittingFormCheck die Prüfung ab (siehe Tab „JavaScript").

   Die Prüfung ist im Standard aktiv und braucht keine Konfiguration. Der POST wartet zwei
   Sekunden, in dieser Zeit lässt sich der zweite Klick ausprobieren. Beim linken Formular
   blockiert er und meldet sich in der Browser-Konsole, beim rechten geht er durch. *@
<div class="row g-4">
    <div class="col-md-6">
        <h5 class="h6">Mit Prüfung (Standard)</h5>
        <form id="frmMitPruefung" asp-area="Basis" asp-controller="AbsendenCheck" asp-action="Index" method="post">
            <input type="hidden" name="OhnePruefung" value="false" />
            @Html.EditorFor(m => m.Bezeichnung)
            @Html.Fmh(Fmh.Button()
                .Text("Anlegen")
                .Size(ButtonSize.Sm)
                .Attributes(new { @class = "mt-2" }))
        </form>
    </div>

    <div class="col-md-6">
        <h5 class="h6">Ohne Prüfung</h5>
        <form id="frmOhnePruefung" asp-area="Basis" asp-controller="AbsendenCheck" asp-action="Index" method="post">
            <input type="hidden" name="OhnePruefung" value="true" />
            @Html.EditorFor(m => m.Bezeichnung)
            @Html.Fmh(Fmh.Button()
                .Text("Anlegen")
                .Size(ButtonSize.Sm)
                .Attributes(new { @class = "mt-2" }))
        </form>
    </div>
</div>

<p class="small text-muted mt-3 mb-0">Der zweite Klick innerhalb der Wartezeit wird links
    blockiert – die Browser-Konsole meldet «Erneute Formularübermittlung blockiert.».</p>

@Html.PostScript(@<script src="~/js/demos/absenden-check.js" asp-append-version="true"></script>)
using System.ComponentModel.DataAnnotations;

namespace FMH.Komponente.WebUI.Areas.Basis.Models
{
    /// <summary>
    /// Ein Formular für das Beispiel zur Mehrfach-Absende-Prüfung.
    /// </summary>
    /// <remarks>
    /// Der Inhalt ist nebensächlich – gezeigt wird, was beim zweiten Klick auf «Absenden» passiert.
    /// </remarks>
    public class AbsendenCheckBeispielViewModel
    {
        /// <summary>
        /// Holt oder setzt die Bezeichnung des angelegten Datensatzes.
        /// </summary>
        [Display(Name = "Bezeichnung")]
        [Required(ErrorMessage = "Bitte eine Bezeichnung erfassen.")]
        public string Bezeichnung { get; set; }

        /// <summary>
        /// Holt oder setzt die Angabe, ob die Prüfung für dieses Formular abgeschaltet ist.
        /// </summary>
        /// <remarks>
        /// Reines Transportfeld der Demo: Es entscheidet, welche der beiden Formularvarianten
        /// abgesendet wurde, damit die Rückmeldung das benennen kann.
        /// </remarks>
        public bool OhnePruefung { get; set; }
    }
}
using System.Threading;
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>
    /// Die Gegenstelle des Beispiels zur Mehrfach-Absende-Prüfung.
    /// </summary>
    /// <remarks>
    /// Die POST-Action wartet bewusst zwei Sekunden. Ohne spürbare Wartezeit gäbe es keinen Grund,
    /// ein zweites Mal zu klicken, und genau das ist der Fall, den die Prüfung abfängt.
    /// </remarks>
    [Area("Basis")]
    public class AbsendenCheckController : Controller
    {
        [HttpGet]
        public IActionResult Index()
        {
            return this.View(new AbsendenCheckBeispielViewModel());
        }

        /// <summary>
        /// Nimmt das Formular entgegen – nach einer künstlichen Verzögerung.
        /// </summary>
        /// <param name="modell">Das abgesendete Formular.</param>
        /// <returns>Die Umleitung nach erfolgreichem POST, sonst dieselbe Seite mit den Fehlern.</returns>
        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(AbsendenCheckBeispielViewModel modell)
        {
            if (this.ModelState.IsValid == false)
            {
                return this.View(modell);
            }

            // Nur für die Demo: die Wartezeit, die im Alltag zum zweiten Klick verleitet.
            Thread.Sleep(2000);

            this.AddAlert(
                modell.OhnePruefung
                    ? $"«{modell.Bezeichnung}» angelegt – dieses Formular hat die Prüfung abgeschaltet."
                    : $"«{modell.Bezeichnung}» angelegt.",
                AlertType.Success,
                autoHide: true);

            return this.RedirectToAction(nameof(this.Index));
        }
    }
}
// Beispiel «Mehrfaches Absenden verhindern» (Basis).
//
// Die Prüfung ist im Standard aktiv und braucht keinen Aufruf: Beim ersten gültigen Absenden
// bekommt das Formular die Klasse fmh-is-submitting, ein zweites Absenden mit dieser Klasse wird
// abgebrochen.
//
// disableSubmittingFormCheck schaltet sie für ein einzelnes Formular ab. Anders als die
// enable/disable-Funktionen für Felder erwartet sie einen **echten jQuery-Selektor**, also mit
// führendem '#'.
disableSubmittingFormCheck('#frmOhnePruefung');
// Dieselbe Umsetzung mit jQuery. Diese Datei wird NICHT geladen, sie steht nur im Code-Tab
// «jQuery».
//
// Hier gibt es keinen Unterschied: disableSubmittingFormCheck ist selbst eine jQuery-Funktion aus
// jquery-fmh-validation.js. Der Aufruf ist in beiden Fassungen derselbe.
disableSubmittingFormCheck('#frmOhnePruefung');

Darstellung

Mit Prüfung (Standard)
Ohne Prüfung

Der zweite Klick innerhalb der Wartezeit wird links blockiert – die Browser-Konsole meldet «Erneute Formularübermittlung blockiert.».

Dauert die Antwort länger, wird gern ein zweites Mal geklickt. Bei einer Neuerfassung entstehen so doppelte Datensätze. Die Basis fängt das ab, ohne Zutun, die Prüfung ist im Standard aktiv.

Wie sie arbeitet

Beim Absenden eines Formulars, das die clientseitige Validierung besteht:

  1. Trägt das Formular bereits die Klasse fmh-is-submitting, wird das Absenden abgebrochen und in der Browser-Konsole gemeldet: «Erneute Formularübermittlung blockiert.»
  2. Andernfalls wird die Klasse gesetzt, allerdings nur, wenn gerade kein Ajax-Aufruf läuft ($.active === 0) und die Prüfung für dieses Formular nicht abgeschaltet ist.

Die Markierung verschwindet, wenn die Validierung neu aufgebaut wird, beim gewöhnlichen Seitenwechsel also von selbst.

Abschalten

disableSubmittingFormCheck('#meinFormular');

Das setzt die Klasse fmh-is-submitting-check-disabled an das Formular; ab dann wird es nie vorgemerkt.

Wann das nötig ist

Wird die Seite nach dem Absenden nicht neu aufgebaut, etwa weil die Antwort über Ajax kommt und nur einen Teil der Seite ersetzt ,, bleibt die Markierung stehen. Jedes weitere Absenden desselben Formulars läuft dann ins Leere, obwohl der Ablauf korrekt ist. Für solche Formulare ist die Prüfung abzuschalten.

Was zu beachten ist

  • Der Parameter ist ein Selektor, kein Eigenschaftsname, '#meinFormular' mit Raute. Anders als bei den Feld-Funktionen.
  • Der Aufruf gilt je Formular, nicht global. Ein disableSubmittingFormCheck('form') schaltet die Prüfung für die ganze Seite ab, möglich, aber selten gewollt.
  • Die Prüfung greift erst nach bestandener Validierung. Ein Formular mit Fehlern wird gar nicht vorgemerkt, der zweite Klick funktioniert dort wie erwartet.
  • Die Logik steckt in jquery-fmh-validation.js; ohne das Skript-Bundle der Basis gibt es keine Prüfung.