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
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
DOMContentLoadedbzw.$(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.