JavaScript

Upload-Url zur Laufzeit ergänzen

window.<Feldname>.options.url trägt die Adresse der nächsten Übertragung – hier wird ihr der gewählte Ablageort als Abfrageparameter angehängt.

@model UploadUrlLaufzeitBeispielViewModel

@* Die Auswahl gehört bewusst nicht ins ViewModel: Sie wird nie abgesendet, sondern allein im
   Browser ausgewertet, um die Upload-Adresse der Dropzone zu ergänzen. Welcher Wert am Ende
   mitgeht, entscheidet das Skript im Tab „JavaScript". *@

<form asp-area="FileUpload" asp-controller="UploadUrlLaufzeitBeispiel" asp-action="Index" method="post">

    <div class="mb-3" style="max-width: 20rem;">
        <label for="ablage" class="form-label">Ablageort (nur im Browser)</label>
        <select id="ablage" class="form-select form-select-sm">
            <option value="">ohne Parameter</option>
            <option value="vertraege">Verträge</option>
            <option value="rechnungen">Rechnungen</option>
        </select>
    </div>

    <p class="small text-muted mb-3">
        Adresse der nächsten Übertragung: <code id="aktuelle-upload-url">wird beim Laden gesetzt</code>
    </p>

    @Html.EditorFor(m => m.DokumentIds)
    @Html.EditorFor(m => m.Dokument)

    @Html.Fmh(Fmh.Button()
        .Text("Absenden")
        .Size(ButtonSize.Sm)
        .Attributes(new { @class = "mt-3" }))
</form>

@Html.PostScript(@<script src="~/js/demos/upload-url-laufzeit.js" asp-append-version="true"></script>)
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.Infrastruktur;
using Microsoft.AspNetCore.Http;

namespace FMH.Komponente.WebUI.Areas.FileUpload.Models
{
    /// <summary>
    /// Ausgangslage für «Upload-Url zur Laufzeit ergänzen». Das Feld trägt kein
    /// <c>[UniqueId]</c>: Das Skript spricht die Dropzone über <c>window.Dokument</c> an, und
    /// dieser Name ist nur ohne Zufalls-Suffix vorhersagbar.
    /// </summary>
    public class UploadUrlLaufzeitBeispielViewModel
    {
        /// <summary>
        /// Die Adresse aus dem Attribut ist der Ausgangswert; das Skript hängt den gewählten
        /// Ablageort als Abfrageparameter daran.
        /// </summary>
        [Display(Name = "Dokumente")]
        [UIHint(UiEditor.FileUpload.IFormFile)]
        [AdditionalMetadata(UiMetadata.FileUpload.UploadUrl, "/FileUpload/UploadUrlLaufzeitBeispiel/Upload")]
        [AdditionalMetadata(UiMetadata.FileUpload.MaxFileSizeInMb, "1")]
        [AdditionalMetadata(UiMetadata.FileUpload.AllowedFiles, ".pdf, .jpg, .jpeg, .png")]
        public IFormFile Dokument { get; set; }

        /// <summary>
        /// Sammelt die Ids der hochgeladenen Dateien, Eigenschaft des Upload-Feldes + "Ids".
        /// </summary>
        [UIHint(UiEditor.Basis.HiddenList)]
        public List<int> DokumentIds { get; set; } = new List<int>();
    }
}
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.Basis.Ui.Feedback.Alert;
using FMH.Komponente.WebUI.Areas.FileUpload.Models;
using Microsoft.AspNetCore.Mvc;

namespace FMH.Komponente.WebUI.Areas.FileUpload.Controllers.Beispiele
{
    /// <summary>
    /// Serverseitig unterscheidet sich dieses Beispiel nur in einem Punkt vom Grundfall: Der
    /// Upload-Endpunkt liest den Abfrageparameter <c>ablage</c> mit, den das Skript des Partials
    /// an die Adresse der Dropzone gehängt hat, und merkt ihn sich je Datei. Die Rückmeldung nach
    /// dem Absenden macht damit sichtbar, was tatsächlich angekommen ist.
    /// </summary>
    [Area("FileUpload")]
    public class UploadUrlLaufzeitBeispielController : Controller
    {
        /// <summary>
        /// Ablage der Demo: je vergebener Id der Dateiname und der Ablageort aus der Adresse.
        /// Statisch, weil die Demo keine Sitzung kennt.
        /// </summary>
        private static readonly ConcurrentDictionary<int, string> Dokumente = new ConcurrentDictionary<int, string>();

        private static int letzteId;

