IFormFile

Dokumenttyp im Dialog

Der Dialog verlangt neben der Bezeichnung einen Dokumenttyp. Die Auswahl kommt aus einer Eigenschaft des umschliessenden ViewModels und wird je Datei an den Upload-Endpunkt übertragen.

@model IFormFileDokumentTypBeispielViewModel

@* Der Dialog nach dem Ablegen einer Datei verlangt zusätzlich einen Dokumenttyp. Gefüllt wird die
   Auswahl aus der Eigenschaft DokumentTypen dieses ViewModels — benannt über
   MvcConst.DropdownListSelection am Upload-Feld. *@

<form asp-area="FileUpload" asp-controller="IFormFileDokumentTypBeispiel" asp-action="Index" method="post">
    <div asp-validation-summary="All" class="text-danger"></div>
    @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>
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FMH.Komponente.Basis.Ui.Validation.DataAnnotation;
using FMH.Komponente.Infrastruktur;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace FMH.Komponente.WebUI.Areas.FileUpload.Models
{
    public class IFormFileDokumentTypBeispielViewModel
    {
        // DropdownListSelection nennt hier eine Eigenschaft DIESES ViewModels: Das EditorTemplate
        // IFormFile liest sie über ViewData.ModelExplorer.Container.Model, also am umschliessenden
        // Modell. Beim EditorTemplate DokumentListe ist es umgekehrt — dort gehört die Liste ans
        // DokumentListeViewModel selbst.
        [Display(Name = "Dokumente")]
        [UIHint("IFormFile")]
        [AdditionalMetadata(MvcConst.DropdownListSelection, nameof(DokumentTypen))]
        [AdditionalMetadata(MvcConst.MetaDataUploadUrl, "/FileUpload/IFormFileDokumentTypBeispiel/Upload")]
        [AdditionalMetadata(MvcConst.MaxFileSizeInMb, "1")]
        [AdditionalMetadata(MvcConst.MaxFiles, "5")]
        [AdditionalMetadata(MvcConst.MetaDataAllowedFiles, ".pdf, .jpg, .jpeg, .png")]
        public IFormFile Dokument { get; set; }

        // Füllt die Auswahl im Upload-Dialog. Der Controller setzt sie je Aufruf, damit die Typen
        // aus der Fachlogik kommen können und nicht im ViewModel festgeschrieben sind.
        public IEnumerable<SelectListItem> DokumentTypen { get; set; } = new List<SelectListItem>();

        // Sammelt die Ids der hochgeladenen Dateien — Eigenschaft des Upload-Feldes + "Ids".
        [UIHint("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;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace FMH.Komponente.WebUI.Areas.FileUpload.Controllers.Beispiele
{
    // Zeigt die Dokumenttyp-Auswahl im Upload-Dialog des EditorTemplates IFormFile. Die Liste dafür
    // kommt aus der Eigenschaft DokumentTypen des Beispiel-ViewModels — benannt über
    // MvcConst.DropdownListSelection am Upload-Feld.
    [Area("FileUpload")]
    public class IFormFileDokumentTypBeispielController : Controller
    {
        // Ablage der Demo: Dateiname und der im Dialog gewählte Dokumenttyp je vergebener Id.
        private static readonly ConcurrentDictionary<int, (string Name, string Typ)> Dokumente =
            new ConcurrentDictionary<int, (string Name, string Typ)>();

        private static int letzteId;

        // Auch von der Demo-Seite genutzt, damit dort dieselbe Auswahl erscheint wie im
        // interaktiven Beispiel.
        public static IFormFileDokumentTypBeispielViewModel ErstelleModell()
        {
            return new IFormFileDokumentTypBeispielViewModel
            {
                DokumentTypen = new List<SelectListItem>
                {
                    new SelectListItem { Value = "2", Text = "Entscheid" },
                    new SelectListItem { Value = "4", Text = "Einsprache" },
                    new SelectListItem { Value = "8", Text = "Abschluss" },
                },
            };
        }

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

        [HttpPost]
        [ValidateAntiForgeryToken]
        public IActionResult Index(IFormFileDokumentTypBeispielViewModel modell)
        {
            // Die Dateien sind längst übertragen; das Formular bringt nur noch die Ids mit. Der
            // gewählte Typ steckt in der Ablage, nicht im Formular.
            this.AddAlert(
                $"Formular übertragen – {BeschreibeUpload(modell.DokumentIds)}",
                AlertType.Success,
                autoHide: true);

            return this.RedirectToAction(nameof(this.Index));
        }

        /// <summary>
        /// Der Upload-Endpunkt dieses Beispiels. Zusätzlich zur Datei und zur Bezeichnung kommt der
        /// im Dialog gewählte Dokumenttyp als eigenes Formularfeld <c>DokumentTyp</c> an.
        /// </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 Typ gehört zur einzelnen Datei, nicht zum Feld — er wird deshalb je Upload
            // übertragen. Ankommen tut der Wert (Value) des gewählten Eintrags, nicht sein Text.
            var typ = this.Request.Form["DokumentTyp"].ToString();

            var id = Interlocked.Increment(ref letzteId);
            Dokumente[id] = (name, TypBezeichnung(typ));

            return this.Json(new { id });
        }

        // Nur für die Rückmeldung: macht aus den Ids wieder Dateiname und Typ.
        private static 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.Name} ({eintrag.Typ})"
                    : $"Id {id}");

            return $"{ids.Count} Datei(en): {string.Join(", ", namen)}";
        }

        // Übersetzt den übertragenen Wert zurück in den angezeigten Text.
        private static string TypBezeichnung(string wert)
        {
            var treffer = ErstelleModell()
                .DokumentTypen.FirstOrDefault(eintrag => eintrag.Value == wert);

            return treffer?.Text ?? "ohne Typ";
        }
    }
}

