Skip to content
IzzyBlazor 0.1.0-alpha.17

Enter all results ↑ ↓ move Esc close

FileUpload

A drop zone around a real file input.

JavaScript off: what the HTML and CSS do on their own.

Usage

razor
<Form Model="Model" FormName="profile" Multipart OnValidSubmit="Save">
    <FileUpload Name="Avatar" Label="Profile photo" Accept="image/png,image/jpeg"
                MaxFileSize="2_000_000" />
    <Button Submit>Save</Button>
</Form>

@code {
    // The file binds by its Name; check its size and type in Save.
    [SupplyParameterFromForm(FormName = "profile")]
    private IFormFile? Avatar { get; set; }
}

The zone is a real file input with a label that opens the picker, so it works on static pages with JavaScript off: the file posts with the form, and the page binds it by its Name. Set Multipart on the Form, or only the file's name posts.

Types, size and posting

Use Accept for the types offered and MaxFileSize for the largest file, in bytes; together they write the line under the prompt ("PNG or JPEG · up to 2 MB").

Preview

or drag it herePNG or JPEG · up to 2 MB

Square images work best.

Code

Photo.razor
@using Microsoft.AspNetCore.Http

<Form Model="Model" FormName="file-upload-photo" Multipart OnValidSubmit="Save" MaxWidth="26.Rem" Grow>
    <FileUpload Name="Avatar" Label="Profile photo" Accept="image/png,image/jpeg" MaxFileSize="MaxSize"
                Hint="Square images work best." OnChange="Chosen" />
    <Row><Button Submit StartIcon="Lucide.Upload">Upload</Button></Row>
    @if (_result is not null)
    {
        <Alert Status="_status">@_result</Alert>
    }
</Form>

@code {
    private const long MaxSize = 2_000_000;

    [SupplyParameterFromForm(FormName = "file-upload-photo")]
    private Upload? Model { get; set; }

    // Static pages: the file posts with the form, bound by its Name.
    [SupplyParameterFromForm(FormName = "file-upload-photo")]
    private IFormFile? Avatar { get; set; }

    // Interactive pages: OnChange gets the file as it's chosen.
    private IBrowserFile? _chosen;
    private string? _result;
    private Status _status;

    protected override void OnInitialized() => Model ??= new();

    private void Chosen(IReadOnlyList<IBrowserFile> files) => _chosen = files.FirstOrDefault();

    // Accept and MaxFileSize only guide the browser, and the name and type come from it too:
    // check the size, then the file's first bytes.
    private async Task Save()
    {
        var (name, size) = Avatar is { Length: > 0 } posted ? (posted.FileName, posted.Length)
            : _chosen is { } chosen ? (chosen.Name, chosen.Size)
            : (null, 0L);
        if (name is null) (_status, _result) = (Status.Warning, "Choose a photo first.");
        else if (size > MaxSize) (_status, _result) = (Status.Danger, $"{name} is larger than 2 MB.");
        else
        {
            await using var stream = Avatar is { Length: > 0 } ? Avatar.OpenReadStream() : _chosen!.OpenReadStream(MaxSize);
            var head = new byte[4];
            var read = await stream.ReadAtLeastAsync(head, head.Length, throwOnEndOfStream: false);
            (_status, _result) = IsPngOrJpeg(head.AsSpan(0, read))
                ? (Status.Success, $"Uploaded {name} ({size:N0} bytes).")
                : (Status.Danger, $"{name} isn't a PNG or JPEG.");
        }
    }

    // PNG files start with 89 50 4E 47, JPEG files with FF D8 FF.
    private static bool IsPngOrJpeg(ReadOnlySpan<byte> head) =>
        head.StartsWith((byte[])[0x89, 0x50, 0x4E, 0x47]) || head.StartsWith((byte[])[0xFF, 0xD8, 0xFF]);

    // The form's model; the file itself binds to Avatar above.
    public sealed class Upload;
}

Off: Choose a file opens the picker, the browser names the chosen file under the prompt, and Upload posts the photo. Dropping a file and the size check need the script.

On a static page, bind the file to an IFormFile property with [SupplyParameterFromForm], named like the field. Both limits only guide the browser: the picker's "All files" choice gets past Accept, and with JavaScript off nothing checks the size, so check both on the server, as the example does by size and by the file's first bytes.

Several files

Use Multiple to take several files at once, and bind them to an IReadOnlyList<IFormFile> (or an IFormFileCollection) on a static page.

Preview

or drag them herePDF or DOCX · up to 10 MB

Contracts and statements of work.

Code

Multiple.razor
@using Microsoft.AspNetCore.Http

