DokumentListe
Download-Url zur Laufzeit
Routen, die sich nicht über die Data Annotationen abbilden lassen, werden als Vorlage mit dem Platzhalter {dokumentId} gesetzt.
@model DokumentListeDownloadUrlBeispielViewModel
@* Das Skript überschreibt die Download-Adresse zur Laufzeit. Es steht am Ende des Partials und
wartet zusätzlich auf "load": window.Belege entsteht erst im ready-Block des Templates, der
über die BodyIncludes des Layouts nach diesem Partial gerendert wird. *@
<form asp-area="FileUpload" asp-controller="DokumentListeDownloadUrlBeispiel" asp-action="Index" method="post">
<div asp-validation-summary="All" class="text-danger"></div>
@Html.EditorFor(m => m.Belege)
@Html.Fmh(Fmh.Button()
.Text("Absenden")
.Size(ButtonSize.Sm)
.Attributes(new { @class = "mt-3" }))
</form>
<script src="~/js/demos/dokumentliste-downloadurl.js" asp-append-version="true"></script>
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.FileUpload.Models;
using FMH.Komponente.Infrastruktur;
namespace FMH.Komponente.WebUI.Areas.FileUpload.Models
{
public class DokumentListeDownloadUrlBeispielViewModel
{
// Die hier eingetragene Download-Adresse ist nur der Ausgangspunkt: Das Skript des
// Beispiels überschreibt sie zur Laufzeit über window.Belege.options.dokumentDownloadUrl.
// Der Eigenschaftsname "Belege" ist zugleich der Name der globalen Dropzone.
[UIHint("DokumentListe")]
[Display(Name = "Dossier-Belege")]
[AdditionalMetadata(MvcConst.MetaDataUploadUrl, "/FileUpload/DokumentListeDownloadUrlBeispiel/Upload")]
[AdditionalMetadata(MvcConst.MaxFileSizeInMb, "1")]
[AdditionalMetadata(MvcConst.MaxFiles, "5")]
[AdditionalMetadata(MvcConst.MetaDataDownloadActionName, "Download")]
[AdditionalMetadata(MvcConst.MetaDataDownloadControllerName, "DokumentListeDownloadUrlBeispiel")]
public DokumentListeViewModel Belege { get; set; } = new DokumentListeViewModel();
}
}
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using FMH.Komponente.Basis.Erweiterungen;
using FMH.Komponente.Basis.Ui.Feedback.Alert;
using FMH.Komponente.FileUpload.Models;
using FMH.Komponente.WebUI.Areas.FileUpload.Models;
using Microsoft.AspNetCore.Mvc;
namespace FMH.Komponente.WebUI.Areas.FileUpload.Controllers.Beispiele
{
// Zeigt eine Download-Route, die sich nicht über MetaDataDownloadActionName /
// MetaDataDownloadControllerName abbilden lässt, weil sie einen zusätzlichen Wegabschnitt
// (die Dossier-Nummer) enthält. Das Skript des Partials setzt sie deshalb zur Laufzeit.
[Area("FileUpload")]
public class DokumentListeDownloadUrlBeispielController : Controller
{
private static readonly ConcurrentDictionary<int, string> Dokumente =
new ConcurrentDictionary<int, string>(
new Dictionary<int, string> { [5] = "Kurs.pdf", [6] = "Kurs2.pdf" });
private static int letzteId = 100;
// Auch die Doku-Seite zeigt die gefüllte Liste — deshalb öffentlich.
public static DokumentListeDownloadUrlBeispielViewModel ErstelleModell()
{
var modell = new DokumentListeDownloadUrlBeispielViewModel();
modell.Belege.Dokumente = Dokumente
.OrderBy(eintrag => eintrag.Key)
.Select(eintrag => new DokumentEintrag
{
DokumentEintragId = eintrag.Key,
Name = eintrag.Value,
Groesse = 30000 + (eintrag.Key * 100),
})
.ToList();
return modell;
}
[HttpGet]
public IActionResult Index()
{
return this.View(ErstelleModell());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(DokumentListeDownloadUrlBeispielViewModel modell)
{
var anzahlNeu = modell.Belege.DokumentIds?.Length ?? 0;
this.AddAlert(
$"Formular übertragen – {anzahlNeu} neue(s) Dokument(e).",
AlertType.Success,
autoHide: true);
return this.RedirectToAction(nameof(this.Index));
}
/// <summary>
/// Der Upload-Endpunkt dieses Beispiels.
/// </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;
var id = Interlocked.Increment(ref letzteId);
Dokumente[id] = name;
return this.Json(new { id });
}
/// <summary>
/// Der Download-Endpunkt, den die Data Annotationen erzeugen. Er dient hier nur als
/// Ausgangswert — geklickt wird die Adresse aus <c>dokumentDownloadUrl</c>.
/// </summary>
/// <param name="id">Die Id des Dokuments aus der Tabellenzeile.</param>
/// <returns>Die Datei zum Herunterladen.</returns>
[HttpGet]
public IActionResult Download(int id)
{
return this.LiefereDatei(id, dossierId: 0);
}
/// <summary>
/// Die eigentliche Ziel-Route dieses Beispiels: Sie trägt die Dossier-Nummer im Weg und
/// lässt sich deshalb nicht über die Data Annotationen erzeugen. Das Skript setzt sie über
/// <c>window.Belege.options.dokumentDownloadUrl</c> mit dem Platzhalter
/// <c>{dokumentId}</c>.
/// </summary>
/// <param name="dossierId">Die Nummer des Dossiers.</param>
/// <param name="id">Die Id des Dokuments.</param>
/// <returns>Die Datei zum Herunterladen.</returns>
[HttpGet]
[Route("FileUpload/DokumentListeDownloadUrlBeispiel/Dossier/{dossierId:int}/Download/{id:int}")]
public IActionResult DossierDownload(int dossierId, int id)
{
return this.LiefereDatei(id, dossierId);
}
private IActionResult LiefereDatei(int id, int dossierId)
{
if (Dokumente.TryGetValue(id, out var name) == false)
{
return this.NotFound();
}
var herkunft = dossierId == 0 ? "ohne Dossier" : $"Dossier {dossierId}";
var inhalt = Encoding.UTF8.GetBytes($"Platzhalter-Inhalt für «{name}» (Id {id}, {herkunft}).");
return this.File(inhalt, "text/plain", name);
}
}
}
// Beispiel «Download-Url zur Laufzeit» (Area FileUpload, EditorTemplate DokumentListe).
// Lässt sich eine Download-Route nicht über MetaDataDownloadActionName/-ControllerName abbilden
// (hier: eine Dossier-Nummer mitten im Weg), wird sie als Vorlage an der Dropzone hinterlegt. Beim
// Klick auf einen Download-Link ersetzt das Template darin den Platzhalter {dokumentId}.
(function () {
"use strict";
// In einem Projekt käme dieser Wert aus der Seite; hier steht er stellvertretend fest.
var dossierId = 25;
// window.Belege ist die Dropzone der Eigenschaft "Belege" und entsteht erst im ready-Block des
// Templates. "load" läuft garantiert danach.
window.addEventListener("load", function () {
if (typeof window.Belege === "undefined") {
return;
}
window.Belege.options.dokumentDownloadUrl =
"/FileUpload/DokumentListeDownloadUrlBeispiel/Dossier/" + dossierId + "/Download/{dokumentId}";
});
})();
// Dieselbe Umsetzung mit jQuery. Diese Datei wird bewusst nicht geladen — sie dient nur dem
// Code-Tab «jQuery», sonst liefen beide Varianten gleichzeitig.
$(window).on("load", function () {
var dossierId = 25;
if (typeof window.Belege === "undefined") {
return;
}
// $(document).ready reicht hier nicht: Die Dropzone wird selbst in einem ready-Block angelegt,
// und der des Templates läuft nach diesem hier. "load" ist der sichere Zeitpunkt.
window.Belege.options.dokumentDownloadUrl =
"/FileUpload/DokumentListeDownloadUrlBeispiel/Dossier/" + dossierId + "/Download/{dokumentId}";
});
Darstellung
Lässt sich die Download-Route nicht über MetaDataDownloadActionName und
MetaDataDownloadControllerName abbilden — etwa weil sie einen zusätzlichen Wegabschnitt enthält —
wird sie als Vorlage an der Dropzone hinterlegt. Beim Klick auf einen Dateinamen ersetzt das
Template darin den Platzhalter {dokumentId} durch die Id der jeweiligen Zeile.
window.addEventListener("load", function () {
var dossierId = 25;
window.Belege.options.dokumentDownloadUrl =
"/FileUpload/DokumentListeDownloadUrlBeispiel/Dossier/" + dossierId + "/Download/{dokumentId}";
});
Wie die Dropzone heisst
window.Belege ist die Dropzone der Eigenschaft Belege. Der Name entspricht der Html-Id, also dem
Eigenschaftsnamen. Trägt die Eigenschaft [UniqueId], enthält der Name zusätzlich ein
Zufalls-Suffix — dann ist dieser Weg nicht mehr vorhersagbar.
Was zu beachten ist
- Der Zeitpunkt ist entscheidend:
window.<Feldname>entsteht erst imready-Block des Templates, der über die BodyIncludes des Layouts nach dem Beispiel-Partial gerendert wird. Ein$(document).readyim Partial läuft davor und fände die Dropzone noch nicht.loadist der sichere Zeitpunkt. - Der Platzhalter heisst exakt
{dokumentId}. Fehlt er in der Vorlage, zeigen alle Zeilen auf dieselbe Adresse. - Ist
dokumentDownloadUrlnicht gesetzt, bleibt der aus den Data Annotationen gebildete Link unverändert bestehen — die Vorlage ist reine Ergänzung. - Die Ersetzung passiert beim Klick, nicht beim Rendern. Die Vorlage darf also erst nach dem Seitenaufbau feststehen; das ist gerade ihr Zweck.