Darstellung

attach_file

Drag files here or click here to upload.

report_problem

You can not upload any more files.

Ist eine Typenliste konfiguriert, verlangt der Dialog nach dem Ablegen einer Datei neben der Bezeichnung zusätzlich einen Dokumenttyp.

[Display(Name = "Dokumente")]
[UIHint("IFormFile")]
[AdditionalMetadata(MvcConst.DropdownListSelection, nameof(DokumentTypen))]
[AdditionalMetadata(MvcConst.MetaDataUploadUrl, "/FileUpload/IFormFileDokumentTypBeispiel/Upload")]
public IFormFile Dokument { get; set; }

public IEnumerable<SelectListItem> DokumentTypen { get; set; } = new List<SelectListItem>();

Die Liste füllt der Controller, damit die Typen aus der Fachlogik kommen können:

modell.DokumentTypen = new List<SelectListItem>
{
    new SelectListItem { Value = "2", Text = "Entscheid" },
    new SelectListItem { Value = "4", Text = "Einsprache" },
    new SelectListItem { Value = "8", Text = "Abschluss" },
};

Im Upload-Endpunkt kommt die Auswahl als eigenes Formularfeld an — übertragen wird der Value, nicht der angezeigte Text:

var typ = this.Request.Form["DokumentTyp"].ToString();

Was zu beachten ist

  • Die Liste gehört an das umschliessende ViewModel. Das ist der Unterschied zum EditorTemplate DokumentListe: Dort wird der Eigenschaftsname am DokumentListeViewModel gesucht, hier am Modell, das die IFormFile-Eigenschaft enthält (ViewData.ModelExplorer.Container.Model). nameof(DokumentTypen) zeigt deshalb auf eine Eigenschaft derselben Klasse.
  • Der Name wird zur Laufzeit per Reflexion aufgelöst. Ein Tippfehler führt zu einer Ausnahme beim Rendern, nicht zu einer stillen leeren Liste — nameof schützt davor.
  • Ist die Liste leer, entfällt die Auswahl im Dialog ersatzlos; das Feld verhält sich dann wie im Grundfall. Das ist der Grund, warum die Liste bei jedem Aufruf gesetzt werden muss, auch beim erneuten Anzeigen nach einem Validierungsfehler.
  • Der Typ gehört zur einzelnen Datei, nicht zum Feld. Er wird je Upload übertragen und muss deshalb im Endpunkt zusammen mit der Datei abgelegt werden — im Formular-Post am Ende steht er nicht mehr.