        [HttpGet]
        public IActionResult Index()
        {
            return this.View(new UploadUrlLaufzeitBeispielViewModel());
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(UploadUrlLaufzeitBeispielViewModel modell)
        {
            // Die Dateien sind längst übertragen; das Formular bringt nur noch die Ids mit. Die
            // Rückmeldung nennt je Id, welcher Ablageort beim Endpunkt angekommen ist.
            this.AddAlert($"Formular übertragen – {this.BeschreibeUpload(modell.DokumentIds)}", AlertType.Success, autoHide: true);

            return this.RedirectToAction(nameof(this.Index));
        }

        /// <summary>
        /// Der Upload-Endpunkt dieses Beispiels. Die Datei kommt wie im Grundfall, der Ablageort
        /// dagegen aus der Abfragezeichenfolge: Er steht nicht am ViewModel, sondern wurde
        /// im Browser an <c>window.Dokument.options.url</c> gehängt.
        /// </summary>
        /// <returns>Json mit der vergebenen Id.</returns>
        [HttpPost]
        public IActionResult Upload()
        {
            if (this.Request.Headers.ContainsKey("IsDropzoneRequest") == false || this.Request.Form.Files.Count == 0)
            {
                return this.Json(new { id = 0 });
            }

            var datei = this.Request.Form.Files[0];
            var bezeichnung = this.Request.Form["DateiBezeichnung"].ToString();
            var name = string.IsNullOrWhiteSpace(bezeichnung) ? datei.FileName : bezeichnung;

            // Der Parameter aus der ergänzten Adresse. Fehlt er, war die Auswahl leer oder das
            // Skript ist nicht gelaufen – beides ist ein gültiger Zustand, nur eben ohne Ablageort.
            var ablage = this.Request.Query["ablage"].ToString();
            var ablageText = string.IsNullOrWhiteSpace(ablage) ? "ohne Parameter" : ablage;

            var id = Interlocked.Increment(ref letzteId);
            Dokumente[id] = $"{name} (ablage={ablageText})";

            return this.Json(new { id });
        }

        /// <summary>
        /// Nur für die Rückmeldung: macht aus den Ids wieder Dateiname und Ablageort.
        /// </summary>
        /// <param name="ids">Die Ids der hochgeladenen Dateien.</param>
        /// <returns>Der Text fuer die Rueckmeldung.</returns>
        private string BeschreibeUpload(IReadOnlyCollection<int> ids)
        {
            if (ids == null || ids.Count == 0)
            {
                return "Keine Dateien übertragen.";
            }

            var namen = ids.Select(id => Dokumente.TryGetValue(id, out var eintrag) ? eintrag : $"Id {id}");

            return $"{ids.Count} Datei(en): {string.Join(", ", namen)}";
        }
    }
}
// Beispiel «Upload-Url zur Laufzeit ergänzen» (FileUpload).
//
// Jede Dropzone liegt unter dem Namen ihrer Eigenschaft global im Browser: window.Dokument gehört
// zur Eigenschaft Dokument. Ihre Adresse steht in options.url und lässt sich vor der nächsten
// Übertragung ändern – so kommen Parameter mit, die erst im Browser bekannt sind.
//
// Die Dropzone entsteht erst, wenn das Skript des EditorTemplates gelaufen ist. Deshalb wird sie
// hier bei jedem Zugriff frisch geholt und nicht beim Laden dieser Datei in einer Variablen
// festgehalten.
var ablageBasisUrl = null;

function ergaenzeUploadUrl(wert) {
    var dropzone = window.Dokument;

    if (!dropzone) {
        return;
    }

    // Die unveränderte Adresse einmal merken. Ohne diesen Ausgangswert würde jede Auswahl an die
    // bereits ergänzte Adresse anhängen und die Parameter würden sich aufstapeln.
    if (ablageBasisUrl === null) {
        ablageBasisUrl = dropzone.options.url;
    }

    dropzone.options.url = wert
        ? ablageBasisUrl + '?ablage=' + encodeURIComponent(wert)
        : ablageBasisUrl;

    var anzeige = document.getElementById('aktuelle-upload-url');

    if (anzeige !== null) {
        anzeige.textContent = dropzone.options.url;
    }
}

// Delegation an das document: Der Zeitpunkt des Renderns spielt damit keine Rolle.
document.addEventListener('change', function (ereignis) {
    if (ereignis.target.id !== 'ablage') {
        return;
    }

    ergaenzeUploadUrl(ereignis.target.value);
});

// Einmal beim Laden, damit die angezeigte Adresse von Anfang an stimmt.
document.addEventListener('DOMContentLoaded', function () {
    var auswahl = document.getElementById('ablage');

    if (auswahl !== null) {
        ergaenzeUploadUrl(auswahl.value);
    }
});
// Dieselbe Umsetzung mit jQuery. Diese Datei wird NICHT geladen, sie steht nur im Code-Tab
// «jQuery».
//
// Unterschiede zur JavaScript-Fassung: die delegierte Bindung über $(document).on(...) und
// $(function () { ... }) statt DOMContentLoaded. Der Zugriff auf die Dropzone bleibt gleich, sie
// ist ein globales Objekt und kein jQuery-Element.
var ablageBasisUrl = null;

function ergaenzeUploadUrl(wert) {
    var dropzone = window.Dokument;

    if (!dropzone) {
        return;
    }

    // Die unveränderte Adresse einmal merken, sonst stapeln sich die Parameter mit jeder Auswahl.
    if (ablageBasisUrl === null) {
        ablageBasisUrl = dropzone.options.url;
    }

    dropzone.options.url = wert
        ? ablageBasisUrl + '?ablage=' + encodeURIComponent(wert)
        : ablageBasisUrl;

    $('#aktuelle-upload-url').text(dropzone.options.url);
}

$(document).on('change', '#ablage', function () {
    ergaenzeUploadUrl($(this).val());
});

// $(function () { ... }) läuft nach den ready-Handlern, die vorher registriert wurden – also nach
// dem Skript des EditorTemplates, das die Dropzone anlegt.
$(function () {
    ergaenzeUploadUrl($('#ablage').val());
});

Darstellung

Adresse der nächsten Übertragung: wird beim Laden gesetzt

attach_file

Drag files here or click here to upload.

report_problem

You can not upload any more files.

Jede Dropzone liegt unter dem Namen ihrer Eigenschaft global im Browser, window.Dokument gehört zur Eigenschaft Dokument. Ihre Adresse steht in options.url und gilt für die nächste Übertragung; sie lässt sich also jederzeit ergänzen, etwa um einen Wert, der erst im Browser feststeht:

var url = window.Dokument.options.url;
window.Dokument.options.url = url + "?ablage=vertraege";

Im Beispiel wählt eine Auswahlliste den Ablageort, das Skript hängt ihn als Abfrageparameter an, und der Upload-Endpunkt liest ihn aus der Abfragezeichenfolge:

var ablage = this.Request.Query["ablage"].ToString();

Die Ausgangsadresse einmal merken

Der häufigste Fehler ist, die bereits ergänzte Adresse erneut zu ergänzen: Nach dem dritten Wechsel der Auswahl steht dann …/Upload?ablage=a?ablage=b?ablage=c in options.url. Deshalb hält das Beispiel den unveränderten Wert beim ersten Zugriff fest und baut jede weitere Adresse aus diesem Ausgangswert.

Wann der Zugriff möglich ist

Die Dropzone entsteht im $(document).ready des EditorTemplates. Ein Skript, das schon beim Laden auf window.Dokument zugreift, findet dort undefined. Zwei Wege führen daran vorbei, das Beispiel geht beide:

