IFormFile

Zwei Upload-Bereiche in einem Formular

Zwei Felder, zwei Dropzones, zwei Endpunkte – die Zuordnung der Ids läuft über den Eigenschaftsnamen.

@model IFormFileZweiBereicheBeispielViewModel

@* Zwei Upload-Felder in einem Formular: eigene Dropzone, eigener Endpunkt, eigene Id-Liste je
   Eigenschaft. Die Zuordnung läuft über den Eigenschaftsnamen. *@

<form asp-area="FileUpload" asp-controller="IFormFileZweiBereicheBeispiel" asp-action="Index" method="post">
    <div asp-validation-summary="All" class="text-danger"></div>

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

    <div class="mt-4">
        @Html.EditorFor(m => m.ZeugnisIds)
        @Html.EditorFor(m => m.Zeugnis)
    </div>

    @Html.Fmh(Fmh.Button()
        .Text("Absenden")
        .Size(ButtonSize.Sm)
        .Attributes(new { @class = "mt-3" }))
</form>
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
{
    public class IFormFileZweiBereicheBeispielViewModel
    {
        [Display(Name = "Dokumente")]
        [UIHint("IFormFile")]
        [AdditionalMetadata(MvcConst.MetaDataUploadUrl, "/FileUpload/IFormFileZweiBereicheBeispiel/Upload")]
        public IFormFile Dokument { get; set; }

        [UIHint("HiddenList")]
        public List<int> DokumentIds { get; set; } = new List<int>();

        // Zweiter Bereich mit eigenem Endpunkt: Jede Eigenschaft bringt ihre eigene Dropzone,
        // ihre eigene Upload-Url und ihre eigene Id-Liste mit.
        [Display(Name = "Zeugnisse")]
        [UIHint("IFormFile")]
        [AdditionalMetadata(MvcConst.MetaDataUploadUrl, "/FileUpload/IFormFileZweiBereicheBeispiel/UploadZeugnis")]
        public IFormFile Zeugnis { get; set; }

        [UIHint("HiddenList")]
        public List<int> ZeugnisIds { 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
{
    // Ein Beispiel, ein Controller: Upload-Endpunkte, Ablage und Formular-Round-Trip stehen
    // vollständig in dieser Datei. Lerninhalt hier: zwei Upload-Felder in einem Formular, jedes mit
    // eigener Dropzone, eigener Upload-Adresse und eigener Id-Liste.
    [Area("FileUpload")]
    public class IFormFileZweiBereicheBeispielController : Controller
    {
        // Ablage der Demo: In einem Projekt landen die Dateien in der Datenbank oder im
        // Dokumentenspeicher; hier genügt der Dateiname je vergebener Id, damit sich der
        // Round-Trip nachvollziehen lässt. Statisch, weil die Demo keine Sitzung kennt.
        private static readonly ConcurrentDictionary<int, string> Dokumente = new ConcurrentDictionary<int, string>();

        private static int letzteId;

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

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(IFormFileZweiBereicheBeispielViewModel modell)
        {
            // Beide Bereiche bringen ihre eigene Id-Liste mit; zugeordnet werden sie über den
            // Eigenschaftsnamen (Dokument -> DokumentIds, Zeugnis -> ZeugnisIds).
            var meldung = $"Dokumente: {this.BeschreibeUpload(modell.DokumentIds)} — "
                          + $"Zeugnisse: {this.BeschreibeUpload(modell.ZeugnisIds)}";

            // PRG-Pattern (Post-Redirect-Get): Meldung merken und per Redirect auf die GET-Action.
            this.AddAlert($"Formular übertragen – {meldung}", AlertType.Success, autoHide: true);
            return this.RedirectToAction(nameof(this.Index));
        }

        /// <summary>
        /// Upload-Endpunkt des ersten Bereichs (Eigenschaft <c>Dokument</c>); seine Adresse steht als
        /// <see cref="FMH.Komponente.Infrastruktur.MvcConst.MetaDataUploadUrl" /> am ViewModel.
        /// </summary>
        /// <returns>Json mit der vergebenen Id.</returns>
        [HttpPost]
        public IActionResult Upload()
        {
            return this.SpeichereDatei();
        }

        /// <summary>
        /// Zweiter Endpunkt, für die Eigenschaft <c>Zeugnis</c> — jede Dropzone kann ihre eigene
        /// Adresse haben. Fachlich landen die Dateien hier in einer anderen Ablage.
        /// </summary>
        /// <returns>Json mit der vergebenen Id.</returns>
        [HttpPost]
        public IActionResult UploadZeugnis()
        {
            return this.SpeichereDatei();
        }

        // Beide Endpunkte nehmen die Datei gleich entgegen; unterschiedlich ist in einem Projekt
        // nur, wohin sie gespeichert wird. Antwort muss ein Json mit der Eigenschaft id sein — das
        // Template legt daraus ein verstecktes Feld <Feldname>Ids im Formular an.
        private IActionResult SpeichereDatei()
        {
            // Die Dropzone meldet sich mit diesem Kopfzeilen-Eintrag. In einem Projekt lässt sich
            // daran ein normaler Formular-Post von einem Dropzone-Upload unterscheiden.
            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];

            // Der im Dialog erfasste Dateiname kommt als eigenes Formularfeld mit, der Inhalt als Datei.
            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 });
        }

        // Nur für die Rückmeldung: macht aus den Ids wieder die Dateinamen.
        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 name) ? name : $"Id {id}");
            return $"{ids.Count} Datei(en): {string.Join(", ", namen)}";
        }
    }
}

Darstellung

attach_file

Drag files here or click here to upload.

report_problem

You can not upload any more files.

attach_file

Drag files here or click here to upload.

report_problem

You can not upload any more files.

Mehrere Upload-Felder in einem Formular sind der Normalfall — jede Eigenschaft bringt ihre eigene Ablagefläche, ihre eigene Upload-Adresse und ihre eigene Id-Liste mit:

[UIHint("IFormFile")]
[AdditionalMetadata(MvcConst.MetaDataUploadUrl, "/FileUpload/IFormFileZweiBereicheBeispiel/Upload")]
public IFormFile Dokument { get; set; }

[UIHint("HiddenList")]
public List<int> DokumentIds { get; set; } = new List<int>();

[UIHint("IFormFile")]
[AdditionalMetadata(MvcConst.MetaDataUploadUrl, "/FileUpload/IFormFileZweiBereicheBeispiel/UploadZeugnis")]
public IFormFile Zeugnis { get; set; }

[UIHint("HiddenList")]
public List<int> ZeugnisIds { get; set; } = new List<int>();

Was zu beachten ist

  • Die Trennung läuft ausschliesslich über den Eigenschaftsnamen: Er bestimmt die Id der Ablagefläche, den Namen der Id-Felder (DokumentIds, ZeugnisIds) und den Namen der globalen Dropzone (window.Dokument, window.Zeugnis).
  • Beide Bereiche dürfen denselben Endpunkt verwenden; getrennte Endpunkte lohnen sich, wenn die Ablage fachlich unterschiedlich ist (anderer Dokumenttyp, andere Berechtigung).
  • Die Ids landen unabhängig voneinander im Formular und kommen im POST als zwei getrennte Listen an.