Uploading multiple files using InputFile in Blazor

 
 
  • Gérald Barré

Blazor provides the <InputFile> component for file uploads through its OnChange event. While this works well for single-file uploads, selecting new files while previous ones are still being uploaded can trigger an error. You can reproduce it with the following Razor component:

Razor
<InputFile OnChange="OnFileChanged" multiple></InputFile>
<pre>@status</pre>

@code {
    string? status;

    async Task OnFileChanged(InputFileChangeEventArgs e)
    {
        status += "\nChanged";
        try
        {
            var buffer = new byte[4096];
            foreach (var file in e.GetMultipleFiles(e.FileCount))
            {
                status += "\nReading file: " + file.Name;
                _ = InvokeAsync(StateHasChanged);

                using var stream = file.OpenReadStream(maxAllowedSize: long.MaxValue);
                while (true)
                {
                    var read = await stream.ReadAsync(buffer);
                    if (read == 0)
                        break;
                }
            }
        }
        catch (Exception ex)
        {
            status += "\nError: " + ex.Message;
            _ = InvokeAsync(StateHasChanged);
        }
    }
}

You may see an error like the following:

Error: There is no file with ID 3. The file list may have changed.
    at Pe (https://localhost:7220/_framework/blazor.server.js:1:31704)
    at Object.readFileData (https://localhost:7220/_framework/blazor.server.js:1:31630)
    at https://localhost:7220/_framework/blazor.server.js:1:3501
    at new Promise (<anonymous>)
    at kt.beginInvokeJSFromDotNet (https://localhost:7220/_framework/blazor.server.js:1:3475)
    at https://localhost:7220/_framework/blazor.server.js:1:72001
    at Array.forEach (<anonymous>)
    at kt._invokeClientMethod (https://localhost:7220/_framework/blazor.server.js:1:71987)
    at kt._processIncomingData (https://localhost:7220/_framework/blazor.server.js:1:70029)
    at vt.connection.onreceive (https://localhost:7220/_framework/blazor.server.js:1:64432)

#Why does the error occur?

This error is by design in the InputFile component. When a user selects files, the component stores them locally and assigns an ID to each one. When you call file.OpenReadStream, Blazor reads the file using that ID.

Here's the source from the dotnet/aspnetcore repository:

InputFile.ts (TypeScript)
  elem.addEventListener('change', function(): void {
    // Reduce to purely serializable data, plus an index by ID.
    elem._blazorFilesById = {}; // 👈 Clear the list of uploaded files

    const fileList = Array.prototype.map.call(elem.files, function(file: File): BrowserFile {
      const result = {
        id: ++elem._blazorInputFileNextFileId, 👈 Generate a new id for the file
        lastModified: new Date(file.lastModified).toISOString(),
        name: file.name,
        size: file.size,
        contentType: file.type,
        readPromise: undefined,
        arrayBuffer: undefined,
        blob: file,
      };

      elem._blazorFilesById[result.id] = result; // 👈 Store the file in the list of uploaded files
      return result;
    });

    callbackWrapper.invokeMethodAsync('NotifyChange', fileList); // 👈 Notify the InputFile component
  });
}

When the user selects new files, the InputFile component clears the previous list and adds the new ones. If a stream was already open to a previous file, reading from it will fail because no file matches the old ID.

#Workaround

The workaround is to retain the previous InputFile instances and create a new one each time the user selects files. Hide the inactive inputs from the UI using display: none.

Razor
@for (int i = 0; i < numberOfInputFiles; i++)
{
    <InputFile @key="i" OnChange="OnFileChanged" multiple style="@GetInputFileStyle(i)"></InputFile>
}

<pre>@status</pre>

@code {
    string? status;
    int numberOfInputFiles = 1;

    string GetInputFileStyle(int index)
    {
        return index == numberOfInputFiles - 1 ? "" : "display: none";
    }

    async Task OnFileChanged(InputFileChangeEventArgs e)
    {
        // Create a new InputFile component
        numberOfInputFiles++;

        // Handle selected files
        status += "\nChanged";
        try
        {
            var buffer = new byte[4096];
            foreach (var file in e.GetMultipleFiles(e.FileCount))
            {
                status += "\nReading file: " + file.Name;
                using var stream = file.OpenReadStream(maxAllowedSize: long.MaxValue);
                while (true)
                {
                    var read = await stream.ReadAsync(buffer);
                    if (read == 0)
                        break;
                }

                status += "\nRead file: " + file.Name;
                await InvokeAsync(StateHasChanged);
            }
        }
        catch (Exception ex)
        {
            status += "\nError: " + ex.Message;
            await InvokeAsync(StateHasChanged);
        }
    }
}

With this approach, uploads work as expected.

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?