  • im Ereignis-Handler zugreifen, der ohnehin erst nach einer Benutzeraktion läuft,
  • für den Startwert ein eigenes DOMContentLoaded bzw. $(function () { … }) verwenden, das nach dem ready-Handler des Templates registriert wird.

Was zu beachten ist

Punkt Wirkung
[UniqueId] am Feld Der globale Name bekommt ein Zufalls-Suffix und ist nicht mehr vorhersagbar; dieser Weg fällt dann aus.
Mehrere Upload-Felder Jedes Feld hat seine eigene Dropzone und damit seine eigene Adresse (window.Dokument, window.Zeugnis), ein Aufruf ändert nie beide.
Laufende Übertragung Geändert wird die Adresse der nächsten Datei; bereits laufende Übertragungen behalten ihre.
Prüfung am Server Der Parameter kommt aus dem Browser und ist damit frei wählbar. Der Endpunkt muss ihn prüfen wie jede andere Eingabe.

Die beiden Helfer der Basis

In jquery-fmh-sitesetup.js der Komponente Basis liegen zwei globale Funktionen für denselben Zweck:

function getDokumentUploadUrl() { return window.Dokument.options.url; }
function setDokumentUploadUrl(url) { window.Dokument.options.url = url; }

Sie sparen nichts und verdecken eine Einschränkung: Der Feldname Dokument steht fest im Code. Heisst die Eigenschaft anders oder gibt es ein zweites Upload-Feld, greifen sie am falschen Objekt oder laufen in einen Fehler. Der direkte Zugriff über window.<Feldname> wie oben ist deshalb der zuverlässigere Weg.