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

Description Size Actions
Kurs.pdf 29.79 KB
Kurs2.pdf 29.88 KB
attach_file

Drag files here or click here to upload.

report_problem

You can not upload any more files.

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 im ready-Block des Templates, der über die BodyIncludes des Layouts nach dem Beispiel-Partial gerendert wird. Ein $(document).ready im Partial läuft davor und fände die Dropzone noch nicht. load ist der sichere Zeitpunkt.
  • Der Platzhalter heisst exakt {dokumentId}. Fehlt er in der Vorlage, zeigen alle Zeilen auf dieselbe Adresse.
  • Ist dokumentDownloadUrl nicht 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.