IFormFile
Datei-Upload (Grundfall)
Ablagefläche mit eigener Upload-Adresse, Grössenbeschränkung und erlaubten Dateitypen. Die Ids der übertragenen Dateien sammelt die Eigenschaft DokumentIds.
@model IFormFileStandardBeispielViewModel
@* Die Dateien gehen sofort beim Ablegen an den Upload-Endpunkt. Das Formular überträgt später nur
noch die gesammelten Ids — deshalb gehören beide Felder in dasselbe <form>. *@
<form asp-area="FileUpload" asp-controller="IFormFileStandardBeispiel" 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;
namespace FMH.Komponente.WebUI.Areas.FileUpload.Models
{
public class IFormFileStandardBeispielViewModel
{
[Display(Name = "Dokumente")]
[UIHint("IFormFile")]
[AdditionalMetadata(MvcConst.MetaDataUploadUrl, "/FileUpload/IFormFileStandardBeispiel/Upload")]
[AdditionalMetadata(MvcConst.MaxFileSizeInMb, "1")]
[AdditionalMetadata(MvcConst.MaxFiles, "2")]
[AdditionalMetadata(MvcConst.MetaDataAllowedFiles, ".pdf, .jpg, .jpeg, .png")]
public IFormFile Dokument { get; set; }
// Sammelt die Ids der hochgeladenen Dateien. Der Name ist Konvention: Eigenschaft des
// Upload-Feldes + "Ids" — genau so heissen die versteckten Felder, die das Template beim
// erfolgreichen Upload ins Formular schreibt.
[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;
namespace FMH.Komponente.WebUI.Areas.FileUpload.Controllers.Beispiele
{
// Ein Beispiel, ein Controller: Upload-Endpunkt, Ablage und Formular-Round-Trip stehen
// vollständig in dieser Datei — der Grundfall lässt sich damit ohne Sprünge nachlesen.
[Area("FileUpload")]
public class IFormFileStandardBeispielController : 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 IFormFileStandardBeispielViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Index(IFormFileStandardBeispielViewModel modell)
{
// Die Dateien sind zu diesem Zeitpunkt längst übertragen; das Formular bringt nur noch
// die gesammelten Ids mit. Die Eigenschaft Dokument selbst bleibt leer.
// PRG-Pattern (Post-Redirect-Get): Meldung merken und per Redirect auf die GET-Action.
this.AddAlert($"Formular übertragen – {this.BeschreibeUpload(modell.DokumentIds)}", AlertType.Success, autoHide: true);
return this.RedirectToAction(nameof(this.Index));
}
/// <summary>
/// Der Upload-Endpunkt dieses Beispiels; seine Adresse steht als
/// <see cref="FMH.Komponente.Infrastruktur.MvcConst.MetaDataUploadUrl" /> am ViewModel.
/// Die Dropzone schickt jede Datei einzeln hierher, sobald der Dialog bestätigt wurde.
/// Antwort muss ein Json mit der Eigenschaft <c>id</c> sein — das Template legt daraus ein
/// verstecktes Feld <c><Feldname>Ids</c> im Formular an. Fehlt die Id, lädt die Seite neu.
/// </summary>
/// <returns>Json mit der vergebenen Id.</returns>
[HttpPost]
public IActionResult Upload()
{
// 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
Das EditorTemplate IFormFile rendert eine Ablagefläche (Dropzone). Jede Datei geht sofort an einen Endpunkt der Anwendung — nicht erst beim Absenden des Formulars.
[Display(Name = "Dokumente")]
[UIHint("IFormFile")]
[AdditionalMetadata(MvcConst.MetaDataUploadUrl, "/FileUpload/IFormFileStandardBeispiel/Upload")]
[AdditionalMetadata(MvcConst.MaxFileSizeInMb, "1")]
[AdditionalMetadata(MvcConst.MaxFiles, "2")]
[AdditionalMetadata(MvcConst.MetaDataAllowedFiles, ".pdf, .jpg, .jpeg, .png")]
public IFormFile Dokument { get; set; }
[UIHint("HiddenList")]
public List<int> DokumentIds { get; set; } = new List<int>();
<form asp-controller="IFormFileStandardBeispiel" asp-action="Index" method="post">
@Html.EditorFor(m => m.DokumentIds)
@Html.EditorFor(m => m.Dokument)
</form>
Der Ablauf in vier Schritten
- Der Benutzer legt eine Datei ab. Ein Dialog fragt nach der Dateibezeichnung (und, wenn konfiguriert, nach dem Dokumenttyp).
- Nach dem Bestätigen überträgt die Dropzone die Datei einzeln an die Upload-Url. Die Anfrage trägt den Kopfzeilen-Eintrag
IsDropzoneRequest: true, die Datei liegt inRequest.Form.Files[0], die erfasste Bezeichnung im FormularfeldDateiBezeichnung. - Der Endpunkt speichert die Datei und antwortet mit Json, das eine Eigenschaft
identhält. Das Template legt daraufhin ein verstecktes Feld<Feldname>Idsmit dieser Id ins Formular. - Beim Absenden kommen nur noch diese Ids beim Server an. Die
IFormFile-Eigenschaft selbst ist im POST leer — sie ist nur das Renderziel des Templates.
Fehlt die id in der Antwort, meldet das Skript eine Warnung ins Log und lädt die Seite neu — ein stiller Datenverlust wird so vermieden.
Die zweite Eigenschaft ist Pflicht
Zu jedem Upload-Feld gehört eine Eigenschaft <Feldname>Ids mit [UIHint("HiddenList")]:
- Sie nimmt die Ids entgegen (Namenskonvention, das Template baut den Namen aus dem Feldnamen).
- Sie rendert sie wieder, wenn die Seite erneut aufgebaut wird (z. B. nach einem Validierungsfehler) — ohne sie wären bereits übertragene Dateien nach dem ersten Fehlversuch vergessen.
Optionen am Feld
| Attribut | Wirkung |
|---|---|
[UIHint("IFormFile")] |
Wählt das Template. Ohne die Angabe greift MVC über den Typ IFormFile ebenfalls darauf zu; explizit ist es lesbarer. |
[Display(Name = "…")] |
Beschriftung über der Ablagefläche. |
[AdditionalMetadata(MvcConst.MetaDataUploadUrl, "…")] |
Adresse des Upload-Endpunkts. Standard upload. |
[AdditionalMetadata(MvcConst.MaxFileSizeInMb, "1")] |
Maximale Grösse je Datei in MB. |
[AdditionalMetadata(MvcConst.MaxFiles, "2")] |
Maximale Anzahl Dateien; Standard 100. Feste Meldung — flexibler ist [MaxUploadableFiles]. |
[AdditionalMetadata(MvcConst.MetaDataAllowedFiles, ".pdf, .jpg")] |
Erlaubte Endungen. Standard .pdf, .jpg, .jpeg, .png, .docx, .msg. |
[AdditionalMetadata(MvcConst.FilenameMaxLength, "80")] |
Maximale Länge der Dateibezeichnung im Dialog; Standard 150. |
[AdditionalMetadata(MvcConst.DropdownListSelection, nameof(Dokumenttypen))] |
Ergänzt den Dialog um eine Auswahl des Dokumenttyps (Pflichtfeld im Dialog). |
[HelpText("…")] |
Hilfetext unterhalb der Ablagefläche. |
Upload-Url: relativ oder absolut
Enthält der Wert keinen Schrägstrich, setzt das Template die Adresse aus der aktuellen Route zusammen: /<Area>/<Controller>/<Wert>. Mit einem Schrägstrich im Wert gilt die Angabe unverändert — so lässt sich ein Endpunkt in einem anderen Controller ansprechen (wie in dieser Demo).
Die Grössenbeschränkung wird zusätzlich an der Server-Grenze gekappt: Das Template liest KestrelServerOptions.Limits.MaxRequestBodySize und nimmt davon 2 MB Reserve. Eine grössere Angabe im Attribut wird auf diesen Wert reduziert — die Prüfung im Browser meldet also nie mehr, als der Server annimmt.
Die Url zur Laufzeit ergänzen
Jede Dropzone liegt unter ihrem Feldnamen global im Browser. Damit lassen sich Parameter nachtragen, die erst im Browser bekannt sind:
var url = window.Dokument.options.url;
window.Dokument.options.url = url + "?param1=456";
window.Dokument ist die Dropzone der Eigenschaft Dokument. Trägt die Eigenschaft [UniqueId], enthält der Name zusätzlich das Zufalls-Suffix — dann ist dieser Weg nicht mehr vorhersagbar.
Was zu beachten ist
- Ein Formular ist Pflicht. Das Skript hängt die versteckten Id-Felder an
form— ohne umschliessendes<form>gehen die Ids verloren. - Mehrere Ablageflächen auf einer Seite: Die Ids werden an alle Formulare der Seite angehängt. Bei mehreren unabhängigen Formularen auf einer Seite kommen deshalb fremde Ids mit; ein Formular je Seite ist der sichere Aufbau (mehrere Felder in einem Formular sind dagegen unproblematisch — siehe Beispiel „Zwei Upload-Bereiche").
- Während eines laufenden Uploads sperrt das Skript die Schaltflächen des Formulars und gibt sie danach wieder frei.
- Die Prüfung von Grösse, Anzahl und Dateityp läuft im Browser. Der Endpunkt muss dieselben Regeln serverseitig noch einmal anwenden — er ist von aussen direkt erreichbar.