Uploading multiple files using InputFile in Blazor

  • Gérald Barré

Blazor provides a component to upload files. You can use the <InputFile> component and its OnChange event to get the selected files. It works well for single file uploads. But, if the user selects new files while the first files are still uploading, you may get an error. You can reproduce this error using 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 can get errors such as 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 the expected behavior for the FileUpload component. Indeed, when a user selects a file, the InputFile components store the list of files locally and associate an id to each file. When you use file.OpenReadStream to read the file, Blazor reads the file using its 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 a new file, the InputFile component removes the previous files and adds the new ones. If you haven't opened a stream to a previous file yet, you'll get an error when you do as no file match the id.

#Workaround

A workaround is to keep the previous InputFile components and create a new one each time the user selects new files. Of course, you need to hide the previous 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);
        }
    }
}

Using the previous code, it should work as expected 😃

#Addional resources

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

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub