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">
@Html.EditorFor(m => m.Belege)
@Html.Fmh(Fmh.Button()
.Text("Absenden")
.Size(ButtonSize.Sm)
.Attributes(new { @class = "mt-3" }))
</form>
@Html.PostScript(@<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
{
/// <summary>
/// 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.
/// </summary>
[UIHint(UiEditor.FileUpload.DokumentListe)]
[Display(Name = "Dossier-Belege")]
[AdditionalMetadata(UiMetadata.FileUpload.UploadUrl, "/FileUpload/DokumentListeDownloadUrlBeispiel/Upload")]
[AdditionalMetadata(UiMetadata.FileUpload.MaxFileSizeInMb, "1")]
[AdditionalMetadata(UiMetadata.FileUpload.MaxFiles, "5")]
[AdditionalMetadata(UiMetadata.FileUpload.DownloadActionName, "Download")]
[AdditionalMetadata(UiMetadata.FileUpload.DownloadControllerName, "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
{
/// <summary>
/// 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.
/// </summary>
[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;
/// <summary>
/// Auch die Doku-Seite zeigt die gefüllte Liste, deshalb öffentlich.
/// </summary>
/// <returns>Das Modell mit der Ausgangslage des Beispiels.</returns>
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.