<Form Model="Model" FormName="file-upload-multiple" Multipart OnValidSubmit="Send" MaxWidth="26.Rem" Grow>
    <FileUpload Name="Attachments" Label="Attachments" Multiple Accept=".pdf,.docx" MaxFileSize="MaxSize"
                Hint="Contracts and statements of work." OnChange="Chosen" />
    <Row><Button Submit StartIcon="Lucide.Paperclip">Attach</Button></Row>
    @if (_result is not null)
    {
        <Alert Status="_status">@_result</Alert>
    }
</Form>

@code {
    private const long MaxSize = 10_000_000;

    [SupplyParameterFromForm(FormName = "file-upload-multiple")]
    private Message? Model { get; set; }

    // Static pages: every chosen file posts under the same name.
    [SupplyParameterFromForm(FormName = "file-upload-multiple")]
    private IReadOnlyList<IFormFile>? Attachments { get; set; }

    // Interactive pages: OnChange gets the files as they're chosen.
    private IReadOnlyList<IBrowserFile> _chosen = [];
    private string? _result;
    private Status _status;

    protected override void OnInitialized() => Model ??= new();

    private void Chosen(IReadOnlyList<IBrowserFile> files) => _chosen = files;

    private void Send()
    {
        var files = Attachments is { Count: > 0 } posted
            ? posted.Where(f => f.Length > 0).Select(f => (f.FileName, f.Length)).ToList()
            : _chosen.Select(f => (FileName: f.Name, Length: f.Size)).ToList();
        // The picker's "All files" and a crafted post get past Accept: check here too.
        var wrong = files.FirstOrDefault(f => !f.FileName.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)
                                             && !f.FileName.EndsWith(".docx", StringComparison.OrdinalIgnoreCase));
        (_status, _result) = files.Count == 0 ? (Status.Warning, "Choose a file first.")
            : wrong.FileName is { } name ? (Status.Danger, $"{name} isn't a PDF or Word file.")
            : files.Any(f => f.Length > MaxSize) ? (Status.Danger, "Each file must be 10 MB or less.")
            : (Status.Success, $"Attached {string.Join(", ", files.Select(f => f.FileName))}.");
    }

    // The form's model; the files themselves bind to Attachments above.
    public sealed class Message;
}

Off: Choose files opens the picker, and Attach posts every file chosen. Dropping files needs the script.

Every chosen file posts under the same name. A file dropped on the zone, or chosen again, replaces the earlier choice rather than adding to it, and the zone doesn't list the files, so show what arrived after the post.

Required and disabled

Use Required for a file the form can't post without, and Disabled to turn the zone off.

Preview

or drag it herePDF · up to 5 MB
or drag it hereSVG

Disabled: only the workspace owner can change it.

Code

States.razor
@using Microsoft.AspNetCore.Http

<Form Model="Model" FormName="file-upload-states" Multipart OnValidSubmit="Apply" MaxWidth="26.Rem" Grow>
    <FileUpload Name="Resume" Label="Résumé" Required Accept=".pdf" MaxFileSize="5_000_000" OnChange="Chosen" />
    <FileUpload Name="Logo" Label="Company logo" Accept="image/svg+xml" Disabled
                Hint="Disabled: only the workspace owner can change it." />
    <Row><Button Submit>Apply</Button></Row>
    @if (_result is not null)
    {
        <Alert Status="_status">@_result</Alert>
    }
</Form>

@code {
    [SupplyParameterFromForm(FormName = "file-upload-states")]
    private Application? Model { get; set; }

    [SupplyParameterFromForm(FormName = "file-upload-states")]
    private IFormFile? Resume { get; set; }

    private IBrowserFile? _chosen;
    private string? _result;
    private Status _status;

    protected override void OnInitialized() => Model ??= new();

    private void Chosen(IReadOnlyList<IBrowserFile> files) => _chosen = files.FirstOrDefault();

    // Required and Accept are the browser's checks: a crafted post can leave the file out or send
    // another type. A disabled field posts nothing, so Logo never arrives.
    private void Apply() => (_status, _result) = (Resume is { Length: > 0 } ? Resume.FileName : _chosen?.Name) switch
    {
        null => (Status.Warning, "Choose a résumé first."),
        var name when !name.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) => (Status.Danger, $"{name} isn't a PDF."),
        var name => (Status.Success, $"Sent {name}."),
    };

    public sealed class Application;
}

Off: The browser stops Apply until a résumé is chosen, in its own words.

Required is the browser's check, so a crafted post can still leave the file out: check that it arrived on the server too. A file can't be read-only, so a read-only Form disables the zone. A disabled zone posts nothing, so keep its file on the server rather than clearing it after the post.

Checking on the server

Check every file's size and type where you save it, and set the server's request limits to match MaxFileSize: Kestrel turns away a request over 30,000,000 bytes (about 28.6 MB) by default, before your code runs.

Program.cs
using Microsoft.AspNetCore.Http.Features;

// Kestrel's limit for the whole request (30,000,000 bytes by default).
builder.WebHost.ConfigureKestrel(kestrel =>
    kestrel.Limits.MaxRequestBodySize = 12_000_000);

// The limit for a form's files (128 MB by default).
builder.Services.Configure<FormOptions>(form =>
    form.MultipartBodyLengthLimit = 12_000_000);

The posted content type and file name come from the browser and can say anything, so don't trust either to name the saved file or to decide how it's served; check the file's first bytes for its type, as the photo example does. IIS has its own request limit (about 28.6 MB by default) when the app runs behind it.

On an interactive page

Use OnChange to get the chosen files as IBrowserFile as soon as they're chosen, and read each with OpenReadStream; nothing posts.

razor
<FileUpload Name="Avatar" Label="Profile photo" Accept="image/png,image/jpeg"
            MaxFileSize="MaxSize" OnChange="UploadAsync" />

@* PhotoStore is your own service. *@
@inject PhotoStore Photos

@code {
    private const long MaxSize = 2_000_000;

    private async Task UploadAsync(IReadOnlyList<IBrowserFile> files)
    {
        if (files is not [var file]) return;
        await using var stream = file.OpenReadStream(MaxSize);
        // Check the file's first bytes for its type before keeping it.
        await Photos.SaveAsync(stream);
    }
}

OnChange leaves out files over MaxFileSize and gives at most MaxFiles (10). OpenReadStream throws for a file over 512,000 bytes unless you pass a larger limit, so pass MaxFileSize.

Its own words

Use ChooseText and DragText for the prompt, AcceptText for the line under it (empty for none), and TooLargeText and WrongTypeText for the messages, or set them for the whole app as in Text and other languages.

Mode support

What works in each mode, FileUpload
FeatureOffStaticLive
Choose files with the picker, posted with the formYesYesYes
Drop files on the zone; the wrong type refusedNoYesYes
The chosen file's name and size, too large flagged before postingNoYesYes
OnChange gets the filesNoNoYes

Off: JavaScript disabled. Static: a static SSR page with IzzyBlazor's script. Live: an interactive page (Server, WebAssembly or Auto).

Accessibility

  • It renders a real file input, out of sight but focusable once scripts run: Tab reaches it, the zone shows the focus ring, and Enter or Space opens the picker.
  • The input is named by Label; without one, by the "Choose a file" link ("Choose files" with Multiple; ChooseText).
  • The allowed types and size, the Hint and any Errors are linked with aria-describedby; Errors also sets aria-invalid.
  • With script, the chosen file's name and size (or "3 files") are announced politely; there's no list of the files.
  • With script, a file of the wrong type or over MaxFileSize gets the browser's validation message, and the browser's own validation keeps the form from posting (not with NativeValidation="false" on the Form).
  • Dropping is a pointer-only extra: the picker does the same from the keyboard.

Parameters

FileUpload parameters
PropTypeDefault
NameThe form field name the files post under.stringRequired
LabelThe visible label.string?
HideLabelHides the label but keeps it for screen readers.boolfalse
HintHelp text under the zone.string?
RequiredWhether a file must be chosen before the form posts.boolfalse
DisabledDisables the picker.boolfalse
AcceptThe file types to offer, as the accept attribute: "image/*", ".pdf,.docx".string?
MultipleAllows choosing several files.boolfalse
MaxFileSizeThe largest file allowed, in bytes.long?
MaxFilesOn interactive pages, the most files OnChange receives; extra ones are left out.int10
TooLargeTextThe message for a file over MaxFileSize: {0} is its name, {1} the limit.string?
WrongTypeTextThe message for a chosen or dropped file that doesn't match Accept: {0} is its name.string?
ChooseTextThe link that opens the picker.string?computed
DragTextThe text after the link.string?computed
AcceptTextSmall text under the prompt.string?computed
IconThe zone's icon.IconData?computed
ErrorsValidation messages to show under the zone, such as from server checks.IReadOnlyList<string>?
OnChangeOn interactive pages, runs with the chosen files, leaving out any over MaxFileSize (the zone flags those).EventCallback<IReadOnlyList<IBrowserFile>>

See also

  • Form: posts on static pages, validates live on interactive ones.
  • TextField: a labeled text input with add-ons and a password reveal.
  • FieldFrame: the label, hint and errors around a control of your own.